egui_animate 0.2.3

Custom egui animations and transitions
Documentation
use std::any::Any;

use crate::Animation;
use crate::mem;

/// Create an animation that transitions between changes of the given `value`.
///
/// Requires a unique [`egui::Id`], and [`Animation`]. See [`Animation`] for details
/// on how to define an animation.
///
/// # Example
/// ```
/// # use egui;
/// # use eframe;
/// # use egui_animate::*;
/// // A linear 0.3 second fade out/in animation.
/// const FADE_ANIM: Animation = Animation::new(
///     0.3,
///     |ui, normal| ui.set_opacity(1.0 - normal),
///     |ui, normal| ui.set_opacity(normal),
/// );
///
/// // The variable state.
/// let mut my_state: u32 = 0;
///
/// # let ctx = egui::Context::default();
/// #
/// # ctx.run(egui::RawInput::default(), |ctx| {
/// # egui::CentralPanel::default().show(ctx, |ui| {
/// #
/// // An animation that triggers on button press.
/// animate(ui, "my_fade", my_state, FADE_ANIM, |ui, value| {
///     if ui.button(format!("Value is {}", value)).clicked() {
///         my_state += 1;
///     };
/// });
/// #
/// # });
/// # });
/// ```
pub fn animate<T, R>(
    ui: &mut egui::Ui,
    id: impl Into<egui::Id>,
    value: T,
    animation: Animation,
    add_contents: impl FnOnce(&mut egui::Ui, T) -> R,
) where
    T: 'static + Any + Clone + Send + Sync + Default + PartialEq,
{
    let id: egui::Id = id.into();

    let current_time = ui.ctx().input(|input| input.time);
    let current_value = value;
    let start_value = mem::get_or_insert_start_value(ui, id, current_value.clone());

    match start_value == current_value {
        true => add_contents(ui, current_value),
        false => {
            let start_time = mem::get_or_insert_start_time(ui, id, current_time);
            let animation = AnimationState::new(start_time, current_time, animation);

            ui.ctx().request_repaint();
            animation.animate(ui, id, start_value, current_value, add_contents)
        }
    };
}

/// Get the [`RunState`] for the animation of the given `id`. Returns `RunState::None`
/// for animations that do not exist.
///
/// # Example
/// ```
/// # use egui;
/// # use eframe;
/// # use egui_animate::*;
/// # const MY_ANIM: Animation = Animation::EMPTY;
/// # let mut my_state: u32 = 0;
/// #
/// # let ctx = egui::Context::default();
/// #
/// # ctx.run(egui::RawInput::default(), |ctx| {
/// # egui::CentralPanel::default().show(ctx, |ui| {
/// #
/// // Define an animation with a unique `id`.
/// animate(ui, "my_anim", my_state, MY_ANIM, |ui, value| {
///     // ...
/// });
///
/// // Render a ui label if the animation is running.
/// if run_state(ui, "my_anim", MY_ANIM).is_running() {
///     ui.label("Animation running...");
/// }
/// #
/// # });
/// # });
/// ```
pub fn run_state(ui: &mut egui::Ui, id: impl Into<egui::Id>, animation: Animation) -> RunState {
    let id: egui::Id = id.into();

    match mem::get_start_time(ui, id) {
        Some(start_time) => {
            let current_time = ui.ctx().input(|input| input.time);
            AnimationState::new(start_time, current_time, animation).run_state()
        }
        None => Default::default(),
    }
}

/// The current state of an animation. Defines an animation scope, delegating variables
/// to the currently progressing animation.
struct AnimationState {
    start_time: f64,
    current_time: f64,

    animation: Animation,
}

impl AnimationState {
    /// Create a new `AnimationState` from the `start_time`, `current_time` and `Animation`.
    pub const fn new(start_time: f64, current_time: f64, animation: Animation) -> Self {
        Self {
            start_time,
            current_time,
            animation,
        }
    }

    /// Get the **out** segment duration.
    #[inline]
    fn out_dur(&self) -> f32 {
        self.animation.out_seg.duration
    }

    /// Get the **out** segment start time.
    #[inline]
    fn out_start(&self) -> f64 {
        self.start_time
    }

    /// Get the **out** segment end time.
    #[inline]
    fn out_end(&self) -> f64 {
        self.out_start() + self.out_dur() as f64
    }

    /// Get the elapsed time of the **out** segment. Returns `Some(0.0)` if the animation
    /// has yet to begin, and `None` if the animation has finished.
    fn out_elapsed(&self) -> Option<f32> {
        let out_elapsed = (self.current_time - self.out_start()).max(0.0) as f32;
        (out_elapsed < self.out_dur()).then_some(out_elapsed)
    }

    /// Get the elapsed normal of the **out** segment. Returns `Some(0.0)` if the animation
    /// has yet to begin, and `None` if the animation has finished.
    fn out_elapsed_normal(&self) -> Option<f32> {
        self.out_elapsed().map(|elapsed| elapsed / self.out_dur())
    }

    /// Get the **in** segment duration.
    #[inline]
    fn in_dur(&self) -> f32 {
        self.animation.in_seg.duration
    }

    /// Get the **in** segment start time.
    #[inline]
    fn in_start(&self) -> f64 {
        self.out_end()
    }

    /// Get the **in** segment end time.
    #[allow(dead_code)]
    #[inline]
    fn in_end(&self) -> f64 {
        self.in_start() + self.in_dur() as f64
    }

    /// Get the elapsed time of the **in** segment. Returns `Some(0.0)` if the animation
    /// has yet to begin, and `None` if the animation has finished.
    fn in_elapsed(&self) -> Option<f32> {
        let in_elapsed = (self.current_time - self.in_start()).max(0.0) as f32;
        (in_elapsed < self.in_dur()).then_some(in_elapsed)
    }

