use std::time::Duration;
use gpui::Animation;
use super::timing::{clamp01, parallel_progress, sequence_progress};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct TrackId(pub usize);
#[derive(Debug, Clone, Copy)]
struct TrackSpec {
duration: Duration,
delay: Duration,
}
#[derive(Debug, Default, Clone)]
pub struct Orchestration {
steps: Vec<Step>,
}
#[derive(Debug, Default, Clone)]
struct Step {
tracks: Vec<TrackSpec>,
}
impl Orchestration {
pub fn new() -> Self {
Self { steps: Vec::new() }
}
pub fn then(self, duration: Duration) -> (Self, TrackId) {
self.then_delayed(duration, Duration::ZERO)
}
pub fn then_delayed(mut self, duration: Duration, delay: Duration) -> (Self, TrackId) {
let track_id = TrackId(self.total_tracks());
self.steps.push(Step {
tracks: vec![TrackSpec { duration, delay }],
});
(self, track_id)
}
pub fn with_parallel(
self,
durations: impl IntoIterator<Item = Duration>,
) -> (Self, Vec<TrackId>) {
let items = durations
.into_iter()
.map(|duration| (duration, Duration::ZERO));
self.with_parallel_delayed(items)
}
pub fn with_parallel_delayed(
mut self,
tracks: impl IntoIterator<Item = (Duration, Duration)>,
) -> (Self, Vec<TrackId>) {
let mut ids = Vec::new();
let mut specs = Vec::new();
for (duration, delay) in tracks {
let track_id = TrackId(self.total_tracks() + specs.len());
ids.push(track_id);
specs.push(TrackSpec { duration, delay });
}
self.steps.push(Step { tracks: specs });
(self, ids)
}
pub fn delay(mut self, duration: Duration) -> Self {
if duration.is_zero() {
return self;
}
self.steps.push(Step {
tracks: vec![TrackSpec {
duration,
delay: Duration::ZERO,
}],
});
self
}
pub fn step_count(&self) -> usize {
self.steps.len()
}
pub fn track_count(&self) -> usize {
self.total_tracks()
}
fn total_tracks(&self) -> usize {
self.steps.iter().map(|s| s.tracks.len()).sum()
}
pub fn compile(&self) -> Vec<Animation> {
self.steps
.iter()
.map(|step| Animation::new(step_duration(step)))
.collect()
}
pub fn track_progress(&self, animation_index: usize, delta: f32, track_id: TrackId) -> f32 {
let delta = clamp01(delta);
let Some(step) = self.steps.get(animation_index) else {
return 1.0;
};
let mut elapsed = Duration::ZERO;
for step in self.steps.iter().take(animation_index) {
elapsed += step_duration(step);
}
let current_step_duration = step_duration(step);
if !current_step_duration.is_zero() {
elapsed += Duration::from_secs_f32(delta * current_step_duration.as_secs_f32());
}
let Some((track_step_index, track)) = self.locate_track(track_id) else {
return 1.0;
};
let mut track_step_start = Duration::ZERO;
for step in self.steps.iter().take(track_step_index) {
track_step_start += step_duration(step);
}
let track_start = track_step_start + track.delay;
track_progress_from_elapsed(elapsed, track_start, track.duration)
}
fn locate_track(&self, track_id: TrackId) -> Option<(usize, &TrackSpec)> {
let mut current = 0usize;
for (step_index, step) in self.steps.iter().enumerate() {
for track in &step.tracks {
if current == track_id.0 {
return Some((step_index, track));
}
current += 1;
}
}
None
}
}
fn step_duration(step: &Step) -> Duration {
step.tracks
.iter()
.map(|t| t.delay + t.duration)
.max()
.unwrap_or(Duration::ZERO)
}
fn track_progress_from_elapsed(elapsed: Duration, start: Duration, duration: Duration) -> f32 {
if elapsed < start {
return 0.0;
}
if duration.is_zero() {
return 1.0;
}
let active_elapsed = elapsed - start;
clamp01(active_elapsed.as_secs_f32() / duration.as_secs_f32())
}
#[derive(Debug, Default, Clone)]
pub struct AnimationSequence {
durations: Vec<Duration>,
}
impl AnimationSequence {
pub fn new() -> Self {
Self {
durations: Vec::new(),
}
}
pub fn then(mut self, duration: Duration) -> Self {
self.durations.push(duration);
self
}
pub fn then_all(mut self, durations: impl IntoIterator<Item = Duration>) -> Self {
self.durations.extend(durations);
self
}
pub fn total_duration(&self) -> Duration {
self.durations.iter().copied().sum()
}
pub fn len(&self) -> usize {
self.durations.len()
}
pub fn is_empty(&self) -> bool {
self.durations.is_empty()
}
pub fn calculate_progress(&self, total_progress: f32) -> (usize, f32) {
sequence_progress(&self.durations, total_progress)
}
}
#[derive(Debug, Default, Clone)]
pub struct AnimationParallel {
durations: Vec<Duration>,
}
impl AnimationParallel {
pub fn new() -> Self {
Self {
durations: Vec::new(),
}
}
pub fn with(mut self, duration: Duration) -> Self {
self.durations.push(duration);
self
}
pub fn with_all(mut self, durations: impl IntoIterator<Item = Duration>) -> Self {
self.durations.extend(durations);
self
}
pub fn max_duration(&self) -> Duration {
self.durations
.iter()
.copied()
.max()
.unwrap_or(Duration::ZERO)
}
pub fn len(&self) -> usize {
self.durations.len()
}
pub fn is_empty(&self) -> bool {
self.durations.is_empty()
}
pub fn calculate_progress(&self, total_progress: f32, animation_index: usize) -> f32 {
parallel_progress(&self.durations, total_progress, animation_index)
}
}
pub fn sequence(durations: &[Duration]) -> AnimationSequence {
AnimationSequence::new().then_all(durations.iter().copied())
}
pub fn parallel(durations: &[Duration]) -> AnimationParallel {
AnimationParallel::new().with_all(durations.iter().copied())
}
pub trait Staggered {
fn stagger(self, item_count: usize, delay: Duration) -> Vec<Duration>;
}
impl Staggered for Duration {
fn stagger(self, item_count: usize, delay: Duration) -> Vec<Duration> {
let base_ms = self.as_millis() as f32;
let delay_ms = delay.as_millis() as f32;
(0..item_count)
.map(|i| Duration::from_millis((base_ms + i as f32 * delay_ms) as u64))
.collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn assert_approx(actual: f32, expected: f32) {
let diff = (actual - expected).abs();
assert!(
diff <= 1e-4,
"expected {expected} but got {actual} (diff={diff})"
);
}
#[test]
fn sequential_track_delay_is_applied() {
let (orch, track) = Orchestration::new()
.then_delayed(Duration::from_millis(100), Duration::from_millis(50));
assert_eq!(orch.compile().len(), 1);
assert_approx(orch.track_progress(0, 0.0, track), 0.0);
assert_approx(orch.track_progress(0, 50.0 / 150.0, track), 0.0);
assert_approx(orch.track_progress(0, 75.0 / 150.0, track), 0.25);
assert_approx(orch.track_progress(0, 1.0, track), 1.0);
}
#[test]
fn parallel_tracks_can_have_individual_delays() {
let (orch, tracks) = Orchestration::new().with_parallel_delayed([
(Duration::from_millis(100), Duration::from_millis(0)),
(Duration::from_millis(100), Duration::from_millis(50)),
]);
let a = tracks[0];
let b = tracks[1];
assert_eq!(orch.compile().len(), 1);
let delta = 50.0 / 150.0;
assert_approx(orch.track_progress(0, delta, a), 0.5);
assert_approx(orch.track_progress(0, delta, b), 0.0);
let delta = 100.0 / 150.0;
assert_approx(orch.track_progress(0, delta, a), 1.0);
assert_approx(orch.track_progress(0, delta, b), 0.5);
}
#[test]
fn track_progress_is_global_across_steps() {
let (orch, a) = Orchestration::new().then(Duration::from_millis(100));
let (orch, b) = orch.then(Duration::from_millis(100));
assert_approx(orch.track_progress(0, 0.5, a), 0.5);
assert_approx(orch.track_progress(0, 0.5, b), 0.0);
assert_approx(orch.track_progress(1, 0.5, a), 1.0);
assert_approx(orch.track_progress(1, 0.5, b), 0.5);
}
}