use proc_macro::TokenStream;
use proc_macro_crate::{FoundCrate, crate_name};
use quote::{format_ident, quote};
use syn::{
Attribute, Data, DeriveInput, Fields, Generics, Ident, Member, Path, Type, Visibility,
parse_macro_input, parse_quote,
};
#[proc_macro_derive(Animatable, attributes(animatable))]
pub fn derive_animatable(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
expand(input)
.unwrap_or_else(syn::Error::into_compile_error)
.into()
}
#[proc_macro]
pub fn field(input: TokenStream) -> TokenStream {
expand_field(input.into())
.unwrap_or_else(syn::Error::into_compile_error)
.into()
}
fn expand(input: DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
let path = crate_path();
let name = input.ident;
let visibility = input.vis;
let generated_fields_type = fields_name(&input.attrs, &name)?;
let Data::Struct(data) = input.data else {
return Err(syn::Error::new_spanned(
name,
"Animatable can only be derived for structs",
));
};
let descriptor_generics = input.generics.clone();
let mut interpolation_generics = input.generics;
let field_types = data
.fields
.iter()
.map(|field| field.ty.clone())
.collect::<Vec<_>>();
let where_clause = interpolation_generics.make_where_clause();
for field_type in &field_types {
where_clause
.predicates
.push(parse_quote!(#field_type: #path::Animatable));
}
let (impl_generics, type_generics, where_clause) = interpolation_generics.split_for_impl();
let interpolate_body = match &data.fields {
Fields::Named(fields) => {
let names = fields
.named
.iter()
.map(|field| field.ident.as_ref().unwrap())
.collect::<Vec<_>>();
quote! {
Self {
#(
#names: #path::Interpolate::interpolate_progress(
&from.#names,
&to.#names,
progress,
)
),*
}
}
}
Fields::Unnamed(fields) => {
let indexes = (0..fields.unnamed.len())
.map(syn::Index::from)
.collect::<Vec<_>>();
quote! {
Self(
#(
#path::Interpolate::interpolate_progress(
&from.#indexes,
&to.#indexes,
progress,
)
),*
)
}
}
Fields::Unit => quote!(Self),
};
let field_descriptors = expand_field_descriptors(
&path,
&name,
&visibility,
&descriptor_generics,
&data.fields,
&generated_fields_type,
);
let interpolate_impl = quote! {
impl #impl_generics #path::Interpolate for #name #type_generics #where_clause {
fn interpolate_progress(
from: &Self,
to: &Self,
progress: #path::InterpolationProgress,
) -> Self {
#interpolate_body
}
}
};
Ok(quote! {
#interpolate_impl
#field_descriptors
})
}
fn expand_field_descriptors(
path: &proc_macro2::TokenStream,
struct_name: &Ident,
visibility: &Visibility,
generics: &Generics,
fields: &Fields,
generated_type: &Ident,
) -> proc_macro2::TokenStream {
let (impl_generics, type_generics, where_clause) = generics.split_for_impl();
let struct_type = quote!(#struct_name #type_generics);
let descriptor_constants = match fields {
Fields::Named(fields) => fields
.named
.iter()
.map(|field| {
let field_visibility = &field.vis;
let member = field.ident.as_ref().expect("named field has an identifier");
let field_type = &field.ty;
quote! {
#[allow(non_upper_case_globals)]
#field_visibility const #member: #path::Field<#struct_type, #field_type> =
#path::Field::new(
stringify!(#member),
|value: &#struct_type| &value.#member,
|value: &mut #struct_type| &mut value.#member,
);
}
})
.collect::<Vec<_>>(),
Fields::Unnamed(fields) => fields
.unnamed
.iter()
.enumerate()
.map(|(index, field)| {
let field_visibility = &field.vis;
let descriptor_name = format_ident!("_{index}");
let field_index = syn::Index::from(index);
let field_type = &field.ty;
quote! {
#[allow(non_upper_case_globals)]
#field_visibility const #descriptor_name: #path::Field<#struct_type, #field_type> =
#path::Field::new(
stringify!(#field_index),
|value: &#struct_type| &value.#field_index,
|value: &mut #struct_type| &mut value.#field_index,
);
}
})
.collect::<Vec<_>>(),
Fields::Unit => Vec::new(),
};
quote! {
#[doc = concat!("Field descriptors generated for [`", stringify!(#struct_name), "`].")]
#visibility struct #generated_type #generics {
__aura_anim_marker: ::core::marker::PhantomData<fn() -> #struct_type>,
}
impl #impl_generics #generated_type #type_generics #where_clause
{
#(#descriptor_constants)*
}
}
}
fn fields_name(attributes: &[Attribute], struct_name: &Ident) -> syn::Result<Ident> {
let mut name = format_ident!("{struct_name}Fields");
let mut configured = false;
for attribute in attributes {
if !attribute.path().is_ident("animatable") {
continue;
}
attribute.parse_nested_meta(|meta| {
if !meta.path.is_ident("fields") {
return Err(meta.error("unsupported animatable option"));
}
if configured {
return Err(meta.error("field descriptor name was already configured"));
}
let path: Path = meta.value()?.parse()?;
let Some(identifier) = path.get_ident() else {
return Err(meta.error("field descriptor name must be a single identifier"));
};
name = identifier.clone();
configured = true;
Ok(())
})?;
}
Ok(name)
}
fn expand_field(input: proc_macro2::TokenStream) -> syn::Result<proc_macro2::TokenStream> {
let tokens = input.into_iter().collect::<Vec<_>>();
let separator = tokens
.windows(2)
.enumerate()
.filter(|(_, pair)| {
matches!(&pair[0], proc_macro2::TokenTree::Punct(punct) if punct.as_char() == ':')
&& matches!(&pair[1], proc_macro2::TokenTree::Punct(punct) if punct.as_char() == ':')
})
.map(|(index, _)| index)
.next_back()
.ok_or_else(|| {
syn::Error::new(
proc_macro2::Span::call_site(),
"expected a field path such as Position::x",
)
})?;
let field_type = syn::parse2::<Type>(tokens[..separator].iter().cloned().collect())?;
let member = syn::parse2::<Member>(tokens[separator + 2..].iter().cloned().collect())?;
let path = crate_path();
let name = match &member {
Member::Named(identifier) => identifier.to_string(),
Member::Unnamed(index) => index.index.to_string(),
};
Ok(quote! {
#path::Field::new(
#name,
|value: &#field_type| &value.#member,
|value: &mut #field_type| &mut value.#member,
)
})
}
fn crate_path() -> proc_macro2::TokenStream {
for package in ["aura-anim-core", "aura-anim"] {
if let Ok(found) = crate_name(package) {
return match found {
FoundCrate::Itself if package == "aura-anim-core" => quote!(crate),
FoundCrate::Itself => quote!(::aura_anim),
FoundCrate::Name(name) => {
let ident = format_ident!("{name}");
quote!(::#ident)
}
};
}
}
quote!(::aura_anim)
}