we (web engine): Experimental web browser project to understand the limits of Claude
2
fork

Configure Feed

Select the types of activity you want to include in your feed.

Implement CSS Animations Level 1: @keyframes rules, animation-* properties, and animation engine

- Parse @keyframes rules with from/to keywords, percentage stops, and
multiple selectors per block (including -webkit-/-moz- prefixes)
- Parse animation shorthand and all 8 longhand properties:
animation-name, animation-duration, animation-timing-function,
animation-delay, animation-iteration-count, animation-direction,
animation-fill-mode, animation-play-state
- Animation engine reuses transition timing functions and interpolation:
cubic-bezier, steps, linear, and all ease-* variants
- Multi-keyframe interpolation across arbitrary percentage stops
- Direction support: normal, reverse, alternate, alternate-reverse
- Fill modes: none, forwards, backwards, both
- Iteration count: finite numbers and infinite
- Pause/resume support with elapsed time tracking
- Animation events: animationstart, animationiteration, animationend
- AnimationMap for per-element animation state management
- Keyframe property resolution with value resolver callback
- Style system integration: AnimationSpec in ComputedStyle, cascade
handling for animation properties at all priority levels
- 40+ new tests covering parsing, keyframe interpolation, timing,
direction, fill modes, pause/resume, events, and animation map

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

