scrin 0.1.79

A terminal UI toolkit with panes, widgets, overlays, animations, and Aisling-powered effects/loaders.
Documentation
use crate::effects::easing::EasingFn;
use std::time::{Duration, Instant};

#[derive(Debug, Clone, Copy, PartialEq)]
pub enum PlayState {
    Playing,
    Paused,
    Completed,
}

#[derive(Debug, Clone)]
pub struct Animation {
    pub start: f32,
    pub end: f32,
    pub current: f32,
    pub duration: Duration,
    pub elapsed: Duration,
    pub easing: EasingFn,
    pub state: PlayState,
    pub loop_animation: bool,
    pub alternate: bool,
    pub direction_forward: bool,
}

impl Animation {
    pub fn new(start: f32, end: f32, duration: Duration) -> Self {
        Self {
            start,
            end,
            current: start,
            duration,
            elapsed: Duration::ZERO,
            easing: EasingFn::Linear,
            state: PlayState::Playing,
            loop_animation: false,
            alternate: false,
            direction_forward: true,
        }
    }

    pub fn with_easing(mut self, easing: EasingFn) -> Self {
        self.easing = easing;
        self
    }

    pub fn with_loop(mut self) -> Self {
        self.loop_animation = true;
        self
    }

    pub fn with_alternate(mut self) -> Self {
        self.alternate = true;
        self
    }

    pub fn update(&mut self, delta: Duration) {
        if self.state != PlayState::Playing {
            return;
        }
        self.elapsed += delta;
        let t = if self.duration.is_zero() {
            1.0
        } else {
            (self.elapsed.as_secs_f32() / self.duration.as_secs_f32()).min(1.0)
        };
        let eased = if self.direction_forward {
            self.easing.apply(t)
        } else {
            1.0 - self.easing.apply(t)
        };
        self.current = self.start + (self.end - self.start) * eased;

        if t >= 1.0 {
            if self.loop_animation {
                self.elapsed = Duration::ZERO;
                if self.alternate {
                    self.direction_forward = !self.direction_forward;
                }
            } else {
                self.state = PlayState::Completed;
                self.current = if self.direction_forward {
                    self.end
                } else {
                    self.start
                };
            }
        }
    }

    pub fn progress(&self) -> f32 {
        if self.duration.is_zero() {
            1.0
        } else {
            (self.elapsed.as_secs_f32() / self.duration.as_secs_f32()).min(1.0)
        }
    }

    pub fn restart(&mut self) {
        self.elapsed = Duration::ZERO;
        self.current = self.start;
        self.state = PlayState::Playing;
        self.direction_forward = true;
    }

    pub fn pause(&mut self) {
        self.state = PlayState::Paused;
    }

    pub fn resume(&mut self) {
        if self.state == PlayState::Paused {
            self.state = PlayState::Playing;
        }
    }
}

#[derive(Debug, Clone)]
pub struct Tween {
    pub from: f32,
    pub to: f32,
    pub value: f32,
    pub progress: f32,
    pub duration: Duration,
    pub elapsed: Duration,
    pub easing: EasingFn,
}

impl Tween {
    pub fn new(from: f32, to: f32, duration: Duration) -> Self {
        Self {
            from,
            to,
            value: from,
            progress: 0.0,
            duration,
            elapsed: Duration::ZERO,
            easing: EasingFn::OutQuad,
        }
    }

    pub fn with_easing(mut self, easing: EasingFn) -> Self {
        self.easing = easing;
        self
    }

    pub fn update(&mut self, delta: Duration) {
        self.elapsed += delta;
        self.progress = if self.duration.is_zero() {
            1.0
        } else {
            (self.elapsed.as_secs_f32() / self.duration.as_secs_f32()).min(1.0)
        };
        let eased = self.easing.apply(self.progress);
        self.value = self.from + (self.to - self.from) * eased;
    }

    pub fn is_complete(&self) -> bool {
        self.progress >= 1.0
    }

    pub fn restart(&mut self) {
        self.elapsed = Duration::ZERO;
        self.progress = 0.0;
        self.value = self.from;
    }
}

#[derive(Debug, Clone)]
pub struct TimelineItem {
    pub start_time: Duration,
    pub animation: Animation,
    pub label: Option<String>,
}

