aura-anim-iced 0.2.1

Iced-first animation primitives.
Documentation
use float_cmp::assert_approx_eq;

use super::{
    AnimationClock, AnimationPlaybackState, AnimationRuntime, AnimationTargetId, AnimationTick,
    TestClock, TickPolicy,
};
use crate::{
    keyframes::{Keyframes, KeyframesBuilder},
    property::{OPACITY, PropertySnapshot, PropertyValue, SCALE, WIDTH},
    timeline::{Timeline, Track},
    timing::{Direction, Duration, Timing},
};

fn keyframes(
    spec: crate::property::PropertySpec<crate::property::Scalar>,
    from: f32,
    to: f32,
) -> Keyframes {
    KeyframesBuilder::new()
        .with_timing(Timing::new(100.0))
        .at(0.0, (spec, from))
        .at(1.0, (spec, to))
        .finish()
}

fn scalar(
    snapshot: &PropertySnapshot,
    spec: crate::property::PropertySpec<crate::property::Scalar>,
) -> f32 {
    let Some(entry) = snapshot.find_property(&spec.raw()) else {
        panic!("expected scalar property {}", spec.raw().key().name());
    };
    let PropertyValue::Scalar(value) = entry.value() else {
        panic!("expected scalar value");
    };

    *value
}

#[test]
fn runtime_stores_clock_policy_and_reports_idle_state() {
    let clock = TestClock::at(Duration::from_millis(250.0));
    let mut runtime = AnimationRuntime::with_clock(clock);

    assert!(runtime.is_idle());
    assert_eq!(runtime.active_count(), 0);
    assert_eq!(runtime.clock().now(), Duration::from_millis(250.0));
    assert!(!runtime.should_subscribe());

    let policy = TickPolicy::new(Duration::from_millis(33.0));
    runtime.set_motion_policy(policy);

    assert_eq!(runtime.motion_policy(), policy);
}

#[test]
fn registration_returns_handle_initial_snapshot_and_completion_for_zero_duration() {
    let mut runtime = AnimationRuntime::with_clock(TestClock::at(Duration::from_millis(10.0)));
    let target = AnimationTargetId::new();

    let registration = runtime.register_keyframes(
        target,
        KeyframesBuilder::new()
            .opacity(0.0, 0.0)
            .opacity(1.0, 1.0)
            .finish(),
    );

    assert!(registration.handle().id() > 0);
    assert_eq!(registration.state(), AnimationPlaybackState::Completed);
    assert_eq!(
        registration.completed_at(),
        Some(Duration::from_millis(10.0))
    );
    assert_approx_eq!(
        f32,
        scalar(
            registration.properties().expect("initial properties"),
            OPACITY
        ),
        1.0,
        epsilon = 1e-5
    );
    assert_eq!(runtime.active_count(), 0);
    assert!(runtime.is_idle());
}

#[test]
fn tick_keeps_property_composition_scoped_to_each_target() {
    let mut runtime = AnimationRuntime::testing();
    let first = AnimationTargetId::new();
    let second = AnimationTargetId::new();

    runtime.register_keyframes(first, keyframes(OPACITY, 0.0, 1.0));
    runtime.register_keyframes(first, keyframes(SCALE, 1.0, 2.0));
    runtime.register_keyframes(second, keyframes(OPACITY, 10.0, 20.0));

    runtime.clock_mut().set_now(Duration::from_millis(50.0));
    let tick = runtime.tick();

    let first_snapshot = tick.properties_for(first).expect("first target");
    let second_snapshot = tick.properties_for(second).expect("second target");

    assert_eq!(tick.properties().targets().count(), 2);
    assert_approx_eq!(f32, scalar(first_snapshot, OPACITY), 0.5, epsilon = 1e-5);
    assert_approx_eq!(f32, scalar(first_snapshot, SCALE), 1.5, epsilon = 1e-5);
    assert_approx_eq!(f32, scalar(second_snapshot, OPACITY), 15.0, epsilon = 1e-5);
    assert_eq!(second_snapshot.find_property(&SCALE.raw()), None);
}

#[test]
fn tick_into_reuses_output_and_matches_tick_shape() {
    let mut runtime = AnimationRuntime::testing();
    let target = AnimationTargetId::new();
    let mut output = AnimationTick::empty();

    runtime.register_keyframes(target, keyframes(OPACITY, 0.0, 1.0));

    runtime.clock_mut().set_now(Duration::from_millis(40.0));
    runtime.tick_into(&mut output);

    assert_eq!(output.timestamp(), Duration::from_millis(40.0));
    assert_approx_eq!(
        f32,
        scalar(
            output.properties_for(target).expect("target output"),
            OPACITY
        ),
        0.4,
        epsilon = 1e-5
    );
    assert!(output.completed().is_empty());

    runtime.clock_mut().set_now(Duration::from_millis(100.0));
    runtime.tick_into(&mut output);

    assert_eq!(output.completed().len(), 1);
    assert_approx_eq!(
        f32,
        scalar(
            output.properties_for(target).expect("target output"),
            OPACITY
        ),
        1.0,
        epsilon = 1e-5
    );

    runtime.tick_into(&mut output);

    assert!(output.is_empty());
    assert!(output.completed().is_empty());
}