+2054 -2
+1665
crates/css/src/animations.rs
··· 1 + //! CSS Animations Level 1: @keyframes rules, animation-* properties, and animation engine. 2 + //! 3 + //! This module provides: 4 + //! - Animation specification types (direction, fill mode, play state, iteration count) 5 + //! - Parsing for `animation` shorthand and longhand properties 6 + //! - Active animation state tracking and keyframe interpolation 7 + //! - Animation event generation (animationstart, animationend, animationiteration) 8 + 9 + use crate::parser::{ComponentValue, Declaration}; 10 + use crate::transitions::{ 11 + is_animatable_property, parse_time_value, parse_timing_function, split_by_comma, 12 + AnimatableValue, TimingFunction, 13 + }; 14 + 15 + // --------------------------------------------------------------------------- 16 + // Animation specification types 17 + // --------------------------------------------------------------------------- 18 + 19 + /// How many times an animation should repeat. 20 + #[derive(Debug, Clone, PartialEq)] 21 + pub enum IterationCount { 22 + /// A finite number of iterations. 23 + Number(f64), 24 + /// Repeat indefinitely. 25 + Infinite, 26 + } 27 + 28 + impl Default for IterationCount { 29 + fn default() -> Self { 30 + IterationCount::Number(1.0) 31 + } 32 + } 33 + 34 + /// The direction an animation plays. 35 + #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] 36 + pub enum AnimationDirection { 37 + /// Play forward each cycle. 38 + #[default] 39 + Normal, 40 + /// Play backward each cycle. 41 + Reverse, 42 + /// Alternate forward/backward each cycle. 43 + Alternate, 44 + /// Alternate backward/forward each cycle. 45 + AlternateReverse, 46 + } 47 + 48 + /// How styles are applied before/after the animation. 49 + #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] 50 + pub enum AnimationFillMode { 51 + /// No fill — styles revert after animation. 52 + #[default] 53 + None, 54 + /// Retain the final keyframe values after the animation ends. 55 + Forwards, 56 + /// Apply the first keyframe values during the delay period. 57 + Backwards, 58 + /// Both forwards and backwards. 59 + Both, 60 + } 61 + 62 + /// Whether the animation is running or paused. 63 + #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] 64 + pub enum AnimationPlayState { 65 + /// Animation is actively running. 66 + #[default] 67 + Running, 68 + /// Animation is paused. 69 + Paused, 70 + } 71 + 72 + /// A single animation specification (one entry in a comma-separated list). 73 + #[derive(Debug, Clone, PartialEq)] 74 + pub struct SingleAnimation { 75 + /// The `@keyframes` name to use. 76 + pub name: String, 77 + /// Duration in seconds. 78 + pub duration: f64, 79 + /// Timing function. 80 + pub timing_function: TimingFunction, 81 + /// Delay in seconds. 82 + pub delay: f64, 83 + /// Number of iterations. 84 + pub iteration_count: IterationCount, 85 + /// Play direction. 86 + pub direction: AnimationDirection, 87 + /// Fill mode. 88 + pub fill_mode: AnimationFillMode, 89 + /// Play state. 90 + pub play_state: AnimationPlayState, 91 + } 92 + 93 + impl Default for SingleAnimation { 94 + fn default() -> Self { 95 + SingleAnimation { 96 + name: String::new(), 97 + duration: 0.0, 98 + timing_function: TimingFunction::Ease, 99 + delay: 0.0, 100 + iteration_count: IterationCount::Number(1.0), 101 + direction: AnimationDirection::Normal, 102 + fill_mode: AnimationFillMode::None, 103 + play_state: AnimationPlayState::Running, 104 + } 105 + } 106 + } 107 + 108 + /// Parsed animation specification for an element, possibly multiple animations. 109 + #[derive(Debug, Clone, PartialEq, Default)] 110 + pub struct AnimationSpec { 111 + pub animations: Vec<SingleAnimation>, 112 + } 113 + 114 + // --------------------------------------------------------------------------- 115 + // Keyframe types 116 + // --------------------------------------------------------------------------- 117 + 118 + /// A single keyframe selector: either a percentage or `from`/`to`. 119 + #[derive(Debug, Clone, PartialEq)] 120 + pub enum KeyframeSelector { 121 + /// A percentage (0.0 to 100.0). 122 + Percentage(f64), 123 + /// `from` keyword (equivalent to 0%). 124 + From, 125 + /// `to` keyword (equivalent to 100%). 126 + To, 127 + } 128 + 129 + impl KeyframeSelector { 130 + /// Convert to a normalized percentage (0.0 to 1.0). 131 + pub fn to_percentage(&self) -> f64 { 132 + match self { 133 + KeyframeSelector::Percentage(p) => *p / 100.0, 134 + KeyframeSelector::From => 0.0, 135 + KeyframeSelector::To => 1.0, 136 + } 137 + } 138 + } 139 + 140 + /// A single keyframe block within a `@keyframes` rule. 141 + #[derive(Debug, Clone, PartialEq)] 142 + pub struct Keyframe { 143 + /// The percentage selectors for this keyframe block (can be multiple: `0%, 100% { ... }`). 144 + pub selectors: Vec<KeyframeSelector>, 145 + /// The declarations in this keyframe block. 146 + pub declarations: Vec<Declaration>, 147 + } 148 + 149 + /// A resolved keyframe stop for a single property: percentage + value. 150 + #[derive(Debug, Clone)] 151 + pub struct ResolvedKeyframeStop { 152 + /// Normalized offset (0.0 to 1.0). 153 + pub offset: f64, 154 + /// The value at this stop. 155 + pub value: AnimatableValue, 156 + } 157 + 158 + // --------------------------------------------------------------------------- 159 + // Parsing: animation shorthand and longhands 160 + // --------------------------------------------------------------------------- 161 + 162 + /// Parse the `animation` shorthand property value. 163 + /// Syntax: `[name || duration || timing-function || delay || iteration-count || 164 + /// direction || fill-mode || play-state] [, ...]` 165 + pub fn parse_animation_shorthand(values: &[ComponentValue]) -> AnimationSpec { 166 + let groups = split_by_comma(values); 167 + let mut animations = Vec::new(); 168 + 169 + for group in groups { 170 + animations.push(parse_single_animation(&group)); 171 + } 172 + 173 + AnimationSpec { animations } 174 + } 175 + 176 + /// Parse `animation-name` value (comma-separated list of names). 177 + pub fn parse_animation_name(values: &[ComponentValue]) -> Vec<String> { 178 + let groups = split_by_comma(values); 179 + let mut result = Vec::new(); 180 + 181 + for group in groups { 182 + let non_ws: Vec<&ComponentValue> = group 183 + .iter() 184 + .copied() 185 + .filter(|cv| !matches!(cv, ComponentValue::Whitespace)) 186 + .collect(); 187 + 188 + if non_ws.len() == 1 { 189 + match non_ws[0] { 190 + ComponentValue::Ident(s) => { 191 + let lower = s.to_ascii_lowercase(); 192 + if lower == "none" { 193 + result.push(String::new()); 194 + } else { 195 + result.push(s.clone()); 196 + } 197 + } 198 + ComponentValue::String(s) => { 199 + result.push(s.clone()); 200 + } 201 + _ => result.push(String::new()), 202 + } 203 + } else { 204 + result.push(String::new()); 205 + } 206 + } 207 + 208 + if result.is_empty() { 209 + result.push(String::new()); 210 + } 211 + 212 + result 213 + } 214 + 215 + /// Parse `animation-duration` value (comma-separated time list). 216 + pub fn parse_animation_duration(values: &[ComponentValue]) -> Vec<f64> { 217 + parse_animation_time_list(values) 218 + } 219 + 220 + /// Parse `animation-delay` value (comma-separated time list). 221 + pub fn parse_animation_delay(values: &[ComponentValue]) -> Vec<f64> { 222 + parse_animation_time_list(values) 223 + } 224 + 225 + /// Parse `animation-timing-function` value (comma-separated list). 226 + pub fn parse_animation_timing_function(values: &[ComponentValue]) -> Vec<TimingFunction> { 227 + let groups = split_by_comma(values); 228 + let mut result = Vec::new(); 229 + 230 + for group in groups { 231 + let non_ws: Vec<&ComponentValue> = group 232 + .iter() 233 + .copied() 234 + .filter(|cv| !matches!(cv, ComponentValue::Whitespace)) 235 + .collect(); 236 + 237 + if non_ws.len() == 1 { 238 + if let Some(tf) = parse_timing_function(non_ws[0]) { 239 + result.push(tf); 240 + continue; 241 + } 242 + } 243 + result.push(TimingFunction::Ease); 244 + } 245 + 246 + if result.is_empty() { 247 + result.push(TimingFunction::Ease); 248 + } 249 + 250 + result 251 + } 252 + 253 + /// Parse `animation-iteration-count` value (comma-separated list). 254 + pub fn parse_animation_iteration_count(values: &[ComponentValue]) -> Vec<IterationCount> { 255 + let groups = split_by_comma(values); 256 + let mut result = Vec::new(); 257 + 258 + for group in groups { 259 + let non_ws: Vec<&ComponentValue> = group 260 + .iter() 261 + .copied() 262 + .filter(|cv| !matches!(cv, ComponentValue::Whitespace)) 263 + .collect(); 264 + 265 + if non_ws.len() == 1 { 266 + match non_ws[0] { 267 + ComponentValue::Ident(s) if s.eq_ignore_ascii_case("infinite") => { 268 + result.push(IterationCount::Infinite); 269 + continue; 270 + } 271 + ComponentValue::Number(n, _) if *n >= 0.0 => { 272 + result.push(IterationCount::Number(*n)); 273 + continue; 274 + } 275 + _ => {} 276 + } 277 + } 278 + result.push(IterationCount::Number(1.0)); 279 + } 280 + 281 + if result.is_empty() { 282 + result.push(IterationCount::Number(1.0)); 283 + } 284 + 285 + result 286 + } 287 + 288 + /// Parse `animation-direction` value (comma-separated list). 289 + pub fn parse_animation_direction(values: &[ComponentValue]) -> Vec<AnimationDirection> { 290 + let groups = split_by_comma(values); 291 + let mut result = Vec::new(); 292 + 293 + for group in groups { 294 + let non_ws: Vec<&ComponentValue> = group 295 + .iter() 296 + .copied() 297 + .filter(|cv| !matches!(cv, ComponentValue::Whitespace)) 298 + .collect(); 299 + 300 + if non_ws.len() == 1 { 301 + if let ComponentValue::Ident(s) = non_ws[0] { 302 + match s.to_ascii_lowercase().as_str() { 303 + "normal" => { 304 + result.push(AnimationDirection::Normal); 305 + continue; 306 + } 307 + "reverse" => { 308 + result.push(AnimationDirection::Reverse); 309 + continue; 310 + } 311 + "alternate" => { 312 + result.push(AnimationDirection::Alternate); 313 + continue; 314 + } 315 + "alternate-reverse" => { 316 + result.push(AnimationDirection::AlternateReverse); 317 + continue; 318 + } 319 + _ => {} 320 + } 321 + } 322 + } 323 + result.push(AnimationDirection::Normal); 324 + } 325 + 326 + if result.is_empty() { 327 + result.push(AnimationDirection::Normal); 328 + } 329 + 330 + result 331 + } 332 + 333 + /// Parse `animation-fill-mode` value (comma-separated list). 334 + pub fn parse_animation_fill_mode(values: &[ComponentValue]) -> Vec<AnimationFillMode> { 335 + let groups = split_by_comma(values); 336 + let mut result = Vec::new(); 337 + 338 + for group in groups { 339 + let non_ws: Vec<&ComponentValue> = group 340 + .iter() 341 + .copied() 342 + .filter(|cv| !matches!(cv, ComponentValue::Whitespace)) 343 + .collect(); 344 + 345 + if non_ws.len() == 1 { 346 + if let ComponentValue::Ident(s) = non_ws[0] { 347 + match s.to_ascii_lowercase().as_str() { 348 + "none" => { 349 + result.push(AnimationFillMode::None); 350 + continue; 351 + } 352 + "forwards" => { 353 + result.push(AnimationFillMode::Forwards); 354 + continue; 355 + } 356 + "backwards" => { 357 + result.push(AnimationFillMode::Backwards); 358 + continue; 359 + } 360 + "both" => { 361 + result.push(AnimationFillMode::Both); 362 + continue; 363 + } 364 + _ => {} 365 + } 366 + } 367 + } 368 + result.push(AnimationFillMode::None); 369 + } 370 + 371 + if result.is_empty() { 372 + result.push(AnimationFillMode::None); 373 + } 374 + 375 + result 376 + } 377 + 378 + /// Parse `animation-play-state` value (comma-separated list). 379 + pub fn parse_animation_play_state(values: &[ComponentValue]) -> Vec<AnimationPlayState> { 380 + let groups = split_by_comma(values); 381 + let mut result = Vec::new(); 382 + 383 + for group in groups { 384 + let non_ws: Vec<&ComponentValue> = group 385 + .iter() 386 + .copied() 387 + .filter(|cv| !matches!(cv, ComponentValue::Whitespace)) 388 + .collect(); 389 + 390 + if non_ws.len() == 1 { 391 + if let ComponentValue::Ident(s) = non_ws[0] { 392 + match s.to_ascii_lowercase().as_str() { 393 + "running" => { 394 + result.push(AnimationPlayState::Running); 395 + continue; 396 + } 397 + "paused" => { 398 + result.push(AnimationPlayState::Paused); 399 + continue; 400 + } 401 + _ => {} 402 + } 403 + } 404 + } 405 + result.push(AnimationPlayState::Running); 406 + } 407 + 408 + if result.is_empty() { 409 + result.push(AnimationPlayState::Running); 410 + } 411 + 412 + result 413 + } 414 + 415 + fn parse_animation_time_list(values: &[ComponentValue]) -> Vec<f64> { 416 + let groups = split_by_comma(values); 417 + let mut result = Vec::new(); 418 + 419 + for group in groups { 420 + let non_ws: Vec<&ComponentValue> = group 421 + .iter() 422 + .copied() 423 + .filter(|cv| !matches!(cv, ComponentValue::Whitespace)) 424 + .collect(); 425 + 426 + if non_ws.len() == 1 { 427 + if let Some(t) = parse_time_value(non_ws[0]) { 428 + result.push(t); 429 + continue; 430 + } 431 + } 432 + result.push(0.0); 433 + } 434 + 435 + if result.is_empty() { 436 + result.push(0.0); 437 + } 438 + 439 + result 440 + } 441 + 442 + fn parse_single_animation(values: &[&ComponentValue]) -> SingleAnimation { 443 + let non_ws: Vec<&ComponentValue> = values 444 + .iter() 445 + .copied() 446 + .filter(|cv| !matches!(cv, ComponentValue::Whitespace)) 447 + .collect(); 448 + 449 + let mut anim = SingleAnimation::default(); 450 + let mut time_count = 0; // First time = duration, second = delay 451 + 452 + for cv in &non_ws { 453 + // Try timing function first (keywords like ease, linear, and functions) 454 + if let Some(tf) = parse_timing_function(cv) { 455 + anim.timing_function = tf; 456 + continue; 457 + } 458 + 459 + // Try time value 460 + if let Some(t) = parse_time_value(cv) { 461 + if time_count == 0 { 462 + anim.duration = t; 463 + } else { 464 + anim.delay = t; 465 + } 466 + time_count += 1; 467 + continue; 468 + } 469 + 470 + if let ComponentValue::Ident(s) = cv { 471 + let lower = s.to_ascii_lowercase(); 472 + // Try animation-specific keywords 473 + match lower.as_str() { 474 + "infinite" => { 475 + anim.iteration_count = IterationCount::Infinite; 476 + continue; 477 + } 478 + "normal" => { 479 + anim.direction = AnimationDirection::Normal; 480 + continue; 481 + } 482 + "reverse" => { 483 + anim.direction = AnimationDirection::Reverse; 484 + continue; 485 + } 486 + "alternate" => { 487 + anim.direction = AnimationDirection::Alternate; 488 + continue; 489 + } 490 + "alternate-reverse" => { 491 + anim.direction = AnimationDirection::AlternateReverse; 492 + continue; 493 + } 494 + "forwards" => { 495 + anim.fill_mode = AnimationFillMode::Forwards; 496 + continue; 497 + } 498 + "backwards" => { 499 + anim.fill_mode = AnimationFillMode::Backwards; 500 + continue; 501 + } 502 + "both" => { 503 + anim.fill_mode = AnimationFillMode::Both; 504 + continue; 505 + } 506 + "running" => { 507 + anim.play_state = AnimationPlayState::Running; 508 + continue; 509 + } 510 + "paused" => { 511 + anim.play_state = AnimationPlayState::Paused; 512 + continue; 513 + } 514 + "none" => { 515 + anim.name = String::new(); 516 + continue; 517 + } 518 + _ => { 519 + // Animation name (custom identifier) 520 + anim.name = s.clone(); 521 + continue; 522 + } 523 + } 524 + } 525 + 526 + // Try iteration count (number) 527 + if let ComponentValue::Number(n, _) = cv { 528 + if *n >= 0.0 { 529 + anim.iteration_count = IterationCount::Number(*n); 530 + } 531 + } 532 + } 533 + 534 + anim 535 + } 536 + 537 + // --------------------------------------------------------------------------- 538 + // Animation state tracking 539 + // --------------------------------------------------------------------------- 540 + 541 + /// Events fired by the animation engine. 542 + #[derive(Debug, Clone, PartialEq)] 543 + pub enum AnimationEvent { 544 + /// Fired when the animation starts (after delay). 545 + Start { 546 + animation_name: String, 547 + elapsed_time: f64, 548 + }, 549 + /// Fired at the end of each iteration (except the last). 550 + Iteration { 551 + animation_name: String, 552 + elapsed_time: f64, 553 + }, 554 + /// Fired when the animation completes. 555 + End { 556 + animation_name: String, 557 + elapsed_time: f64, 558 + }, 559 + } 560 + 561 + /// An active CSS animation on a single element. 562 + #[derive(Debug, Clone)] 563 + pub struct ActiveAnimation { 564 + /// The animation name (from `@keyframes`). 565 + pub name: String, 566 + /// Resolved keyframe stops per property, sorted by offset. 567 + pub keyframes: Vec<(String, Vec<ResolvedKeyframeStop>)>, 568 + /// Start time in seconds (monotonic clock). 569 + pub start_time: f64, 570 + /// Duration of one cycle in seconds. 571 + pub duration: f64, 572 + /// Delay in seconds. 573 + pub delay: f64, 574 + /// Timing function. 575 + pub timing_function: TimingFunction, 576 + /// Number of iterations. 577 + pub iteration_count: IterationCount, 578 + /// Direction. 579 + pub direction: AnimationDirection, 580 + /// Fill mode. 581 + pub fill_mode: AnimationFillMode, 582 + /// Play state. 583 + pub play_state: AnimationPlayState, 584 + /// Time accumulated while paused (to offset from elapsed time). 585 + pub paused_elapsed: f64, 586 + /// Time when the animation was paused (None if not paused). 587 + pub paused_at: Option<f64>, 588 + /// Whether the animationstart event has been fired. 589 + pub started_event_fired: bool, 590 + /// The last iteration for which animationiteration was fired. 591 + pub last_iteration_event: u32, 592 + /// Whether the animation has finished. 593 + pub finished: bool, 594 + } 595 + 596 + impl ActiveAnimation { 597 + /// Create a new active animation. 598 + pub fn new( 599 + name: String, 600 + keyframes: Vec<(String, Vec<ResolvedKeyframeStop>)>, 601 + spec: &SingleAnimation, 602 + now: f64, 603 + ) -> Self { 604 + ActiveAnimation { 605 + name, 606 + keyframes, 607 + start_time: now, 608 + duration: spec.duration, 609 + delay: spec.delay, 610 + timing_function: spec.timing_function.clone(), 611 + iteration_count: spec.iteration_count.clone(), 612 + direction: spec.direction, 613 + fill_mode: spec.fill_mode, 614 + play_state: spec.play_state, 615 + paused_elapsed: 0.0, 616 + paused_at: if spec.play_state == AnimationPlayState::Paused { 617 + Some(now) 618 + } else { 619 + None 620 + }, 621 + started_event_fired: false, 622 + last_iteration_event: 0, 623 + finished: false, 624 + } 625 + } 626 + 627 + /// Get the effective elapsed time accounting for pauses. 628 + fn effective_elapsed(&self, now: f64) -> f64 { 629 + let base = now - self.start_time - self.paused_elapsed; 630 + if let Some(paused_at) = self.paused_at { 631 + // Currently paused: don't count time since pause 632 + base - (now - paused_at) 633 + } else { 634 + base 635 + } 636 + } 637 + 638 + /// Compute the total animation duration (all iterations). 639 + fn total_duration(&self) -> Option<f64> { 640 + match &self.iteration_count { 641 + IterationCount::Number(n) => Some(self.duration * n), 642 + IterationCount::Infinite => None, 643 + } 644 + } 645 + 646 + /// Pause the animation at the given time. 647 + pub fn pause(&mut self, now: f64) { 648 + if self.paused_at.is_none() { 649 + self.paused_at = Some(now); 650 + self.play_state = AnimationPlayState::Paused; 651 + } 652 + } 653 + 654 + /// Resume the animation at the given time. 655 + pub fn resume(&mut self, now: f64) { 656 + if let Some(paused_at) = self.paused_at { 657 + self.paused_elapsed += now - paused_at; 658 + self.paused_at = None; 659 + self.play_state = AnimationPlayState::Running; 660 + } 661 + } 662 + 663 + /// Evaluate the animation at the given time. 664 + /// Returns the current value for each animated property, or None if the animation 665 + /// is not affecting this property at this time. 666 + pub fn evaluate(&self, now: f64) -> Vec<(String, AnimatableValue)> { 667 + let elapsed = self.effective_elapsed(now); 668 + let mut result = Vec::new(); 669 + 670 + if self.duration <= 0.0 { 671 + return result; 672 + } 673 + 674 + // Check if we're in the delay period 675 + if elapsed < self.delay { 676 + // During delay, only apply values if fill-mode is backwards or both 677 + if matches!( 678 + self.fill_mode, 679 + AnimationFillMode::Backwards | AnimationFillMode::Both 680 + ) { 681 + // Apply the first keyframe values (considering direction) 682 + let target_offset = match self.direction { 683 + AnimationDirection::Normal | AnimationDirection::Alternate => 0.0, 684 + AnimationDirection::Reverse | AnimationDirection::AlternateReverse => 1.0, 685 + }; 686 + for (prop, stops) in &self.keyframes { 687 + if let Some(val) = interpolate_keyframes(stops, target_offset) { 688 + result.push((prop.clone(), val)); 689 + } 690 + } 691 + } 692 + return result; 693 + } 694 + 695 + let active_elapsed = elapsed - self.delay; 696 + 697 + // Check if animation is complete 698 + if let Some(total) = self.total_duration() { 699 + if active_elapsed >= total { 700 + // Animation finished 701 + if matches!( 702 + self.fill_mode, 703 + AnimationFillMode::Forwards | AnimationFillMode::Both 704 + ) { 705 + // Apply the final keyframe values 706 + let target_offset = self.final_offset(); 707 + for (prop, stops) in &self.keyframes { 708 + if let Some(val) = interpolate_keyframes(stops, target_offset) { 709 + result.push((prop.clone(), val)); 710 + } 711 + } 712 + } 713 + return result; 714 + } 715 + } 716 + 717 + // Compute current iteration and progress within that iteration 718 + let iteration_progress = active_elapsed / self.duration; 719 + let current_iteration = iteration_progress.floor() as u32; 720 + let raw_progress = iteration_progress - current_iteration as f64; 721 + 722 + // Apply direction 723 + let directed_progress = match self.direction { 724 + AnimationDirection::Normal => raw_progress, 725 + AnimationDirection::Reverse => 1.0 - raw_progress, 726 + AnimationDirection::Alternate => { 727 + if current_iteration.is_multiple_of(2) { 728 + raw_progress 729 + } else { 730 + 1.0 - raw_progress 731 + } 732 + } 733 + AnimationDirection::AlternateReverse => { 734 + if current_iteration.is_multiple_of(2) { 735 + 1.0 - raw_progress 736 + } else { 737 + raw_progress 738 + } 739 + } 740 + }; 741 + 742 + // Apply timing function to the directed progress 743 + let eased_progress = self.timing_function.evaluate(directed_progress); 744 + 745 + // Interpolate each property 746 + for (prop, stops) in &self.keyframes { 747 + if let Some(val) = interpolate_keyframes(stops, eased_progress) { 748 + result.push((prop.clone(), val)); 749 + } 750 + } 751 + 752 + result 753 + } 754 + 755 + /// Compute the final offset value based on direction and iteration count. 756 + fn final_offset(&self) -> f64 { 757 + let total_iterations = match &self.iteration_count { 758 + IterationCount::Number(n) => *n, 759 + IterationCount::Infinite => return 1.0, // Shouldn't reach here 760 + }; 761 + 762 + // If iteration count is fractional, use the fractional progress 763 + let last_iteration = (total_iterations.ceil() as u32).saturating_sub(1); 764 + let fractional = total_iterations - total_iterations.floor(); 765 + 766 + let final_progress = if fractional > 0.0 { fractional } else { 1.0 }; 767 + 768 + match self.direction { 769 + AnimationDirection::Normal => final_progress, 770 + AnimationDirection::Reverse => 1.0 - final_progress, 771 + AnimationDirection::Alternate => { 772 + if last_iteration.is_multiple_of(2) { 773 + final_progress 774 + } else { 775 + 1.0 - final_progress 776 + } 777 + } 778 + AnimationDirection::AlternateReverse => { 779 + if last_iteration.is_multiple_of(2) { 780 + 1.0 - final_progress 781 + } else { 782 + final_progress 783 + } 784 + } 785 + } 786 + } 787 + 788 + /// Check and generate animation events. 789 + /// Call this each frame to get any events that should be fired. 790 + pub fn poll_events(&mut self, now: f64) -> Vec<AnimationEvent> { 791 + let mut events = Vec::new(); 792 + let elapsed = self.effective_elapsed(now); 793 + 794 + if self.duration <= 0.0 || self.finished { 795 + return events; 796 + } 797 + 798 + // Check for animationstart 799 + if elapsed >= self.delay && !self.started_event_fired { 800 + self.started_event_fired = true; 801 + events.push(AnimationEvent::Start { 802 + animation_name: self.name.clone(), 803 + elapsed_time: 0.0, 804 + }); 805 + } 806 + 807 + if elapsed < self.delay { 808 + return events; 809 + } 810 + 811 + let active_elapsed = elapsed - self.delay; 812 + 813 + // Check for animationiteration 814 + if self.duration > 0.0 { 815 + let current_iteration = (active_elapsed / self.duration).floor() as u32; 816 + let is_finished = if let Some(total) = self.total_duration() { 817 + active_elapsed >= total 818 + } else { 819 + false 820 + }; 821 + 822 + // Fire iteration events for iterations we haven't fired yet 823 + // Don't fire on the last iteration (that's an end event) 824 + if !is_finished { 825 + while self.last_iteration_event < current_iteration { 826 + self.last_iteration_event += 1; 827 + events.push(AnimationEvent::Iteration { 828 + animation_name: self.name.clone(), 829 + elapsed_time: self.last_iteration_event as f64 * self.duration, 830 + }); 831 + } 832 + } 833 + 834 + // Check for animationend 835 + if is_finished && !self.finished { 836 + self.finished = true; 837 + let total = self.total_duration().unwrap_or(0.0); 838 + events.push(AnimationEvent::End { 839 + animation_name: self.name.clone(), 840 + elapsed_time: total, 841 + }); 842 + } 843 + } 844 + 845 + events 846 + } 847 + 848 + /// Whether this animation is still active (not finished, or has fill-mode keeping it alive). 849 + pub fn is_active(&self, now: f64) -> bool { 850 + if self.finished { 851 + matches!( 852 + self.fill_mode, 853 + AnimationFillMode::Forwards | AnimationFillMode::Both 854 + ) 855 + } else { 856 + let elapsed = self.effective_elapsed(now); 857 + if elapsed < self.delay { 858 + // During delay: active if fill-mode backwards/both, or just waiting 859 + true 860 + } else { 861 + true 862 + } 863 + } 864 + } 865 + 866 + /// Whether this animation has completed all iterations. 867 + pub fn is_finished(&self) -> bool { 868 + self.finished 869 + } 870 + } 871 + 872 + /// Interpolate between keyframe stops at the given progress (0.0 to 1.0). 873 + fn interpolate_keyframes(stops: &[ResolvedKeyframeStop], progress: f64) -> Option<AnimatableValue> { 874 + if stops.is_empty() { 875 + return None; 876 + } 877 + if stops.len() == 1 { 878 + return Some(stops[0].value.clone()); 879 + } 880 + 881 + let progress = progress.clamp(0.0, 1.0); 882 + 883 + // Find the two surrounding stops 884 + // Stops should be sorted by offset 885 + if progress <= stops[0].offset { 886 + return Some(stops[0].value.clone()); 887 + } 888 + if progress >= stops[stops.len() - 1].offset { 889 + return Some(stops[stops.len() - 1].value.clone()); 890 + } 891 + 892 + for i in 0..stops.len() - 1 { 893 + let from = &stops[i]; 894 + let to = &stops[i + 1]; 895 + 896 + if progress >= from.offset && progress <= to.offset { 897 + let range = to.offset - from.offset; 898 + if range <= 0.0 { 899 + return Some(from.value.clone()); 900 + } 901 + let local_progress = (progress - from.offset) / range; 902 + return Some(from.value.interpolate(&to.value, local_progress)); 903 + } 904 + } 905 + 906 + Some(stops[stops.len() - 1].value.clone()) 907 + } 908 + 909 + // --------------------------------------------------------------------------- 910 + // Animation map: per-element animation management 911 + // --------------------------------------------------------------------------- 912 + 913 + /// Manages animations for a single element. 914 + #[derive(Debug, Clone, Default)] 915 + pub struct AnimationMap { 916 + /// Active animations. 917 + pub active: Vec<ActiveAnimation>, 918 + } 919 + 920 + impl AnimationMap { 921 + /// Update the animation state based on the element's animation specification. 922 + /// 923 + /// `spec` is the element's animation spec from CSS. 924 + /// `resolve_keyframes` is a callback that resolves a `@keyframes` name to a list 925 + /// of (property, stops) pairs. 926 + /// `now` is the current time in seconds. 927 + pub fn update<F>(&mut self, spec: &AnimationSpec, resolve_keyframes: F, now: f64) 928 + where 929 + F: Fn(&str) -> Vec<(String, Vec<ResolvedKeyframeStop>)>, 930 + { 931 + // Build a set of animation names from the spec 932 + let spec_names: Vec<&str> = spec.animations.iter().map(|a| a.name.as_str()).collect(); 933 + 934 + // Remove animations whose names are no longer in the spec 935 + self.active 936 + .retain(|a| spec_names.contains(&a.name.as_str())); 937 + 938 + // For each animation in the spec, check if it's already running 939 + for anim_spec in &spec.animations { 940 + if anim_spec.name.is_empty() { 941 + continue; 942 + } 943 + 944 + let existing = self.active.iter_mut().find(|a| a.name == anim_spec.name); 945 + if let Some(existing) = existing { 946 + // Update play state 947 + match anim_spec.play_state { 948 + AnimationPlayState::Paused => existing.pause(now), 949 + AnimationPlayState::Running => existing.resume(now), 950 + } 951 + } else { 952 + // Start a new animation 953 + let keyframes = resolve_keyframes(&anim_spec.name); 954 + if !keyframes.is_empty() { 955 + self.active.push(ActiveAnimation::new( 956 + anim_spec.name.clone(), 957 + keyframes, 958 + anim_spec, 959 + now, 960 + )); 961 + } 962 + } 963 + } 964 + } 965 + 966 + /// Get the current animated value for a property. 967 + /// Returns `None` if no animation is affecting this property. 968 + /// Later animations in the list take priority. 969 + pub fn get_value(&self, property: &str, now: f64) -> Option<AnimatableValue> { 970 + // Last animation wins (later in the list = higher priority) 971 + for anim in self.active.iter().rev() { 972 + let values = anim.evaluate(now); 973 + for (prop, val) in &values { 974 + if prop == property { 975 + return Some(val.clone()); 976 + } 977 + } 978 + } 979 + None 980 + } 981 + 982 + /// Returns true if any animations are currently active. 983 + pub fn has_active_animations(&self, now: f64) -> bool { 984 + self.active.iter().any(|a| a.is_active(now)) 985 + } 986 + 987 + /// Poll all animations for events. Call this each frame. 988 + pub fn poll_events(&mut self, now: f64) -> Vec<AnimationEvent> { 989 + let mut events = Vec::new(); 990 + for anim in &mut self.active { 991 + events.extend(anim.poll_events(now)); 992 + } 993 + events 994 + } 995 + 996 + /// Remove finished animations that don't have a fill mode keeping them alive. 997 + pub fn cleanup(&mut self, now: f64) { 998 + self.active.retain(|a| a.is_active(now)); 999 + } 1000 + } 1001 + 1002 + /// Resolve keyframe declarations for a given animation name from a list of keyframe rules. 1003 + /// Returns per-property sorted keyframe stops. 1004 + /// 1005 + /// `keyframes` is the list of `Keyframe` blocks from the `@keyframes` rule. 1006 + /// `value_resolver` converts a property name + declaration values into an `AnimatableValue`. 1007 + pub fn resolve_keyframe_properties<F>( 1008 + keyframes: &[Keyframe], 1009 + value_resolver: F, 1010 + ) -> Vec<(String, Vec<ResolvedKeyframeStop>)> 1011 + where 1012 + F: Fn(&str, &[ComponentValue]) -> Option<AnimatableValue>, 1013 + { 1014 + // Collect all property -> (offset, value) pairs 1015 + let mut property_stops: Vec<(String, Vec<ResolvedKeyframeStop>)> = Vec::new(); 1016 + 1017 + for kf in keyframes { 1018 + for selector in &kf.selectors { 1019 + let offset = selector.to_percentage(); 1020 + 1021 + for decl in &kf.declarations { 1022 + if !is_animatable_property(&decl.property) { 1023 + continue; 1024 + } 1025 + 1026 + if let Some(value) = value_resolver(&decl.property, &decl.value) { 1027 + // Find or create the entry for this property 1028 + if let Some(entry) = 1029 + property_stops.iter_mut().find(|(p, _)| *p == decl.property) 1030 + { 1031 + entry.1.push(ResolvedKeyframeStop { offset, value }); 1032 + } else { 1033 + property_stops.push(( 1034 + decl.property.clone(), 1035 + vec![ResolvedKeyframeStop { offset, value }], 1036 + )); 1037 + } 1038 + } 1039 + } 1040 + } 1041 + } 1042 + 1043 + // Sort stops by offset for each property 1044 + for (_, stops) in &mut property_stops { 1045 + stops.sort_by(|a, b| { 1046 + a.offset 1047 + .partial_cmp(&b.offset) 1048 + .unwrap_or(core::cmp::Ordering::Equal) 1049 + }); 1050 + } 1051 + 1052 + property_stops 1053 + } 1054 + 1055 + // --------------------------------------------------------------------------- 1056 + // Tests 1057 + // --------------------------------------------------------------------------- 1058 + 1059 + #[cfg(test)] 1060 + mod tests { 1061 + use super::*; 1062 + use crate::parser::ComponentValue; 1063 + use crate::tokenizer::NumericType; 1064 + use crate::values::Color; 1065 + 1066 + // -- Parsing tests -- 1067 + 1068 + #[test] 1069 + fn test_parse_animation_shorthand_simple() { 1070 + // animation: slidein 1s ease-in 1071 + let values = vec![ 1072 + ComponentValue::Ident("slidein".into()), 1073 + ComponentValue::Whitespace, 1074 + ComponentValue::Dimension(1.0, NumericType::Number, "s".into()), 1075 + ComponentValue::Whitespace, 1076 + ComponentValue::Ident("ease-in".into()), 1077 + ]; 1078 + let spec = parse_animation_shorthand(&values); 1079 + assert_eq!(spec.animations.len(), 1); 1080 + let a = &spec.animations[0]; 1081 + assert_eq!(a.name, "slidein"); 1082 + assert_eq!(a.duration, 1.0); 1083 + assert_eq!(a.timing_function, TimingFunction::EaseIn); 1084 + } 1085 + 1086 + #[test] 1087 + fn test_parse_animation_shorthand_full() { 1088 + // animation: slidein 2s ease-in-out 0.5s infinite alternate forwards running 1089 + let values = vec![ 1090 + ComponentValue::Ident("slidein".into()), 1091 + ComponentValue::Whitespace, 1092 + ComponentValue::Dimension(2.0, NumericType::Number, "s".into()), 1093 + ComponentValue::Whitespace, 1094 + ComponentValue::Ident("ease-in-out".into()), 1095 + ComponentValue::Whitespace, 1096 + ComponentValue::Dimension(0.5, NumericType::Number, "s".into()), 1097 + ComponentValue::Whitespace, 1098 + ComponentValue::Ident("infinite".into()), 1099 + ComponentValue::Whitespace, 1100 + ComponentValue::Ident("alternate".into()), 1101 + ComponentValue::Whitespace, 1102 + ComponentValue::Ident("forwards".into()), 1103 + ComponentValue::Whitespace, 1104 + ComponentValue::Ident("running".into()), 1105 + ]; 1106 + let spec = parse_animation_shorthand(&values); 1107 + assert_eq!(spec.animations.len(), 1); 1108 + let a = &spec.animations[0]; 1109 + assert_eq!(a.name, "slidein"); 1110 + assert_eq!(a.duration, 2.0); 1111 + assert_eq!(a.timing_function, TimingFunction::EaseInOut); 1112 + assert_eq!(a.delay, 0.5); 1113 + assert_eq!(a.iteration_count, IterationCount::Infinite); 1114 + assert_eq!(a.direction, AnimationDirection::Alternate); 1115 + assert_eq!(a.fill_mode, AnimationFillMode::Forwards); 1116 + assert_eq!(a.play_state, AnimationPlayState::Running); 1117 + } 1118 + 1119 + #[test] 1120 + fn test_parse_animation_shorthand_multiple() { 1121 + // animation: slidein 1s, fadeout 2s linear 1122 + let values = vec![ 1123 + ComponentValue::Ident("slidein".into()), 1124 + ComponentValue::Whitespace, 1125 + ComponentValue::Dimension(1.0, NumericType::Number, "s".into()), 1126 + ComponentValue::Comma, 1127 + ComponentValue::Whitespace, 1128 + ComponentValue::Ident("fadeout".into()), 1129 + ComponentValue::Whitespace, 1130 + ComponentValue::Dimension(2.0, NumericType::Number, "s".into()), 1131 + ComponentValue::Whitespace, 1132 + ComponentValue::Ident("linear".into()), 1133 + ]; 1134 + let spec = parse_animation_shorthand(&values); 1135 + assert_eq!(spec.animations.len(), 2); 1136 + assert_eq!(spec.animations[0].name, "slidein"); 1137 + assert_eq!(spec.animations[0].duration, 1.0); 1138 + assert_eq!(spec.animations[1].name, "fadeout"); 1139 + assert_eq!(spec.animations[1].duration, 2.0); 1140 + assert_eq!(spec.animations[1].timing_function, TimingFunction::Linear); 1141 + } 1142 + 1143 + #[test] 1144 + fn test_parse_animation_name() { 1145 + let values = vec![ 1146 + ComponentValue::Ident("slidein".into()), 1147 + ComponentValue::Comma, 1148 + ComponentValue::Whitespace, 1149 + ComponentValue::Ident("fadeout".into()), 1150 + ]; 1151 + let names = parse_animation_name(&values); 1152 + assert_eq!(names, vec!["slidein", "fadeout"]); 1153 + } 1154 + 1155 + #[test] 1156 + fn test_parse_animation_name_none() { 1157 + let values = vec![ComponentValue::Ident("none".into())]; 1158 + let names = parse_animation_name(&values); 1159 + assert_eq!(names, vec![""]); 1160 + } 1161 + 1162 + #[test] 1163 + fn test_parse_animation_iteration_count_infinite() { 1164 + let values = vec![ComponentValue::Ident("infinite".into())]; 1165 + let counts = parse_animation_iteration_count(&values); 1166 + assert_eq!(counts, vec![IterationCount::Infinite]); 1167 + } 1168 + 1169 + #[test] 1170 + fn test_parse_animation_iteration_count_number() { 1171 + let values = vec![ComponentValue::Number(3.0, NumericType::Number)]; 1172 + let counts = parse_animation_iteration_count(&values); 1173 + assert_eq!(counts, vec![IterationCount::Number(3.0)]); 1174 + } 1175 + 1176 + #[test] 1177 + fn test_parse_animation_direction() { 1178 + let values = vec![ 1179 + ComponentValue::Ident("alternate".into()), 1180 + ComponentValue::Comma, 1181 + ComponentValue::Whitespace, 1182 + ComponentValue::Ident("reverse".into()), 1183 + ]; 1184 + let dirs = parse_animation_direction(&values); 1185 + assert_eq!( 1186 + dirs, 1187 + vec![AnimationDirection::Alternate, AnimationDirection::Reverse] 1188 + ); 1189 + } 1190 + 1191 + #[test] 1192 + fn test_parse_animation_fill_mode() { 1193 + let values = vec![ComponentValue::Ident("both".into())]; 1194 + let modes = parse_animation_fill_mode(&values); 1195 + assert_eq!(modes, vec![AnimationFillMode::Both]); 1196 + } 1197 + 1198 + #[test] 1199 + fn test_parse_animation_play_state() { 1200 + let values = vec![ComponentValue::Ident("paused".into())]; 1201 + let states = parse_animation_play_state(&values); 1202 + assert_eq!(states, vec![AnimationPlayState::Paused]); 1203 + } 1204 + 1205 + // -- Keyframe interpolation tests -- 1206 + 1207 + #[test] 1208 + fn test_interpolate_keyframes_two_stops() { 1209 + let stops = vec![ 1210 + ResolvedKeyframeStop { 1211 + offset: 0.0, 1212 + value: AnimatableValue::Number(0.0), 1213 + }, 1214 + ResolvedKeyframeStop { 1215 + offset: 1.0, 1216 + value: AnimatableValue::Number(1.0), 1217 + }, 1218 + ]; 1219 + 1220 + let val = interpolate_keyframes(&stops, 0.0).unwrap(); 1221 + assert_eq!(val, AnimatableValue::Number(0.0)); 1222 + 1223 + let val = interpolate_keyframes(&stops, 0.5).unwrap(); 1224 + assert_eq!(val, AnimatableValue::Number(0.5)); 1225 + 1226 + let val = interpolate_keyframes(&stops, 1.0).unwrap(); 1227 + assert_eq!(val, AnimatableValue::Number(1.0)); 1228 + } 1229 + 1230 + #[test] 1231 + fn test_interpolate_keyframes_three_stops() { 1232 + let stops = vec![ 1233 + ResolvedKeyframeStop { 1234 + offset: 0.0, 1235 + value: AnimatableValue::Number(0.0), 1236 + }, 1237 + ResolvedKeyframeStop { 1238 + offset: 0.5, 1239 + value: AnimatableValue::Number(1.0), 1240 + }, 1241 + ResolvedKeyframeStop { 1242 + offset: 1.0, 1243 + value: AnimatableValue::Number(0.0), 1244 + }, 1245 + ]; 1246 + 1247 + let val = interpolate_keyframes(&stops, 0.25).unwrap(); 1248 + assert_eq!(val, AnimatableValue::Number(0.5)); 1249 + 1250 + let val = interpolate_keyframes(&stops, 0.5).unwrap(); 1251 + assert_eq!(val, AnimatableValue::Number(1.0)); 1252 + 1253 + let val = interpolate_keyframes(&stops, 0.75).unwrap(); 1254 + assert_eq!(val, AnimatableValue::Number(0.5)); 1255 + } 1256 + 1257 + #[test] 1258 + fn test_interpolate_keyframes_color() { 1259 + let stops = vec![ 1260 + ResolvedKeyframeStop { 1261 + offset: 0.0, 1262 + value: AnimatableValue::Color(Color::new(0, 0, 0, 255)), 1263 + }, 1264 + ResolvedKeyframeStop { 1265 + offset: 1.0, 1266 + value: AnimatableValue::Color(Color::new(255, 255, 255, 255)), 1267 + }, 1268 + ]; 1269 + 1270 + let val = interpolate_keyframes(&stops, 0.5).unwrap(); 1271 + if let AnimatableValue::Color(c) = val { 1272 + assert_eq!(c.r, 128); 1273 + assert_eq!(c.g, 128); 1274 + assert_eq!(c.b, 128); 1275 + } else { 1276 + panic!("Expected color"); 1277 + } 1278 + } 1279 + 1280 + // -- Active animation tests -- 1281 + 1282 + fn make_test_animation( 1283 + direction: AnimationDirection, 1284 + fill_mode: AnimationFillMode, 1285 + iteration_count: IterationCount, 1286 + ) -> ActiveAnimation { 1287 + let stops = vec![ 1288 + ResolvedKeyframeStop { 1289 + offset: 0.0, 1290 + value: AnimatableValue::Number(0.0), 1291 + }, 1292 + ResolvedKeyframeStop { 1293 + offset: 1.0, 1294 + value: AnimatableValue::Number(1.0), 1295 + }, 1296 + ]; 1297 + 1298 + let spec = SingleAnimation { 1299 + name: "test".into(), 1300 + duration: 1.0, 1301 + timing_function: TimingFunction::Linear, 1302 + delay: 0.0, 1303 + iteration_count, 1304 + direction, 1305 + fill_mode, 1306 + play_state: AnimationPlayState::Running, 1307 + }; 1308 + 1309 + ActiveAnimation::new("test".into(), vec![("opacity".into(), stops)], &spec, 0.0) 1310 + } 1311 + 1312 + #[test] 1313 + fn test_active_animation_normal_direction() { 1314 + let anim = make_test_animation( 1315 + AnimationDirection::Normal, 1316 + AnimationFillMode::None, 1317 + IterationCount::Number(1.0), 1318 + ); 1319 + 1320 + let vals = anim.evaluate(0.0); 1321 + assert_eq!(vals.len(), 1); 1322 + assert_eq!(vals[0], ("opacity".into(), AnimatableValue::Number(0.0))); 1323 + 1324 + let vals = anim.evaluate(0.5); 1325 + assert_eq!(vals[0], ("opacity".into(), AnimatableValue::Number(0.5))); 1326 + 1327 + // After completion with no fill mode → no values 1328 + let vals = anim.evaluate(1.5); 1329 + assert!(vals.is_empty()); 1330 + } 1331 + 1332 + #[test] 1333 + fn test_active_animation_reverse_direction() { 1334 + let anim = make_test_animation( 1335 + AnimationDirection::Reverse, 1336 + AnimationFillMode::None, 1337 + IterationCount::Number(1.0), 1338 + ); 1339 + 1340 + let vals = anim.evaluate(0.0); 1341 + assert_eq!(vals[0], ("opacity".into(), AnimatableValue::Number(1.0))); 1342 + 1343 + let vals = anim.evaluate(0.5); 1344 + assert_eq!(vals[0], ("opacity".into(), AnimatableValue::Number(0.5))); 1345 + } 1346 + 1347 + #[test] 1348 + fn test_active_animation_alternate_direction() { 1349 + let anim = make_test_animation( 1350 + AnimationDirection::Alternate, 1351 + AnimationFillMode::None, 1352 + IterationCount::Number(2.0), 1353 + ); 1354 + 1355 + // First iteration: forward (0 → 1) 1356 + let vals = anim.evaluate(0.5); 1357 + assert_eq!(vals[0], ("opacity".into(), AnimatableValue::Number(0.5))); 1358 + 1359 + // Second iteration: backward (1 → 0) 1360 + let vals = anim.evaluate(1.5); 1361 + assert_eq!(vals[0], ("opacity".into(), AnimatableValue::Number(0.5))); 1362 + } 1363 + 1364 + #[test] 1365 + fn test_active_animation_fill_forwards() { 1366 + let anim = make_test_animation( 1367 + AnimationDirection::Normal, 1368 + AnimationFillMode::Forwards, 1369 + IterationCount::Number(1.0), 1370 + ); 1371 + 1372 + // After completion: should retain final value 1373 + let vals = anim.evaluate(2.0); 1374 + assert_eq!(vals.len(), 1); 1375 + assert_eq!(vals[0], ("opacity".into(), AnimatableValue::Number(1.0))); 1376 + } 1377 + 1378 + #[test] 1379 + fn test_active_animation_fill_backwards() { 1380 + let spec = SingleAnimation { 1381 + name: "test".into(), 1382 + duration: 1.0, 1383 + timing_function: TimingFunction::Linear, 1384 + delay: 1.0, // 1 second delay 1385 + iteration_count: IterationCount::Number(1.0), 1386 + direction: AnimationDirection::Normal, 1387 + fill_mode: AnimationFillMode::Backwards, 1388 + play_state: AnimationPlayState::Running, 1389 + }; 1390 + 1391 + let stops = vec![ 1392 + ResolvedKeyframeStop { 1393 + offset: 0.0, 1394 + value: AnimatableValue::Number(0.0), 1395 + }, 1396 + ResolvedKeyframeStop { 1397 + offset: 1.0, 1398 + value: AnimatableValue::Number(1.0), 1399 + }, 1400 + ]; 1401 + 1402 + let anim = ActiveAnimation::new("test".into(), vec![("opacity".into(), stops)], &spec, 0.0); 1403 + 1404 + // During delay: should apply 0% keyframe 1405 + let vals = anim.evaluate(0.5); 1406 + assert_eq!(vals.len(), 1); 1407 + assert_eq!(vals[0], ("opacity".into(), AnimatableValue::Number(0.0))); 1408 + } 1409 + 1410 + #[test] 1411 + fn test_active_animation_infinite() { 1412 + let anim = make_test_animation( 1413 + AnimationDirection::Normal, 1414 + AnimationFillMode::None, 1415 + IterationCount::Infinite, 1416 + ); 1417 + 1418 + // Should still be animating at t=100 1419 + let vals = anim.evaluate(100.5); 1420 + assert_eq!(vals.len(), 1); 1421 + assert_eq!(vals[0], ("opacity".into(), AnimatableValue::Number(0.5))); 1422 + } 1423 + 1424 + #[test] 1425 + fn test_active_animation_pause_resume() { 1426 + let mut anim = make_test_animation( 1427 + AnimationDirection::Normal, 1428 + AnimationFillMode::None, 1429 + IterationCount::Number(1.0), 1430 + ); 1431 + 1432 + // At t=0.25, opacity should be 0.25 1433 + let vals = anim.evaluate(0.25); 1434 + assert_eq!(vals[0], ("opacity".into(), AnimatableValue::Number(0.25))); 1435 + 1436 + // Pause at t=0.5 1437 + anim.pause(0.5); 1438 + 1439 + // At t=1.0, should still show the value at pause time (0.5) 1440 + let vals = anim.evaluate(1.0); 1441 + assert_eq!(vals[0], ("opacity".into(), AnimatableValue::Number(0.5))); 1442 + 1443 + // Resume at t=1.0 1444 + anim.resume(1.0); 1445 + 1446 + // At t=1.25, should be at 0.75 (0.5 + 0.25 progress since resume) 1447 + let vals = anim.evaluate(1.25); 1448 + assert_eq!(vals[0], ("opacity".into(), AnimatableValue::Number(0.75))); 1449 + } 1450 + 1451 + // -- Animation events tests -- 1452 + 1453 + #[test] 1454 + fn test_animation_events_basic() { 1455 + let mut anim = make_test_animation( 1456 + AnimationDirection::Normal, 1457 + AnimationFillMode::None, 1458 + IterationCount::Number(1.0), 1459 + ); 1460 + 1461 + // Start event at t=0 1462 + let events = anim.poll_events(0.0); 1463 + assert_eq!(events.len(), 1); 1464 + assert!(matches!(events[0], AnimationEvent::Start { .. })); 1465 + 1466 + // No events mid-animation 1467 + let events = anim.poll_events(0.5); 1468 + assert!(events.is_empty()); 1469 + 1470 + // End event 1471 + let events = anim.poll_events(1.0); 1472 + assert_eq!(events.len(), 1); 1473 + assert!(matches!(events[0], AnimationEvent::End { .. })); 1474 + } 1475 + 1476 + #[test] 1477 + fn test_animation_events_iteration() { 1478 + let mut anim = make_test_animation( 1479 + AnimationDirection::Normal, 1480 + AnimationFillMode::None, 1481 + IterationCount::Number(3.0), 1482 + ); 1483 + 1484 + // Start event 1485 + let events = anim.poll_events(0.0); 1486 + assert_eq!(events.len(), 1); 1487 + assert!(matches!(events[0], AnimationEvent::Start { .. })); 1488 + 1489 + // Iteration event at t=1.0 1490 + let events = anim.poll_events(1.1); 1491 + assert_eq!(events.len(), 1); 1492 + assert!(matches!(events[0], AnimationEvent::Iteration { .. })); 1493 + 1494 + // Iteration event at t=2.0 1495 + let events = anim.poll_events(2.1); 1496 + assert_eq!(events.len(), 1); 1497 + assert!(matches!(events[0], AnimationEvent::Iteration { .. })); 1498 + 1499 + // End event at t=3.0 1500 + let events = anim.poll_events(3.0); 1501 + assert_eq!(events.len(), 1); 1502 + assert!(matches!(events[0], AnimationEvent::End { .. })); 1503 + } 1504 + 1505 + // -- Animation map tests -- 1506 + 1507 + #[test] 1508 + fn test_animation_map_basic() { 1509 + let mut map = AnimationMap::default(); 1510 + 1511 + let spec = AnimationSpec { 1512 + animations: vec![SingleAnimation { 1513 + name: "test".into(), 1514 + duration: 1.0, 1515 + timing_function: TimingFunction::Linear, 1516 + delay: 0.0, 1517 + iteration_count: IterationCount::Number(1.0), 1518 + direction: AnimationDirection::Normal, 1519 + fill_mode: AnimationFillMode::None, 1520 + play_state: AnimationPlayState::Running, 1521 + }], 1522 + }; 1523 + 1524 + let resolve = |name: &str| -> Vec<(String, Vec<ResolvedKeyframeStop>)> { 1525 + if name == "test" { 1526 + vec![( 1527 + "opacity".into(), 1528 + vec![ 1529 + ResolvedKeyframeStop { 1530 + offset: 0.0, 1531 + value: AnimatableValue::Number(0.0), 1532 + }, 1533 + ResolvedKeyframeStop { 1534 + offset: 1.0, 1535 + value: AnimatableValue::Number(1.0), 1536 + }, 1537 + ], 1538 + )] 1539 + } else { 1540 + vec![] 1541 + } 1542 + }; 1543 + 1544 + map.update(&spec, resolve, 0.0); 1545 + assert!(map.has_active_animations(0.0)); 1546 + 1547 + let val = map.get_value("opacity", 0.5); 1548 + assert_eq!(val, Some(AnimatableValue::Number(0.5))); 1549 + } 1550 + 1551 + #[test] 1552 + fn test_animation_map_removes_when_name_changes() { 1553 + let mut map = AnimationMap::default(); 1554 + 1555 + let spec = AnimationSpec { 1556 + animations: vec![SingleAnimation { 1557 + name: "test".into(), 1558 + duration: 1.0, 1559 + ..SingleAnimation::default() 1560 + }], 1561 + }; 1562 + 1563 + let resolve = |name: &str| -> Vec<(String, Vec<ResolvedKeyframeStop>)> { 1564 + if name == "test" { 1565 + vec![( 1566 + "opacity".into(), 1567 + vec![ 1568 + ResolvedKeyframeStop { 1569 + offset: 0.0, 1570 + value: AnimatableValue::Number(0.0), 1571 + }, 1572 + ResolvedKeyframeStop { 1573 + offset: 1.0, 1574 + value: AnimatableValue::Number(1.0), 1575 + }, 1576 + ], 1577 + )] 1578 + } else { 1579 + vec![] 1580 + } 1581 + }; 1582 + 1583 + map.update(&spec, resolve, 0.0); 1584 + assert_eq!(map.active.len(), 1); 1585 + 1586 + // Change the animation name 1587 + let spec2 = AnimationSpec { 1588 + animations: vec![SingleAnimation { 1589 + name: "other".into(), 1590 + duration: 1.0, 1591 + ..SingleAnimation::default() 1592 + }], 1593 + }; 1594 + 1595 + map.update(&spec2, resolve, 0.5); 1596 + // "test" should be removed since it's no longer in the spec 1597 + assert!(map.active.iter().all(|a| a.name != "test")); 1598 + } 1599 + 1600 + // -- Resolve keyframe properties tests -- 1601 + 1602 + #[test] 1603 + fn test_resolve_keyframe_properties() { 1604 + let keyframes = vec![ 1605 + Keyframe { 1606 + selectors: vec![KeyframeSelector::From], 1607 + declarations: vec![Declaration { 1608 + property: "opacity".into(), 1609 + value: vec![ComponentValue::Number(0.0, NumericType::Number)], 1610 + important: false, 1611 + }], 1612 + }, 1613 + Keyframe { 1614 + selectors: vec![KeyframeSelector::To], 1615 + declarations: vec![Declaration { 1616 + property: "opacity".into(), 1617 + value: vec![ComponentValue::Number(1.0, NumericType::Number)], 1618 + important: false, 1619 + }], 1620 + }, 1621 + ]; 1622 + 1623 + let resolver = |_property: &str, values: &[ComponentValue]| -> Option<AnimatableValue> { 1624 + if let Some(ComponentValue::Number(n, _)) = values.first() { 1625 + Some(AnimatableValue::Number(*n as f32)) 1626 + } else { 1627 + None 1628 + } 1629 + }; 1630 + 1631 + let result = resolve_keyframe_properties(&keyframes, resolver); 1632 + assert_eq!(result.len(), 1); 1633 + assert_eq!(result[0].0, "opacity"); 1634 + assert_eq!(result[0].1.len(), 2); 1635 + assert_eq!(result[0].1[0].offset, 0.0); 1636 + assert_eq!(result[0].1[1].offset, 1.0); 1637 + } 1638 + 1639 + #[test] 1640 + fn test_resolve_keyframe_properties_multiple_selectors() { 1641 + let keyframes = vec![Keyframe { 1642 + selectors: vec![ 1643 + KeyframeSelector::Percentage(0.0), 1644 + KeyframeSelector::Percentage(100.0), 1645 + ], 1646 + declarations: vec![Declaration { 1647 + property: "opacity".into(), 1648 + value: vec![ComponentValue::Number(0.5, NumericType::Number)], 1649 + important: false, 1650 + }], 1651 + }]; 1652 + 1653 + let resolver = |_property: &str, values: &[ComponentValue]| -> Option<AnimatableValue> { 1654 + if let Some(ComponentValue::Number(n, _)) = values.first() { 1655 + Some(AnimatableValue::Number(*n as f32)) 1656 + } else { 1657 + None 1658 + } 1659 + }; 1660 + 1661 + let result = resolve_keyframe_properties(&keyframes, resolver); 1662 + assert_eq!(result.len(), 1); 1663 + assert_eq!(result[0].1.len(), 2); // Two stops: 0% and 100% 1664 + } 1665 + }
+1
crates/css/src/lib.rs
··· 1 1 //! CSS tokenizer, parser, and CSSOM. 2 2 3 + pub mod animations; 3 4 pub mod media; 4 5 pub mod parser; 5 6 pub mod tokenizer;
+241
crates/css/src/parser.rs
··· 2 2 //! 3 3 //! Consumes tokens from the tokenizer and produces a structured stylesheet. 4 4 5 + use crate::animations::{Keyframe, KeyframeSelector}; 5 6 use crate::media::{parse_media_query_list, MediaQueryList}; 6 7 use crate::tokenizer::{HashType, NumericType, Token, Tokenizer}; 7 8 ··· 21 22 Style(StyleRule), 22 23 Media(MediaRule), 23 24 Import(ImportRule), 25 + Keyframes(KeyframesRule), 26 + } 27 + 28 + /// A `@keyframes` rule with a name and a list of keyframes. 29 + #[derive(Debug, Clone, PartialEq)] 30 + pub struct KeyframesRule { 31 + pub name: String, 32 + pub keyframes: Vec<Keyframe>, 24 33 } 25 34 26 35 /// A style rule: selector list + declarations. ··· 236 245 match name.to_ascii_lowercase().as_str() { 237 246 "media" => self.parse_media_rule(), 238 247 "import" => self.parse_import_rule(), 248 + "keyframes" | "-webkit-keyframes" | "-moz-keyframes" => self.parse_keyframes_rule(), 239 249 _ => { 240 250 // Unknown at-rule: skip to end of block or semicolon 241 251 self.skip_at_rule_body(); ··· 337 347 // Consume optional trailing tokens until semicolon 338 348 self.skip_to_semicolon(); 339 349 Some(Rule::Import(ImportRule { url })) 350 + } 351 + 352 + fn parse_keyframes_rule(&mut self) -> Option<Rule> { 353 + self.skip_whitespace(); 354 + 355 + // Parse the animation name (identifier or string) 356 + let name = match self.peek() { 357 + Token::Ident(_) => { 358 + if let Token::Ident(s) = self.advance() { 359 + s 360 + } else { 361 + unreachable!() 362 + } 363 + } 364 + Token::String(_) => { 365 + if let Token::String(s) = self.advance() { 366 + s 367 + } else { 368 + unreachable!() 369 + } 370 + } 371 + _ => { 372 + self.skip_at_rule_body(); 373 + return None; 374 + } 375 + }; 376 + 377 + self.skip_whitespace(); 378 + 379 + // Expect `{` 380 + if !matches!(self.peek(), Token::LeftBrace) { 381 + self.skip_at_rule_body(); 382 + return None; 383 + } 384 + self.advance(); 385 + 386 + // Parse keyframe blocks 387 + let mut keyframes = Vec::new(); 388 + loop { 389 + self.skip_whitespace(); 390 + if self.is_eof() { 391 + break; 392 + } 393 + if matches!(self.peek(), Token::RightBrace) { 394 + self.advance(); 395 + break; 396 + } 397 + 398 + if let Some(kf) = self.parse_keyframe_block() { 399 + keyframes.push(kf); 400 + } 401 + } 402 + 403 + Some(Rule::Keyframes(KeyframesRule { name, keyframes })) 404 + } 405 + 406 + fn parse_keyframe_block(&mut self) -> Option<Keyframe> { 407 + // Parse keyframe selectors (comma-separated: `from`, `to`, or percentages) 408 + let mut selectors = Vec::new(); 409 + loop { 410 + self.skip_whitespace(); 411 + match self.peek() { 412 + Token::LeftBrace | Token::Eof | Token::RightBrace => break, 413 + Token::Comma => { 414 + self.advance(); 415 + continue; 416 + } 417 + Token::Ident(s) if s.eq_ignore_ascii_case("from") => { 418 + selectors.push(KeyframeSelector::From); 419 + self.advance(); 420 + } 421 + Token::Ident(s) if s.eq_ignore_ascii_case("to") => { 422 + selectors.push(KeyframeSelector::To); 423 + self.advance(); 424 + } 425 + Token::Percentage(_) => { 426 + if let Token::Percentage(p) = self.advance() { 427 + selectors.push(KeyframeSelector::Percentage(p)); 428 + } 429 + } 430 + Token::Number(n, _) if *n == 0.0 => { 431 + // Allow bare `0` as 0% 432 + self.advance(); 433 + selectors.push(KeyframeSelector::Percentage(0.0)); 434 + // Check for optional `%` sign 435 + if matches!(self.peek(), Token::Delim('%')) { 436 + self.advance(); 437 + } 438 + } 439 + _ => { 440 + // Unknown token in keyframe selector — skip to next block 441 + self.skip_at_rule_body(); 442 + return None; 443 + } 444 + } 445 + } 446 + 447 + if selectors.is_empty() { 448 + return None; 449 + } 450 + 451 + // Expect `{` 452 + if !matches!(self.peek(), Token::LeftBrace) { 453 + return None; 454 + } 455 + self.advance(); 456 + 457 + let declarations = self.parse_declaration_list_until_brace(); 458 + 459 + // Consume `}` 460 + if matches!(self.peek(), Token::RightBrace) { 461 + self.advance(); 462 + } 463 + 464 + Some(Keyframe { 465 + selectors, 466 + declarations, 467 + }) 340 468 } 341 469 342 470 fn skip_at_rule_body(&mut self) { ··· 1482 1610 fn test_cdo_cdc_ignored() { 1483 1611 let ss = Parser::parse("<!-- p { color: red; } -->"); 1484 1612 assert_eq!(ss.rules.len(), 1); 1613 + } 1614 + 1615 + // -- @keyframes tests -- 1616 + 1617 + #[test] 1618 + fn test_keyframes_from_to() { 1619 + let ss = Parser::parse("@keyframes slidein { from { opacity: 0; } to { opacity: 1; } }"); 1620 + assert_eq!(ss.rules.len(), 1); 1621 + let kf = match &ss.rules[0] { 1622 + Rule::Keyframes(k) => k, 1623 + _ => panic!("expected keyframes rule"), 1624 + }; 1625 + assert_eq!(kf.name, "slidein"); 1626 + assert_eq!(kf.keyframes.len(), 2); 1627 + assert_eq!(kf.keyframes[0].selectors, vec![KeyframeSelector::From]); 1628 + assert_eq!(kf.keyframes[1].selectors, vec![KeyframeSelector::To]); 1629 + assert_eq!(kf.keyframes[0].declarations.len(), 1); 1630 + assert_eq!(kf.keyframes[0].declarations[0].property, "opacity"); 1631 + } 1632 + 1633 + #[test] 1634 + fn test_keyframes_percentages() { 1635 + let ss = Parser::parse( 1636 + "@keyframes fade { 0% { opacity: 0; } 50% { opacity: 1; } 100% { opacity: 0.5; } }", 1637 + ); 1638 + let kf = match &ss.rules[0] { 1639 + Rule::Keyframes(k) => k, 1640 + _ => panic!("expected keyframes rule"), 1641 + }; 1642 + assert_eq!(kf.name, "fade"); 1643 + assert_eq!(kf.keyframes.len(), 3); 1644 + assert_eq!( 1645 + kf.keyframes[0].selectors, 1646 + vec![KeyframeSelector::Percentage(0.0)] 1647 + ); 1648 + assert_eq!( 1649 + kf.keyframes[1].selectors, 1650 + vec![KeyframeSelector::Percentage(50.0)] 1651 + ); 1652 + assert_eq!( 1653 + kf.keyframes[2].selectors, 1654 + vec![KeyframeSelector::Percentage(100.0)] 1655 + ); 1656 + } 1657 + 1658 + #[test] 1659 + fn test_keyframes_multiple_selectors() { 1660 + let ss = 1661 + Parser::parse("@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } }"); 1662 + let kf = match &ss.rules[0] { 1663 + Rule::Keyframes(k) => k, 1664 + _ => panic!("expected keyframes rule"), 1665 + }; 1666 + assert_eq!(kf.keyframes.len(), 2); 1667 + assert_eq!( 1668 + kf.keyframes[0].selectors, 1669 + vec![ 1670 + KeyframeSelector::Percentage(0.0), 1671 + KeyframeSelector::Percentage(100.0), 1672 + ] 1673 + ); 1674 + } 1675 + 1676 + #[test] 1677 + fn test_keyframes_string_name() { 1678 + let ss = Parser::parse( 1679 + "@keyframes \"my animation\" { from { color: red; } to { color: blue; } }", 1680 + ); 1681 + let kf = match &ss.rules[0] { 1682 + Rule::Keyframes(k) => k, 1683 + _ => panic!("expected keyframes rule"), 1684 + }; 1685 + assert_eq!(kf.name, "my animation"); 1686 + } 1687 + 1688 + #[test] 1689 + fn test_keyframes_webkit_prefix() { 1690 + let ss = 1691 + Parser::parse("@-webkit-keyframes spin { from { opacity: 0; } to { opacity: 1; } }"); 1692 + assert_eq!(ss.rules.len(), 1); 1693 + let kf = match &ss.rules[0] { 1694 + Rule::Keyframes(k) => k, 1695 + _ => panic!("expected keyframes rule"), 1696 + }; 1697 + assert_eq!(kf.name, "spin"); 1698 + } 1699 + 1700 + #[test] 1701 + fn test_keyframes_multiple_declarations() { 1702 + let ss = Parser::parse( 1703 + "@keyframes move { from { left: 0px; top: 0px; } to { left: 100px; top: 50px; } }", 1704 + ); 1705 + let kf = match &ss.rules[0] { 1706 + Rule::Keyframes(k) => k, 1707 + _ => panic!("expected keyframes rule"), 1708 + }; 1709 + assert_eq!(kf.keyframes[0].declarations.len(), 2); 1710 + assert_eq!(kf.keyframes[1].declarations.len(), 2); 1711 + } 1712 + 1713 + #[test] 1714 + fn test_keyframes_with_style_rules() { 1715 + let ss = Parser::parse( 1716 + r#" 1717 + .box { color: red; } 1718 + @keyframes fade { from { opacity: 0; } to { opacity: 1; } } 1719 + p { font-size: 16px; } 1720 + "#, 1721 + ); 1722 + assert_eq!(ss.rules.len(), 3); 1723 + assert!(matches!(ss.rules[0], Rule::Style(_))); 1724 + assert!(matches!(ss.rules[1], Rule::Keyframes(_))); 1725 + assert!(matches!(ss.rules[2], Rule::Style(_))); 1485 1726 } 1486 1727 }
+1 -1
crates/css/src/transitions.rs
··· 441 441 trans 442 442 } 443 443 444 - fn split_by_comma(values: &[ComponentValue]) -> Vec<Vec<&ComponentValue>> { 444 + pub fn split_by_comma(values: &[ComponentValue]) -> Vec<Vec<&ComponentValue>> { 445 445 let mut groups: Vec<Vec<&ComponentValue>> = Vec::new(); 446 446 let mut current: Vec<&ComponentValue> = Vec::new(); 447 447
+143 -1
crates/style/src/computed.rs
··· 6 6 7 7 use std::collections::HashMap; 8 8 9 + use we_css::animations::{ 10 + parse_animation_delay, parse_animation_direction, parse_animation_duration, 11 + parse_animation_fill_mode, parse_animation_iteration_count, parse_animation_name, 12 + parse_animation_play_state, parse_animation_shorthand, parse_animation_timing_function, 13 + AnimationSpec, SingleAnimation, 14 + }; 9 15 use we_css::media::MediaContext; 10 16 use we_css::parser::{ComponentValue, Declaration, Stylesheet}; 11 17 use we_css::transitions::{ ··· 386 392 // CSS Transitions 387 393 pub transition: TransitionSpec, 388 394 395 + // CSS Animations 396 + pub animation: AnimationSpec, 397 + 389 398 // CSS Custom Properties (inherited by default) 390 399 pub custom_properties: HashMap<String, Vec<ComponentValue>>, 391 400 } ··· 466 475 order: 0, 467 476 468 477 transition: TransitionSpec::default(), 478 + 479 + animation: AnimationSpec::default(), 469 480 470 481 custom_properties: HashMap::new(), 471 482 } ··· 1542 1553 } 1543 1554 } 1544 1555 1556 + /// Handle animation-related properties from raw component values. 1557 + /// Returns `true` if the property was handled (caller should skip normal processing). 1558 + fn apply_animation_property( 1559 + style: &mut ComputedStyle, 1560 + property: &str, 1561 + values: &[ComponentValue], 1562 + ) -> bool { 1563 + match property { 1564 + "animation" => { 1565 + style.animation = parse_animation_shorthand(values); 1566 + true 1567 + } 1568 + "animation-name" => { 1569 + let names = parse_animation_name(values); 1570 + let len = names.len(); 1571 + style 1572 + .animation 1573 + .animations 1574 + .resize_with(len, SingleAnimation::default); 1575 + for (i, name) in names.into_iter().enumerate() { 1576 + style.animation.animations[i].name = name; 1577 + } 1578 + true 1579 + } 1580 + "animation-duration" => { 1581 + let durations = parse_animation_duration(values); 1582 + let len = durations.len().max(style.animation.animations.len()); 1583 + style 1584 + .animation 1585 + .animations 1586 + .resize_with(len, SingleAnimation::default); 1587 + for (i, dur) in durations.iter().enumerate() { 1588 + if i < style.animation.animations.len() { 1589 + style.animation.animations[i].duration = *dur; 1590 + } 1591 + } 1592 + true 1593 + } 1594 + "animation-timing-function" => { 1595 + let tfs = parse_animation_timing_function(values); 1596 + let len = tfs.len().max(style.animation.animations.len()); 1597 + style 1598 + .animation 1599 + .animations 1600 + .resize_with(len, SingleAnimation::default); 1601 + for (i, tf) in tfs.into_iter().enumerate() { 1602 + if i < style.animation.animations.len() { 1603 + style.animation.animations[i].timing_function = tf; 1604 + } 1605 + } 1606 + true 1607 + } 1608 + "animation-delay" => { 1609 + let delays = parse_animation_delay(values); 1610 + let len = delays.len().max(style.animation.animations.len()); 1611 + style 1612 + .animation 1613 + .animations 1614 + .resize_with(len, SingleAnimation::default); 1615 + for (i, delay) in delays.iter().enumerate() { 1616 + if i < style.animation.animations.len() { 1617 + style.animation.animations[i].delay = *delay; 1618 + } 1619 + } 1620 + true 1621 + } 1622 + "animation-iteration-count" => { 1623 + let counts = parse_animation_iteration_count(values); 1624 + let len = counts.len().max(style.animation.animations.len()); 1625 + style 1626 + .animation 1627 + .animations 1628 + .resize_with(len, SingleAnimation::default); 1629 + for (i, count) in counts.into_iter().enumerate() { 1630 + if i < style.animation.animations.len() { 1631 + style.animation.animations[i].iteration_count = count; 1632 + } 1633 + } 1634 + true 1635 + } 1636 + "animation-direction" => { 1637 + let dirs = parse_animation_direction(values); 1638 + let len = dirs.len().max(style.animation.animations.len()); 1639 + style 1640 + .animation 1641 + .animations 1642 + .resize_with(len, SingleAnimation::default); 1643 + for (i, dir) in dirs.into_iter().enumerate() { 1644 + if i < style.animation.animations.len() { 1645 + style.animation.animations[i].direction = dir; 1646 + } 1647 + } 1648 + true 1649 + } 1650 + "animation-fill-mode" => { 1651 + let modes = parse_animation_fill_mode(values); 1652 + let len = modes.len().max(style.animation.animations.len()); 1653 + style 1654 + .animation 1655 + .animations 1656 + .resize_with(len, SingleAnimation::default); 1657 + for (i, mode) in modes.into_iter().enumerate() { 1658 + if i < style.animation.animations.len() { 1659 + style.animation.animations[i].fill_mode = mode; 1660 + } 1661 + } 1662 + true 1663 + } 1664 + "animation-play-state" => { 1665 + let states = parse_animation_play_state(values); 1666 + let len = states.len().max(style.animation.animations.len()); 1667 + style 1668 + .animation 1669 + .animations 1670 + .resize_with(len, SingleAnimation::default); 1671 + for (i, state) in states.into_iter().enumerate() { 1672 + if i < style.animation.animations.len() { 1673 + style.animation.animations[i].play_state = state; 1674 + } 1675 + } 1676 + true 1677 + } 1678 + _ => false, 1679 + } 1680 + } 1681 + 1545 1682 fn resolve_border_width(value: &CssValue, em_base: f32, viewport: (f32, f32)) -> f32 { 1546 1683 match value { 1547 1684 CssValue::Length(n, unit) => resolve_length_unit(*n, *unit, em_base, viewport), ··· 1625 1762 "align-self" => style.align_self = parent.align_self, 1626 1763 "order" => style.order = parent.order, 1627 1764 "transition" => style.transition = parent.transition.clone(), 1765 + "animation" => style.animation = parent.animation.clone(), 1628 1766 _ => {} 1629 1767 } 1630 1768 } ··· 1684 1822 "align-self" => style.align_self = initial.align_self, 1685 1823 "order" => style.order = initial.order, 1686 1824 "transition" => style.transition = initial.transition, 1825 + "animation" => style.animation = initial.animation, 1687 1826 _ => {} 1688 1827 } 1689 1828 } ··· 2091 2230 &decl.value 2092 2231 }; 2093 2232 2094 - // Handle transition properties specially (need raw ComponentValues) 2233 + // Handle transition/animation properties specially (need raw ComponentValues) 2095 2234 if apply_transition_property(&mut style, &decl.property, values) { 2235 + continue; 2236 + } 2237 + if apply_animation_property(&mut style, &decl.property, values) { 2096 2238 continue; 2097 2239 } 2098 2240
+3
crates/style/src/matching.rs
··· 327 327 Rule::Import(_) => { 328 328 // Imports are resolved at a higher level; skip here. 329 329 } 330 + Rule::Keyframes(_) => { 331 + // Keyframes are resolved at a higher level; skip here. 332 + } 330 333 } 331 334 } 332 335 }