#[derive(Debug, Clone)]
pub struct Timeline {
    pub items: Vec<TimelineItem>,
    pub current_time: Duration,
    pub is_playing: bool,
    pub loop_timeline: bool,
    pub duration: Duration,
}

impl Timeline {
    pub fn new() -> Self {
        Self {
            items: Vec::new(),
            current_time: Duration::ZERO,
            is_playing: false,
            loop_timeline: false,
            duration: Duration::ZERO,
        }
    }

    pub fn with_loop(mut self) -> Self {
        self.loop_timeline = true;
        self
    }

    pub fn add(&mut self, start_time: Duration, animation: Animation, label: Option<String>) {
        let end = start_time + animation.duration;
        if end > self.duration {
            self.duration = end;
        }
        self.items.push(TimelineItem {
            start_time,
            animation,
            label,
        });
    }

    pub fn play(&mut self) {
        self.is_playing = true;
        self.current_time = Duration::ZERO;
        for item in &mut self.items {
            item.animation.restart();
            item.animation.state = PlayState::Paused;
        }
    }

    pub fn pause(&mut self) {
        self.is_playing = false;
        for item in &mut self.items {
            item.animation.pause();
        }
    }

    pub fn update(&mut self, delta: Duration) {
        if !self.is_playing {
            return;
        }
        self.current_time += delta;
        for item in &mut self.items {
            if self.current_time >= item.start_time {
                if item.animation.state == PlayState::Paused {
                    item.animation.resume();
                }
                let item_delta = if self.current_time - item.start_time < delta {
                    self.current_time - item.start_time
                } else {
                    delta
                };
                item.animation.update(item_delta);
            }
        }
        if !self.loop_timeline && self.current_time >= self.duration {
            self.is_playing = false;
            for item in &mut self.items {
                item.animation.state = PlayState::Completed;
            }
        } else if self.loop_timeline && self.current_time >= self.duration {
            self.current_time = Duration::ZERO;
            for item in &mut self.items {
                item.animation.restart();
                item.animation.state = PlayState::Paused;
            }
        }
    }

    pub fn get_value(&self, index: usize) -> Option<f32> {
        self.items.get(index).map(|item| item.animation.current)
    }

    pub fn get_label_value(&self, label: &str) -> Option<f32> {
        self.items
            .iter()
            .find(|item| item.label.as_deref() == Some(label))
            .map(|item| item.animation.current)
    }

    pub fn is_complete(&self) -> bool {
        self.items
            .iter()
            .all(|item| item.animation.state == PlayState::Completed)
    }
}

impl Default for Timeline {
    fn default() -> Self {
        Self::new()
    }
}

pub struct AnimationClock {
    last_tick: Instant,
    total_elapsed: Duration,
    tick_count: u64,
    fps: f64,
}

impl AnimationClock {
    pub fn new(fps: f64) -> Self {
        Self {
            last_tick: Instant::now(),
            total_elapsed: Duration::ZERO,
            tick_count: 0,
            fps,
        }
    }

    pub fn tick(&mut self) -> Duration {
        let now = Instant::now();
        let delta = now.duration_since(self.last_tick);
        self.last_tick = now;
        self.total_elapsed += delta;
        self.tick_count += 1;
        delta
    }

    pub fn frame_delay(&self) -> Duration {
        Duration::from_secs_f64(1.0 / self.fps)
    }

    pub fn should_render(&self) -> bool {
        let target_frame_time = Duration::from_secs_f64(1.0 / self.fps);
        let avg = if self.tick_count == 0 {
            Duration::ZERO
        } else {
            Duration::from_secs_f64(self.total_elapsed.as_secs_f64() / self.tick_count as f64)
        };
        avg <= Duration::from_secs_f64(target_frame_time.as_secs_f64() * 1.1)
    }

    pub fn total_elapsed(&self) -> Duration {
        self.total_elapsed
    }

    pub fn tick_count(&self) -> u64 {
        self.tick_count
    }

    pub fn fps(&self) -> f64 {
        self.fps
    }

    pub fn reset(&mut self) {
        self.last_tick = Instant::now();
        self.total_elapsed = Duration::ZERO;
        self.tick_count = 0;
    }
}