use std::time::{Duration, Instant};
#[derive(Debug, Clone, Copy, Default)]
pub enum Easing {
#[default]
Linear,
EaseIn,
EaseOut,
EaseInOut,
Bounce,
}
impl Easing {
pub fn apply(self, t: f64) -> f64 {
let t = t.clamp(0.0, 1.0);
match self {
Easing::Linear => t,
Easing::EaseIn => t * t * t,
Easing::EaseOut => 1.0 - (1.0 - t).powi(3),
Easing::EaseInOut => {
if t < 0.5 {
4.0 * t * t * t
} else {
1.0 - (-2.0 * t + 2.0).powi(3) / 2.0
}
}
Easing::Bounce => {
if t < 1.0 / 2.75 {
7.5625 * t * t
} else if t < 2.0 / 2.75 {
let t = t - 1.5 / 2.75;
7.5625 * t * t + 0.75
} else if t < 2.5 / 2.75 {
let t = t - 2.25 / 2.75;
7.5625 * t * t + 0.9375
} else {
let t = t - 2.625 / 2.75;
7.5625 * t * t + 0.984375
}
}
}
}
}
#[derive(Debug, Clone)]
pub struct Animation {
start_value: f64,
end_value: f64,
duration: Duration,
start_time: Option<Instant>,
easing: Easing,
}
impl Animation {
pub fn new(start: f64, end: f64, duration: Duration) -> Self {
Self {
start_value: start,
end_value: end,
duration,
start_time: None,
easing: Easing::EaseOut,
}
}
pub fn with_easing(mut self, easing: Easing) -> Self {
self.easing = easing;
self
}
pub fn start(&mut self) {
self.start_time = Some(Instant::now());
}
pub fn value(&self) -> f64 {
let Some(start) = self.start_time else {
return self.start_value;
};
let elapsed = start.elapsed();
if elapsed >= self.duration {
return self.end_value;
}
let progress = elapsed.as_secs_f64() / self.duration.as_secs_f64();
let eased = self.easing.apply(progress);
self.start_value + (self.end_value - self.start_value) * eased
}
pub fn is_complete(&self) -> bool {
self.start_time
.map(|t| t.elapsed() >= self.duration)
.unwrap_or(false)
}
pub fn is_running(&self) -> bool {
self.start_time.is_some() && !self.is_complete()
}
pub fn retarget(&mut self, new_end: f64) {
self.start_value = self.value();
self.end_value = new_end;
self.start_time = Some(Instant::now());
}
}
#[derive(Debug, Clone)]
pub struct SmoothScroll {
target: f64,
current: f64,
velocity: f64,
smoothness: f64,
}
impl Default for SmoothScroll {
fn default() -> Self {
Self {
target: 0.0,
current: 0.0,
velocity: 0.0,
smoothness: 0.15, }
}
}
impl SmoothScroll {
pub fn new() -> Self {
Self::default()
}
pub fn with_smoothness(mut self, smoothness: f64) -> Self {
self.smoothness = smoothness.clamp(0.01, 1.0);
self
}
pub fn scroll_to(&mut self, target: f64) {
self.target = target;
}
pub fn scroll_by(&mut self, delta: f64) {
self.target += delta;
}
pub fn update(&mut self) {
let diff = self.target - self.current;
self.velocity = diff * self.smoothness;
self.current += self.velocity;
if diff.abs() < 0.5 {
self.current = self.target;
self.velocity = 0.0;
}
}
pub fn position(&self) -> usize {
self.current.max(0.0) as usize
}
pub fn position_f64(&self) -> f64 {
self.current
}
pub fn is_scrolling(&self) -> bool {
self.velocity.abs() > 0.1
}
pub fn set_immediate(&mut self, position: f64) {
self.current = position;
self.target = position;
self.velocity = 0.0;
}
}
#[derive(Debug, Clone)]
pub struct Fade {
opacity: f64,
target_opacity: f64,
fade_speed: f64,
}
impl Default for Fade {
fn default() -> Self {
Self {
opacity: 1.0,
target_opacity: 1.0,
fade_speed: 0.15,
}
}
}
impl Fade {
pub fn new() -> Self {
Self::default()
}
pub fn fade_in(&mut self) {
self.target_opacity = 1.0;
}
pub fn fade_out(&mut self) {
self.target_opacity = 0.0;
}
pub fn set_target(&mut self, target: f64) {
self.target_opacity = target.clamp(0.0, 1.0);
}
pub fn update(&mut self) {
let diff = self.target_opacity - self.opacity;
if diff.abs() < 0.01 {
self.opacity = self.target_opacity;
} else {
self.opacity += diff * self.fade_speed;
}
}
pub fn opacity(&self) -> f64 {
self.opacity
}
pub fn is_visible(&self) -> bool {
self.opacity > 0.01
}
}
#[derive(Debug, Clone)]
pub struct Pulse {
phase: f64,
speed: f64,
min_value: f64,
max_value: f64,
}
impl Default for Pulse {
fn default() -> Self {
Self {
phase: 0.0,
speed: 0.1,
min_value: 0.7,
max_value: 1.0,
}
}
}
impl Pulse {
pub fn new() -> Self {
Self::default()
}
pub fn with_range(mut self, min: f64, max: f64) -> Self {
self.min_value = min;
self.max_value = max;
self
}
pub fn with_speed(mut self, speed: f64) -> Self {
self.speed = speed;
self
}
pub fn update(&mut self) {
self.phase += self.speed;
if self.phase > std::f64::consts::TAU {
self.phase -= std::f64::consts::TAU;
}
}
pub fn value(&self) -> f64 {
let sin = (self.phase.sin() + 1.0) / 2.0; self.min_value + sin * (self.max_value - self.min_value)
}
}
#[derive(Debug, Default)]
pub struct AnimationState {
pub list_scroll: SmoothScroll,
pub inspector_scroll: SmoothScroll,
pub search_cursor: Pulse,
pub selection_highlight: f64, pub transition_progress: f64, }
impl AnimationState {
pub fn new() -> Self {
Self {
list_scroll: SmoothScroll::new().with_smoothness(0.2),
inspector_scroll: SmoothScroll::new().with_smoothness(0.15),
search_cursor: Pulse::new().with_speed(0.15),
selection_highlight: 1.0,
transition_progress: 1.0,
}
}
pub fn update(&mut self) {
self.list_scroll.update();
self.inspector_scroll.update();
self.search_cursor.update();
if self.selection_highlight < 1.0 {
self.selection_highlight = (self.selection_highlight + 0.2).min(1.0);
}
if self.transition_progress < 1.0 {
self.transition_progress = (self.transition_progress + 0.15).min(1.0);
}
}
pub fn on_selection_change(&mut self) {
self.selection_highlight = 0.0;
}
pub fn on_tab_change(&mut self) {
self.transition_progress = 0.0;
}
pub fn is_animating(&self) -> bool {
self.list_scroll.is_scrolling()
|| self.inspector_scroll.is_scrolling()
|| self.selection_highlight < 1.0
|| self.transition_progress < 1.0
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_easing_bounds() {
for easing in [
Easing::Linear,
Easing::EaseIn,
Easing::EaseOut,
Easing::EaseInOut,
] {
assert!((easing.apply(0.0) - 0.0).abs() < 0.001);
assert!((easing.apply(1.0) - 1.0).abs() < 0.001);
}
}
#[test]
fn test_smooth_scroll() {
let mut scroll = SmoothScroll::new();
scroll.scroll_to(100.0);
for _ in 0..50 {
scroll.update();
}
assert!((scroll.position_f64() - 100.0).abs() < 1.0);
}
}