#[test]
fn completion_removes_only_finished_entries_and_reports_completed_handles() {
    let mut runtime = AnimationRuntime::testing();
    let target = AnimationTargetId::new();
    let short = runtime.register_keyframes(target, keyframes(OPACITY, 0.0, 1.0));
    let long = runtime.register_timeline(
        target,
        Timeline::track(
            Track::from(SCALE, 1.0)
                .to(2.0)
                .duration(Duration::from_millis(200.0)),
        ),
    );

    runtime.clock_mut().set_now(Duration::from_millis(100.0));
    let tick = runtime.tick();

    assert_eq!(tick.completed(), &[short.handle()]);
    assert_eq!(runtime.active_count(), 1);
    assert!(runtime.should_subscribe());

    let snapshot = tick.properties_for(target).expect("target output");
    assert_approx_eq!(f32, scalar(snapshot, OPACITY), 1.0, epsilon = 1e-5);
    assert_approx_eq!(f32, scalar(snapshot, SCALE), 1.5, epsilon = 1e-5);

    runtime.clock_mut().set_now(Duration::from_millis(200.0));
    let final_tick = runtime.tick();

    assert_eq!(final_tick.completed(), &[long.handle()]);
    assert_eq!(runtime.active_count(), 0);
    assert!(!runtime.should_subscribe());
}

#[test]
fn completion_output_respects_keyframe_direction() {
    let mut runtime = AnimationRuntime::testing();
    let target = AnimationTargetId::new();
    let registration = runtime.register_keyframes(
        target,
        KeyframesBuilder::new()
            .with_timing(Timing::new(100.0).with_direction(Direction::Reverse))
            .opacity(0.0, 0.0)
            .opacity(1.0, 1.0)
            .finish(),
    );

    runtime.clock_mut().set_now(Duration::from_millis(100.0));
    let tick = runtime.tick();
    let snapshot = tick.properties_for(target).expect("target output");

    assert_eq!(tick.completed(), &[registration.handle()]);
    assert_approx_eq!(f32, scalar(snapshot, OPACITY), 0.0, epsilon = 1e-5);
}

#[test]
fn pause_target_freezes_position_and_disables_subscription_gate() {
    let mut runtime = AnimationRuntime::testing();
    let target = AnimationTargetId::new();

    runtime.register_keyframes(target, keyframes(OPACITY, 0.0, 1.0));
    runtime.clock_mut().set_now(Duration::from_millis(40.0));
    let before_pause = runtime.tick();
    assert_approx_eq!(
        f32,
        scalar(
            before_pause.properties_for(target).expect("target output"),
            OPACITY
        ),
        0.4,
        epsilon = 1e-5
    );

    runtime.pause_target(target);
    assert_eq!(runtime.active_count(), 1);
    assert!(!runtime.should_subscribe());

    runtime.clock_mut().set_now(Duration::from_millis(100.0));
    let paused_tick = runtime.tick();

    assert_approx_eq!(
        f32,
        scalar(
            paused_tick.properties_for(target).expect("paused output"),
            OPACITY
        ),
        0.4,
        epsilon = 1e-5
    );
    assert!(paused_tick.completed().is_empty());
}

#[test]
fn seek_target_moves_all_sources_for_target_without_touching_other_targets() {
    let mut runtime = AnimationRuntime::testing();
    let target = AnimationTargetId::new();
    let other = AnimationTargetId::new();

    runtime.register_keyframes(target, keyframes(OPACITY, 0.0, 1.0));
    runtime.register_keyframes(target, keyframes(WIDTH, 100.0, 200.0));
    runtime.register_keyframes(other, keyframes(OPACITY, 10.0, 20.0));

    runtime.seek_target(target, Duration::from_millis(75.0));
    let tick = runtime.tick();

    let target_snapshot = tick.properties_for(target).expect("target output");
    let other_snapshot = tick.properties_for(other).expect("other output");

    assert_approx_eq!(f32, scalar(target_snapshot, OPACITY), 0.75, epsilon = 1e-5);
    assert_approx_eq!(f32, scalar(target_snapshot, WIDTH), 175.0, epsilon = 1e-5);
    assert_approx_eq!(f32, scalar(other_snapshot, OPACITY), 10.0, epsilon = 1e-5);
}

#[test]
fn handle_level_cancel_seek_and_pause_validate_target_ownership() {
    let mut runtime = AnimationRuntime::testing();
    let target = AnimationTargetId::new();
    let wrong_target = AnimationTargetId::new();
    let registration = runtime.register_keyframes(target, keyframes(OPACITY, 0.0, 1.0));

    assert!(!runtime.seek(
        wrong_target,
        registration.handle(),
        Duration::from_millis(50.0)
    ));
    assert!(runtime.seek(target, registration.handle(), Duration::from_millis(50.0)));

    let tick = runtime.tick();
    assert_approx_eq!(
        f32,
        scalar(tick.properties_for(target).expect("target output"), OPACITY),
        0.5,
        epsilon = 1e-5
    );

    assert!(!runtime.pause(wrong_target, registration.handle()));
    assert!(runtime.pause(target, registration.handle()));
    assert!(!runtime.cancel(wrong_target, registration.handle()));
    assert!(runtime.cancel(target, registration.handle()));
    assert_eq!(runtime.active_count(), 0);
}