use gpui::SharedString;
use super::motion::fold_time;
use super::{Frame, Loop, Motion};
#[derive(Debug, Clone, PartialEq)]
pub enum At {
End,
Abs(f32),
Rel(f32),
With(f32),
Label(SharedString, f32),
}
impl At {
pub fn with_previous() -> Self {
At::With(0.0)
}
}
#[derive(Debug, Clone, PartialEq)]
struct Entry {
start: f32,
motion: Motion,
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct Sequence {
entries: Vec<Entry>,
labels: Vec<(SharedString, f32)>,
loops: Loop,
alternate: bool,
reversed: bool,
previous_start: f32,
}
impl Sequence {
pub fn new() -> Self {
Sequence::default()
}
#[allow(clippy::should_implement_trait)]
pub fn add(self, motion: Motion) -> Self {
self.add_at(motion, At::End)
}
pub fn add_at(mut self, motion: Motion, at: At) -> Self {
let start = self.resolve(&at).max(0.0);
self.previous_start = start;
self.entries.push(Entry { start, motion });
self
}
pub fn label(mut self, name: impl Into<SharedString>, at: At) -> Self {
let position = self.resolve(&at).max(0.0);
self.labels.push((name.into(), position));
self
}
pub fn loops(mut self, loops: Loop) -> Self {
self.loops = loops;
self
}
pub fn repeat(mut self, times: u32) -> Self {
self.loops = Loop::Times(times);
self
}
pub fn repeat_forever(mut self) -> Self {
self.loops = Loop::Forever;
self
}
pub fn alternate(mut self, alternate: bool) -> Self {
self.alternate = alternate;
self
}
pub fn reversed(mut self, reversed: bool) -> Self {
self.reversed = reversed;
self
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
pub fn is_alternating(&self) -> bool {
self.alternate
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn resolve(&self, at: &At) -> f32 {
match at {
At::End => self.iteration_ms(),
At::Abs(ms) => *ms,
At::Rel(ms) => self.iteration_ms() + ms,
At::With(ms) => self.previous_start + ms,
At::Label(name, ms) => {
let base = self
.labels
.iter()
.find(|(label, _)| label == name)
.map(|(_, position)| *position)
.unwrap_or(0.0);
base + ms
}
}
}
pub fn iteration_ms(&self) -> f32 {
self.entries
.iter()
.map(|entry| {
let span = match entry.motion.loops {
Loop::Forever => entry.motion.iteration_ms(),
_ => entry.motion.total_ms(),
};
entry.start + span
})
.fold(0.0_f32, f32::max)
}
pub fn total_ms(&self) -> f32 {
match self.loops.count() {
Some(n) => self.iteration_ms() * n as f32,
None => f32::INFINITY,
}
}
pub fn sample(&self, t: f32) -> Frame {
let mut frame = Frame::new();
self.sample_into(t, &mut frame);
frame
}
pub fn sample_into(&self, t: f32, frame: &mut Frame) {
let (local, progress, finished) = fold_time(
t,
self.iteration_ms(),
self.loops,
self.alternate,
self.reversed,
);
for entry in &self.entries {
if local + f32::EPSILON >= entry.start {
entry.motion.sample_into(local - entry.start, frame);
}
}
frame.progress = progress;
frame.finished = finished;
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::anim::{Easing, Prop};
fn leg(prop: Prop, from: f32, to: f32, ms: f32) -> Motion {
Motion::new()
.duration(ms)
.ease(Easing::Linear)
.tween(prop, from, to)
}
#[test]
fn entries_queue_up_end_to_end() {
let sequence = Sequence::new()
.add(leg(Prop::Opacity, 0.0, 1.0, 100.0))
.add(leg(Prop::X, 0.0, 50.0, 100.0));
assert_eq!(sequence.iteration_ms(), 200.0);
let early = sequence.sample(50.0);
assert!((early.number(Prop::Opacity).unwrap() - 0.5).abs() < 1e-5);
assert_eq!(early.number(Prop::X), None, "not its turn yet");
let late = sequence.sample(150.0);
assert_eq!(late.number(Prop::Opacity), Some(1.0));
assert!((late.number(Prop::X).unwrap() - 25.0).abs() < 1e-4);
}
#[test]
fn relative_placement_overlaps_the_tail() {
let sequence = Sequence::new()
.add(leg(Prop::Opacity, 0.0, 1.0, 100.0))
.add_at(leg(Prop::X, 0.0, 50.0, 100.0), At::Rel(-50.0));
assert_eq!(sequence.iteration_ms(), 150.0);
let mid = sequence.sample(75.0);
assert!(mid.number(Prop::Opacity).unwrap() > 0.5);
assert!(mid.number(Prop::X).unwrap() > 0.0);
}
#[test]
fn with_previous_starts_them_together() {
let sequence = Sequence::new()
.add(leg(Prop::Opacity, 0.0, 1.0, 100.0))
.add_at(leg(Prop::X, 0.0, 50.0, 100.0), At::with_previous());
assert_eq!(sequence.iteration_ms(), 100.0);
let mid = sequence.sample(50.0);
assert!((mid.number(Prop::Opacity).unwrap() - 0.5).abs() < 1e-5);
assert!((mid.number(Prop::X).unwrap() - 25.0).abs() < 1e-4);
}
#[test]
fn labels_anchor_later_entries() {
let sequence = Sequence::new()
.add(leg(Prop::Opacity, 0.0, 1.0, 100.0))
.label("settled", At::End)
.add(leg(Prop::X, 0.0, 50.0, 100.0))
.add_at(
leg(Prop::Y, 0.0, 10.0, 50.0),
At::Label("settled".into(), 25.0),
);
assert_eq!(sequence.resolve(&At::Label("settled".into(), 0.0)), 100.0);
assert_eq!(sequence.sample(120.0).number(Prop::Y), None);
assert!(sequence.sample(130.0).number(Prop::Y).is_some());
}
#[test]
fn a_missing_label_falls_back_to_the_start() {
let sequence = Sequence::new().add(leg(Prop::X, 0.0, 1.0, 100.0));
assert_eq!(sequence.resolve(&At::Label("nope".into(), 10.0)), 10.0);
}
#[test]
fn the_last_writer_wins_when_motions_overlap() {
let sequence = Sequence::new()
.add(leg(Prop::Opacity, 0.0, 1.0, 100.0))
.add_at(leg(Prop::Opacity, 1.0, 0.0, 100.0), At::with_previous());
assert!((sequence.sample(50.0).number(Prop::Opacity).unwrap() - 0.5).abs() < 1e-5);
assert_eq!(sequence.sample(100.0).number(Prop::Opacity), Some(0.0));
}
#[test]
fn a_forever_child_does_not_make_the_sequence_infinite() {
let sequence = Sequence::new().add(leg(Prop::Opacity, 0.0, 1.0, 100.0).repeat_forever());
assert_eq!(sequence.iteration_ms(), 100.0);
assert!(sequence.total_ms().is_finite());
}
#[test]
fn the_sequence_can_loop_as_a_whole() {
let sequence = Sequence::new()
.add(leg(Prop::Opacity, 0.0, 1.0, 100.0))
.repeat(2);
assert_eq!(sequence.total_ms(), 200.0);
assert!((sequence.sample(150.0).number(Prop::Opacity).unwrap() - 0.5).abs() < 1e-5);
assert!(sequence.sample(200.0).finished);
}
}