1use crate::engine::animation::types::{
4 AnimationDirection, AnimationFillMode, AnimationPlayState, AnimationSpec, Easing,
5 IterationCount, StepPosition, TimeMs, TransitionSpec,
6};
7use crate::engine::css::values::{CssValue, Unit};
8
9fn take_number<'a, I: Iterator<Item = &'a CssValue>>(iter: &mut I) -> Option<f32> {
12 match iter.next() {
13 Some(CssValue::Number(n)) => Some(*n),
14 _ => None,
15 }
16}
17
18#[allow(dead_code)]
19fn take_keyword<'a, I: Iterator<Item = &'a CssValue>>(iter: &mut I) -> Option<String> {
20 match iter.next() {
21 Some(CssValue::Keyword(k)) => Some(k.to_ascii_lowercase()),
22 _ => None,
23 }
24}
25
26pub fn parse_time_ms(value: &CssValue) -> Option<TimeMs> {
27 match value {
28 CssValue::Length(v, Unit::S) => Some((*v as f64) * 1000.0),
29 CssValue::Length(v, Unit::Ms) => Some(*v as f64),
30 CssValue::Number(v) if *v == 0.0 => Some(0.0),
31 _ => None,
32 }
33}
34
35pub fn parse_easing(value: &CssValue) -> Option<Easing> {
36 match value {
37 CssValue::Keyword(k) => match k.to_ascii_lowercase().as_str() {
38 "linear" => Some(Easing::Linear),
39 "ease" => Some(Easing::Ease),
40 "ease-in" => Some(Easing::EaseIn),
41 "ease-out" => Some(Easing::EaseOut),
42 "ease-in-out" => Some(Easing::EaseInOut),
43 "step-start" => Some(Easing::Steps(1, StepPosition::JumpStart)),
44 "step-end" => Some(Easing::Steps(1, StepPosition::JumpEnd)),
45 _ => None,
46 },
47 CssValue::Function(name, args) if name.eq_ignore_ascii_case("cubic-bezier") => {
48 let mut iter = args.iter().flatten();
49 Some(Easing::CubicBezier(
50 take_number(&mut iter)?,
51 take_number(&mut iter)?,
52 take_number(&mut iter)?,
53 take_number(&mut iter)?,
54 ))
55 }
56 CssValue::Function(name, args) if name.eq_ignore_ascii_case("steps") => {
57 let mut count: Option<u32> = None;
58 let mut pos = StepPosition::JumpEnd;
59 for arg in args.iter().flatten() {
60 match arg {
61 CssValue::Number(n) if *n >= 0.0 => count = Some(*n as u32),
62 CssValue::Keyword(k) => {
63 pos = match k.to_ascii_lowercase().as_str() {
64 "jump-start" | "start" => StepPosition::JumpStart,
65 "jump-end" | "end" => StepPosition::JumpEnd,
66 "jump-none" => StepPosition::JumpNone,
67 "jump-both" => StepPosition::JumpBoth,
68 _ => pos,
69 };
70 }
71 _ => {}
72 }
73 }
74 Some(Easing::Steps(count.unwrap_or(1).max(1), pos))
75 }
76 _ => None,
77 }
78}
79
80fn split_comma_list(value: &CssValue) -> Vec<Vec<CssValue>> {
81 match value {
82 CssValue::List(items) => {
83 let mut groups: Vec<Vec<CssValue>> = vec![Vec::new()];
84 for item in items {
85 if matches!(item, CssValue::Separator) {
86 groups.push(Vec::new());
87 } else {
88 groups.last_mut().unwrap().push(item.clone());
89 }
90 }
91 groups
92 }
93 other => vec![vec![other.clone()]],
94 }
95}
96
97fn flatten_group(group: &[CssValue]) -> Vec<CssValue> {
98 let mut out = Vec::new();
99 for v in group {
100 match v {
101 CssValue::List(inner) => out.extend(inner.clone()),
102 _ => out.push(v.clone()),
103 }
104 }
105 out
106}
107
108pub fn parse_transition_property_list(value: &CssValue) -> Vec<String> {
109 split_comma_list(value)
110 .into_iter()
111 .map(|g| {
112 flatten_group(&g)
113 .into_iter()
114 .find_map(|v| match v {
115 CssValue::Keyword(k) => Some(k.to_string()),
116 CssValue::String(s) => Some(s),
117 _ => None,
118 })
119 .unwrap_or_else(|| "all".to_string())
120 })
121 .collect()
122}
123
124pub fn parse_time_list(value: &CssValue) -> Vec<TimeMs> {
125 split_comma_list(value)
126 .into_iter()
127 .map(|g| {
128 flatten_group(&g)
129 .into_iter()
130 .find_map(|v| parse_time_ms(&v))
131 .unwrap_or(0.0)
132 })
133 .collect()
134}
135
136pub fn parse_easing_list(value: &CssValue) -> Vec<Easing> {
137 split_comma_list(value)
138 .into_iter()
139 .map(|g| {
140 flatten_group(&g)
141 .into_iter()
142 .find_map(|v| parse_easing(&v))
143 .unwrap_or(Easing::Ease)
144 })
145 .collect()
146}
147
148pub fn parse_iteration_count(value: &CssValue) -> Option<IterationCount> {
149 match value {
150 CssValue::Keyword(k) if k.eq_ignore_ascii_case("infinite") => {
151 Some(IterationCount::Infinite)
152 }
153 CssValue::Number(n) if *n >= 0.0 => Some(IterationCount::Finite(*n)),
154 _ => None,
155 }
156}
157
158pub fn parse_iteration_count_list(value: &CssValue) -> Vec<IterationCount> {
159 split_comma_list(value)
160 .into_iter()
161 .map(|g| {
162 flatten_group(&g)
163 .into_iter()
164 .find_map(|v| parse_iteration_count(&v))
165 .unwrap_or(IterationCount::Finite(1.0))
166 })
167 .collect()
168}
169
170pub fn parse_direction(value: &CssValue) -> Option<AnimationDirection> {
171 match value {
172 CssValue::Keyword(k) => match k.to_ascii_lowercase().as_str() {
173 "normal" => Some(AnimationDirection::Normal),
174 "reverse" => Some(AnimationDirection::Reverse),
175 "alternate" => Some(AnimationDirection::Alternate),
176 "alternate-reverse" => Some(AnimationDirection::AlternateReverse),
177 _ => None,
178 },
179 _ => None,
180 }
181}
182
183pub fn parse_direction_list(value: &CssValue) -> Vec<AnimationDirection> {
184 split_comma_list(value)
185 .into_iter()
186 .map(|g| {
187 flatten_group(&g)
188 .into_iter()
189 .find_map(|v| parse_direction(&v))
190 .unwrap_or_default()
191 })
192 .collect()
193}
194
195pub fn parse_fill_mode(value: &CssValue) -> Option<AnimationFillMode> {
196 match value {
197 CssValue::Keyword(k) => match k.to_ascii_lowercase().as_str() {
198 "none" => Some(AnimationFillMode::None),
199 "forwards" => Some(AnimationFillMode::Forwards),
200 "backwards" => Some(AnimationFillMode::Backwards),
201 "both" => Some(AnimationFillMode::Both),
202 _ => None,
203 },
204 _ => None,
205 }
206}
207
208pub fn parse_fill_mode_list(value: &CssValue) -> Vec<AnimationFillMode> {
209 split_comma_list(value)
210 .into_iter()
211 .map(|g| {
212 flatten_group(&g)
213 .into_iter()
214 .find_map(|v| parse_fill_mode(&v))
215 .unwrap_or_default()
216 })
217 .collect()
218}
219
220pub fn parse_play_state(value: &CssValue) -> Option<AnimationPlayState> {
221 match value {
222 CssValue::Keyword(k) => match k.to_ascii_lowercase().as_str() {
223 "running" => Some(AnimationPlayState::Running),
224 "paused" => Some(AnimationPlayState::Paused),
225 _ => None,
226 },
227 _ => None,
228 }
229}
230
231pub fn parse_play_state_list(value: &CssValue) -> Vec<AnimationPlayState> {
232 split_comma_list(value)
233 .into_iter()
234 .map(|g| {
235 flatten_group(&g)
236 .into_iter()
237 .find_map(|v| parse_play_state(&v))
238 .unwrap_or_default()
239 })
240 .collect()
241}
242
243pub fn parse_animation_name_list(value: &CssValue) -> Vec<String> {
244 split_comma_list(value)
245 .into_iter()
246 .map(|g| {
247 flatten_group(&g)
248 .into_iter()
249 .find_map(|v| match v {
250 CssValue::Keyword(k) => Some(k.to_string()),
251 CssValue::String(s) => Some(s),
252 _ => None,
253 })
254 .unwrap_or_else(|| "none".to_string())
255 })
256 .collect()
257}
258
259fn list_nth<T: Clone>(list: &[T], i: usize, default: T) -> T {
260 if list.is_empty() {
261 default
262 } else {
263 list.get(i % list.len()).cloned().unwrap_or(default)
264 }
265}
266
267#[derive(Debug, Clone, Default)]
270pub struct TransitionInputs {
271 pub property: Option<CssValue>,
272 pub duration: Option<CssValue>,
273 pub delay: Option<CssValue>,
274 pub timing_function: Option<CssValue>,
275}
276
277pub fn build_transition_specs(input: &TransitionInputs) -> Vec<TransitionSpec> {
278 let properties = input
279 .property
280 .as_ref()
281 .map(parse_transition_property_list)
282 .unwrap_or_else(|| vec!["all".to_string()]);
283 let durations = input
284 .duration
285 .as_ref()
286 .map(parse_time_list)
287 .unwrap_or_else(|| vec![0.0]);
288 let delays = input
289 .delay
290 .as_ref()
291 .map(parse_time_list)
292 .unwrap_or_else(|| vec![0.0]);
293 let easings = input
294 .timing_function
295 .as_ref()
296 .map(parse_easing_list)
297 .unwrap_or_else(|| vec![Easing::Ease]);
298
299 let count = properties.len().max(1);
300 (0..count)
301 .map(|i| TransitionSpec {
302 property: list_nth(&properties, i, "all".to_string()),
303 duration_ms: list_nth(&durations, i, 0.0),
304 delay_ms: list_nth(&delays, i, 0.0),
305 easing: list_nth(&easings, i, Easing::Ease),
306 })
307 .collect()
308}
309
310#[derive(Debug, Clone, Default)]
312pub struct AnimationInputs {
313 pub name: Option<CssValue>,
314 pub duration: Option<CssValue>,
315 pub delay: Option<CssValue>,
316 pub timing_function: Option<CssValue>,
317 pub iteration_count: Option<CssValue>,
318 pub direction: Option<CssValue>,
319 pub fill_mode: Option<CssValue>,
320 pub play_state: Option<CssValue>,
321}
322
323pub fn build_animation_specs(input: &AnimationInputs) -> Vec<AnimationSpec> {
324 let names = input
325 .name
326 .as_ref()
327 .map(parse_animation_name_list)
328 .unwrap_or_else(|| vec!["none".to_string()]);
329 let durations = input
330 .duration
331 .as_ref()
332 .map(parse_time_list)
333 .unwrap_or_else(|| vec![0.0]);
334 let delays = input
335 .delay
336 .as_ref()
337 .map(parse_time_list)
338 .unwrap_or_else(|| vec![0.0]);
339 let easings = input
340 .timing_function
341 .as_ref()
342 .map(parse_easing_list)
343 .unwrap_or_else(|| vec![Easing::Ease]);
344 let iterations = input
345 .iteration_count
346 .as_ref()
347 .map(parse_iteration_count_list)
348 .unwrap_or_else(|| vec![IterationCount::Finite(1.0)]);
349 let directions = input
350 .direction
351 .as_ref()
352 .map(parse_direction_list)
353 .unwrap_or_else(|| vec![AnimationDirection::Normal]);
354 let fills = input
355 .fill_mode
356 .as_ref()
357 .map(parse_fill_mode_list)
358 .unwrap_or_else(|| vec![AnimationFillMode::None]);
359 let states = input
360 .play_state
361 .as_ref()
362 .map(parse_play_state_list)
363 .unwrap_or_else(|| vec![AnimationPlayState::Running]);
364
365 let count = names.len().max(1);
366 (0..count)
367 .map(|i| AnimationSpec {
368 name: list_nth(&names, i, "none".to_string()),
369 duration_ms: list_nth(&durations, i, 0.0),
370 delay_ms: list_nth(&delays, i, 0.0),
371 easing: list_nth(&easings, i, Easing::Ease),
372 iteration_count: list_nth(&iterations, i, IterationCount::Finite(1.0)),
373 direction: list_nth(&directions, i, AnimationDirection::Normal),
374 fill_mode: list_nth(&fills, i, AnimationFillMode::None),
375 play_state: list_nth(&states, i, AnimationPlayState::Running),
376 })
377 .collect()
378}
379
380#[cfg(test)]
381mod tests {
382 use super::*;
383
384 #[test]
385 fn parse_easing_step_start() {
386 let v = CssValue::Keyword("step-start".into());
387 assert_eq!(
388 parse_easing(&v),
389 Some(Easing::Steps(1, StepPosition::JumpStart))
390 );
391 }
392
393 #[test]
394 fn parse_easing_step_end() {
395 let v = CssValue::Keyword("step-end".into());
396 assert_eq!(
397 parse_easing(&v),
398 Some(Easing::Steps(1, StepPosition::JumpEnd))
399 );
400 }
401
402 #[test]
403 fn parse_easing_steps_jump_end() {
404 let v = CssValue::Function(
405 "steps".into(),
406 vec![vec![
407 CssValue::Number(4.0),
408 CssValue::Keyword("jump-end".into()),
409 ]],
410 );
411 assert_eq!(
412 parse_easing(&v),
413 Some(Easing::Steps(4, StepPosition::JumpEnd))
414 );
415 }
416
417 #[test]
418 fn parse_easing_steps_jump_start() {
419 let v = CssValue::Function(
420 "steps".into(),
421 vec![vec![
422 CssValue::Number(3.0),
423 CssValue::Keyword("jump-start".into()),
424 ]],
425 );
426 assert_eq!(
427 parse_easing(&v),
428 Some(Easing::Steps(3, StepPosition::JumpStart))
429 );
430 }
431
432 #[test]
433 fn parse_easing_steps_jump_none() {
434 let v = CssValue::Function(
435 "steps".into(),
436 vec![vec![
437 CssValue::Number(5.0),
438 CssValue::Keyword("jump-none".into()),
439 ]],
440 );
441 assert_eq!(
442 parse_easing(&v),
443 Some(Easing::Steps(5, StepPosition::JumpNone))
444 );
445 }
446
447 #[test]
448 fn parse_easing_steps_jump_both() {
449 let v = CssValue::Function(
450 "steps".into(),
451 vec![vec![
452 CssValue::Number(2.0),
453 CssValue::Keyword("jump-both".into()),
454 ]],
455 );
456 assert_eq!(
457 parse_easing(&v),
458 Some(Easing::Steps(2, StepPosition::JumpBoth))
459 );
460 }
461
462 #[test]
463 fn parse_easing_steps_defaults_to_jump_end() {
464 let v = CssValue::Function("steps".into(), vec![vec![CssValue::Number(3.0)]]);
465 assert_eq!(
466 parse_easing(&v),
467 Some(Easing::Steps(3, StepPosition::JumpEnd))
468 );
469 }
470
471 #[test]
472 fn parse_easing_steps_clamp_zero_to_one() {
473 let v = CssValue::Function("steps".into(), vec![vec![CssValue::Number(0.0)]]);
474 assert_eq!(
475 parse_easing(&v),
476 Some(Easing::Steps(1, StepPosition::JumpEnd))
477 );
478 }
479}