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}