From aec768617ac73b404a9b77a631d24eb5d96b6875 Mon Sep 17 00:00:00 2001 From: Roland Fredenhagen Date: Tue, 14 Nov 2023 20:03:30 +0100 Subject: [PATCH] derive(LinearInterpolate) on enum --- Cargo.lock | 33 ++++- Cargo.toml | 1 + gooey-macros/Cargo.toml | 2 +- gooey-macros/src/animation.rs | 120 ++++++++++++------ gooey-macros/src/lib.rs | 22 ++++ ...y_macros__animation__test__empty_enum.snap | 23 ++++ .../gooey_macros__animation__test__enum_.snap | 28 ++++ ...oey_macros__animation__test__struct_.snap} | 6 +- ...acros__animation__test__tuple_struct.snap} | 4 +- src/animation.rs | 42 +++++- 10 files changed, 233 insertions(+), 48 deletions(-) create mode 100644 gooey-macros/src/snapshots/gooey_macros__animation__test__empty_enum.snap create mode 100644 gooey-macros/src/snapshots/gooey_macros__animation__test__enum_.snap rename gooey-macros/src/snapshots/{gooey_macros__animation__test.snap => gooey_macros__animation__test__struct_.snap} (74%) rename gooey-macros/src/snapshots/{gooey_macros__animation__test-2.snap => gooey_macros__animation__test__tuple_struct.snap} (79%) diff --git a/Cargo.lock b/Cargo.lock index 6fff768..1a64d5f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -473,6 +473,26 @@ dependencies = [ "syn", ] +[[package]] +name = "derive_more" +version = "1.0.0-beta.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7abbfc297053be59290e3152f8cbcd52c8642e0728b69ee187d991d4c1af08d" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "1.0.0-beta.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bba3e9872d7c58ce7ef0fcf1844fcc3e23ef2a58377b50df35dd98e42a5726e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "dispatch" version = "0.2.0" @@ -721,6 +741,7 @@ version = "0.1.0" dependencies = [ "ahash", "alot", + "derive_more", "gooey-macros", "intentional", "interner", @@ -738,7 +759,7 @@ version = "0.1.0" dependencies = [ "attribute-derive", "insta", - "manyhow 0.9.0", + "manyhow 0.10.0", "prettyplease", "proc-macro2", "quote", @@ -1118,11 +1139,11 @@ dependencies = [ [[package]] name = "manyhow" -version = "0.9.0" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9aebef87880bafc898c6bed1435e8fdc58634275ff97693a4bb96ad561c73c43" +checksum = "4efde575f79afb9c637eb4663aa451f0bf227413aa734fbbec077cab5900be85" dependencies = [ - "manyhow-macros 0.9.0", + "manyhow-macros 0.10.0", "proc-macro2", "quote", "syn", @@ -1141,9 +1162,9 @@ dependencies = [ [[package]] name = "manyhow-macros" -version = "0.9.0" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f74cc8a0d8b05a7e919011c78a2744e7dea66567c05fb046666f3bae383d8d04" +checksum = "fcee04599474650eb26ae5a5c7837e30e55242267ff1bf0adc760b6fcdc3fa2a" dependencies = [ "proc-macro-utils", "proc-macro2", diff --git a/Cargo.toml b/Cargo.toml index f8dbfa8..6b642d1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,6 +25,7 @@ tracing-subscriber = { version = "0.3", optional = true, features = [ palette = "0.7.3" ahash = "0.8.6" gooey-macros = { version = "0.1.0", path = "gooey-macros" } +derive_more = { version = "1.0.0-beta.6", features = ["from"] } # [patch."https://github.com/khonsulabs/kludgine"] diff --git a/gooey-macros/Cargo.toml b/gooey-macros/Cargo.toml index a38fa69..e9c650d 100644 --- a/gooey-macros/Cargo.toml +++ b/gooey-macros/Cargo.toml @@ -10,7 +10,7 @@ proc-macro = true [dependencies] attribute-derive = "0.8.1" -manyhow = "0.9.0" +manyhow = "0.10.0" proc-macro2 = "1.0.69" quote = "1.0.33" quote-use = "0.7.2" diff --git a/gooey-macros/src/animation.rs b/gooey-macros/src/animation.rs index 2ef277a..15a7a12 100644 --- a/gooey-macros/src/animation.rs +++ b/gooey-macros/src/animation.rs @@ -1,66 +1,114 @@ -use manyhow::bail; +use manyhow::{bail, ensure}; use quote::ToTokens; -use syn::{Field, ItemStruct}; +use syn::{Data, DeriveInput, Field, Variant}; use crate::*; pub fn linear_interpolate( - ItemStruct { - ident, + DeriveInput { + ident: item_ident, generics, - fields, + data, .. - }: ItemStruct, + }: DeriveInput, ) -> Result { if let Some(generic) = generics.type_params().next() { bail!(generic, "generics not supported"); } - let fields = match fields { - syn::Fields::Unit => bail!(ident, "unit structs are not supported"), - fields => fields - .into_iter() - .enumerate() - .map(|(idx, Field { ident, .. })| { - let ident = ident - .map(ToTokens::into_token_stream) - .unwrap_or_else(|| proc_macro2::Literal::usize_unsuffixed(idx).into_token_stream()); - quote!(#ident: ::gooey::animation::LinearInterpolate::lerp(&self.#ident, &__target.#ident, __percent),) - }), + let doc; + + let body = match data { + Data::Struct(data) => { + let fields = match data.fields { + syn::Fields::Unit => bail!(item_ident, "unit structs are not supported"), + fields => fields + .into_iter() + .enumerate() + .map(|(idx, Field { ident, .. })| { + let ident = ident + .map(ToTokens::into_token_stream) + .unwrap_or_else(|| proc_macro2::Literal::usize_unsuffixed(idx).into_token_stream()); + quote!(#ident: ::gooey::animation::LinearInterpolate::lerp(&self.#ident, &__target.#ident, __percent),) + }), + }; + doc = "# Panics\n Panics if any field's lerp panics (this should only happen on percentages outside 0..1 range)."; + quote!(#item_ident{#(#fields)*}) + } + Data::Enum(data) => { + let variants = data + .variants + .into_iter() + .map( + |Variant { + ident, + fields, + discriminant, + .. + }| { + if let Some(discriminant) = discriminant { + bail!(discriminant, "discriminants are not supported"); + } + ensure!(fields.is_empty(), fields, "enum fields are not supported"); + Ok(quote!(#item_ident::#ident #fields)) + }, + ) + .collect::>>()?; + let last = variants + .last() + .map(ToTokens::to_token_stream) + .unwrap_or_else(|| quote!(unreachable!())); + + let idx: Vec<_> = (0..variants.len()).collect(); + doc = "# Panics\n Panics if the the enum variants are overflown (this can only happen on percentages outside 0..1 range)."; + quote! { + # use ::gooey::animation::LinearInterpolate; + fn variant_to_index(__v: &#item_ident) -> usize { + match __v { + #(#variants => #idx,)* + } + } + let __self = variant_to_index(&self); + let __target = variant_to_index(&__target); + match LinearInterpolate::lerp(&__self, &__target, __percent) { + #(#idx => #variants,)* + _ => #last, + } + } + } + Data::Union(union) => bail!((union.union_token, union.fields), "unions not supported"), }; Ok(quote! { - impl ::gooey::animation::LinearInterpolate for #ident { + impl ::gooey::animation::LinearInterpolate for #item_ident { + #[doc = #doc] fn lerp(&self, __target: &Self, __percent: f32) -> Self { - #ident{#(#fields)*} + #body } } }) } #[cfg(test)] -macro_rules! expansion_snapshot { - (#[derive($fn:expr)]$($tokens:tt)*) => {{ - use insta::assert_snapshot; - use prettyplease::unparse; - use syn::{parse2, parse_quote}; - let input = parse_quote!($($tokens)*); - let output = $fn(input).unwrap(); - assert_snapshot!(unparse(&parse2(output).unwrap())) - }}; -} - -#[test] -fn test() { - expansion_snapshot! { +mod test { + use super::*; + expansion_snapshot! {struct_ #[derive(linear_interpolate)] struct HelloWorld { fielda: Hello, fieldb: World, } - }; - expansion_snapshot! { + } + expansion_snapshot! {tuple_struct #[derive(linear_interpolate)] struct HelloWorld(Hello, World); - }; + } + expansion_snapshot! {enum_ + #[derive(linear_interpolate)] + enum Enum{A, B} + } + expansion_snapshot! {empty_enum + #[derive(linear_interpolate)] + enum Enum{} + } } diff --git a/gooey-macros/src/lib.rs b/gooey-macros/src/lib.rs index 5780e60..2c793ce 100644 --- a/gooey-macros/src/lib.rs +++ b/gooey-macros/src/lib.rs @@ -1,6 +1,28 @@ use manyhow::{manyhow, Result}; use proc_macro2::TokenStream; use quote_use::quote_use as quote; + +#[cfg(test)] +macro_rules! expansion_snapshot { + ($name:ident $($tokens:tt)*) => { + #[test] + fn $name() { + expansion_snapshot!{$($tokens)*} + } + }; + (#[derive($fn:expr)]$($tokens:tt)*) => {{ + use insta::assert_snapshot; + use prettyplease::unparse; + use syn::{parse2, parse_quote}; + let input = parse_quote!($($tokens)*); + let output = $fn(input).unwrap(); + match &parse2(output.clone()) { + Ok(ok) => assert_snapshot!(unparse(ok)), + Err(_) => panic!("{output}"), + } + }}; +} + mod animation; #[manyhow(proc_macro_derive(LinearInterpolate))] diff --git a/gooey-macros/src/snapshots/gooey_macros__animation__test__empty_enum.snap b/gooey-macros/src/snapshots/gooey_macros__animation__test__empty_enum.snap new file mode 100644 index 0000000..792d903 --- /dev/null +++ b/gooey-macros/src/snapshots/gooey_macros__animation__test__empty_enum.snap @@ -0,0 +1,23 @@ +--- +source: gooey-macros/src/animation.rs +expression: unparse(ok) +--- +impl ::gooey::animation::LinearInterpolate for Enum { + /**# Panics + Panics if the the enum variants are overflown (this can only happen on percentages outside 0..1 range).*/ + fn lerp(&self, __target: &Self, __percent: f32) -> Self { + fn variant_to_index(__v: &Enum) -> usize { + match __v {} + } + let __self = variant_to_index(&self); + let __target = variant_to_index(&self); + match ::gooey::animation::LinearInterpolate::lerp( + &__self, + &__target, + __percent, + ) { + _ => unreachable!(), + } + } +} + diff --git a/gooey-macros/src/snapshots/gooey_macros__animation__test__enum_.snap b/gooey-macros/src/snapshots/gooey_macros__animation__test__enum_.snap new file mode 100644 index 0000000..7076744 --- /dev/null +++ b/gooey-macros/src/snapshots/gooey_macros__animation__test__enum_.snap @@ -0,0 +1,28 @@ +--- +source: gooey-macros/src/animation.rs +expression: unparse(ok) +--- +impl ::gooey::animation::LinearInterpolate for Enum { + /**# Panics + Panics if the the enum variants are overflown (this can only happen on percentages outside 0..1 range).*/ + fn lerp(&self, __target: &Self, __percent: f32) -> Self { + fn variant_to_index(__v: &Enum) -> usize { + match __v { + Enum::A => 0usize, + Enum::B => 1usize, + } + } + let __self = variant_to_index(&self); + let __target = variant_to_index(&self); + match ::gooey::animation::LinearInterpolate::lerp( + &__self, + &__target, + __percent, + ) { + 0usize => Enum::A, + 1usize => Enum::B, + _ => Enum::B, + } + } +} + diff --git a/gooey-macros/src/snapshots/gooey_macros__animation__test.snap b/gooey-macros/src/snapshots/gooey_macros__animation__test__struct_.snap similarity index 74% rename from gooey-macros/src/snapshots/gooey_macros__animation__test.snap rename to gooey-macros/src/snapshots/gooey_macros__animation__test__struct_.snap index 8a133d1..f32fcd0 100644 --- a/gooey-macros/src/snapshots/gooey_macros__animation__test.snap +++ b/gooey-macros/src/snapshots/gooey_macros__animation__test__struct_.snap @@ -1,8 +1,10 @@ --- -source: src/animation.rs -expression: unparse(&parse2(output).unwrap()) +source: gooey-macros/src/animation.rs +expression: unparse(ok) --- impl ::gooey::animation::LinearInterpolate for HelloWorld { + /**# Panics + Panics if any field's lerp panics (this should only happen on percentages outside 0..1 range).*/ fn lerp(&self, __target: &Self, __percent: f32) -> Self { HelloWorld { fielda: ::gooey::animation::LinearInterpolate::lerp( diff --git a/gooey-macros/src/snapshots/gooey_macros__animation__test-2.snap b/gooey-macros/src/snapshots/gooey_macros__animation__test__tuple_struct.snap similarity index 79% rename from gooey-macros/src/snapshots/gooey_macros__animation__test-2.snap rename to gooey-macros/src/snapshots/gooey_macros__animation__test__tuple_struct.snap index 117910f..6152743 100644 --- a/gooey-macros/src/snapshots/gooey_macros__animation__test-2.snap +++ b/gooey-macros/src/snapshots/gooey_macros__animation__test__tuple_struct.snap @@ -1,8 +1,10 @@ --- source: gooey-macros/src/animation.rs -expression: unparse(&parse2(output).unwrap()) +expression: unparse(ok) --- impl ::gooey::animation::LinearInterpolate for HelloWorld { + /**# Panics + Panics if any field's lerp panics (this should only happen on percentages outside 0..1 range).*/ fn lerp(&self, __target: &Self, __percent: f32) -> Self { HelloWorld { 0: ::gooey::animation::LinearInterpolate::lerp( diff --git a/src/animation.rs b/src/animation.rs index 30cc1c0..4c20f57 100644 --- a/src/animation.rs +++ b/src/animation.rs @@ -48,6 +48,7 @@ use std::thread; use std::time::{Duration, Instant}; use alot::{LotId, Lots}; +use derive_more::From; use intentional::Cast; use kempt::Set; use kludgine::figures::Ranged; @@ -607,12 +608,49 @@ impl Animate for Duration { } /// Performs a linear interpolation between two values. +/// +/// This trait can be derived for structs and fieldless enums. +/// +/// Note: for fields that don't implement [`LinerarInterpolate`](trait@LinearInterpolate) +/// the wrappers [`BinaryLerp`] and [`ImmediateLerp`] can be used. +/// +/// ``` +/// use gooey::animation::{BinaryLerp, ImmediateLerp, LinearInterpolate}; +/// use gooey::kludgine::Color; +/// +/// #[derive(LinearInterpolate, PartialEq, Debug)] +/// struct Struct(Color, BinaryLerp<&'static str>, ImmediateLerp<&'static str>); +/// +/// let from = Struct(Color::BLACK, "hello".into(), "hello".into()); +/// let to = Struct(Color::WHITE, "world".into(), "world".into()); +/// +/// assert_eq!( +/// from.lerp(&to, 0.41), +/// Struct(Color::DIMGRAY, "hello".into(), "world".into()) +/// ); +/// assert_eq!( +/// from.lerp(&to, 0.663), +/// Struct(Color::DARKGRAY, "world".into(), "world".into()) +/// ); +/// +/// #[derive(LinearInterpolate, PartialEq, Debug)] +/// enum Enum { +/// A, +/// B, +/// C, +/// } +/// assert_eq!(Enum::A.lerp(&Enum::B, 0.4), Enum::A); +/// assert_eq!(Enum::A.lerp(&Enum::C, 0.1), Enum::A); +/// assert_eq!(Enum::A.lerp(&Enum::C, 0.4), Enum::B); +/// assert_eq!(Enum::A.lerp(&Enum::C, 0.9), Enum::C); +/// ``` pub trait LinearInterpolate: PartialEq { /// Interpolate linearly between `self` and `target` using `percent`. #[must_use] fn lerp(&self, target: &Self, percent: f32) -> Self; } +/// Derives [`LinerarInterpolate`](trait@LinearInterpolate) for structs and fieldless enums. pub use gooey_macros::LinearInterpolate; macro_rules! impl_lerp_for_int { @@ -732,7 +770,7 @@ impl LinearInterpolate for Color { /// /// This wrapper can be used to add [`LinearInterpolate`] to types that normally /// don't support interpolation. -#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Ord, PartialOrd)] +#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Ord, PartialOrd, From)] pub struct BinaryLerp(T); impl LinearInterpolate for BinaryLerp @@ -753,7 +791,7 @@ where /// /// This wrapper can be used to add [`LinearInterpolate`] to types that normally /// don't support interpolation. -#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Ord, PartialOrd)] +#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Ord, PartialOrd, From)] pub struct ImmediateLerp(T); impl LinearInterpolate for ImmediateLerp