Skip to main content

motion_content/
asset.rs

1//! Typed resource access through the unified lazy owner.
2
3use crate::ImageAsset;
4#[cfg(feature = "text")]
5use motion_core::LoadContext;
6use motion_core::{Asset, Assets, Handle};
7#[cfg(test)]
8use std::sync::Arc;
9
10/// Typed loading through shared lazy handles.
11pub trait AssetsExt {
12    /// Loads one typed resource without reading or decoding it.
13    fn asset<A: Asset>(&self, path: &str) -> Handle<A>;
14
15    /// Loads or resolves an immutable image.
16    fn image(&self, path: &str) -> Handle<ImageAsset> {
17        self.asset(path)
18    }
19
20    /// Loads or resolves immutable SVG content.
21    #[cfg(feature = "svg")]
22    fn svg(&self, path: &str) -> Handle<crate::SvgAsset> {
23        self.asset(path)
24    }
25
26    /// Loads or resolves an immutable animation.
27    #[cfg(feature = "image")]
28    fn animated_image(&self, path: &str) -> Handle<crate::AnimatedImageAsset> {
29        self.asset(path)
30    }
31
32    /// Loads or resolves one explicit font face.
33    #[cfg(feature = "text")]
34    fn font(&self, path: &str) -> Handle<crate::FontFace> {
35        self.asset(path)
36    }
37}
38
39impl AssetsExt for Assets {
40    fn asset<A: Asset>(&self, path: &str) -> Handle<A> {
41        self.load(path)
42    }
43}
44
45#[cfg(feature = "text")]
46impl Asset for crate::FontFace {
47    const KIND: &'static str = "font";
48
49    fn decode(bytes: &[u8], _: &LoadContext) -> motion_core::Result<Self> {
50        Ok(Self::from_bytes(bytes.to_vec(), 0)?)
51    }
52
53    fn retained_bytes(&self) -> usize {
54        self.byte_len()
55    }
56}
57
58#[cfg(test)]
59mod tests {
60    use super::*;
61
62    #[cfg(feature = "svg")]
63    #[test]
64    fn source_drawables_use_the_same_owner_as_the_scene_callback() -> motion_core::Result {
65        use motion_core::{Node, NodeModifiers, Size, Source};
66        use motion_render::{Composition, RenderContext};
67        let assets = Assets::new(".")?;
68        assets.preload(
69            "logo.png",
70            Arc::new(ImageAsset::from_rgba8(2, 2, vec![255; 16])?),
71        )?;
72        let svg = assets.load::<crate::SvgAsset>(Source::Bytes {
73            key: "parsed-svg".into(),
74            data: Arc::from(&br##"<svg xmlns="http://www.w3.org/2000/svg" width="2" height="2"><rect width="2" height="2" fill="#00ff00"/></svg>"##[..]),
75        }).get()?;
76        assets.preload("diagram.svg", svg)?;
77        let composition = Composition::new(
78            8,
79            4,
80            motion_render::FrameClock::new(motion_core::fps!(30), 150),
81        )
82        .with_options(
83            motion_render::RenderOptions::default()
84                .context(RenderContext::new().with_assets(assets.clone())),
85        )?;
86        let frame = composition.render_frame(0, Size::new(8.0, 4.0), |context| {
87            assert!(context.assets().store().same_store(assets.store()));
88            for node in [
89                Node::from(crate::Image::open("logo.png")),
90                Node::from(crate::Svg::open("diagram.svg")),
91            ] {
92                assert_eq!(
93                    context.measure(&node).unwrap(),
94                    motion_core::Rect::new(0.0, 0.0, 2.0, 2.0)
95                );
96            }
97            Node::group((
98                crate::Image::open("logo.png"),
99                crate::Svg::open("diagram.svg").translate(4.0, 0.0),
100            ))
101        })?;
102        assert_eq!(frame.packed_rgb()[0], 0xffffff);
103        assert_eq!(frame.packed_rgb()[4], 0x00ff00);
104        let override_assets = Assets::new(".")?;
105        override_assets.preload(
106            "logo.png",
107            Arc::new(ImageAsset::from_rgba8(1, 1, vec![255, 0, 0, 255])?),
108        )?;
109        let frame = composition
110            .configure(|options| options.resources(override_assets.clone()))
111            .render_frame(0, Size::new(8.0, 4.0), |context| {
112                assert!(context.assets().store().same_store(override_assets.store()));
113                crate::Image::open("logo.png").into()
114            })?;
115        assert_eq!(frame.packed_rgb()[0], 0xff0000);
116        Ok(())
117    }
118
119    #[test]
120    fn source_image_missing_files_follow_strict_and_placeholder_policies() -> motion_core::Result {
121        use motion_core::Size;
122        use motion_render::Composition;
123        let directory = tempfile::tempdir()?;
124        let assets = Assets::new(directory.path())?;
125        let composition = Composition::new(
126            2,
127            2,
128            motion_render::FrameClock::new(motion_core::fps!(30), 150),
129        )
130        .with_options(motion_render::RenderOptions::default().resources(assets))?;
131        let build = |_: &motion_core::SceneContext<'_>| {
132            crate::Image::open("absent.png").size(2.0, 2.0).into()
133        };
134        let error = composition
135            .render_frame(0, Size::new(2.0, 2.0), build)
136            .err()
137            .unwrap();
138        assert_eq!(error.kind(), motion_core::ErrorKind::Asset);
139        assert_eq!(error.frame(), Some(0));
140        let frame = composition
141            .configure(|options| options.allow_missing_assets(true))
142            .render_frame(0, Size::new(2.0, 2.0), build)?;
143        assert_eq!(frame.packed_rgb()[0], 0xff00ff);
144        Ok(())
145    }
146
147    #[cfg(feature = "text")]
148    #[test]
149    fn invalid_font_does_not_change_resources() {
150        let assets = Assets::default();
151        assert!(crate::FontFace::from_bytes(Vec::new(), 0).is_err());
152        assert_eq!(assets.store().retained_bytes(), 0);
153    }
154
155    #[test]
156    fn identifier_limit_bounds_tiny_shared_assets() -> motion_core::Result {
157        let pixel = Arc::new(ImageAsset::from_rgba8(1, 1, vec![255; 4])?);
158        let bytes = pixel.retained_bytes();
159        let assets = Assets::new(".")?;
160        for index in 0..4096 {
161            assets.preload(format!("pixel-{index}"), pixel.clone())?;
162        }
163        assert!(assets.preload("overflow", pixel).is_err());
164        assert_eq!(assets.store().retained_bytes(), 4096 * bytes);
165        Ok(())
166    }
167
168    #[test]
169    fn preloads_respect_allowance_and_snapshot_lifetimes() -> motion_core::Result {
170        let image = Arc::new(ImageAsset::from_rgba8(2, 2, vec![255; 16])?);
171        let bytes = image.retained_bytes();
172        let pool = motion_core::ResourceStore::with_limits(motion_core::ResourceLimits {
173            max_entries: 4096,
174            max_bytes: bytes,
175        })?;
176        let assets = Assets::new(".")?.with_store(pool.clone());
177        assets.preload("sun", image.clone())?;
178        assert_eq!(pool.retained_bytes(), bytes);
179        assert!(assets.preload("sun", image.clone()).is_err());
180        assert!(assets.preload("moon", image.clone()).is_err());
181        assert!(Arc::ptr_eq(&image, &assets.image("sun").get()?));
182        let snapshot = assets.snapshot();
183        assert!(assets.remove::<ImageAsset>("sun"));
184        assert_eq!(pool.retained_bytes(), bytes);
185        assert!(Arc::ptr_eq(&image, &snapshot.image("sun").get()?));
186        drop(snapshot);
187        assert_eq!(pool.retained_bytes(), 0);
188        assert!(assets.remove::<ImageAsset>("moon"));
189        assets.preload("moon", image)?;
190        Ok(())
191    }
192}