use super::*;
use motion_core::{Canvas, DrawContext, Drawable, Node, NodeModifiers, ValidationError};
type Selection = Arc<dyn Fn(Node) -> Node + Send + Sync>;
#[derive(Clone)]
enum Input {
Source(motion_core::Source),
Lazy(motion_core::Handle<SvgAsset>),
Loaded(Arc<SvgAsset>),
}
#[derive(Clone)]
pub struct Svg {
input: Input,
size: Option<Size>,
progress: Option<f64>,
selections: Vec<(Arc<str>, Selection)>,
}
impl Svg {
pub fn open(source: impl Into<motion_core::Source>) -> Self {
Self {
input: Input::Source(source.into()),
size: None,
progress: None,
selections: Vec::new(),
}
}
pub fn new(handle: motion_core::Handle<SvgAsset>) -> Self {
Self {
input: Input::Lazy(handle),
size: None,
progress: None,
selections: Vec::new(),
}
}
pub fn from_asset(asset: Arc<SvgAsset>) -> Self {
Self {
input: Input::Loaded(asset),
size: None,
progress: None,
selections: Vec::new(),
}
}
pub fn fit(mut self, size: Size) -> Self {
self.size = Some(size);
self
}
pub fn write(mut self, progress: f64) -> Self {
self.progress = Some(progress);
self
}
pub fn select(
mut self,
id: impl Into<Arc<str>>,
callback: impl Fn(Node) -> Node + Send + Sync + 'static,
) -> Self {
let id = id.into();
self.selections.push((
Arc::from(id.strip_prefix('#').unwrap_or(&id)),
Arc::new(callback),
));
self
}
fn asset(&self, context: &DrawContext) -> motion_core::Result<Arc<SvgAsset>> {
match &self.input {
Input::Source(source) => context
.assets()
.load::<SvgAsset>(source.clone())
.get_with(context.diagnostics()),
Input::Lazy(handle) => handle.get_with(context.diagnostics()),
Input::Loaded(asset) => Ok(asset.clone()),
}
}
pub fn vector_tree(&self) -> motion_core::Result<Arc<VectorTree>> {
Ok(self.asset(&DrawContext::default())?.tree.clone())
}
pub fn try_node(&self) -> motion_core::Result<Node> {
self.node_with(&DrawContext::default())
}
fn node_with(&self, context: &DrawContext) -> motion_core::Result<Node> {
let asset = self.asset(context)?;
let size = self.size.unwrap_or(asset.size());
if !size.width.is_finite()
|| !size.height.is_finite()
|| size.width <= 0.0
|| size.height <= 0.0
|| self.selections.len() > 256
{
return Err(unsupported(
"SVG fit requires positive finite size; at most 256 selections",
));
}
for (id, _) in &self.selections {
if !asset.tree.ids().contains_key(id.as_ref()) {
return Err(unsupported(format!("SVG selection ID not found: {id}")));
}
}
let mut selected = std::collections::BTreeMap::<Vec<usize>, Vec<&Selection>>::new();
for (id, callback) in &self.selections {
selected
.entry(asset.tree.ids()[id.as_ref()].clone())
.or_default()
.push(callback);
}
let map = |address: &[usize], mut node: Node| {
if let Some(callbacks) = selected.get(address) {
for callback in callbacks {
node = callback(node);
}
}
node
};
let mut node = if let Some(progress) = self.progress {
asset
.tree
.write_mapped(progress, crate::WriteOptions::default(), &map)?
} else {
asset.tree.node_mapped(&map)
};
let scale = (size.width / asset.size().width).min(size.height / asset.size().height);
let transform = Transform::scale(scale).then(Transform::translate(
(size.width - asset.size().width * scale) * 0.5,
(size.height - asset.size().height * scale) * 0.5,
));
node = node
.clip_with(asset.size().to_rect(), motion_core::ClipMode::Geometric)
.transformed(transform)
.clip_with(size.to_rect(), motion_core::ClipMode::Geometric);
Ok(Node::group([node, Node::from(Viewport(size.to_rect()))])
.pivot(motion_core::Pivot::Point(size.to_rect().center())))
}
}
#[derive(Clone)]
struct Viewport(Rect);
impl Drawable for Viewport {
fn bounds(&self, _: &DrawContext) -> Rect {
self.0
}
fn draw(&self, _: &DrawContext, _: &mut Canvas<'_>) -> Result<(), ValidationError> {
Ok(())
}
}
#[derive(Clone)]
struct FailedResource(Svg);
impl Drawable for FailedResource {
fn bounds(&self, context: &DrawContext) -> Rect {
let _ = self.0.asset(context);
Rect::new(
0.0,
0.0,
self.0.size.map_or(64.0, |size| size.width),
self.0.size.map_or(64.0, |size| size.height),
)
}
fn draw(&self, context: &DrawContext, canvas: &mut Canvas<'_>) -> Result<(), ValidationError> {
let _ = self.0.asset(context);
canvas.fill(
&self.bounds(context),
Color::rgb(255, 0, 255),
motion_core::FillRule::NonZero,
)
}
}
#[derive(Clone)]
struct DeferredSvg(Svg);
impl Drawable for DeferredSvg {
fn bounds(&self, _: &DrawContext) -> Rect {
Rect::default()
}
fn draw(&self, _: &DrawContext, _: &mut Canvas<'_>) -> Result<(), ValidationError> {
Err(ValidationError::invalid(
"svg",
"resolution",
"SVG must resolve before drawing",
))
}
fn resolve_node(&self, context: &DrawContext) -> Result<Option<Node>, ValidationError> {
match self.0.node_with(context) {
Ok(node) => Ok(Some(node)),
Err(error) => {
if error.kind() == motion_core::ErrorKind::Asset {
Ok(Some(Node::from(FailedResource(self.0.clone()))))
} else {
Err(ValidationError::invalid(
"svg",
"content",
error.to_string(),
))
}
}
}
}
}
impl From<Svg> for Node {
fn from(svg: Svg) -> Node {
Node::from(DeferredSvg(svg))
}
}