use crate::ImageAsset;
#[cfg(feature = "text")]
use motion_core::LoadContext;
use motion_core::{Asset, Assets, Handle};
#[cfg(test)]
use std::sync::Arc;
pub trait AssetsExt {
fn asset<A: Asset>(&self, path: &str) -> Handle<A>;
fn image(&self, path: &str) -> Handle<ImageAsset> {
self.asset(path)
}
#[cfg(feature = "svg")]
fn svg(&self, path: &str) -> Handle<crate::SvgAsset> {
self.asset(path)
}
#[cfg(feature = "image")]
fn animated_image(&self, path: &str) -> Handle<crate::AnimatedImageAsset> {
self.asset(path)
}
#[cfg(feature = "text")]
fn font(&self, path: &str) -> Handle<crate::FontFace> {
self.asset(path)
}
}
impl AssetsExt for Assets {
fn asset<A: Asset>(&self, path: &str) -> Handle<A> {
self.load(path)
}
}
#[cfg(feature = "text")]
impl Asset for crate::FontFace {
const KIND: &'static str = "font";
fn decode(bytes: &[u8], _: &LoadContext) -> motion_core::Result<Self> {
Ok(Self::from_bytes(bytes.to_vec(), 0)?)
}
fn retained_bytes(&self) -> usize {
self.byte_len()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(feature = "svg")]
#[test]
fn source_drawables_use_the_same_owner_as_the_scene_callback() -> motion_core::Result {
use motion_core::{Node, NodeModifiers, Size, Source};
use motion_render::{Composition, RenderContext};
let assets = Assets::new(".")?;
assets.preload(
"logo.png",
Arc::new(ImageAsset::from_rgba8(2, 2, vec![255; 16])?),
)?;
let svg = assets.load::<crate::SvgAsset>(Source::Bytes {
key: "parsed-svg".into(),
data: Arc::from(&br##"<svg xmlns="http://www.w3.org/2000/svg" width="2" height="2"><rect width="2" height="2" fill="#00ff00"/></svg>"##[..]),
}).get()?;
assets.preload("diagram.svg", svg)?;
let composition =
Composition::new(8, 4).render_context(RenderContext::new().with_assets(assets.clone()));
let frame = composition.render_frame(0, Size::new(8.0, 4.0), |context| {
assert!(context.assets().pool().same_pool(assets.pool()));
for node in [
Node::from(crate::Image::open("logo.png")),
Node::from(crate::Svg::open("diagram.svg")),
] {
assert_eq!(
context.measure(&node).unwrap(),
motion_core::Rect::new(0.0, 0.0, 2.0, 2.0)
);
}
Node::group((
crate::Image::open("logo.png"),
crate::Svg::open("diagram.svg").translate(4.0, 0.0),
))
})?;
assert_eq!(frame.pixels()[0], 0xffffff);
assert_eq!(frame.pixels()[4], 0x00ff00);
let override_assets = Assets::new(".")?;
override_assets.preload(
"logo.png",
Arc::new(ImageAsset::from_rgba8(1, 1, vec![255, 0, 0, 255])?),
)?;
let frame = composition
.resources(override_assets.clone())
.render_frame(0, Size::new(8.0, 4.0), |context| {
assert!(context.assets().pool().same_pool(override_assets.pool()));
crate::Image::open("logo.png").into()
})?;
assert_eq!(frame.pixels()[0], 0xff0000);
Ok(())
}
#[test]
fn source_image_missing_files_follow_strict_and_placeholder_policies() -> motion_core::Result {
use motion_core::Size;
use motion_render::Composition;
let directory = tempfile::tempdir()?;
let assets = Assets::new(directory.path())?;
let composition = Composition::new(2, 2).resources(assets);
let build = |_: &motion_core::SceneContext<'_>| {
crate::Image::open("absent.png").size(2.0, 2.0).into()
};
let error = composition
.render_frame(0, Size::new(2.0, 2.0), build)
.err()
.unwrap();
assert_eq!(error.kind(), motion_core::ErrorKind::Asset);
assert_eq!(error.frame(), Some(0));
let frame =
composition
.allow_missing_assets(true)
.render_frame(0, Size::new(2.0, 2.0), build)?;
assert_eq!(frame.pixels()[0], 0xff00ff);
Ok(())
}
#[cfg(feature = "text")]
#[test]
fn invalid_font_does_not_change_resources() {
let assets = Assets::default();
assert!(crate::FontFace::from_bytes(Vec::new(), 0).is_err());
assert_eq!(assets.pool().retained_bytes(), 0);
}
#[test]
fn identifier_limit_bounds_tiny_shared_assets() -> motion_core::Result {
let pixel = Arc::new(ImageAsset::from_rgba8(1, 1, vec![255; 4])?);
let bytes = pixel.retained_bytes();
let assets = Assets::new(".")?;
for index in 0..4096 {
assets.preload(format!("pixel-{index}"), pixel.clone())?;
}
assert!(assets.preload("overflow", pixel).is_err());
assert_eq!(assets.pool().retained_bytes(), 4096 * bytes);
Ok(())
}
#[test]
fn preloads_respect_allowance_and_snapshot_lifetimes() -> motion_core::Result {
let image = Arc::new(ImageAsset::from_rgba8(2, 2, vec![255; 16])?);
let bytes = image.retained_bytes();
let pool = motion_core::ResourcePool::with_limits(motion_core::CacheLimits {
entries: 4096,
bytes,
})?;
let assets = Assets::new(".")?.with_pool(pool.clone());
assets.preload("sun", image.clone())?;
assert_eq!(pool.retained_bytes(), bytes);
assert!(assets.preload("sun", image.clone()).is_err());
assert!(assets.preload("moon", image.clone()).is_err());
assert!(Arc::ptr_eq(&image, &assets.image("sun").get()?));
let snapshot = assets.snapshot();
assert!(assets.remove::<ImageAsset>("sun"));
assert_eq!(pool.retained_bytes(), bytes);
assert!(Arc::ptr_eq(&image, &snapshot.image("sun").get()?));
drop(snapshot);
assert_eq!(pool.retained_bytes(), 0);
assert!(assets.remove::<ImageAsset>("moon"));
assets.preload("moon", image)?;
Ok(())
}
}