Skip to main content

whisker_css/shorthand/
animation.rs

1//! `animation` shorthand — bundles the eight animation longhands
2//! into one declaration. Multiple animations are comma-separated.
3
4use core::fmt;
5
6use crate::css::Css;
7use crate::data_type::Time;
8use crate::data_type_ext::EasingFunction;
9use crate::keyword::{
10    AnimationDirection, AnimationFillMode, AnimationIterationCount, AnimationPlayState,
11};
12use crate::to_css::ToCss;
13
14/// One animation layer.
15#[derive(Clone, Debug, PartialEq)]
16pub struct Animation {
17    /// `@keyframes` name.
18    pub name: String,
19    /// Duration of one cycle.
20    pub duration: Option<Time>,
21    /// Timing function.
22    pub timing: Option<EasingFunction>,
23    /// Delay before the animation starts.
24    pub delay: Option<Time>,
25    /// How many cycles to run.
26    pub iteration_count: Option<AnimationIterationCount>,
27    /// Direction (forward, reverse, alternating).
28    pub direction: Option<AnimationDirection>,
29    /// Fill mode before/after the active period.
30    pub fill_mode: Option<AnimationFillMode>,
31    /// Play state.
32    pub play_state: Option<AnimationPlayState>,
33}
34
35impl Animation {
36    /// Start with the `@keyframes` name.
37    pub fn new(name: impl Into<String>) -> Self {
38        Self {
39            name: name.into(),
40            duration: None,
41            timing: None,
42            delay: None,
43            iteration_count: None,
44            direction: None,
45            fill_mode: None,
46            play_state: None,
47        }
48    }
49
50    /// Set duration.
51    pub fn duration(mut self, d: Time) -> Self {
52        self.duration = Some(d);
53        self
54    }
55
56    /// Set timing function.
57    pub fn timing(mut self, t: EasingFunction) -> Self {
58        self.timing = Some(t);
59        self
60    }
61
62    /// Set delay.
63    pub fn delay(mut self, d: Time) -> Self {
64        self.delay = Some(d);
65        self
66    }
67
68    /// Set iteration count.
69    pub fn iteration_count(mut self, c: AnimationIterationCount) -> Self {
70        self.iteration_count = Some(c);
71        self
72    }
73
74    /// Set direction.
75    pub fn direction(mut self, d: AnimationDirection) -> Self {
76        self.direction = Some(d);
77        self
78    }
79
80    /// Set fill mode.
81    pub fn fill_mode(mut self, f: AnimationFillMode) -> Self {
82        self.fill_mode = Some(f);
83        self
84    }
85
86    /// Set play state.
87    pub fn play_state(mut self, p: AnimationPlayState) -> Self {
88        self.play_state = Some(p);
89        self
90    }
91}
92
93impl ToCss for Animation {
94    fn to_css(&self, dest: &mut dyn fmt::Write) -> fmt::Result {
95        dest.write_str(&self.name)?;
96        // The CSS animation shorthand grammar allows any order; we
97        // emit a stable order matching the Lynx spec for clarity.
98        if let Some(d) = &self.duration {
99            dest.write_char(' ')?;
100            d.to_css(dest)?;
101        }
102        if let Some(t) = &self.timing {
103            dest.write_char(' ')?;
104            t.to_css(dest)?;
105        }
106        if let Some(d) = &self.delay {
107            dest.write_char(' ')?;
108            d.to_css(dest)?;
109        }
110        if let Some(c) = &self.iteration_count {
111            dest.write_char(' ')?;
112            c.to_css(dest)?;
113        }
114        if let Some(d) = &self.direction {
115            dest.write_char(' ')?;
116            d.to_css(dest)?;
117        }
118        if let Some(f) = &self.fill_mode {
119            dest.write_char(' ')?;
120            f.to_css(dest)?;
121        }
122        if let Some(p) = &self.play_state {
123            dest.write_char(' ')?;
124            p.to_css(dest)?;
125        }
126        Ok(())
127    }
128}
129
130impl Css {
131    /// Sets the `animation` shorthand for a single animation.
132    /// <https://lynxjs.org/api/css/properties/animation>
133    pub fn animation(self, a: Animation) -> Self {
134        self.push("animation", a)
135    }
136
137    /// Sets the `animation` shorthand for multiple comma-separated
138    /// animations.
139    pub fn animations(self, anims: impl IntoIterator<Item = Animation>) -> Self {
140        let mut s = String::new();
141        for (i, a) in anims.into_iter().enumerate() {
142            if i > 0 {
143                s.push_str(", ");
144            }
145            let _ = a.to_css(&mut s);
146        }
147        self.push_raw("animation", s)
148    }
149}
150
151#[cfg(test)]
152mod tests {
153    use crate::data_type_ext::EasingFunction;
154    use crate::ext::*;
155    use crate::keyword::*;
156    use crate::Css;
157
158    use super::*;
159
160    #[test]
161    fn animation_name_only() {
162        let s = Css::new().animation(Animation::new("spin"));
163        assert_eq!(s.to_string(), "animation: spin;");
164    }
165
166    #[test]
167    fn animation_full_shorthand() {
168        let s = Css::new().animation(
169            Animation::new("spin")
170                .duration(1.s())
171                .timing(EasingFunction::Linear)
172                .delay(100.ms())
173                .iteration_count(AnimationIterationCount::Infinite)
174                .direction(AnimationDirection::Alternate)
175                .fill_mode(AnimationFillMode::Forwards)
176                .play_state(AnimationPlayState::Running),
177        );
178        assert_eq!(
179            s.to_string(),
180            "animation: spin 1s linear 100ms infinite alternate forwards running;"
181        );
182    }
183
184    #[test]
185    fn animations_multiple() {
186        let s = Css::new().animations([
187            Animation::new("fade").duration(300.ms()),
188            Animation::new("slide").duration(500.ms()).delay(100.ms()),
189        ]);
190        assert_eq!(s.to_string(), "animation: fade 300ms, slide 500ms 100ms;");
191    }
192}