Skip to main content

orinium_browser/engine/animation/
parse.rs

1//! Parsing helpers for CSS transition/animation values.
2
3use crate::engine::animation::types::{
4    AnimationDirection, AnimationFillMode, AnimationPlayState, AnimationSpec, Easing,
5    IterationCount, StepPosition, TimeMs, TransitionSpec,
6};
7use crate::engine::css::values::{CssValue, Unit};
8
9/// CSS ファンクションの引数リストから、順番に指定された型の値を取り出すヘルパー。
10/// 各引数は `CssValue` のバリアントにマッチし、不一致の場合は `None` を返す。
11fn 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/// Build transition specs from resolved longhand/shorthand declaration values.
268/// Callers should pass the winning values for each transition-* property.
269#[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/// Build animation specs from resolved animation-* declaration values.
311#[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}