    /// Get the elapsed normal of the **in** segment. Returns `Some(0.0)` if the animation
    /// has yet to begin, and `None` if the animation has finished.
    fn in_elapsed_normal(&self) -> Option<f32> {
        self.in_elapsed().map(|elapsed| elapsed / self.in_dur())
    }

    /// Call the `AnimationSegment` for the current frame.
    fn animate<T: 'static + Any + Clone + Send + Sync + Default, R>(
        &self,
        ui: &mut egui::Ui,
        id: egui::Id,
        start_value: T,
        current_value: T,
        add_contents: impl FnOnce(&mut egui::Ui, T) -> R,
    ) -> R {
        match self.run_state() {
            RunState::OutSeg(normal) => {
                self.animate_out(ui, id, normal, |ui| add_contents(ui, start_value))
            }
            RunState::InSeg(normal) => {
                mem::clear_animation_layer(ui, id);
                self.animate_in(ui, id, normal, |ui| add_contents(ui, current_value))
            }
            RunState::None => {
                mem::clear_start_value::<T>(ui, id);
                mem::clear_start_time(ui, id);
                mem::clear_animation_layer(ui, id);

                add_contents(ui, current_value)
            }
        }
    }

    /// Delegate to the **out** segment [`AnimationSegment::animate`] fn.
    #[inline]
    fn animate_out<R>(
        &self,
        ui: &mut egui::Ui,
        id: egui::Id,
        normal: f32,
        add_contents: impl FnOnce(&mut egui::Ui) -> R,
    ) -> R {
        self.animation.out_seg.animate(ui, id, normal, add_contents)
    }

    /// Delegate to the **in** segment [`AnimationSegment::animate`] fn.
    #[inline]
    fn animate_in<R>(
        &self,
        ui: &mut egui::Ui,
        id: egui::Id,
        normal: f32,
        add_contents: impl FnOnce(&mut egui::Ui) -> R,
    ) -> R {
        self.animation.in_seg.animate(ui, id, normal, add_contents)
    }

    /// Get the `RunState` for the current frame.
    fn run_state(&self) -> RunState {
        if let Some(normal) = self.out_elapsed_normal() {
            RunState::OutSeg(normal)
        } else if let Some(normal) = self.in_elapsed_normal() {
            RunState::InSeg(normal)
        } else {
            RunState::None
        }
    }
}

/// An identified animation segment and *normal*.
#[derive(Debug, Default, PartialEq, PartialOrd)]
pub enum RunState {
    /// The *out* animation segment normal.
    OutSeg(f32),
    /// The *in* animation segment normal.
    InSeg(f32),
    /// The animation is not currently running.
    #[default]
    None,
}

impl RunState {
    /// Returns `true` if the animation is in either the *out* or *in* state.
    pub fn is_running(&self) -> bool {
        match self {
            RunState::OutSeg(_) | RunState::InSeg(_) => true,
            RunState::None => false,
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    mod animation_state {
        use super::*;

        const TEST_ANIM_STATE: AnimationState = AnimationState::new(
            1.0,
            1.0,
            Animation {
                out_seg: crate::AnimationSegment {
                    duration: 1.5,
                    anim_fn: |_, _| {},
                },
                in_seg: crate::AnimationSegment {
                    duration: 1.5,
                    anim_fn: |_, _| {},
                },
            },
        );

        #[test]
        fn test_out_end() {
            let state = TEST_ANIM_STATE;
            assert_eq!(state.out_end(), 2.5);
        }

        #[test]
        fn test_out_elapsed() {
            let mut state = TEST_ANIM_STATE;

            assert_eq!(state.out_elapsed(), Some(0.0));
            state.current_time = 2.0;
            assert_eq!(state.out_elapsed(), Some(1.0));
            state.current_time = 3.0;
            assert_eq!(state.out_elapsed(), None);
            state.current_time = 4.0;
            assert_eq!(state.out_elapsed(), None);
        }
        #[test]
        fn test_out_elapsed_normal() {
            let mut state = TEST_ANIM_STATE;

            assert_eq!(state.out_elapsed_normal(), Some(0.0));
            state.current_time = 1.75;
            assert_eq!(state.out_elapsed_normal(), Some(0.5));
            state.current_time = 3.0;
            assert_eq!(state.out_elapsed_normal(), None);
            state.current_time = 4.0;
            assert_eq!(state.out_elapsed_normal(), None);
        }

        #[test]
        fn test_in_end() {
            let state = TEST_ANIM_STATE;
            assert_eq!(state.in_end(), 4.0);
        }

        #[test]
        fn test_in_elapsed() {
            let mut state = TEST_ANIM_STATE;

            assert_eq!(state.in_elapsed(), Some(0.0));
            state.current_time = 2.0;
            assert_eq!(state.in_elapsed(), Some(0.0));
            state.current_time = 3.0;
            assert_eq!(state.in_elapsed(), Some(0.5));
            state.current_time = 4.0;
            assert_eq!(state.in_elapsed(), None);
            state.current_time = 5.0;
            assert_eq!(state.in_elapsed(), None);
        }
        #[test]
        fn test_in_elapsed_normal() {
            let mut state = TEST_ANIM_STATE;

            assert_eq!(state.in_elapsed_normal(), Some(0.0));
            state.current_time = 2.0;
            assert_eq!(state.in_elapsed_normal(), Some(0.0));
            state.current_time = 3.25;
            assert_eq!(state.in_elapsed_normal(), Some(0.5));
            state.current_time = 4.0;
            assert_eq!(state.in_elapsed_normal(), None);
            state.current_time = 5.0;
            assert_eq!(state.in_elapsed_normal(), None);
        }
    }
}