Skip to main content

gpui/
assets.rs

1use crate::{DevicePixels, Pixels, Result, SharedString, Size, size};
2use gpui_util::ResultExt;
3use smallvec::SmallVec;
4
5use image::{Delay, EncodableLayout, Frame};
6use std::{
7    borrow::Cow,
8    collections::BTreeMap,
9    fmt,
10    hash::Hash,
11    sync::{
12        Arc,
13        atomic::{AtomicUsize, Ordering::SeqCst},
14    },
15};
16
17/// A read-only source of files that the app can load by path, such as a directory or a bundle.
18///
19/// Prefer [`AssetRegistry`] when you control the entries: it can hold preloaded bytes as well as
20/// on-demand loaders. An `AssetSource` is useful when the source already owns the path lookup.
21/// It converts into an [`AssetRegistry`] with `From`, and
22/// [`crate::Application::with_assets`] accepts that conversion. The converted registry loads
23/// each file on demand.
24pub trait AssetSource: 'static + Send + Sync {
25    /// Returns the bytes of the file at `path`, or `None` if there is no such file.
26    ///
27    /// The paths are the ones that [`AssetSource::list`] returns.
28    fn load(&self, path: &str) -> Result<Option<Cow<'static, [u8]>>>;
29
30    /// Returns the paths of the assets under `path`. Pass `""` to list all of them.
31    ///
32    /// Each returned path is one that [`AssetSource::load`] accepts.
33    fn list(&self, path: &str) -> Result<Vec<SharedString>>;
34
35    /// Loads every asset now and yields each one as an [`AssetEntry::PreLoaded`].
36    ///
37    /// It lists everything with `list("")`, loads each path, and skips the paths that fail or are
38    /// missing. Pass the result to [`AssetRegistry::extend`].
39    fn iter_preloaded(&self) -> Box<dyn Iterator<Item = (SharedString, AssetEntry)> + '_> {
40        let paths = self.list("").unwrap_or_default();
41        let iter = paths.into_iter().filter_map(move |path| {
42            let entry = AssetEntry::from(self.load(&path).ok().flatten()?);
43            Some((path, entry))
44        });
45        Box::new(iter)
46    }
47
48    /// Yields one [`AssetEntry::OnDemand`] per path. Each entry calls [`AssetSource::load`] on this
49    /// source when the asset is requested.
50    ///
51    /// Nothing is read until an asset is requested, and each request reads the file again. A failed
52    /// load is logged and reported as `None`. Pass the result to [`AssetRegistry::extend`].
53    fn iter_ondemand(self: Arc<Self>) -> Box<dyn Iterator<Item = (SharedString, AssetEntry)>> {
54        use gpui_util::ResultExt;
55        let paths = self.list("").unwrap_or_default();
56        let iter = paths.into_iter().map(move |path| {
57            // constructing OnDemand for loading assets from shared:Arc only when they are requested
58            let entry = AssetEntry::from({
59                let source = self.clone();
60                let path = path.clone();
61                move || source.load(path.as_str()).log_err().flatten()
62            });
63            (path, entry)
64        });
65        Box::new(iter)
66    }
67}
68
69impl AssetSource for () {
70    fn load(&self, _path: &str) -> Result<Option<Cow<'static, [u8]>>> {
71        Ok(None)
72    }
73
74    fn list(&self, _path: &str) -> Result<Vec<SharedString>> {
75        Ok(vec![])
76    }
77}
78
79/// The bytes of one asset. `Cow::Borrowed` for data compiled into the binary, and `Cow::Owned` for
80/// data read at runtime.
81pub type AssetData = Cow<'static, [u8]>;
82/// A callback that produces an asset's bytes when the asset is requested, or `None` if it can't.
83///
84/// The cache may call it from a worker thread, so the closure and everything it captures must be
85/// `Send` and `Sync`.
86pub type FnAssetLoader = Arc<dyn Fn() -> Option<AssetData> + 'static + Send + Sync>;
87/// One asset in the [`AssetRegistry`]: its bytes, or a loader that produces them on request.
88///
89/// `Cow<'static, [u8]>` converts into the `PreLoaded` variant, and a closure that returns
90/// `Option<AssetData>` converts into `OnDemand`.
91pub enum AssetEntry {
92    /// The bytes are ready now: static data compiled into the binary, or a file that was already
93    /// read. The registry returns them without copying.
94    PreLoaded(AssetData),
95    /// The bytes come from a loader that runs on every [`AssetRegistry::load`]. The registry does
96    /// not cache the result, so the loader decides whether to read the disk again.
97    OnDemand(FnAssetLoader),
98}
99impl From<AssetData> for AssetEntry {
100    fn from(value: AssetData) -> Self {
101        Self::PreLoaded(value)
102    }
103}
104impl<F> From<F> for AssetEntry
105where
106    F: Fn() -> Option<AssetData> + 'static + Send + Sync,
107{
108    fn from(value: F) -> Self {
109        Self::OnDemand(Arc::new(value))
110    }
111}
112
113/// Returned when a path is already registered in an [`AssetRegistry`]. It holds the duplicated path.
114#[derive(thiserror::Error, Debug)]
115#[error("An asset with the pathkey {0:?} already exists")]
116pub struct DuplicateAssetPath(SharedString);
117
118/// The named assets that the app loads by path, such as images, SVGs and fonts.
119///
120/// Set the registry with [`crate::Application::with_assets`] and read it with
121/// [`crate::App::assets`]. Paths are exact lookup keys such as `"icons/star.svg"`; the registry
122/// does not resolve them against the filesystem.
123/// Each path has one [`AssetEntry`] and can be registered only once. Build a registry with
124/// [`AssetRegistry::insert`], [`AssetRegistry::extend`] or `collect()`, or convert an
125/// [`AssetSource`].
126#[derive(Default)]
127pub struct AssetRegistry {
128    entries: BTreeMap<SharedString, AssetEntry>,
129}
130
131impl<T: AssetSource + 'static> From<T> for AssetRegistry {
132    fn from(value: T) -> Self {
133        let shared: Arc<dyn AssetSource> = Arc::new(value);
134        Self::from(shared)
135    }
136}
137
138/// Converts the provided [`AssetSource`] into a fresh [`AssetRegistry`] where all entries in
139/// the asset source are loaded [OnDemand](AssetEntry::OnDemand) via [`AssetSource::iter_ondemand`].
140impl From<Arc<dyn AssetSource>> for AssetRegistry {
141    fn from(value: Arc<dyn AssetSource>) -> Self {
142        let mut assets = Self::default();
143        let _ = assets.extend(value.iter_ondemand());
144        assets
145    }
146}
147
148impl<KeyType, ValueType> FromIterator<(KeyType, ValueType)> for AssetRegistry
149where
150    KeyType: AsRef<str>,
151    ValueType: Into<AssetEntry>,
152{
153    fn from_iter<T: IntoIterator<Item = (KeyType, ValueType)>>(iter: T) -> Self {
154        iter.into_iter()
155            .fold(Self::default(), |mut assets, (key, value)| {
156                assets.insert(key.as_ref(), value).log_err();
157                assets
158            })
159    }
160}
161
162impl AssetRegistry {
163    /// Adds the asset at `path`.
164    ///
165    /// The asset can be `Cow::Borrowed(&'static [u8])` for static bytes, `Cow::Owned(Vec<u8>)` for
166    /// bytes you already have, or a closure that returns them on demand (see [`FnAssetLoader`]).
167    ///
168    /// # Errors
169    ///
170    /// Returns [`DuplicateAssetPath`] if `path` is already registered. The existing entry is kept.
171    ///
172    /// # Examples
173    ///
174    /// ```
175    /// # use gpui::*;
176    /// # use std::borrow::Cow;
177    /// let mut assets = AssetRegistry::default();
178    /// assets
179    ///     .insert(
180    ///         "icons/star.svg",
181    ///         Cow::Borrowed(
182    ///             &b"<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 8 8\"><rect width=\"8\" height=\"8\"/></svg>"[..],
183    ///         ),
184    ///     )
185    ///     .unwrap();
186    ///
187    /// assert!(assets.load("icons/star.svg").is_some());
188    /// assert!(assets.load("icons/missing.svg").is_none());
189    /// assert!(assets.insert("icons/star.svg", Cow::Borrowed(&b""[..])).is_err());
190    /// ```
191    pub fn insert(
192        &mut self,
193        path: impl Into<SharedString>,
194        asset: impl Into<AssetEntry>,
195    ) -> Result<(), DuplicateAssetPath> {
196        let path = path.into();
197        if self.entries.contains_key(&path) {
198            return Err(DuplicateAssetPath(path));
199        }
200        self.entries.insert(path, asset.into());
201        Ok(())
202    }
203
204    /// Returns the bytes at `path`, or `None` if the path isn't registered.
205    ///
206    /// Preloaded entries are returned as [`Cow::Borrowed`] with no copy. An on-demand entry runs its
207    /// loader on each call, so the bytes may be read from disk again. An on-demand loader that
208    /// returns `None` makes this return `None` too.
209    pub fn load<'this>(&'this self, path: impl AsRef<str>) -> Option<Cow<'this, [u8]>> {
210        match self.entries.get(path.as_ref())? {
211            AssetEntry::PreLoaded(Cow::Borrowed(data)) => Some(Cow::Borrowed(data)),
212            AssetEntry::PreLoaded(Cow::Owned(data)) => Some(Cow::Borrowed(data.as_bytes())),
213            AssetEntry::OnDemand(loader) => loader(),
214        }
215    }
216
217    /// Adds each `(path, entry)` pair in order.
218    ///
219    /// # Errors
220    ///
221    /// Returns the [`DuplicateAssetPath`] of every pair whose path was already registered, in
222    /// order. The other pairs are still added, and each duplicate is logged.
223    pub fn extend<IterType, KeyType, EntryType>(
224        &mut self,
225        into_iter: IterType,
226    ) -> Result<(), Vec<DuplicateAssetPath>>
227    where
228        IterType: IntoIterator<Item = (KeyType, EntryType)>,
229        KeyType: AsRef<str>,
230        EntryType: Into<AssetEntry>,
231    {
232        let mut result = Ok(());
233        for (path_key, into_entry) in into_iter.into_iter() {
234            if let Err(err) = self.insert(path_key.as_ref(), into_entry.into()) {
235                gpui_util::log_err(&err);
236                result = Err({
237                    let mut errs: Vec<DuplicateAssetPath> = result.err().unwrap_or_default();
238                    errs.push(err);
239                    errs
240                });
241            }
242        }
243        result
244    }
245
246    /// Yields every file in a [`RustEmbed`](rust_embed::RustEmbed) folder as a preloaded entry.
247    ///
248    /// `T` is a type that derives `RustEmbed` for a folder. Each path is the file name relative to
249    /// that folder. Pass the iterator to [`AssetRegistry::extend`]. This needs the
250    /// `embedded-assets` feature of gpui, which also brings in the `rust-embed` crate.
251    #[cfg(feature = "embedded-assets")]
252    pub fn iter_embed<T: rust_embed::RustEmbed>()
253    -> impl Iterator<Item = (Cow<'static, str>, AssetEntry)> + 'static {
254        T::iter().filter_map(|path| {
255            let file = T::get(path.as_ref())?;
256            Some((path, AssetEntry::PreLoaded(file.data)))
257        })
258    }
259}
260
261/// Identifies a [`RenderImage`]. [`RenderImage::new`] gives each image a new ID, so two images
262/// never share one. The wrapped `usize` is the counter value that identifies the image.
263#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
264pub struct ImageId(pub usize);
265
266#[derive(PartialEq, Eq, Hash, Clone)]
267#[expect(missing_docs)]
268pub struct RenderImageParams {
269    pub image_id: ImageId,
270    pub frame_index: usize,
271}
272
273/// A decoded image: one or more frames of BGRA pixels, ready to draw.
274///
275/// Create one with [`RenderImage::new`]. Two images are equal when their [`ImageId`]s are equal.
276/// Still images have one frame. Animated images have one frame per step of the animation.
277pub struct RenderImage {
278    /// The ID that identifies this image. See [`ImageId`].
279    pub id: ImageId,
280    /// The scale factor of this image on render.
281    pub(crate) scale_factor: f32,
282    data: SmallVec<[Frame; 1]>,
283}
284
285impl PartialEq for RenderImage {
286    fn eq(&self, other: &Self) -> bool {
287        self.id == other.id
288    }
289}
290
291impl Eq for RenderImage {}
292
293impl RenderImage {
294    /// Creates an image from decoded frames, with a new [`ImageId`].
295    ///
296    /// The frames are moved in, not copied. Pass a `SmallVec<[Frame; 1]>`, or anything that converts
297    /// into one, such as a `Vec<Frame>`.
298    ///
299    /// # Examples
300    ///
301    /// ```
302    /// # use gpui::*;
303    /// use image::{Frame, ImageBuffer, Rgba};
304    ///
305    /// let pixels = ImageBuffer::from_pixel(2, 3, Rgba([255u8, 0, 0, 255]));
306    /// let image = RenderImage::new(vec![Frame::new(pixels)]);
307    ///
308    /// assert_eq!(image.frame_count(), 1);
309    /// assert_eq!(image.as_bytes(0).map(|bytes| bytes.len()), Some(2 * 3 * 4));
310    /// ```
311    pub fn new(data: impl Into<SmallVec<[Frame; 1]>>) -> Self {
312        static NEXT_ID: AtomicUsize = AtomicUsize::new(0);
313
314        Self {
315            id: ImageId(NEXT_ID.fetch_add(1, SeqCst)),
316            scale_factor: 1.0,
317            data: data.into(),
318        }
319    }
320
321    /// Returns the pixels of frame `frame_index` as BGRA bytes, four per pixel, with rows packed and
322    /// no padding.
323    ///
324    /// Returns `None` if there is no such frame.
325    pub fn as_bytes(&self, frame_index: usize) -> Option<&[u8]> {
326        self.data
327            .get(frame_index)
328            .map(|frame| frame.buffer().as_raw().as_slice())
329    }
330
331    /// Returns the size of frame `frame_index` in device pixels. Returns zero for a missing frame.
332    pub fn size(&self, frame_index: usize) -> Size<DevicePixels> {
333        self.data
334            .get(frame_index)
335            .map(|frame| {
336                let (width, height) = frame.buffer().dimensions();
337                size(width.into(), height.into())
338            })
339            .unwrap_or_default()
340    }
341
342    /// Get the size of this image, in pixels for display, adjusted for the scale factor.
343    pub(crate) fn render_size(&self, frame_index: usize) -> Size<Pixels> {
344        self.size(frame_index)
345            .map(|v| (v.0 as f32 / self.scale_factor).into())
346    }
347
348    /// Returns how long to show frame `frame_index` before the next one. Returns 100 ms for a
349    /// missing frame.
350    pub fn delay(&self, frame_index: usize) -> Delay {
351        self.data
352            .get(frame_index)
353            .map(|frame| frame.delay())
354            .unwrap_or(Delay::from_numer_denom_ms(100, 1))
355    }
356
357    /// Returns the number of frames. A still image has one. A GIF or animated WebP has one per frame.
358    pub fn frame_count(&self) -> usize {
359        self.data.len()
360    }
361}
362
363impl fmt::Debug for RenderImage {
364    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
365        f.debug_struct("ImageData")
366            .field("id", &self.id)
367            .field("size", &self.data.first().map(|f| f.buffer().dimensions()))
368            .finish()
369    }
370}
371
372#[cfg(test)]
373mod tests {
374    use super::*;
375    use smallvec::SmallVec;
376
377    #[test]
378    fn empty_render_image_does_not_panic() {
379        let image = RenderImage::new(SmallVec::new());
380        assert_eq!(image.frame_count(), 0);
381        assert_eq!(image.size(0), Size::default());
382        assert_eq!(image.as_bytes(0), None);
383        assert_eq!(image.render_size(0), Size::default());
384        assert_eq!(image.delay(0), Delay::from_numer_denom_ms(100, 1));
385        let _ = format!("{image:?}");
386    }
387
388    fn example_assets() -> Vec<(&'static str, AssetData)> {
389        vec![
390            ("asset0", Cow::Owned(vec![0u8])),
391            ("asset1", Cow::Owned(vec![1u8])),
392            ("asset2", Cow::Owned(vec![2u8])),
393            ("asset3", Cow::Owned(vec![3u8])),
394        ]
395    }
396
397    fn test_load_from(registry: &AssetRegistry) {
398        for (key, value) in example_assets() {
399            let loaded = registry.load(key).map(|cow| cow.to_vec());
400            assert_eq!(loaded, Some(value.to_vec()));
401        }
402    }
403
404    struct StubAssetSource;
405    impl AssetSource for StubAssetSource {
406        fn load(&self, path: &str) -> Result<Option<Cow<'static, [u8]>>> {
407            for (key, data) in example_assets() {
408                if key == path {
409                    return Ok(Some(data));
410                }
411            }
412            unimplemented!()
413        }
414
415        fn list(&self, _path: &str) -> Result<Vec<SharedString>> {
416            Ok(example_assets()
417                .into_iter()
418                .map(|(key, _)| key.into())
419                .collect())
420        }
421    }
422
423    #[test]
424    fn test_from_assetsource() {
425        let assets = AssetRegistry::from(StubAssetSource);
426        test_load_from(&assets);
427    }
428
429    #[test]
430    fn test_from_arc_assetsource() {
431        let arc: Arc<dyn AssetSource> = Arc::new(StubAssetSource);
432        let assets = AssetRegistry::from(arc);
433        test_load_from(&assets);
434    }
435
436    #[test]
437    fn test_fromiter_assetdata() {
438        let assets = AssetRegistry::from_iter(example_assets());
439        test_load_from(&assets);
440    }
441
442    #[test]
443    fn test_fromiter_assetentry() {
444        let assets = AssetRegistry::from_iter(
445            example_assets()
446                .into_iter()
447                .map(|(key, value)| (key, AssetEntry::from(value)))
448                .chain(std::iter::once((
449                    "asset6",
450                    AssetEntry::OnDemand(Arc::new(|| Some(AssetData::Owned(vec![6u8])))),
451                ))),
452        );
453        test_load_from(&assets);
454        assert_eq!(
455            assets.load("asset6").map(|cow| cow.to_vec()),
456            Some(vec![6u8])
457        );
458    }
459
460    #[test]
461    fn test_insert() {
462        let mut assets = AssetRegistry::default();
463        for (key, value) in example_assets() {
464            let result = assets.insert(key, value);
465            assert!(result.is_ok());
466        }
467        test_load_from(&assets);
468    }
469
470    #[test]
471    fn test_extend() {
472        let mut assets = AssetRegistry::default();
473        let result = assets.extend(
474            example_assets()
475                .into_iter()
476                .map(|(key, value)| (key, AssetEntry::PreLoaded(value))),
477        );
478        assert!(result.is_ok());
479        test_load_from(&assets);
480    }
481
482    #[test]
483    fn test_extend_assetsource_preloaded() {
484        let mut assets = AssetRegistry::default();
485        let result = assets.extend(StubAssetSource.iter_preloaded());
486        assert!(result.is_ok());
487        test_load_from(&assets);
488    }
489
490    #[test]
491    fn test_extend_assetsource_ondemand() {
492        let mut assets = AssetRegistry::default();
493        let result = assets.extend(Arc::new(StubAssetSource).iter_ondemand());
494        assert!(result.is_ok());
495        test_load_from(&assets);
496    }
497}