kael_ui 0.2.0

Professional shadcn-inspired UI component library for Kael. 100+ accessible components for building beautiful, performant desktop applications.
use std::collections::HashMap;
use std::time::{Duration, Instant};

type CompletionCallback = Box<dyn FnOnce() + 'static>;

struct AnimationEntry {
    start: Instant,
    duration: Duration,
    on_complete: Option<CompletionCallback>,
    completed: bool,
}

pub struct AnimationCoordinator {
    animations: HashMap<String, AnimationEntry>,
}

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

impl AnimationCoordinator {
    pub fn new() -> Self {
        Self {
            animations: HashMap::new(),
        }
    }

    pub fn start(&mut self, id: impl Into<String>, duration: Duration) {
        let id = id.into();
        self.animations.insert(
            id,
            AnimationEntry {
                start: Instant::now(),
                duration,
                on_complete: None,
                completed: false,
            },
        );
    }

    pub fn start_with_callback(
        &mut self,
        id: impl Into<String>,
        duration: Duration,
        on_complete: impl FnOnce() + 'static,
    ) {
        let id = id.into();
        self.animations.insert(
            id,
            AnimationEntry {
                start: Instant::now(),
                duration,
                on_complete: Some(Box::new(on_complete)),
                completed: false,
            },
        );
    }

    pub fn on_complete(&mut self, id: &str, callback: impl FnOnce() + 'static) {
        if let Some(entry) = self.animations.get_mut(id) {
            entry.on_complete = Some(Box::new(callback));
        }
    }

    pub fn progress(&self, id: &str) -> Option<f32> {
        self.animations.get(id).map(|entry| {
            let elapsed = entry.start.elapsed().as_secs_f32();
            let total = entry.duration.as_secs_f32();
            if total <= 0.0 {
                1.0
            } else {
                (elapsed / total).clamp(0.0, 1.0)
            }
        })
    }

    pub fn is_active(&self, id: &str) -> bool {
        self.animations
            .get(id)
            .map(|entry| !entry.completed && entry.start.elapsed() < entry.duration)
            .unwrap_or(false)
    }

    pub fn is_complete(&self, id: &str) -> bool {
        self.animations
            .get(id)
            .map(|entry| entry.completed || entry.start.elapsed() >= entry.duration)
            .unwrap_or(false)
    }

    pub fn tick(&mut self) -> Vec<String> {
        let mut completed = Vec::new();

        let keys: Vec<String> = self.animations.keys().cloned().collect();
        for key in keys {
            let entry = self.animations.get(&key).unwrap();
            if !entry.completed && entry.start.elapsed() >= entry.duration {
                completed.push(key.clone());
            }
        }

        for key in &completed {
            if let Some(mut entry) = self.animations.remove(key) {
                entry.completed = true;
                if let Some(callback) = entry.on_complete.take() {
                    (callback)();
                }
            }
        }

        completed
    }

    pub fn cancel(&mut self, id: &str) {
        self.animations.remove(id);
    }

    pub fn cancel_all(&mut self) {
        self.animations.clear();
    }

    pub fn has_active_animations(&self) -> bool {
        self.animations
            .values()
            .any(|entry| !entry.completed && entry.start.elapsed() < entry.duration)
    }

    pub fn active_count(&self) -> usize {
        self.animations
            .values()
            .filter(|entry| !entry.completed && entry.start.elapsed() < entry.duration)
            .count()
    }
}