use crate::{
MotionFrameDemand, MotionFrameReason, MotionModel, MotionRunState, MotionScalarSample,
};
use std::time::Duration;
#[derive(Debug, Clone, PartialEq)]
pub struct MotionSequence<K> {
steps: Vec<MotionSequenceStep<K>>,
}
impl<K> Default for MotionSequence<K> {
fn default() -> Self {
Self::new()
}
}
impl<K> MotionSequence<K> {
pub const fn new() -> Self {
Self { steps: Vec::new() }
}
pub fn steps(&self) -> &[MotionSequenceStep<K>] {
&self.steps
}
pub fn is_empty(&self) -> bool {
self.steps.is_empty()
}
pub fn len(&self) -> usize {
self.steps.len()
}
pub fn insert_at(&mut self, key: K, model: MotionModel, start_at: Duration) -> &mut Self {
self.steps
.push(MotionSequenceStep::new(key, model, start_at));
self
}
pub fn append(&mut self, key: K, model: MotionModel) -> &mut Self {
self.insert_at(key, model, self.duration_hint())
}
pub fn insert_with_previous(&mut self, key: K, model: MotionModel) -> &mut Self {
let start_at = self
.steps
.last()
.map(MotionSequenceStep::start_at)
.unwrap_or(Duration::ZERO);
self.insert_at(key, model, start_at)
}
pub fn insert_after_previous(
&mut self,
key: K,
model: MotionModel,
delay: Duration,
) -> &mut Self {
let start_at = self
.steps
.last()
.map(MotionSequenceStep::end_hint)
.unwrap_or(Duration::ZERO);
self.insert_at(key, model, saturating_duration_add(start_at, delay))
}
pub fn insert_staggered(
&mut self,
keys: impl IntoIterator<Item = K>,
model: MotionModel,
start_at: Duration,
stagger: Duration,
) -> &mut Self {
let mut next_start = start_at;
for key in keys {
self.insert_at(key, model, next_start);
next_start = saturating_duration_add(next_start, stagger);
}
self
}
pub fn duration_hint(&self) -> Duration {
self.steps
.iter()
.map(MotionSequenceStep::end_hint)
.max()
.unwrap_or(Duration::ZERO)
}
}
impl<K: Clone> MotionSequence<K> {
pub fn sample_at(&self, elapsed: Duration) -> MotionSequenceSample<K> {
let steps = self
.steps
.iter()
.map(|step| step.sample_at(elapsed))
.collect::<Vec<_>>();
let needs_frame = steps
.iter()
.any(|step| step.state().needs_frame_for_sequence());
let frame_demand = if needs_frame {
MotionFrameDemand::NeedsFrame(MotionFrameReason::UpdateRender)
} else {
MotionFrameDemand::Idle
};
MotionSequenceSample::new(steps, frame_demand)
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct MotionSequenceStep<K> {
key: K,
model: MotionModel,
start_at: Duration,
duration_hint: Duration,
}
impl<K> MotionSequenceStep<K> {
pub fn new(key: K, model: MotionModel, start_at: Duration) -> Self {
Self {
key,
model,
start_at,
duration_hint: model.sequence_duration_hint(),
}
}
pub const fn key(&self) -> &K {
&self.key
}
pub const fn model(&self) -> MotionModel {
self.model
}
pub const fn start_at(&self) -> Duration {
self.start_at
}
pub const fn duration_hint(&self) -> Duration {
self.duration_hint
}
pub fn end_hint(&self) -> Duration {
saturating_duration_add(self.start_at, self.duration_hint)
}
}
impl<K: Clone> MotionSequenceStep<K> {
pub fn sample_at(&self, elapsed: Duration) -> MotionSequenceStepSample<K> {
if elapsed < self.start_at {
return MotionSequenceStepSample::pending(self.key.clone());
}
let local_elapsed = elapsed.saturating_sub(self.start_at);
let sample = self
.model
.sample_scalar_elapsed(0.0, 1.0, 0.0, local_elapsed);
MotionSequenceStepSample::from_scalar(self.key.clone(), sample)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MotionSequenceStepState {
Pending,
Active,
Immediate,
Completed,
Cancelled,
}
impl MotionSequenceStepState {
pub const fn needs_frame_for_sequence(self) -> bool {
matches!(self, Self::Pending | Self::Active)
}
pub const fn is_terminal(self) -> bool {
!self.needs_frame_for_sequence()
}
pub const fn reached_final_state(self) -> bool {
matches!(self, Self::Immediate | Self::Completed)
}
fn from_run_state(state: MotionRunState) -> Self {
match state {
MotionRunState::Immediate => Self::Immediate,
MotionRunState::Active => Self::Active,
MotionRunState::Completed => Self::Completed,
MotionRunState::Cancelled => Self::Cancelled,
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct MotionSequenceStepSample<K> {
key: K,
state: MotionSequenceStepState,
elapsed: Duration,
value: f32,
velocity: f32,
target: f32,
}
impl<K> MotionSequenceStepSample<K> {
fn pending(key: K) -> Self {
Self {
key,
state: MotionSequenceStepState::Pending,
elapsed: Duration::ZERO,
value: 0.0,
velocity: 0.0,
target: 1.0,
}
}
fn from_scalar(key: K, sample: MotionScalarSample) -> Self {
Self {
key,
state: MotionSequenceStepState::from_run_state(sample.state()),
elapsed: sample.elapsed(),
value: sample.value(),
velocity: sample.velocity(),
target: sample.target(),
}
}
pub const fn key(&self) -> &K {
&self.key
}
pub const fn state(&self) -> MotionSequenceStepState {
self.state
}
pub const fn elapsed(&self) -> Duration {
self.elapsed
}
pub const fn value(&self) -> f32 {
self.value
}
pub const fn velocity(&self) -> f32 {
self.velocity
}
pub const fn target(&self) -> f32 {
self.target
}
pub const fn needs_frame(&self) -> bool {
self.state.needs_frame_for_sequence()
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct MotionSequenceSample<K> {
steps: Vec<MotionSequenceStepSample<K>>,
frame_demand: MotionFrameDemand,
}
impl<K> MotionSequenceSample<K> {
pub fn new(steps: Vec<MotionSequenceStepSample<K>>, frame_demand: MotionFrameDemand) -> Self {
Self {
steps,
frame_demand,
}
}
pub fn steps(&self) -> &[MotionSequenceStepSample<K>] {
&self.steps
}
pub const fn frame_demand(&self) -> MotionFrameDemand {
self.frame_demand
}
pub const fn complete(&self) -> bool {
!self.frame_demand.needs_frame()
}
}
impl<K: PartialEq> MotionSequenceSample<K> {
pub fn step(&self, key: &K) -> Option<&MotionSequenceStepSample<K>> {
self.steps.iter().find(|step| step.key() == key)
}
}
fn saturating_duration_add(left: Duration, right: Duration) -> Duration {
left.checked_add(right).unwrap_or(Duration::MAX)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
MotionDuration, MotionEasing, MotionPreference, MotionSpec, MotionSpringPreset,
MotionSpringSpec,
};
fn linear_model(duration: Duration) -> MotionModel {
MotionModel::timeline(MotionSpec::new(
MotionPreference::Animated,
MotionDuration::Custom(duration),
MotionEasing::Linear,
))
}
#[test]
fn sequence_positions_append_with_previous_and_after_previous() {
let model = linear_model(Duration::from_millis(100));
let mut sequence = MotionSequence::new();
sequence
.append("first", model)
.insert_with_previous("parallel", model)
.insert_after_previous("delayed", model, Duration::from_millis(20));
assert_eq!(sequence.steps()[0].start_at(), Duration::ZERO);
assert_eq!(sequence.steps()[1].start_at(), Duration::ZERO);
assert_eq!(sequence.steps()[2].start_at(), Duration::from_millis(120));
assert_eq!(sequence.duration_hint(), Duration::from_millis(220));
}
#[test]
fn staggered_steps_preserve_start_offsets() {
let model = linear_model(Duration::from_millis(50));
let mut sequence = MotionSequence::new();
sequence.insert_staggered(
["a", "b", "c"],
model,
Duration::from_millis(10),
Duration::from_millis(20),
);
assert_eq!(sequence.steps()[0].start_at(), Duration::from_millis(10));
assert_eq!(sequence.steps()[1].start_at(), Duration::from_millis(30));
assert_eq!(sequence.steps()[2].start_at(), Duration::from_millis(50));
assert_eq!(sequence.duration_hint(), Duration::from_millis(100));
}
#[test]
fn sequence_samples_pending_active_and_completed_steps() {
let model = linear_model(Duration::from_millis(100));
let mut sequence = MotionSequence::new();
sequence.insert_at("row", model, Duration::from_millis(50));
let pending = sequence.sample_at(Duration::ZERO);
let pending_step = pending.step(&"row").expect("row step");
assert_eq!(pending_step.state(), MotionSequenceStepState::Pending);
assert!(pending.frame_demand().needs_frame());
assert!(!pending.complete());
let active = sequence.sample_at(Duration::from_millis(100));
let active_step = active.step(&"row").expect("row step");
assert_eq!(active_step.state(), MotionSequenceStepState::Active);
assert_eq!(active_step.elapsed(), Duration::from_millis(50));
assert_eq!(active_step.value(), 0.5);
assert!(active.frame_demand().needs_frame());
let complete = sequence.sample_at(Duration::from_millis(160));
let complete_step = complete.step(&"row").expect("row step");
assert_eq!(complete_step.state(), MotionSequenceStepState::Completed);
assert_eq!(complete_step.value(), 1.0);
assert!(!complete.frame_demand().needs_frame());
assert!(complete.complete());
}
#[test]
fn reduced_motion_step_completes_without_frame_demand_at_start() {
let model = MotionModel::timeline(MotionSpec::committed_layout(MotionPreference::Reduced));
let mut sequence = MotionSequence::new();
sequence.insert_at("panel", model, Duration::ZERO);
let sample = sequence.sample_at(Duration::ZERO);
let step = sample.step(&"panel").expect("panel step");
assert_eq!(step.state(), MotionSequenceStepState::Immediate);
assert_eq!(step.value(), 1.0);
assert!(step.state().reached_final_state());
assert!(sample.complete());
}
#[test]
fn spring_sequence_duration_hint_uses_review_duration() {
let spring = MotionSpringSpec::layout(MotionPreference::Animated);
let model = MotionModel::spring(spring);
let mut sequence = MotionSequence::new();
sequence.append("spring", model).append("next", model);
assert_eq!(
sequence.steps()[0].duration_hint(),
spring.physics().review_duration()
);
assert_eq!(
sequence.steps()[1].start_at(),
spring.physics().review_duration()
);
assert_eq!(spring.preset(), Some(MotionSpringPreset::Layout));
}
}