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 Transitions Level 1: parsing, timing, interpolation, and style integration

Adds a new `transitions` module to the CSS crate with:
- Transition property parsing (shorthand and longhands: transition-property,
transition-duration, transition-timing-function, transition-delay)
- Timing function evaluation: linear, ease, ease-in, ease-out, ease-in-out,
cubic-bezier (Newton + bisection solver), steps(n, start|end)
- Property value interpolation engine for lengths, colors (sRGB), and numbers
with mismatched-type snap-at-50% behavior
- TransitionMap for per-element transition tracking: start, evaluate at time,
mid-flight interruption (restarts from current interpolated value), and
completion detection for transitionend events
- Integration with ComputedStyle: TransitionSpec field, cascade-level parsing
from raw ComponentValues for transition properties
- 25 unit tests covering timing functions, interpolation, parsing, and
transition state management

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

+1298
+1
crates/css/src/lib.rs
··· 3 3 pub mod media; 4 4 pub mod parser; 5 5 pub mod tokenizer; 6 + pub mod transitions; 6 7 pub mod values;
+1202
crates/css/src/transitions.rs
··· 1 + //! CSS Transitions Level 1: parsing, timing functions, and interpolation. 2 + //! 3 + //! This module provides: 4 + //! - Transition property types (`TransitionProperty`, `TimingFunction`, etc.) 5 + //! - Parsing from `ComponentValue` lists 6 + //! - Cubic-bezier and step timing function evaluation 7 + //! - Property value interpolation for animatable CSS properties 8 + 9 + use crate::parser::ComponentValue; 10 + use crate::values::Color; 11 + 12 + // --------------------------------------------------------------------------- 13 + // Timing functions 14 + // --------------------------------------------------------------------------- 15 + 16 + /// A CSS timing function that maps progress [0, 1] to output [0, 1]. 17 + #[derive(Debug, Clone, PartialEq, Default)] 18 + pub enum TimingFunction { 19 + /// `linear` — constant speed. 20 + Linear, 21 + /// `ease` — default, equivalent to `cubic-bezier(0.25, 0.1, 0.25, 1.0)`. 22 + #[default] 23 + Ease, 24 + /// `ease-in` — equivalent to `cubic-bezier(0.42, 0, 1, 1)`. 25 + EaseIn, 26 + /// `ease-out` — equivalent to `cubic-bezier(0, 0, 0.58, 1)`. 27 + EaseOut, 28 + /// `ease-in-out` — equivalent to `cubic-bezier(0.42, 0, 0.58, 1)`. 29 + EaseInOut, 30 + /// `cubic-bezier(x1, y1, x2, y2)`. 31 + CubicBezier(f64, f64, f64, f64), 32 + /// `steps(count, position)`. 33 + Steps(u32, StepPosition), 34 + } 35 + 36 + /// Step position for `steps()` timing function. 37 + #[derive(Debug, Clone, Copy, PartialEq, Eq)] 38 + pub enum StepPosition { 39 + Start, 40 + End, 41 + } 42 + 43 + impl TimingFunction { 44 + /// Evaluate the timing function at progress `t` in [0, 1]. 45 + /// Returns the output value, typically in [0, 1] but may overshoot for 46 + /// cubic-bezier curves. 47 + pub fn evaluate(&self, t: f64) -> f64 { 48 + let t = t.clamp(0.0, 1.0); 49 + match self { 50 + TimingFunction::Linear => t, 51 + TimingFunction::Ease => cubic_bezier_evaluate(0.25, 0.1, 0.25, 1.0, t), 52 + TimingFunction::EaseIn => cubic_bezier_evaluate(0.42, 0.0, 1.0, 1.0, t), 53 + TimingFunction::EaseOut => cubic_bezier_evaluate(0.0, 0.0, 0.58, 1.0, t), 54 + TimingFunction::EaseInOut => cubic_bezier_evaluate(0.42, 0.0, 0.58, 1.0, t), 55 + TimingFunction::CubicBezier(x1, y1, x2, y2) => { 56 + cubic_bezier_evaluate(*x1, *y1, *x2, *y2, t) 57 + } 58 + TimingFunction::Steps(count, position) => steps_evaluate(*count, *position, t), 59 + } 60 + } 61 + } 62 + 63 + /// Evaluate a cubic-bezier curve at time `t`. 64 + /// 65 + /// The curve is defined by control points P0=(0,0), P1=(x1,y1), P2=(x2,y2), P3=(1,1). 66 + /// We need to find the parameter `s` such that `bezier_x(s) = t`, then return `bezier_y(s)`. 67 + fn cubic_bezier_evaluate(x1: f64, y1: f64, x2: f64, y2: f64, t: f64) -> f64 { 68 + if t <= 0.0 { 69 + return 0.0; 70 + } 71 + if t >= 1.0 { 72 + return 1.0; 73 + } 74 + 75 + // Find parameter s where bezier_x(s) = t using Newton's method 76 + let mut s = t; // Initial guess 77 + for _ in 0..8 { 78 + let x = bezier_sample(x1, x2, s) - t; 79 + if x.abs() < 1e-7 { 80 + break; 81 + } 82 + let dx = bezier_slope(x1, x2, s); 83 + if dx.abs() < 1e-7 { 84 + break; 85 + } 86 + s -= x / dx; 87 + } 88 + 89 + // Clamp s and bisect if Newton didn't converge 90 + s = s.clamp(0.0, 1.0); 91 + let x_at_s = bezier_sample(x1, x2, s) - t; 92 + if x_at_s.abs() > 1e-5 { 93 + // Fall back to bisection 94 + let mut lo = 0.0_f64; 95 + let mut hi = 1.0_f64; 96 + s = t; 97 + for _ in 0..20 { 98 + let x = bezier_sample(x1, x2, s); 99 + if (x - t).abs() < 1e-7 { 100 + break; 101 + } 102 + if x < t { 103 + lo = s; 104 + } else { 105 + hi = s; 106 + } 107 + s = (lo + hi) / 2.0; 108 + } 109 + } 110 + 111 + bezier_sample(y1, y2, s) 112 + } 113 + 114 + /// Sample a 1D cubic bezier at parameter t. 115 + /// B(t) = 3(1-t)^2 * t * p1 + 3(1-t) * t^2 * p2 + t^3 116 + fn bezier_sample(p1: f64, p2: f64, t: f64) -> f64 { 117 + let t2 = t * t; 118 + let t3 = t2 * t; 119 + let mt = 1.0 - t; 120 + let mt2 = mt * mt; 121 + 3.0 * mt2 * t * p1 + 3.0 * mt * t2 * p2 + t3 122 + } 123 + 124 + /// Derivative of the 1D cubic bezier at parameter t. 125 + fn bezier_slope(p1: f64, p2: f64, t: f64) -> f64 { 126 + let mt = 1.0 - t; 127 + 3.0 * mt * mt * p1 + 6.0 * mt * t * (p2 - p1) + 3.0 * t * t * (1.0 - p2) 128 + } 129 + 130 + /// Evaluate `steps(count, position)` at progress `t`. 131 + fn steps_evaluate(count: u32, position: StepPosition, t: f64) -> f64 { 132 + if count == 0 { 133 + return t; 134 + } 135 + let n = count as f64; 136 + let step = match position { 137 + StepPosition::Start => (t * n).ceil() / n, 138 + StepPosition::End => (t * n).floor() / n, 139 + }; 140 + step.clamp(0.0, 1.0) 141 + } 142 + 143 + // --------------------------------------------------------------------------- 144 + // Transition property specification 145 + // --------------------------------------------------------------------------- 146 + 147 + /// Which CSS properties to transition. 148 + #[derive(Debug, Clone, PartialEq)] 149 + pub enum TransitionPropertyName { 150 + /// `all` — transition all animatable properties. 151 + All, 152 + /// `none` — no transitions. 153 + None, 154 + /// A specific CSS property name. 155 + Property(String), 156 + } 157 + 158 + /// A single transition specification (one entry in a comma-separated list). 159 + #[derive(Debug, Clone, PartialEq)] 160 + pub struct SingleTransition { 161 + /// Which property to transition. 162 + pub property: TransitionPropertyName, 163 + /// Duration in seconds. 164 + pub duration: f64, 165 + /// Timing function. 166 + pub timing_function: TimingFunction, 167 + /// Delay in seconds. 168 + pub delay: f64, 169 + } 170 + 171 + impl Default for SingleTransition { 172 + fn default() -> Self { 173 + SingleTransition { 174 + property: TransitionPropertyName::All, 175 + duration: 0.0, 176 + timing_function: TimingFunction::Ease, 177 + delay: 0.0, 178 + } 179 + } 180 + } 181 + 182 + /// Parsed transition specification for an element, possibly multiple transitions. 183 + #[derive(Debug, Clone, PartialEq, Default)] 184 + pub struct TransitionSpec { 185 + pub transitions: Vec<SingleTransition>, 186 + } 187 + 188 + // --------------------------------------------------------------------------- 189 + // Parsing 190 + // --------------------------------------------------------------------------- 191 + 192 + /// Parse a `transition-duration` or `transition-delay` time value. 193 + /// Returns seconds. 194 + pub fn parse_time_value(cv: &ComponentValue) -> Option<f64> { 195 + match cv { 196 + ComponentValue::Dimension(n, _, unit) => { 197 + let u = unit.to_ascii_lowercase(); 198 + match u.as_str() { 199 + "s" => Some(*n), 200 + "ms" => Some(*n / 1000.0), 201 + _ => None, 202 + } 203 + } 204 + ComponentValue::Number(n, _) if *n == 0.0 => Some(0.0), 205 + _ => None, 206 + } 207 + } 208 + 209 + /// Parse a timing function from component values. 210 + pub fn parse_timing_function(cv: &ComponentValue) -> Option<TimingFunction> { 211 + match cv { 212 + ComponentValue::Ident(s) => match s.to_ascii_lowercase().as_str() { 213 + "linear" => Some(TimingFunction::Linear), 214 + "ease" => Some(TimingFunction::Ease), 215 + "ease-in" => Some(TimingFunction::EaseIn), 216 + "ease-out" => Some(TimingFunction::EaseOut), 217 + "ease-in-out" => Some(TimingFunction::EaseInOut), 218 + _ => None, 219 + }, 220 + ComponentValue::Function(name, args) => { 221 + let name_lower = name.to_ascii_lowercase(); 222 + match name_lower.as_str() { 223 + "cubic-bezier" => parse_cubic_bezier_args(args), 224 + "steps" => parse_steps_args(args), 225 + _ => None, 226 + } 227 + } 228 + _ => None, 229 + } 230 + } 231 + 232 + fn parse_cubic_bezier_args(args: &[ComponentValue]) -> Option<TimingFunction> { 233 + let nums: Vec<f64> = args 234 + .iter() 235 + .filter_map(|cv| match cv { 236 + ComponentValue::Number(n, _) => Some(*n), 237 + _ => None, 238 + }) 239 + .collect(); 240 + 241 + if nums.len() == 4 { 242 + // x1 and x2 must be in [0, 1] per spec 243 + let (x1, y1, x2, y2) = (nums[0], nums[1], nums[2], nums[3]); 244 + if (0.0..=1.0).contains(&x1) && (0.0..=1.0).contains(&x2) { 245 + Some(TimingFunction::CubicBezier(x1, y1, x2, y2)) 246 + } else { 247 + None 248 + } 249 + } else { 250 + None 251 + } 252 + } 253 + 254 + fn parse_steps_args(args: &[ComponentValue]) -> Option<TimingFunction> { 255 + let non_ws: Vec<&ComponentValue> = args 256 + .iter() 257 + .filter(|cv| !matches!(cv, ComponentValue::Whitespace | ComponentValue::Comma)) 258 + .collect(); 259 + 260 + if non_ws.is_empty() { 261 + return None; 262 + } 263 + 264 + // First argument: step count 265 + let count = match non_ws[0] { 266 + ComponentValue::Number(n, _) if *n >= 1.0 => *n as u32, 267 + _ => return None, 268 + }; 269 + 270 + // Second argument (optional): step position 271 + let position = if non_ws.len() > 1 { 272 + match non_ws[1] { 273 + ComponentValue::Ident(s) => match s.to_ascii_lowercase().as_str() { 274 + "start" | "jump-start" => StepPosition::Start, 275 + "end" | "jump-end" => StepPosition::End, 276 + _ => StepPosition::End, 277 + }, 278 + _ => StepPosition::End, 279 + } 280 + } else { 281 + StepPosition::End 282 + }; 283 + 284 + Some(TimingFunction::Steps(count, position)) 285 + } 286 + 287 + /// Parse the `transition` shorthand property value. 288 + /// Syntax: `[property || duration || timing-function || delay] [, ...]` 289 + pub fn parse_transition_shorthand(values: &[ComponentValue]) -> TransitionSpec { 290 + // Split by comma to get individual transition specifications 291 + let groups = split_by_comma(values); 292 + let mut transitions = Vec::new(); 293 + 294 + for group in groups { 295 + transitions.push(parse_single_transition(&group)); 296 + } 297 + 298 + if transitions.is_empty() { 299 + transitions.push(SingleTransition::default()); 300 + } 301 + 302 + TransitionSpec { transitions } 303 + } 304 + 305 + /// Parse a `transition-property` value. 306 + pub fn parse_transition_property(values: &[ComponentValue]) -> Vec<TransitionPropertyName> { 307 + let groups = split_by_comma(values); 308 + let mut result = Vec::new(); 309 + 310 + for group in groups { 311 + let non_ws: Vec<&ComponentValue> = group 312 + .iter() 313 + .copied() 314 + .filter(|cv| !matches!(cv, ComponentValue::Whitespace)) 315 + .collect(); 316 + 317 + if non_ws.len() == 1 { 318 + if let ComponentValue::Ident(s) = non_ws[0] { 319 + let lower = s.to_ascii_lowercase(); 320 + match lower.as_str() { 321 + "all" => result.push(TransitionPropertyName::All), 322 + "none" => result.push(TransitionPropertyName::None), 323 + _ => result.push(TransitionPropertyName::Property(lower)), 324 + } 325 + } 326 + } 327 + } 328 + 329 + if result.is_empty() { 330 + result.push(TransitionPropertyName::All); 331 + } 332 + 333 + result 334 + } 335 + 336 + /// Parse a `transition-duration` value (comma-separated time list). 337 + pub fn parse_transition_duration(values: &[ComponentValue]) -> Vec<f64> { 338 + parse_time_list(values) 339 + } 340 + 341 + /// Parse a `transition-delay` value (comma-separated time list). 342 + pub fn parse_transition_delay(values: &[ComponentValue]) -> Vec<f64> { 343 + parse_time_list(values) 344 + } 345 + 346 + /// Parse a `transition-timing-function` value (comma-separated list). 347 + pub fn parse_transition_timing_function(values: &[ComponentValue]) -> Vec<TimingFunction> { 348 + let groups = split_by_comma(values); 349 + let mut result = Vec::new(); 350 + 351 + for group in groups { 352 + let non_ws: Vec<&ComponentValue> = group 353 + .iter() 354 + .copied() 355 + .filter(|cv| !matches!(cv, ComponentValue::Whitespace)) 356 + .collect(); 357 + 358 + if non_ws.len() == 1 { 359 + if let Some(tf) = parse_timing_function(non_ws[0]) { 360 + result.push(tf); 361 + continue; 362 + } 363 + } 364 + // Default for unparseable entries 365 + result.push(TimingFunction::Ease); 366 + } 367 + 368 + if result.is_empty() { 369 + result.push(TimingFunction::Ease); 370 + } 371 + 372 + result 373 + } 374 + 375 + fn parse_time_list(values: &[ComponentValue]) -> Vec<f64> { 376 + let groups = split_by_comma(values); 377 + let mut result = Vec::new(); 378 + 379 + for group in groups { 380 + let non_ws: Vec<&ComponentValue> = group 381 + .iter() 382 + .copied() 383 + .filter(|cv| !matches!(cv, ComponentValue::Whitespace)) 384 + .collect(); 385 + 386 + if non_ws.len() == 1 { 387 + if let Some(t) = parse_time_value(non_ws[0]) { 388 + result.push(t); 389 + continue; 390 + } 391 + } 392 + result.push(0.0); 393 + } 394 + 395 + if result.is_empty() { 396 + result.push(0.0); 397 + } 398 + 399 + result 400 + } 401 + 402 + fn parse_single_transition(values: &[&ComponentValue]) -> SingleTransition { 403 + let non_ws: Vec<&ComponentValue> = values 404 + .iter() 405 + .copied() 406 + .filter(|cv| !matches!(cv, ComponentValue::Whitespace)) 407 + .collect(); 408 + 409 + let mut trans = SingleTransition::default(); 410 + let mut time_count = 0; // First time = duration, second = delay 411 + 412 + for cv in &non_ws { 413 + // Try timing function first (keywords like ease, linear, and functions) 414 + if let Some(tf) = parse_timing_function(cv) { 415 + trans.timing_function = tf; 416 + continue; 417 + } 418 + 419 + // Try time value 420 + if let Some(t) = parse_time_value(cv) { 421 + if time_count == 0 { 422 + trans.duration = t; 423 + } else { 424 + trans.delay = t; 425 + } 426 + time_count += 1; 427 + continue; 428 + } 429 + 430 + // Try property name 431 + if let ComponentValue::Ident(s) = cv { 432 + let lower = s.to_ascii_lowercase(); 433 + match lower.as_str() { 434 + "all" => trans.property = TransitionPropertyName::All, 435 + "none" => trans.property = TransitionPropertyName::None, 436 + _ => trans.property = TransitionPropertyName::Property(lower), 437 + } 438 + } 439 + } 440 + 441 + trans 442 + } 443 + 444 + fn split_by_comma(values: &[ComponentValue]) -> Vec<Vec<&ComponentValue>> { 445 + let mut groups: Vec<Vec<&ComponentValue>> = Vec::new(); 446 + let mut current: Vec<&ComponentValue> = Vec::new(); 447 + 448 + for cv in values { 449 + if matches!(cv, ComponentValue::Comma) { 450 + if !current.is_empty() { 451 + groups.push(current); 452 + current = Vec::new(); 453 + } 454 + } else { 455 + current.push(cv); 456 + } 457 + } 458 + 459 + if !current.is_empty() { 460 + groups.push(current); 461 + } 462 + 463 + groups 464 + } 465 + 466 + // --------------------------------------------------------------------------- 467 + // Property interpolation 468 + // --------------------------------------------------------------------------- 469 + 470 + /// An interpolable property value — a snapshot of a property at a point in time. 471 + #[derive(Debug, Clone, PartialEq)] 472 + pub enum AnimatableValue { 473 + /// A length in px. 474 + Length(f32), 475 + /// A color (RGBA). 476 + Color(Color), 477 + /// Opacity (0.0–1.0) or other unitless float. 478 + Number(f32), 479 + } 480 + 481 + impl AnimatableValue { 482 + /// Linearly interpolate between `self` (from) and `to` at progress `t` (0..1). 483 + pub fn interpolate(&self, to: &AnimatableValue, t: f64) -> AnimatableValue { 484 + match (self, to) { 485 + (AnimatableValue::Length(a), AnimatableValue::Length(b)) => { 486 + AnimatableValue::Length(lerp_f32(*a, *b, t)) 487 + } 488 + (AnimatableValue::Number(a), AnimatableValue::Number(b)) => { 489 + AnimatableValue::Number(lerp_f32(*a, *b, t)) 490 + } 491 + (AnimatableValue::Color(a), AnimatableValue::Color(b)) => { 492 + AnimatableValue::Color(interpolate_color(a, b, t)) 493 + } 494 + // Mismatched types: snap at 50% 495 + _ => { 496 + if t < 0.5 { 497 + self.clone() 498 + } else { 499 + to.clone() 500 + } 501 + } 502 + } 503 + } 504 + } 505 + 506 + fn lerp_f32(a: f32, b: f32, t: f64) -> f32 { 507 + let t = t as f32; 508 + a + (b - a) * t 509 + } 510 + 511 + /// Interpolate between two colors in sRGB space. 512 + fn interpolate_color(a: &Color, b: &Color, t: f64) -> Color { 513 + let t = t as f32; 514 + let r = (a.r as f32 + (b.r as f32 - a.r as f32) * t).round() as u8; 515 + let g = (a.g as f32 + (b.g as f32 - a.g as f32) * t).round() as u8; 516 + let bl = (a.b as f32 + (b.b as f32 - a.b as f32) * t).round() as u8; 517 + let al = (a.a as f32 + (b.a as f32 - a.a as f32) * t).round() as u8; 518 + Color::new(r, g, bl, al) 519 + } 520 + 521 + /// Check whether a CSS property name is animatable. 522 + pub fn is_animatable_property(property: &str) -> bool { 523 + matches!( 524 + property, 525 + "opacity" 526 + | "color" 527 + | "background-color" 528 + | "border-top-color" 529 + | "border-right-color" 530 + | "border-bottom-color" 531 + | "border-left-color" 532 + | "border-top-width" 533 + | "border-right-width" 534 + | "border-bottom-width" 535 + | "border-left-width" 536 + | "margin-top" 537 + | "margin-right" 538 + | "margin-bottom" 539 + | "margin-left" 540 + | "padding-top" 541 + | "padding-right" 542 + | "padding-bottom" 543 + | "padding-left" 544 + | "width" 545 + | "height" 546 + | "top" 547 + | "right" 548 + | "bottom" 549 + | "left" 550 + | "font-size" 551 + | "font-weight" 552 + | "line-height" 553 + | "row-gap" 554 + | "column-gap" 555 + | "flex-grow" 556 + | "flex-shrink" 557 + ) 558 + } 559 + 560 + // --------------------------------------------------------------------------- 561 + // Transition state tracking 562 + // --------------------------------------------------------------------------- 563 + 564 + /// An active CSS transition on a single property. 565 + #[derive(Debug, Clone)] 566 + pub struct ActiveTransition { 567 + /// The CSS property being transitioned. 568 + pub property: String, 569 + /// Value at the start of the transition. 570 + pub from: AnimatableValue, 571 + /// Target value. 572 + pub to: AnimatableValue, 573 + /// Start time in seconds (monotonic clock). 574 + pub start_time: f64, 575 + /// Duration in seconds. 576 + pub duration: f64, 577 + /// Delay in seconds. 578 + pub delay: f64, 579 + /// Timing function. 580 + pub timing_function: TimingFunction, 581 + } 582 + 583 + impl ActiveTransition { 584 + /// Evaluate the transition at the given time (seconds since epoch/start). 585 + /// Returns `None` if the transition hasn't started yet (still in delay). 586 + /// Returns the interpolated value otherwise. 587 + pub fn evaluate(&self, now: f64) -> TransitionResult { 588 + let elapsed = now - self.start_time; 589 + 590 + if elapsed < self.delay { 591 + // Still in delay period — return from value 592 + return TransitionResult::Pending(self.from.clone()); 593 + } 594 + 595 + let active_elapsed = elapsed - self.delay; 596 + 597 + if self.duration <= 0.0 || active_elapsed >= self.duration { 598 + // Transition complete 599 + return TransitionResult::Finished(self.to.clone()); 600 + } 601 + 602 + // Compute progress and apply timing function 603 + let progress = active_elapsed / self.duration; 604 + let eased = self.timing_function.evaluate(progress); 605 + TransitionResult::Active(self.from.interpolate(&self.to, eased)) 606 + } 607 + } 608 + 609 + /// Result of evaluating a transition at a point in time. 610 + #[derive(Debug, Clone)] 611 + pub enum TransitionResult { 612 + /// Transition is still in delay — use the from value. 613 + Pending(AnimatableValue), 614 + /// Transition is actively animating — use interpolated value. 615 + Active(AnimatableValue), 616 + /// Transition is complete — use the to value. 617 + Finished(AnimatableValue), 618 + } 619 + 620 + impl TransitionResult { 621 + /// Get the current value regardless of state. 622 + pub fn value(&self) -> &AnimatableValue { 623 + match self { 624 + TransitionResult::Pending(v) 625 + | TransitionResult::Active(v) 626 + | TransitionResult::Finished(v) => v, 627 + } 628 + } 629 + 630 + /// Whether the transition is still running (pending or active). 631 + pub fn is_running(&self) -> bool { 632 + matches!( 633 + self, 634 + TransitionResult::Pending(_) | TransitionResult::Active(_) 635 + ) 636 + } 637 + } 638 + 639 + /// Manages transitions for a single element. 640 + #[derive(Debug, Clone, Default)] 641 + pub struct TransitionMap { 642 + /// Active transitions keyed by property name. 643 + pub active: Vec<ActiveTransition>, 644 + } 645 + 646 + impl TransitionMap { 647 + /// Check for computed style changes and start new transitions as needed. 648 + /// 649 + /// `spec` is the element's transition specification from CSS. 650 + /// `old_values` and `new_values` are property name → animatable value snapshots. 651 + /// `now` is the current time in seconds. 652 + pub fn update( 653 + &mut self, 654 + spec: &TransitionSpec, 655 + old_values: &[(String, AnimatableValue)], 656 + new_values: &[(String, AnimatableValue)], 657 + now: f64, 658 + ) { 659 + // Build lookup for old values 660 + let old_map: Vec<(&str, &AnimatableValue)> = 661 + old_values.iter().map(|(k, v)| (k.as_str(), v)).collect(); 662 + 663 + for (prop_name, new_val) in new_values { 664 + // Find old value 665 + let old_val = old_map 666 + .iter() 667 + .find(|(k, _)| *k == prop_name.as_str()) 668 + .map(|(_, v)| *v); 669 + 670 + let old_val = match old_val { 671 + Some(v) => v, 672 + None => continue, 673 + }; 674 + 675 + // Check if value changed 676 + if old_val == new_val { 677 + // Value didn't change — keep existing transition if any, remove if finished 678 + self.active.retain(|t| { 679 + if t.property == *prop_name { 680 + t.evaluate(now).is_running() 681 + } else { 682 + true 683 + } 684 + }); 685 + continue; 686 + } 687 + 688 + // Find the applicable transition spec for this property 689 + let applicable = find_transition_for_property(spec, prop_name); 690 + let applicable = match applicable { 691 + Some(t) => t, 692 + None => { 693 + // No transition for this property — remove any active one 694 + self.active.retain(|t| t.property != *prop_name); 695 + continue; 696 + } 697 + }; 698 + 699 + // Duration must be > 0 for a transition to occur 700 + if applicable.duration <= 0.0 { 701 + self.active.retain(|t| t.property != *prop_name); 702 + continue; 703 + } 704 + 705 + // If there's an active transition on this property, start from its current value 706 + let from_val = self 707 + .active 708 + .iter() 709 + .find(|t| t.property == *prop_name) 710 + .and_then(|t| { 711 + let result = t.evaluate(now); 712 + if result.is_running() { 713 + Some(result.value().clone()) 714 + } else { 715 + None 716 + } 717 + }) 718 + .unwrap_or_else(|| old_val.clone()); 719 + 720 + // Remove existing transition for this property 721 + self.active.retain(|t| t.property != *prop_name); 722 + 723 + // Start new transition 724 + self.active.push(ActiveTransition { 725 + property: prop_name.clone(), 726 + from: from_val, 727 + to: new_val.clone(), 728 + start_time: now, 729 + duration: applicable.duration, 730 + delay: applicable.delay, 731 + timing_function: applicable.timing_function.clone(), 732 + }); 733 + } 734 + 735 + // Clean up finished transitions 736 + self.active.retain(|t| t.evaluate(now).is_running()); 737 + } 738 + 739 + /// Get the current value for a property if it has an active transition. 740 + pub fn get_value(&self, property: &str, now: f64) -> Option<AnimatableValue> { 741 + self.active 742 + .iter() 743 + .find(|t| t.property == property) 744 + .map(|t| t.evaluate(now).value().clone()) 745 + } 746 + 747 + /// Returns true if any transitions are currently running. 748 + pub fn has_active_transitions(&self) -> bool { 749 + !self.active.is_empty() 750 + } 751 + 752 + /// Return properties whose transitions have just completed since the last check. 753 + /// Call this after `update()` to determine which `transitionend` events to fire. 754 + pub fn drain_completed(&mut self, now: f64) -> Vec<String> { 755 + let mut completed = Vec::new(); 756 + self.active.retain(|t| { 757 + if !t.evaluate(now).is_running() { 758 + completed.push(t.property.clone()); 759 + false 760 + } else { 761 + true 762 + } 763 + }); 764 + completed 765 + } 766 + } 767 + 768 + fn find_transition_for_property<'a>( 769 + spec: &'a TransitionSpec, 770 + property: &str, 771 + ) -> Option<&'a SingleTransition> { 772 + if spec.transitions.is_empty() { 773 + return None; 774 + } 775 + 776 + // Search from end to beginning (last matching wins) 777 + for trans in spec.transitions.iter().rev() { 778 + match &trans.property { 779 + TransitionPropertyName::None => return None, 780 + TransitionPropertyName::All => { 781 + if is_animatable_property(property) { 782 + return Some(trans); 783 + } 784 + } 785 + TransitionPropertyName::Property(name) => { 786 + if name == property && is_animatable_property(property) { 787 + return Some(trans); 788 + } 789 + } 790 + } 791 + } 792 + 793 + None 794 + } 795 + 796 + // --------------------------------------------------------------------------- 797 + // Tests 798 + // --------------------------------------------------------------------------- 799 + 800 + #[cfg(test)] 801 + mod tests { 802 + use super::*; 803 + use crate::parser::ComponentValue; 804 + use crate::tokenizer::NumericType; 805 + 806 + // -- Timing function evaluation tests -- 807 + 808 + #[test] 809 + fn test_linear_timing() { 810 + let tf = TimingFunction::Linear; 811 + assert_eq!(tf.evaluate(0.0), 0.0); 812 + assert_eq!(tf.evaluate(0.5), 0.5); 813 + assert_eq!(tf.evaluate(1.0), 1.0); 814 + } 815 + 816 + #[test] 817 + fn test_ease_endpoints() { 818 + let tf = TimingFunction::Ease; 819 + assert_eq!(tf.evaluate(0.0), 0.0); 820 + assert_eq!(tf.evaluate(1.0), 1.0); 821 + } 822 + 823 + #[test] 824 + fn test_ease_in_out_symmetry() { 825 + let tf = TimingFunction::EaseInOut; 826 + let at_quarter = tf.evaluate(0.25); 827 + let at_three_quarter = tf.evaluate(0.75); 828 + // ease-in-out is symmetric: f(0.25) + f(0.75) ≈ 1.0 829 + assert!((at_quarter + at_three_quarter - 1.0).abs() < 0.01); 830 + } 831 + 832 + #[test] 833 + fn test_cubic_bezier_linear() { 834 + // cubic-bezier that approximates linear 835 + let tf = TimingFunction::CubicBezier(0.0, 0.0, 1.0, 1.0); 836 + let mid = tf.evaluate(0.5); 837 + assert!((mid - 0.5).abs() < 0.02); 838 + } 839 + 840 + #[test] 841 + fn test_steps_end() { 842 + let tf = TimingFunction::Steps(4, StepPosition::End); 843 + assert_eq!(tf.evaluate(0.0), 0.0); 844 + assert_eq!(tf.evaluate(0.24), 0.0); 845 + assert_eq!(tf.evaluate(0.25), 0.25); 846 + assert_eq!(tf.evaluate(0.49), 0.25); 847 + assert_eq!(tf.evaluate(0.5), 0.5); 848 + assert_eq!(tf.evaluate(1.0), 1.0); 849 + } 850 + 851 + #[test] 852 + fn test_steps_start() { 853 + let tf = TimingFunction::Steps(4, StepPosition::Start); 854 + assert_eq!(tf.evaluate(0.0), 0.0); 855 + // At just past 0, should jump to 0.25 856 + assert_eq!(tf.evaluate(0.01), 0.25); 857 + assert_eq!(tf.evaluate(0.25), 0.25); 858 + assert_eq!(tf.evaluate(0.26), 0.5); 859 + } 860 + 861 + // -- Color interpolation tests -- 862 + 863 + #[test] 864 + fn test_color_interpolation() { 865 + let black = Color::new(0, 0, 0, 255); 866 + let white = Color::new(255, 255, 255, 255); 867 + let a = AnimatableValue::Color(black); 868 + let b = AnimatableValue::Color(white); 869 + 870 + let mid = a.interpolate(&b, 0.5); 871 + if let AnimatableValue::Color(c) = mid { 872 + assert_eq!(c.r, 128); 873 + assert_eq!(c.g, 128); 874 + assert_eq!(c.b, 128); 875 + assert_eq!(c.a, 255); 876 + } else { 877 + panic!("Expected color"); 878 + } 879 + } 880 + 881 + #[test] 882 + fn test_length_interpolation() { 883 + let a = AnimatableValue::Length(0.0); 884 + let b = AnimatableValue::Length(100.0); 885 + 886 + let mid = a.interpolate(&b, 0.5); 887 + assert_eq!(mid, AnimatableValue::Length(50.0)); 888 + 889 + let quarter = a.interpolate(&b, 0.25); 890 + assert_eq!(quarter, AnimatableValue::Length(25.0)); 891 + } 892 + 893 + #[test] 894 + fn test_number_interpolation() { 895 + let a = AnimatableValue::Number(0.0); 896 + let b = AnimatableValue::Number(1.0); 897 + 898 + let mid = a.interpolate(&b, 0.75); 899 + assert_eq!(mid, AnimatableValue::Number(0.75)); 900 + } 901 + 902 + // -- Parsing tests -- 903 + 904 + #[test] 905 + fn test_parse_time_value_seconds() { 906 + let cv = ComponentValue::Dimension(0.3, NumericType::Number, "s".into()); 907 + assert_eq!(parse_time_value(&cv), Some(0.3)); 908 + } 909 + 910 + #[test] 911 + fn test_parse_time_value_milliseconds() { 912 + let cv = ComponentValue::Dimension(300.0, NumericType::Number, "ms".into()); 913 + assert_eq!(parse_time_value(&cv), Some(0.3)); 914 + } 915 + 916 + #[test] 917 + fn test_parse_time_value_zero() { 918 + let cv = ComponentValue::Number(0.0, NumericType::Integer); 919 + assert_eq!(parse_time_value(&cv), Some(0.0)); 920 + } 921 + 922 + #[test] 923 + fn test_parse_timing_function_keywords() { 924 + let cv = ComponentValue::Ident("ease-in".into()); 925 + assert_eq!(parse_timing_function(&cv), Some(TimingFunction::EaseIn)); 926 + 927 + let cv = ComponentValue::Ident("linear".into()); 928 + assert_eq!(parse_timing_function(&cv), Some(TimingFunction::Linear)); 929 + } 930 + 931 + #[test] 932 + fn test_parse_cubic_bezier() { 933 + let args = vec![ 934 + ComponentValue::Number(0.42, NumericType::Number), 935 + ComponentValue::Comma, 936 + ComponentValue::Number(0.0, NumericType::Number), 937 + ComponentValue::Comma, 938 + ComponentValue::Number(0.58, NumericType::Number), 939 + ComponentValue::Comma, 940 + ComponentValue::Number(1.0, NumericType::Number), 941 + ]; 942 + let cv = ComponentValue::Function("cubic-bezier".into(), args); 943 + assert_eq!( 944 + parse_timing_function(&cv), 945 + Some(TimingFunction::CubicBezier(0.42, 0.0, 0.58, 1.0)) 946 + ); 947 + } 948 + 949 + #[test] 950 + fn test_parse_steps() { 951 + let args = vec![ 952 + ComponentValue::Number(4.0, NumericType::Integer), 953 + ComponentValue::Comma, 954 + ComponentValue::Whitespace, 955 + ComponentValue::Ident("start".into()), 956 + ]; 957 + let cv = ComponentValue::Function("steps".into(), args); 958 + assert_eq!( 959 + parse_timing_function(&cv), 960 + Some(TimingFunction::Steps(4, StepPosition::Start)) 961 + ); 962 + } 963 + 964 + #[test] 965 + fn test_parse_transition_shorthand_simple() { 966 + // transition: opacity 0.3s ease-in 0.1s 967 + let values = vec![ 968 + ComponentValue::Ident("opacity".into()), 969 + ComponentValue::Whitespace, 970 + ComponentValue::Dimension(0.3, NumericType::Number, "s".into()), 971 + ComponentValue::Whitespace, 972 + ComponentValue::Ident("ease-in".into()), 973 + ComponentValue::Whitespace, 974 + ComponentValue::Dimension(0.1, NumericType::Number, "s".into()), 975 + ]; 976 + let spec = parse_transition_shorthand(&values); 977 + assert_eq!(spec.transitions.len(), 1); 978 + let t = &spec.transitions[0]; 979 + assert_eq!( 980 + t.property, 981 + TransitionPropertyName::Property("opacity".into()) 982 + ); 983 + assert_eq!(t.duration, 0.3); 984 + assert_eq!(t.timing_function, TimingFunction::EaseIn); 985 + assert_eq!(t.delay, 0.1); 986 + } 987 + 988 + #[test] 989 + fn test_parse_transition_shorthand_multiple() { 990 + // transition: opacity 0.3s, color 0.5s linear 991 + let values = vec![ 992 + ComponentValue::Ident("opacity".into()), 993 + ComponentValue::Whitespace, 994 + ComponentValue::Dimension(0.3, NumericType::Number, "s".into()), 995 + ComponentValue::Comma, 996 + ComponentValue::Whitespace, 997 + ComponentValue::Ident("color".into()), 998 + ComponentValue::Whitespace, 999 + ComponentValue::Dimension(0.5, NumericType::Number, "s".into()), 1000 + ComponentValue::Whitespace, 1001 + ComponentValue::Ident("linear".into()), 1002 + ]; 1003 + let spec = parse_transition_shorthand(&values); 1004 + assert_eq!(spec.transitions.len(), 2); 1005 + assert_eq!( 1006 + spec.transitions[0].property, 1007 + TransitionPropertyName::Property("opacity".into()) 1008 + ); 1009 + assert_eq!(spec.transitions[0].duration, 0.3); 1010 + assert_eq!( 1011 + spec.transitions[1].property, 1012 + TransitionPropertyName::Property("color".into()) 1013 + ); 1014 + assert_eq!(spec.transitions[1].duration, 0.5); 1015 + assert_eq!(spec.transitions[1].timing_function, TimingFunction::Linear); 1016 + } 1017 + 1018 + #[test] 1019 + fn test_parse_transition_property_all() { 1020 + let values = vec![ComponentValue::Ident("all".into())]; 1021 + let result = parse_transition_property(&values); 1022 + assert_eq!(result, vec![TransitionPropertyName::All]); 1023 + } 1024 + 1025 + #[test] 1026 + fn test_parse_transition_property_list() { 1027 + let values = vec![ 1028 + ComponentValue::Ident("opacity".into()), 1029 + ComponentValue::Comma, 1030 + ComponentValue::Whitespace, 1031 + ComponentValue::Ident("color".into()), 1032 + ]; 1033 + let result = parse_transition_property(&values); 1034 + assert_eq!( 1035 + result, 1036 + vec![ 1037 + TransitionPropertyName::Property("opacity".into()), 1038 + TransitionPropertyName::Property("color".into()), 1039 + ] 1040 + ); 1041 + } 1042 + 1043 + // -- Transition state management tests -- 1044 + 1045 + #[test] 1046 + fn test_active_transition_evaluation() { 1047 + let trans = ActiveTransition { 1048 + property: "opacity".into(), 1049 + from: AnimatableValue::Number(1.0), 1050 + to: AnimatableValue::Number(0.0), 1051 + start_time: 0.0, 1052 + duration: 1.0, 1053 + delay: 0.0, 1054 + timing_function: TimingFunction::Linear, 1055 + }; 1056 + 1057 + // At start 1058 + let result = trans.evaluate(0.0); 1059 + assert!(result.is_running()); 1060 + assert_eq!(*result.value(), AnimatableValue::Number(1.0)); 1061 + 1062 + // At midpoint 1063 + let result = trans.evaluate(0.5); 1064 + assert!(result.is_running()); 1065 + assert_eq!(*result.value(), AnimatableValue::Number(0.5)); 1066 + 1067 + // At end 1068 + let result = trans.evaluate(1.0); 1069 + assert!(!result.is_running()); 1070 + assert_eq!(*result.value(), AnimatableValue::Number(0.0)); 1071 + } 1072 + 1073 + #[test] 1074 + fn test_active_transition_with_delay() { 1075 + let trans = ActiveTransition { 1076 + property: "opacity".into(), 1077 + from: AnimatableValue::Number(0.0), 1078 + to: AnimatableValue::Number(1.0), 1079 + start_time: 0.0, 1080 + duration: 1.0, 1081 + delay: 0.5, 1082 + timing_function: TimingFunction::Linear, 1083 + }; 1084 + 1085 + // During delay 1086 + let result = trans.evaluate(0.3); 1087 + assert!(result.is_running()); 1088 + assert_eq!(*result.value(), AnimatableValue::Number(0.0)); 1089 + 1090 + // After delay, at midpoint of duration 1091 + let result = trans.evaluate(1.0); 1092 + assert!(result.is_running()); 1093 + assert_eq!(*result.value(), AnimatableValue::Number(0.5)); 1094 + 1095 + // Complete 1096 + let result = trans.evaluate(1.5); 1097 + assert!(!result.is_running()); 1098 + assert_eq!(*result.value(), AnimatableValue::Number(1.0)); 1099 + } 1100 + 1101 + #[test] 1102 + fn test_transition_map_starts_transition() { 1103 + let mut map = TransitionMap::default(); 1104 + let spec = TransitionSpec { 1105 + transitions: vec![SingleTransition { 1106 + property: TransitionPropertyName::All, 1107 + duration: 0.5, 1108 + timing_function: TimingFunction::Linear, 1109 + delay: 0.0, 1110 + }], 1111 + }; 1112 + 1113 + let old = vec![("opacity".into(), AnimatableValue::Number(1.0))]; 1114 + let new = vec![("opacity".into(), AnimatableValue::Number(0.0))]; 1115 + 1116 + map.update(&spec, &old, &new, 0.0); 1117 + assert!(map.has_active_transitions()); 1118 + assert_eq!(map.active.len(), 1); 1119 + 1120 + let val = map.get_value("opacity", 0.25); 1121 + assert!(val.is_some()); 1122 + if let Some(AnimatableValue::Number(n)) = val { 1123 + assert!((n - 0.5).abs() < 0.01); 1124 + } 1125 + } 1126 + 1127 + #[test] 1128 + fn test_transition_map_interruption() { 1129 + let mut map = TransitionMap::default(); 1130 + let spec = TransitionSpec { 1131 + transitions: vec![SingleTransition { 1132 + property: TransitionPropertyName::All, 1133 + duration: 1.0, 1134 + timing_function: TimingFunction::Linear, 1135 + delay: 0.0, 1136 + }], 1137 + }; 1138 + 1139 + // Start transition from 0 to 100 1140 + let old = vec![("width".into(), AnimatableValue::Length(0.0))]; 1141 + let new = vec![("width".into(), AnimatableValue::Length(100.0))]; 1142 + map.update(&spec, &old, &new, 0.0); 1143 + 1144 + // At t=0.5, width should be ~50. Now interrupt with new target 200. 1145 + let old2 = vec![("width".into(), AnimatableValue::Length(100.0))]; 1146 + let new2 = vec![("width".into(), AnimatableValue::Length(200.0))]; 1147 + map.update(&spec, &old2, &new2, 0.5); 1148 + 1149 + // Should have started a new transition from ~50 to 200 1150 + assert_eq!(map.active.len(), 1); 1151 + let trans = &map.active[0]; 1152 + if let AnimatableValue::Length(from) = &trans.from { 1153 + assert!((*from - 50.0).abs() < 1.0); 1154 + } 1155 + assert_eq!(trans.to, AnimatableValue::Length(200.0)); 1156 + } 1157 + 1158 + #[test] 1159 + fn test_transition_map_no_transition_for_zero_duration() { 1160 + let mut map = TransitionMap::default(); 1161 + let spec = TransitionSpec { 1162 + transitions: vec![SingleTransition { 1163 + property: TransitionPropertyName::All, 1164 + duration: 0.0, 1165 + timing_function: TimingFunction::Linear, 1166 + delay: 0.0, 1167 + }], 1168 + }; 1169 + 1170 + let old = vec![("opacity".into(), AnimatableValue::Number(1.0))]; 1171 + let new = vec![("opacity".into(), AnimatableValue::Number(0.0))]; 1172 + map.update(&spec, &old, &new, 0.0); 1173 + assert!(!map.has_active_transitions()); 1174 + } 1175 + 1176 + #[test] 1177 + fn test_transition_map_drain_completed() { 1178 + let mut map = TransitionMap::default(); 1179 + let spec = TransitionSpec { 1180 + transitions: vec![SingleTransition { 1181 + property: TransitionPropertyName::All, 1182 + duration: 0.5, 1183 + timing_function: TimingFunction::Linear, 1184 + delay: 0.0, 1185 + }], 1186 + }; 1187 + 1188 + let old = vec![("opacity".into(), AnimatableValue::Number(1.0))]; 1189 + let new = vec![("opacity".into(), AnimatableValue::Number(0.0))]; 1190 + map.update(&spec, &old, &new, 0.0); 1191 + 1192 + // Not yet completed 1193 + let completed = map.drain_completed(0.3); 1194 + assert!(completed.is_empty()); 1195 + assert!(map.has_active_transitions()); 1196 + 1197 + // Now completed 1198 + let completed = map.drain_completed(0.6); 1199 + assert_eq!(completed, vec!["opacity".to_string()]); 1200 + assert!(!map.has_active_transitions()); 1201 + } 1202 + }
+95
crates/style/src/computed.rs
··· 8 8 9 9 use we_css::media::MediaContext; 10 10 use we_css::parser::{ComponentValue, Declaration, Stylesheet}; 11 + use we_css::transitions::{ 12 + parse_transition_delay, parse_transition_duration, parse_transition_property, 13 + parse_transition_shorthand, parse_transition_timing_function, SingleTransition, TransitionSpec, 14 + }; 11 15 use we_css::values::{expand_shorthand, parse_value, Color, CssValue, LengthUnit, MathExpr}; 12 16 use we_dom::{Document, NodeData, NodeId}; 13 17 ··· 379 383 pub align_self: AlignSelf, 380 384 pub order: i32, 381 385 386 + // CSS Transitions 387 + pub transition: TransitionSpec, 388 + 382 389 // CSS Custom Properties (inherited by default) 383 390 pub custom_properties: HashMap<String, Vec<ComponentValue>>, 384 391 } ··· 457 464 flex_basis: LengthOrAuto::Auto, 458 465 align_self: AlignSelf::Auto, 459 466 order: 0, 467 + 468 + transition: TransitionSpec::default(), 460 469 461 470 custom_properties: HashMap::new(), 462 471 } ··· 1462 1471 } 1463 1472 } 1464 1473 1474 + /// Handle transition-related properties from raw component values. 1475 + /// Returns `true` if the property was handled (caller should skip normal processing). 1476 + fn apply_transition_property( 1477 + style: &mut ComputedStyle, 1478 + property: &str, 1479 + values: &[ComponentValue], 1480 + ) -> bool { 1481 + match property { 1482 + "transition" => { 1483 + style.transition = parse_transition_shorthand(values); 1484 + true 1485 + } 1486 + "transition-property" => { 1487 + let props = parse_transition_property(values); 1488 + // Update existing transitions or create new ones 1489 + let len = props.len(); 1490 + style 1491 + .transition 1492 + .transitions 1493 + .resize_with(len, SingleTransition::default); 1494 + for (i, prop) in props.into_iter().enumerate() { 1495 + style.transition.transitions[i].property = prop; 1496 + } 1497 + true 1498 + } 1499 + "transition-duration" => { 1500 + let durations = parse_transition_duration(values); 1501 + let len = durations.len().max(style.transition.transitions.len()); 1502 + style 1503 + .transition 1504 + .transitions 1505 + .resize_with(len, SingleTransition::default); 1506 + for (i, dur) in durations.iter().enumerate() { 1507 + if i < style.transition.transitions.len() { 1508 + style.transition.transitions[i].duration = *dur; 1509 + } 1510 + } 1511 + true 1512 + } 1513 + "transition-timing-function" => { 1514 + let tfs = parse_transition_timing_function(values); 1515 + let len = tfs.len().max(style.transition.transitions.len()); 1516 + style 1517 + .transition 1518 + .transitions 1519 + .resize_with(len, SingleTransition::default); 1520 + for (i, tf) in tfs.into_iter().enumerate() { 1521 + if i < style.transition.transitions.len() { 1522 + style.transition.transitions[i].timing_function = tf; 1523 + } 1524 + } 1525 + true 1526 + } 1527 + "transition-delay" => { 1528 + let delays = parse_transition_delay(values); 1529 + let len = delays.len().max(style.transition.transitions.len()); 1530 + style 1531 + .transition 1532 + .transitions 1533 + .resize_with(len, SingleTransition::default); 1534 + for (i, delay) in delays.iter().enumerate() { 1535 + if i < style.transition.transitions.len() { 1536 + style.transition.transitions[i].delay = *delay; 1537 + } 1538 + } 1539 + true 1540 + } 1541 + _ => false, 1542 + } 1543 + } 1544 + 1465 1545 fn resolve_border_width(value: &CssValue, em_base: f32, viewport: (f32, f32)) -> f32 { 1466 1546 match value { 1467 1547 CssValue::Length(n, unit) => resolve_length_unit(*n, *unit, em_base, viewport), ··· 1544 1624 "flex-basis" => style.flex_basis = parent.flex_basis, 1545 1625 "align-self" => style.align_self = parent.align_self, 1546 1626 "order" => style.order = parent.order, 1627 + "transition" => style.transition = parent.transition.clone(), 1547 1628 _ => {} 1548 1629 } 1549 1630 } ··· 1602 1683 "flex-basis" => style.flex_basis = initial.flex_basis, 1603 1684 "align-self" => style.align_self = initial.align_self, 1604 1685 "order" => style.order = initial.order, 1686 + "transition" => style.transition = initial.transition, 1605 1687 _ => {} 1606 1688 } 1607 1689 } ··· 2009 2091 &decl.value 2010 2092 }; 2011 2093 2094 + // Handle transition properties specially (need raw ComponentValues) 2095 + if apply_transition_property(&mut style, &decl.property, values) { 2096 + continue; 2097 + } 2098 + 2012 2099 let property = &decl.property; 2013 2100 if let Some(longhands) = expand_shorthand(property, values, decl.important) { 2014 2101 for lh in longhands { ··· 2048 2135 &decl.value 2049 2136 }; 2050 2137 2138 + if apply_transition_property(&mut style, &decl.property, values) { 2139 + continue; 2140 + } 2141 + 2051 2142 let property = decl.property.as_str(); 2052 2143 if let Some(longhands) = expand_shorthand(property, values, false) { 2053 2144 for lh in &longhands { ··· 2078 2169 } else { 2079 2170 &decl.value 2080 2171 }; 2172 + 2173 + if apply_transition_property(&mut style, &decl.property, values) { 2174 + continue; 2175 + } 2081 2176 2082 2177 let property = decl.property.as_str(); 2083 2178 if let Some(longhands) = expand_shorthand(property, values, true) {