Skip to main content

hypersig_style/
keyframes.rs

1//! [`Keyframes`], an `@keyframes` rule that a [`Style`] animates with.
2
3use std::fmt::Write as _;
4
5use crate::condition::Conditions;
6use crate::style::Style;
7
8/// The steps of an animation, written as `@keyframes <name>`.
9///
10/// [`Style::keyframes`] sets an element's `animation-name` to it and carries
11/// the rule along, so the page or the patch that brings the element also
12/// brings the rule. Two `Keyframes` with the same name are one rule to the
13/// browser, and the later one in the stylesheet wins; nothing checks that
14/// each name is used once.
15///
16/// ```
17/// use hypersig_style::value::{AnimationIterationCount, Transform, deg, ms, raw};
18/// use hypersig_style::{Keyframes, Sheet, Style};
19///
20/// let spin = Keyframes::new("spin")
21///     .at(0, Style::new().transform(raw("rotate(0deg)")))
22///     .at(100, Style::new().transform(raw("rotate(360deg)")));
23/// let spinner = Style::new()
24///     .keyframes(&spin)
25///     .animation_duration(ms(800))
26///     .animation_iteration_count(AnimationIterationCount::Infinite);
27/// let mut sheet = Sheet::new();
28/// sheet.add(&spinner);
29/// assert!(sheet.to_css().contains("@keyframes spin{0%{transform:rotate(0deg)}100%{transform:rotate(360deg)}}"));
30/// # let _ = (Transform::None, deg(0));
31/// ```
32#[derive(Debug, Clone, PartialEq)]
33pub struct Keyframes {
34    name: &'static str,
35    steps: Vec<(u8, Style)>,
36}
37
38impl Keyframes {
39    /// Keyframes named `name`, with no steps yet.
40    ///
41    /// # Panics
42    ///
43    /// Panics if `name` is empty, starts with a digit, or holds a character
44    /// other than an ASCII letter, digit, `-`, or `_`, which an
45    /// `animation-name` would need quoted.
46    #[must_use]
47    pub fn new(name: &'static str) -> Self {
48        assert!(
49            name.bytes().next().is_some_and(|b| !b.is_ascii_digit())
50                && name
51                    .bytes()
52                    .all(|b| b.is_ascii_alphanumeric() || b == b'-' || b == b'_'),
53            "a keyframes name is an identifier of ASCII letters, digits, `-`, and `_`"
54        );
55        Self {
56            name,
57            steps: Vec::new(),
58        }
59    }
60
61    /// Adds the declarations of `style` at `percent` of the animation.
62    ///
63    /// Only the unconditioned declarations of `style` are written, because a
64    /// keyframe takes no selector or media query.
65    ///
66    /// # Panics
67    ///
68    /// Panics if `percent` is over 100.
69    #[must_use]
70    pub fn at(mut self, percent: u8, style: Style) -> Self {
71        assert!(percent <= 100, "a keyframe is at 0 to 100 percent");
72        self.steps.push((percent, style));
73        self
74    }
75
76    /// The name `animation-name` refers to.
77    #[must_use]
78    pub fn name(&self) -> &'static str {
79        self.name
80    }
81
82    pub(crate) fn to_css(&self) -> String {
83        let mut out = format!("@keyframes {}{{", self.name);
84        for (percent, style) in &self.steps {
85            let _ = write!(out, "{percent}%{{");
86            let mut first = true;
87            for atom in style
88                .atoms()
89                .iter()
90                .filter(|a| a.conditions == Conditions::default())
91            {
92                if !first {
93                    out.push(';');
94                }
95                first = false;
96                atom.property.write_name(&mut out);
97                let _ = write!(out, ":{}", atom.value);
98            }
99            out.push('}');
100        }
101        out.push('}');
102        out
103    }
104}