diff --git a/src/animation.rs b/src/animation.rs index 4bc4cfb..8593af0 100644 --- a/src/animation.rs +++ b/src/animation.rs @@ -17,7 +17,7 @@ //! ```rust //! use std::time::Duration; //! -//! use gooey::animation::{AnimationTarget, Spawn}; +//! use gooey::animation::{AnimationTarget, EaseInOutElastic, Spawn}; //! use gooey::value::Dynamic; //! //! let value = Dynamic::new(0); @@ -25,6 +25,7 @@ //! value //! .transition_to(100) //! .over(Duration::from_millis(100)) +//! .with_easing(EaseInOutElastic) //! .launch(); //! //! let mut reader = value.into_reader(); @@ -37,9 +38,9 @@ use std::f32::consts::PI; use std::fmt::Debug; -use std::marker::PhantomData; use std::ops::{ControlFlow, Deref}; -use std::sync::{Condvar, Mutex, MutexGuard, OnceLock, PoisonError}; +use std::panic::{RefUnwindSafe, UnwindSafe}; +use std::sync::{Arc, Condvar, Mutex, MutexGuard, OnceLock, PoisonError}; use std::thread; use std::time::{Duration, Instant}; @@ -47,6 +48,7 @@ use alot::{LotId, Lots}; use kempt::Set; use kludgine::Color; +use crate::styles::Component; use crate::value::Dynamic; static ANIMATIONS: Mutex = Mutex::new(Animating::new()); @@ -224,7 +226,7 @@ where { value: Target, duration: Duration, - _easing: PhantomData, + easing: Easing, } impl Animation @@ -235,16 +237,16 @@ where Self { value, duration, - _easing: PhantomData, + easing: Linear, } } /// Returns this animation with a different easing function. - pub fn with_easing(self) -> Animation { + pub fn with_easing(self, easing: Easing) -> Animation { Animation { value: self.value, duration: self.duration, - _easing: PhantomData, + easing, } } } @@ -261,7 +263,7 @@ where target: self.value.begin(), duration: self.duration, elapsed: Duration::ZERO, - _easing: PhantomData, + easing: self.easing, } } } @@ -401,7 +403,7 @@ where self.target.finish(); ControlFlow::Break(remaining_elapsed) } else { - let progress = Easing::ease(ZeroToOne::new( + let progress = self.easing.ease(ZeroToOne::new( self.elapsed.as_secs_f32() / self.duration.as_secs_f32(), )); self.target.update(progress); @@ -421,7 +423,7 @@ pub struct RunningAnimation { target: T, duration: Duration, elapsed: Duration, - _easing: PhantomData, + easing: Easing, } /// A handle to a spawned animation. When dropped, the associated animation will @@ -653,6 +655,20 @@ impl_lerp_for_int!(i64, u64, f32); impl_lerp_for_int!(i128, u128, f64); impl_lerp_for_int!(isize, usize, f64); +impl LinearInterpolate for f32 { + fn lerp(&self, target: &Self, percent: f32) -> Self { + let delta = *target - *self; + *self + delta * percent + } +} + +impl LinearInterpolate for f64 { + fn lerp(&self, target: &Self, percent: f32) -> Self { + let delta = *target - *self; + *self + delta * f64::from(percent) + } +} + #[test] fn integer_lerps() { #[track_caller] @@ -768,21 +784,57 @@ impl LinearInterpolate for ZeroToOne { } } -/// Performs easing for value interpolation. -pub trait Easing: Send + Sync + 'static { - /// Eases a value ranging between zero and one. The resulting value does not - /// need to be bounded between zero and one. - fn ease(progress: ZeroToOne) -> f32; +/// An easing function for customizing animations. +#[derive(Debug, Clone)] +pub enum EasingFunction { + /// A function pointer to use as an easing function. + Fn(fn(ZeroToOne) -> f32), + /// A custom easing implementation. + Custom(Arc), } -// /// An [`Easing`] function that produces a steady, linear transition. -// pub enum Linear {} +impl Easing for EasingFunction { + fn ease(&self, progress: ZeroToOne) -> f32 { + match self { + EasingFunction::Fn(func) => func(progress), + EasingFunction::Custom(func) => func.ease(progress), + } + } +} -// impl Easing for Linear { -// fn ease(progress: ZeroToOne) -> f32 { -// *progress -// } -// } +impl From for Component { + fn from(value: EasingFunction) -> Self { + Component::Easing(value) + } +} + +impl TryFrom for EasingFunction { + type Error = Component; + + fn try_from(value: Component) -> Result { + match value { + Component::Easing(easing) => Ok(easing), + other => Err(other), + } + } +} + +/// Performs easing for value interpolation. +pub trait Easing: Debug + Send + Sync + RefUnwindSafe + UnwindSafe + 'static { + /// Eases a value ranging between zero and one. The resulting value does not + /// need to be bounded between zero and one. + fn ease(&self, progress: ZeroToOne) -> f32; +} + +/// An [`Easing`] function that produces a steady, linear transition. +#[derive(Clone, Copy, Debug)] +pub struct Linear; + +impl Easing for Linear { + fn ease(&self, progress: ZeroToOne) -> f32 { + *progress + } +} // Think this macro has a long name? This is to ensure rustfmt wraps the closure // onto its own line. Seriously, try shortening the name and see how it changes @@ -793,14 +845,31 @@ macro_rules! declare_easing_function_implementation { /// An [`Easing`] function that eases #[doc = $description] #[doc = concat!(".\n\nSee for a visualization and more information.")] - pub enum $name {} + #[derive(Clone, Copy, Debug)] + pub struct $name; - impl Easing for $name { - fn ease(progress: ZeroToOne) -> f32 { + impl $name { + /// Eases + #[doc = $description] + #[doc = concat!(".\n\nSee for a visualization and more information.")] + #[must_use] + pub fn ease(progress: ZeroToOne) -> f32 { let closure = force_closure_type($closure); closure(*progress) } } + + impl Easing for $name { + fn ease(&self, progress: ZeroToOne) -> f32 { + Self::ease(progress) + } + } + + impl From<$name> for EasingFunction { + fn from(_function: $name) -> Self { + Self::Fn($name::ease) + } + } }; } @@ -809,22 +878,6 @@ fn force_closure_type(f: impl Fn(f32) -> f32) -> impl Fn(f32) -> f32 { f } -/// An [`Easing`] function that produces a steady, linear transition. -pub enum Linear {} - -impl Easing for Linear { - fn ease(progress: ZeroToOne) -> f32 { - *progress - } -} - -declare_easing_function_implementation!( - EaseInSine, - easeInSine, - "in using a sine wave", - |percent| 1. - (percent * PI).cos() / 2. -); - declare_easing_function_implementation!( EaseOutSine, easeOutSine, @@ -1088,7 +1141,7 @@ declare_easing_function_implementation!( EaseInBounce, easeInBounce, "in using a curve that bounces progressively closer as it progresses", - |percent| 1. - EaseOutBounce::ease(ZeroToOne(percent)) + |percent| 1. - EaseOutBounce.ease(ZeroToOne(percent)) ); declare_easing_function_implementation!( diff --git a/src/styles.rs b/src/styles.rs index 8311c4c..721268c 100644 --- a/src/styles.rs +++ b/src/styles.rs @@ -5,6 +5,7 @@ use std::collections::{hash_map, HashMap}; use std::ops::Add; use std::sync::Arc; +use crate::animation::{EasingFunction, ZeroToOne}; use crate::names::Name; use crate::utils::Lazy; use crate::value::{IntoValue, Value}; @@ -145,9 +146,11 @@ pub enum Component { /// A single-dimension measurement. Dimension(Dimension), /// A percentage between 0.0 and 1.0. - Percent(f32), + Percent(ZeroToOne), /// A custom component type. Custom(CustomComponent), + /// An easing function for animations. + Easing(EasingFunction), } impl From for Component { diff --git a/src/styles/components.rs b/src/styles/components.rs index d131f90..d4a147e 100644 --- a/src/styles/components.rs +++ b/src/styles/components.rs @@ -4,6 +4,7 @@ use std::borrow::Cow; use kludgine::figures::units::Lp; use kludgine::Color; +use crate::animation::{EaseInQuadradic, EaseOutQuadradic, EasingFunction}; use crate::styles::{ComponentDefinition, ComponentName, Dimension, Global, NamedComponent}; /// The [`Dimension`] to use as the size to render text. @@ -97,3 +98,64 @@ impl ComponentDefinition for IntrinsicPadding { Dimension::Lp(Lp::points(5)) } } + +/// The [`EasingFunction`] to apply to animations that have no inherent +/// directionality. +#[derive(Clone, Copy, Eq, PartialEq, Debug)] +pub struct Easing; + +impl NamedComponent for Easing { + fn name(&self) -> Cow<'_, ComponentName> { + Cow::Owned(ComponentName::named::("easing")) + } +} + +impl ComponentDefinition for Easing { + type ComponentType = EasingFunction; + + fn default_value(&self) -> Self::ComponentType { + EasingFunction::from(EaseInQuadradic) + } +} + +/// The [`EasingFunction`] to apply to animations that transition a value from +/// "nothing" to "something". For example, if an widget is animating a color's +/// alpha channel towards opaqueness, it would query for this style component. +/// Otherwise, it would use [`EasingOut`]. +#[derive(Clone, Copy, Eq, PartialEq, Debug)] +pub struct EasingIn; + +impl NamedComponent for EasingIn { + fn name(&self) -> Cow<'_, ComponentName> { + Cow::Owned(ComponentName::named::("easing_in")) + } +} + +impl ComponentDefinition for EasingIn { + type ComponentType = EasingFunction; + + fn default_value(&self) -> Self::ComponentType { + EasingFunction::from(EaseInQuadradic) + } +} + +/// The [`EasingFunction`] to apply to animations that transition a value from +/// "something" to "nothing". For example, if an widget is animating a color's +/// alpha channel towards transparency, it would query for this style component. +/// Otherwise, it would use [`EasingIn`]. +#[derive(Clone, Copy, Eq, PartialEq, Debug)] +pub struct EasingOut; + +impl NamedComponent for EasingOut { + fn name(&self) -> Cow<'_, ComponentName> { + Cow::Owned(ComponentName::named::("easing_out")) + } +} + +impl ComponentDefinition for EasingOut { + type ComponentType = EasingFunction; + + fn default_value(&self) -> Self::ComponentType { + EasingFunction::from(EaseOutQuadradic) + } +} diff --git a/src/widgets/align.rs b/src/widgets/align.rs index 99abab6..44a6d9d 100644 --- a/src/widgets/align.rs +++ b/src/widgets/align.rs @@ -52,7 +52,7 @@ impl Align { let (left, right, width) = horizontal.measure(available_space.width, content_size.width); let (top, bottom, height) = vertical.measure(available_space.height, content_size.height); - dbg!(Layout { + Layout { margin: Edges { left, right, @@ -60,7 +60,7 @@ impl Align { bottom, }, content: Size::new(width, height), - }) + } } } diff --git a/src/widgets/button.rs b/src/widgets/button.rs index 1cad4e9..91391a1 100644 --- a/src/widgets/button.rs +++ b/src/widgets/button.rs @@ -14,7 +14,7 @@ use kludgine::Color; use crate::animation::{AnimationHandle, AnimationTarget, Spawn}; use crate::context::{EventContext, GraphicsContext, WidgetContext}; use crate::names::Name; -use crate::styles::components::{HighlightColor, IntrinsicPadding, TextColor}; +use crate::styles::components::{Easing, HighlightColor, IntrinsicPadding, TextColor}; use crate::styles::{ComponentDefinition, ComponentGroup, ComponentName, NamedComponent}; use crate::value::{Dynamic, IntoValue, Value}; use crate::widget::{Callback, EventHandling, Widget, HANDLED, IGNORED}; @@ -66,6 +66,7 @@ impl Button { &ButtonActiveBackground, &ButtonBackground, &ButtonHoverBackground, + &Easing, ]); let background_color = if context.active() { styles.get_or_default(&ButtonActiveBackground) @@ -80,6 +81,7 @@ impl Button { self.background_color_animation = dynamic .transition_to(background_color) .over(Duration::from_millis(150)) + .with_easing(styles.get_or_default(&Easing)) .spawn(); } (true, Some(dynamic)) => { diff --git a/src/widgets/scroll.rs b/src/widgets/scroll.rs index 532a029..7f1532b 100644 --- a/src/widgets/scroll.rs +++ b/src/widgets/scroll.rs @@ -13,6 +13,7 @@ use kludgine::Color; use crate::animation::{AnimationHandle, AnimationTarget, IntoAnimate, Spawn, ZeroToOne}; use crate::context::{AsEventContext, EventContext}; +use crate::styles::components::{EasingIn, EasingOut}; use crate::styles::{ ComponentDefinition, ComponentGroup, ComponentName, Dimension, NamedComponent, }; @@ -95,6 +96,23 @@ impl Scroll { self.scroll.set(clamped); } } + + fn show_scrollbars(&mut self, context: &mut EventContext<'_, '_>) { + let styles = context.query_styles(&[&EasingIn, &EasingOut]); + self.scrollbar_opacity_animation = self + .scrollbar_opacity + .transition_to(ZeroToOne::ONE) + .over(Duration::from_millis(300)) + .with_easing(styles.get_or_default(&EasingIn)) + .and_then(Duration::from_secs(1)) + .and_then( + self.scrollbar_opacity + .transition_to(ZeroToOne::ZERO) + .over(Duration::from_millis(300)) + .with_easing(styles.get_or_default(&EasingOut)), + ) + .spawn(); + } } impl Widget for Scroll { @@ -102,25 +120,16 @@ impl Widget for Scroll { true } - fn hover(&mut self, _location: Point, _context: &mut EventContext<'_, '_>) { - self.scrollbar_opacity_animation = self - .scrollbar_opacity - .transition_to(ZeroToOne::ONE) - .over(Duration::from_millis(300)) - .and_then(Duration::from_secs(1)) - .and_then( - self.scrollbar_opacity - .transition_to(ZeroToOne::ZERO) - .over(Duration::from_millis(300)), - ) - .spawn(); + fn hover(&mut self, _location: Point, context: &mut EventContext<'_, '_>) { + self.show_scrollbars(context); } - fn unhover(&mut self, _context: &mut EventContext<'_, '_>) { + fn unhover(&mut self, context: &mut EventContext<'_, '_>) { self.scrollbar_opacity_animation = self .scrollbar_opacity .transition_to(ZeroToOne::ZERO) .over(Duration::from_millis(300)) + .with_easing(context.query_style(&EasingOut)) .spawn(); } @@ -256,6 +265,7 @@ impl Widget for Scroll { }; self.scroll.map_mut(|scroll| *scroll += amount.cast()); + self.show_scrollbars(context); context.set_needs_redraw(); // TODO make this only returned handled if we actually scrolled.