aura-anim-iced 0.2.1

Iced-first animation primitives.
Documentation
use std::time::Instant;

use super::{EffectSnapshot, effect_snapshot, should_subscribe, subscription, update_tick};
use crate::{
    keyframes::KeyframesBuilder,
    property::{
        BACKGROUND, BORDER_COLOR, HEIGHT, OPACITY, PADDING, PropertyEntry, PropertySnapshot,
        RADIUS, SCALE, SHADOW, TEXT_COLOR, WIDTH,
    },
    runtime::{AnimationRuntime, AnimationTargetId},
    timing::{Duration, Timing},
};

fn animation_tick(_: Instant) {}

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

    assert!(!should_subscribe(&runtime));
    let _idle = subscription(&runtime, animation_tick);

    runtime.register_keyframes(
        target,
        KeyframesBuilder::new()
            .with_timing(Timing::new(100.0))
            .opacity(0.0, 0.0)
            .opacity(1.0, 1.0)
            .finish(),
    );

    assert!(should_subscribe(&runtime));
    let _active = subscription(&runtime, animation_tick);

    runtime.pause_target(target);
    assert!(!should_subscribe(&runtime));
}

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

    runtime.register_keyframes(
        target,
        KeyframesBuilder::new()
            .with_timing(Timing::new(100.0))
            .opacity(0.0, 0.0)
            .opacity(1.0, 1.0)
            .finish(),
    );
    runtime.clock_mut().set_now(Duration::from_millis(50.0));

    let tick = update_tick(&mut runtime, Instant::now());

    assert_eq!(tick.timestamp(), Duration::from_millis(50.0));
    assert!(tick.properties_for(target).is_some());
}

#[test]
fn effect_snapshot_extracts_all_supported_builtin_effects() {
    let background = iced::Color::from_rgb(0.1, 0.2, 0.3);
    let border = iced::Color::from_rgb(0.4, 0.5, 0.6);
    let text = iced::Color::from_rgb(0.7, 0.8, 0.9);
    let shadow = iced::Shadow {
        color: iced::Color::BLACK,
        offset: iced::Vector::new(2.0, 4.0),
        blur_radius: 12.0,
    };
    let properties = PropertySnapshot::from(vec![
        PropertyEntry::new(OPACITY, 0.75),
        PropertyEntry::new(WIDTH, 120.0),
        PropertyEntry::new(HEIGHT, 48.0),
        PropertyEntry::new(PADDING, 16.0),
        PropertyEntry::new(SCALE, 1.2),
        PropertyEntry::new(RADIUS, 8.0),
        PropertyEntry::new(BACKGROUND, background),
        PropertyEntry::new(BORDER_COLOR, border),
        PropertyEntry::new(TEXT_COLOR, text),
        PropertyEntry::new(SHADOW, shadow),
    ]);

    let effects = effect_snapshot(&properties);

    assert_eq!(effects.opacity, Some(0.75));
    assert_eq!(effects.width, Some(120.0));
    assert_eq!(effects.height, Some(48.0));
    assert_eq!(effects.padding, Some(16.0));
    assert_eq!(effects.scale, Some(1.2));
    assert_eq!(effects.radius, Some(8.0));
    assert_eq!(effects.background, Some(background));
    assert_eq!(effects.border_color, Some(border));
    assert_eq!(effects.text_color, Some(text));
    assert_eq!(effects.shadow, Some(shadow));
    assert!(effects.translation.is_none());
}

#[test]
fn effect_snapshot_ignores_wrong_value_shapes_and_unknown_properties() {
    let wrong = PropertySnapshot::from(vec![PropertyEntry::new(
        BACKGROUND,
        iced::Color::from_rgb(0.1, 0.2, 0.3),
    )]);
    let mut wrong_shape = PropertySnapshot::from(vec![PropertyEntry::new(OPACITY, 0.5)]);
    wrong_shape.merge(wrong);

    assert_eq!(effect_snapshot(&wrong_shape).opacity, Some(0.5));

    let properties = PropertySnapshot::from(vec![PropertyEntry::new(
        crate::property::PropertySpec::<crate::property::Size>::new(
            crate::property::PropertyKey::new("test", "opacity-like-size"),
            OPACITY.raw().composition_order(),
        ),
        iced::Size::new(1.0, 2.0),
    )]);

    assert!(effect_snapshot(&properties).is_empty());
}

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

    runtime.register_keyframes(
        first,
        KeyframesBuilder::new()
            .with_timing(Timing::new(100.0))
            .opacity(0.0, 0.0)
            .opacity(1.0, 1.0)
            .finish(),
    );
    runtime.register_keyframes(
        second,
        KeyframesBuilder::new()
            .with_timing(Timing::new(100.0))
            .scale(0.0, 1.0)
            .scale(1.0, 2.0)
            .finish(),
    );
    runtime.clock_mut().set_now(Duration::from_millis(50.0));

    let tick = runtime.tick();
    let first_effects = EffectSnapshot::from_tick_for(&tick, first);
    let second_effects = EffectSnapshot::from_tick_for(&tick, second);
    let missing = EffectSnapshot::from_tick_for(&tick, AnimationTargetId::new());

    assert_eq!(first_effects.opacity, Some(0.5));
    assert_eq!(first_effects.scale, None);
    assert_eq!(second_effects.opacity, None);
    assert_eq!(second_effects.scale, Some(1.5));
    assert!(missing.is_empty());
}

#[test]
fn empty_effect_snapshot_reports_empty() {
    assert!(EffectSnapshot::default().is_empty());
    assert!(EffectSnapshot::from_properties(&PropertySnapshot::new()).is_empty());
    assert!(
        !EffectSnapshot {
            opacity: Some(1.0),
            ..EffectSnapshot::default()
        }
        .is_empty()
    );
}