Skip to main content

lightningcss/properties/
transition.rs

1//! CSS properties related to transitions.
2
3use super::{Property, PropertyId};
4use crate::compat;
5use crate::context::PropertyHandlerContext;
6use crate::declaration::{DeclarationBlock, DeclarationList};
7use crate::error::{ParserError, PrinterError};
8use crate::macros::define_list_shorthand;
9use crate::prefixes::Feature;
10use crate::printer::Printer;
11use crate::properties::masking::get_webkit_mask_property;
12use crate::traits::{Parse, PropertyHandler, Shorthand, ToCss, Zero};
13use crate::values::ident::CustomIdent;
14use crate::values::{easing::EasingFunction, time::Time};
15use crate::vendor_prefix::VendorPrefix;
16#[cfg(feature = "visitor")]
17use crate::visitor::Visit;
18use cssparser::*;
19use itertools::izip;
20use smallvec::SmallVec;
21
22define_list_shorthand! {
23  /// A value for the [transition](https://www.w3.org/TR/2018/WD-css-transitions-1-20181011/#transition-shorthand-property) property.
24  pub struct Transition<'i>(VendorPrefix) {
25    /// The property to transition.
26    #[cfg_attr(feature = "serde", serde(borrow))]
27    property: TransitionProperty(PropertyId<'i>, VendorPrefix),
28    /// The duration of the transition.
29    duration: TransitionDuration(Time, VendorPrefix),
30    /// The delay before the transition starts.
31    delay: TransitionDelay(Time, VendorPrefix),
32    /// The easing function for the transition.
33    timing_function: TransitionTimingFunction(EasingFunction, VendorPrefix),
34  }
35}
36
37impl<'i> Parse<'i> for Transition<'i> {
38  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
39    let mut property = None;
40    let mut duration = None;
41    let mut delay = None;
42    let mut timing_function = None;
43
44    loop {
45      if duration.is_none() {
46        if let Ok(value) = input.try_parse(Time::parse) {
47          duration = Some(value);
48          continue;
49        }
50      }
51
52      if timing_function.is_none() {
53        if let Ok(value) = input.try_parse(EasingFunction::parse) {
54          timing_function = Some(value);
55          continue;
56        }
57      }
58
59      if delay.is_none() {
60        if let Ok(value) = input.try_parse(Time::parse) {
61          delay = Some(value);
62          continue;
63        }
64      }
65
66      if property.is_none() {
67        if let Ok(value) = input.try_parse(PropertyId::parse) {
68          property = Some(value);
69          continue;
70        }
71      }
72
73      break;
74    }
75
76    Ok(Transition {
77      property: property.unwrap_or(PropertyId::All),
78      duration: duration.unwrap_or(Time::Seconds(0.0)),
79      delay: delay.unwrap_or(Time::Seconds(0.0)),
80      timing_function: timing_function.unwrap_or(EasingFunction::Ease),
81    })
82  }
83}
84
85impl<'i> ToCss for Transition<'i> {
86  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
87  where
88    W: std::fmt::Write,
89  {
90    self.property.to_css(dest)?;
91    if !self.duration.is_zero() || !self.delay.is_zero() {
92      dest.write_char(' ')?;
93      self.duration.to_css(dest)?;
94    }
95
96    if !self.timing_function.is_ease() {
97      dest.write_char(' ')?;
98      self.timing_function.to_css(dest)?;
99    }
100
101    if !self.delay.is_zero() {
102      dest.write_char(' ')?;
103      self.delay.to_css(dest)?;
104    }
105
106    Ok(())
107  }
108}
109
110/// A value for the [view-transition-name](https://drafts.csswg.org/css-view-transitions-1/#view-transition-name-prop) property.
111#[derive(Debug, Clone, PartialEq, Default, Parse, ToCss)]
112#[cfg_attr(feature = "visitor", derive(Visit))]
113#[cfg_attr(
114  feature = "serde",
115  derive(serde::Serialize, serde::Deserialize),
116  serde(rename_all = "kebab-case")
117)]
118#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
119#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
120pub enum ViewTransitionName<'i> {
121  /// The element will not participate independently in a view transition.
122  #[default]
123  None,
124  /// The `auto` keyword.
125  Auto,
126  /// A custom name.
127  #[cfg_attr(feature = "serde", serde(borrow, untagged))]
128  Custom(CustomIdent<'i>),
129}
130
131/// A value for the [view-transition-group](https://drafts.csswg.org/css-view-transitions-2/#view-transition-group-prop) property.
132#[derive(Debug, Clone, PartialEq, Default, Parse, ToCss)]
133#[cfg_attr(feature = "visitor", derive(Visit))]
134#[cfg_attr(
135  feature = "serde",
136  derive(serde::Serialize, serde::Deserialize),
137  serde(rename_all = "kebab-case")
138)]
139#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
140#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
141pub enum ViewTransitionGroup<'i> {
142  /// The `normal` keyword.
143  #[default]
144  Normal,
145  /// The `contain` keyword.
146  Contain,
147  /// The `nearest` keyword.
148  Nearest,
149  /// A custom group.
150  #[cfg_attr(feature = "serde", serde(borrow, untagged))]
151  Custom(CustomIdent<'i>),
152}
153
154#[derive(Default)]
155pub(crate) struct TransitionHandler<'i> {
156  properties: Option<(SmallVec<[PropertyId<'i>; 1]>, VendorPrefix)>,
157  durations: Option<(SmallVec<[Time; 1]>, VendorPrefix)>,
158  delays: Option<(SmallVec<[Time; 1]>, VendorPrefix)>,
159  timing_functions: Option<(SmallVec<[EasingFunction; 1]>, VendorPrefix)>,
160  has_any: bool,
161}
162
163impl<'i> PropertyHandler<'i> for TransitionHandler<'i> {
164  fn handle_property(
165    &mut self,
166    property: &Property<'i>,
167    dest: &mut DeclarationList<'i>,
168    context: &mut PropertyHandlerContext<'i, '_>,
169  ) -> bool {
170    use Property::*;
171
172    macro_rules! maybe_flush {
173      ($prop: ident, $val: expr, $vp: ident) => {{
174        // If two vendor prefixes for the same property have different
175        // values, we need to flush what we have immediately to preserve order.
176        if let Some((val, prefixes)) = &self.$prop {
177          if val != $val && !prefixes.contains(*$vp) {
178            self.flush(dest, context);
179          }
180        }
181      }};
182    }
183
184    macro_rules! property {
185      ($feature: ident, $prop: ident, $val: expr, $vp: ident) => {{
186        maybe_flush!($prop, $val, $vp);
187
188        // Otherwise, update the value and add the prefix.
189        if let Some((val, prefixes)) = &mut self.$prop {
190          *val = $val.clone();
191          *prefixes |= *$vp;
192          *prefixes = context.targets.prefixes(*prefixes, Feature::$feature);
193        } else {
194          let prefixes = context.targets.prefixes(*$vp, Feature::$feature);
195          self.$prop = Some(($val.clone(), prefixes));
196          self.has_any = true;
197        }
198      }};
199    }
200
201    match property {
202      TransitionProperty(val, vp) => {
203        let merged_values = merge_properties(val.iter());
204        property!(TransitionProperty, properties, &merged_values, vp);
205      }
206      TransitionDuration(val, vp) => property!(TransitionDuration, durations, val, vp),
207      TransitionDelay(val, vp) => property!(TransitionDelay, delays, val, vp),
208      TransitionTimingFunction(val, vp) => property!(TransitionTimingFunction, timing_functions, val, vp),
209      Transition(val, vp) => {
210        let properties: SmallVec<[PropertyId; 1]> = merge_properties(val.iter().map(|b| &b.property));
211        maybe_flush!(properties, &properties, vp);
212
213        let durations: SmallVec<[Time; 1]> = val.iter().map(|b| b.duration.clone()).collect();
214        maybe_flush!(durations, &durations, vp);
215
216        let delays: SmallVec<[Time; 1]> = val.iter().map(|b| b.delay.clone()).collect();
217        maybe_flush!(delays, &delays, vp);
218
219        let timing_functions: SmallVec<[EasingFunction; 1]> =
220          val.iter().map(|b| b.timing_function.clone()).collect();
221        maybe_flush!(timing_functions, &timing_functions, vp);
222
223        property!(TransitionProperty, properties, &properties, vp);
224        property!(TransitionDuration, durations, &durations, vp);
225        property!(TransitionDelay, delays, &delays, vp);
226        property!(TransitionTimingFunction, timing_functions, &timing_functions, vp);
227      }
228      Unparsed(val) if is_transition_property(&val.property_id) => {
229        self.flush(dest, context);
230        dest.push(Property::Unparsed(
231          val.get_prefixed(context.targets, Feature::Transition),
232        ));
233      }
234      _ => return false,
235    }
236
237    true
238  }
239
240  fn finalize(&mut self, dest: &mut DeclarationList<'i>, context: &mut PropertyHandlerContext<'i, '_>) {
241    self.flush(dest, context);
242  }
243}
244
245impl<'i> TransitionHandler<'i> {
246  fn flush(&mut self, dest: &mut DeclarationList<'i>, context: &mut PropertyHandlerContext<'i, '_>) {
247    if !self.has_any {
248      return;
249    }
250
251    self.has_any = false;
252
253    let mut properties = std::mem::take(&mut self.properties);
254    let mut durations = std::mem::take(&mut self.durations);
255    let mut delays = std::mem::take(&mut self.delays);
256    let mut timing_functions = std::mem::take(&mut self.timing_functions);
257
258    let rtl_properties = if let Some((properties, _)) = &mut properties {
259      expand_properties(properties, context)
260    } else {
261      None
262    };
263
264    if let (
265      Some((properties, property_prefixes)),
266      Some((durations, duration_prefixes)),
267      Some((delays, delay_prefixes)),
268      Some((timing_functions, timing_prefixes)),
269    ) = (&mut properties, &mut durations, &mut delays, &mut timing_functions)
270    {
271      // Find the intersection of prefixes with the same value.
272      // Remove that from the prefixes of each of the properties. The remaining
273      // prefixes will be handled by outputting individual properties below.
274      let intersection = *property_prefixes & *duration_prefixes & *delay_prefixes & *timing_prefixes;
275      if !intersection.is_empty() {
276        macro_rules! get_transitions {
277          ($properties: ident) => {{
278            // transition-property determines the number of transitions. The values of other
279            // properties are repeated to match this length.
280            let mut transitions = SmallVec::with_capacity($properties.len());
281            let mut durations_iter = durations.iter().cycle().cloned();
282            let mut delays_iter = delays.iter().cycle().cloned();
283            let mut timing_iter = timing_functions.iter().cycle().cloned();
284            for property_id in $properties {
285              let duration = durations_iter.next().unwrap_or(Time::Seconds(0.0));
286              let delay = delays_iter.next().unwrap_or(Time::Seconds(0.0));
287              let timing_function = timing_iter.next().unwrap_or(EasingFunction::Ease);
288              let transition = Transition {
289                property: property_id.clone(),
290                duration,
291                delay,
292                timing_function,
293              };
294
295              // Expand vendor prefixes into multiple transitions.
296              for p in property_id.prefix().or_none() {
297                let mut t = transition.clone();
298                t.property = property_id.with_prefix(p);
299                transitions.push(t);
300              }
301            }
302            transitions
303          }};
304        }
305
306        let transitions: SmallVec<[Transition; 1]> = get_transitions!(properties);
307
308        if let Some(rtl_properties) = &rtl_properties {
309          let rtl_transitions = get_transitions!(rtl_properties);
310          context.add_logical_rule(
311            Property::Transition(transitions, intersection),
312            Property::Transition(rtl_transitions, intersection),
313          );
314        } else {
315          dest.push(Property::Transition(transitions.clone(), intersection));
316        }
317
318        property_prefixes.remove(intersection);
319        duration_prefixes.remove(intersection);
320        delay_prefixes.remove(intersection);
321        timing_prefixes.remove(intersection);
322      }
323    }
324
325    if let Some((properties, prefix)) = properties {
326      if !prefix.is_empty() {
327        if let Some(rtl_properties) = rtl_properties {
328          context.add_logical_rule(
329            Property::TransitionProperty(properties, prefix),
330            Property::TransitionProperty(rtl_properties, prefix),
331          );
332        } else {
333          dest.push(Property::TransitionProperty(properties, prefix));
334        }
335      }
336    }
337
338    if let Some((durations, prefix)) = durations {
339      if !prefix.is_empty() {
340        dest.push(Property::TransitionDuration(durations, prefix));
341      }
342    }
343
344    if let Some((delays, prefix)) = delays {
345      if !prefix.is_empty() {
346        dest.push(Property::TransitionDelay(delays, prefix));
347      }
348    }
349
350    if let Some((timing_functions, prefix)) = timing_functions {
351      if !prefix.is_empty() {
352        dest.push(Property::TransitionTimingFunction(timing_functions, prefix));
353      }
354    }
355
356    self.reset();
357  }
358
359  fn reset(&mut self) {
360    self.properties = None;
361    self.durations = None;
362    self.delays = None;
363    self.timing_functions = None;
364  }
365}
366
367#[inline]
368fn is_transition_property(property_id: &PropertyId) -> bool {
369  match property_id {
370    PropertyId::TransitionProperty(_)
371    | PropertyId::TransitionDuration(_)
372    | PropertyId::TransitionDelay(_)
373    | PropertyId::TransitionTimingFunction(_)
374    | PropertyId::Transition(_) => true,
375    _ => false,
376  }
377}
378
379fn merge_properties<'i: 'a, 'a>(val: impl Iterator<Item = &'a PropertyId<'i>>) -> SmallVec<[PropertyId<'i>; 1]> {
380  let mut merged_values = SmallVec::<[PropertyId<'_>; 1]>::with_capacity(val.size_hint().1.unwrap_or(1));
381  for p in val {
382    let without_prefix = p.with_prefix(VendorPrefix::empty());
383    if let Some(idx) = merged_values
384      .iter()
385      .position(|c| c.with_prefix(VendorPrefix::empty()) == without_prefix)
386    {
387      merged_values[idx].add_prefix(p.prefix());
388    } else {
389      merged_values.push(p.clone());
390    }
391  }
392
393  merged_values
394}
395
396fn expand_properties<'i>(
397  properties: &mut SmallVec<[PropertyId<'i>; 1]>,
398  context: &mut PropertyHandlerContext,
399) -> Option<SmallVec<[PropertyId<'i>; 1]>> {
400  let mut rtl_properties: Option<SmallVec<[PropertyId; 1]>> = None;
401  let mut i = 0;
402
403  macro_rules! replace {
404    ($properties: ident, $props: ident) => {
405      $properties[i] = $props[0].clone();
406      if $props.len() > 1 {
407        $properties.insert_many(i + 1, $props[1..].into_iter().cloned());
408      }
409    };
410  }
411
412  // Expand logical properties in place.
413  while i < properties.len() {
414    match get_logical_properties(&properties[i]) {
415      LogicalPropertyId::Block(feature, props) if context.should_compile_logical(feature) => {
416        replace!(properties, props);
417        if let Some(rtl_properties) = &mut rtl_properties {
418          replace!(rtl_properties, props);
419        }
420        i += props.len();
421      }
422      LogicalPropertyId::Inline(feature, ltr, rtl) if context.should_compile_logical(feature) => {
423        // Clone properties to create RTL version only when needed.
424        if rtl_properties.is_none() {
425          rtl_properties = Some(properties.clone());
426        }
427
428        replace!(properties, ltr);
429        if let Some(rtl_properties) = &mut rtl_properties {
430          replace!(rtl_properties, rtl);
431        }
432
433        i += ltr.len();
434      }
435      _ => {
436        // Expand vendor prefixes for targets.
437        properties[i].set_prefixes_for_targets(context.targets);
438
439        // Expand mask properties, which use different vendor-prefixed names.
440        if let Some(property_id) = get_webkit_mask_property(&properties[i]) {
441          if context
442            .targets
443            .prefixes(VendorPrefix::None, Feature::MaskBorder)
444            .contains(VendorPrefix::WebKit)
445          {
446            properties.insert(i, property_id);
447            i += 1;
448          }
449        }
450
451        if let Some(rtl_properties) = &mut rtl_properties {
452          rtl_properties[i].set_prefixes_for_targets(context.targets);
453
454          if let Some(property_id) = get_webkit_mask_property(&rtl_properties[i]) {
455            if context
456              .targets
457              .prefixes(VendorPrefix::None, Feature::MaskBorder)
458              .contains(VendorPrefix::WebKit)
459            {
460              rtl_properties.insert(i, property_id);
461            }
462          }
463        }
464        i += 1;
465      }
466    }
467  }
468
469  rtl_properties
470}
471
472enum LogicalPropertyId {
473  None,
474  Block(compat::Feature, &'static [PropertyId<'static>]),
475  Inline(
476    compat::Feature,
477    &'static [PropertyId<'static>],
478    &'static [PropertyId<'static>],
479  ),
480}
481
482#[inline]
483fn get_logical_properties(property_id: &PropertyId) -> LogicalPropertyId {
484  use compat::Feature::*;
485  use LogicalPropertyId::*;
486  use PropertyId::*;
487  match property_id {
488    BlockSize => Block(LogicalSize, &[Height]),
489    InlineSize => Inline(LogicalSize, &[Width], &[Height]),
490    MinBlockSize => Block(LogicalSize, &[MinHeight]),
491    MaxBlockSize => Block(LogicalSize, &[MaxHeight]),
492    MinInlineSize => Inline(LogicalSize, &[MinWidth], &[MinHeight]),
493    MaxInlineSize => Inline(LogicalSize, &[MaxWidth], &[MaxHeight]),
494
495    InsetBlockStart => Block(LogicalInset, &[Top]),
496    InsetBlockEnd => Block(LogicalInset, &[Bottom]),
497    InsetInlineStart => Inline(LogicalInset, &[Left], &[Right]),
498    InsetInlineEnd => Inline(LogicalInset, &[Right], &[Left]),
499    InsetBlock => Block(LogicalInset, &[Top, Bottom]),
500    InsetInline => Block(LogicalInset, &[Left, Right]),
501    Inset => Block(LogicalInset, &[Top, Bottom, Left, Right]),
502
503    MarginBlockStart => Block(LogicalMargin, &[MarginTop]),
504    MarginBlockEnd => Block(LogicalMargin, &[MarginBottom]),
505    MarginInlineStart => Inline(LogicalMargin, &[MarginLeft], &[MarginRight]),
506    MarginInlineEnd => Inline(LogicalMargin, &[MarginRight], &[MarginLeft]),
507    MarginBlock => Block(LogicalMargin, &[MarginTop, MarginBottom]),
508    MarginInline => Block(LogicalMargin, &[MarginLeft, MarginRight]),
509
510    PaddingBlockStart => Block(LogicalPadding, &[PaddingTop]),
511    PaddingBlockEnd => Block(LogicalPadding, &[PaddingBottom]),
512    PaddingInlineStart => Inline(LogicalPadding, &[PaddingLeft], &[PaddingRight]),
513    PaddingInlineEnd => Inline(LogicalPadding, &[PaddingRight], &[PaddingLeft]),
514    PaddingBlock => Block(LogicalPadding, &[PaddingTop, PaddingBottom]),
515    PaddingInline => Block(LogicalPadding, &[PaddingLeft, PaddingRight]),
516
517    BorderBlockStart => Block(LogicalBorders, &[BorderTop]),
518    BorderBlockStartWidth => Block(LogicalBorders, &[BorderTopWidth]),
519    BorderBlockStartColor => Block(LogicalBorders, &[BorderTopColor]),
520    BorderBlockStartStyle => Block(LogicalBorders, &[BorderTopStyle]),
521
522    BorderBlockEnd => Block(LogicalBorders, &[BorderBottom]),
523    BorderBlockEndWidth => Block(LogicalBorders, &[BorderBottomWidth]),
524    BorderBlockEndColor => Block(LogicalBorders, &[BorderBottomColor]),
525    BorderBlockEndStyle => Block(LogicalBorders, &[BorderBottomStyle]),
526
527    BorderInlineStart => Inline(LogicalBorders, &[BorderLeft], &[BorderRight]),
528    BorderInlineStartWidth => Inline(LogicalBorders, &[BorderLeftWidth], &[BorderRightWidth]),
529    BorderInlineStartColor => Inline(LogicalBorders, &[BorderLeftColor], &[BorderRightColor]),
530    BorderInlineStartStyle => Inline(LogicalBorders, &[BorderLeftStyle], &[BorderRightStyle]),
531
532    BorderInlineEnd => Inline(LogicalBorders, &[BorderRight], &[BorderLeft]),
533    BorderInlineEndWidth => Inline(LogicalBorders, &[BorderRightWidth], &[BorderLeftWidth]),
534    BorderInlineEndColor => Inline(LogicalBorders, &[BorderRightColor], &[BorderLeftColor]),
535    BorderInlineEndStyle => Inline(LogicalBorders, &[BorderRightStyle], &[BorderLeftStyle]),
536
537    BorderBlock => Block(LogicalBorders, &[BorderTop, BorderBottom]),
538    BorderBlockColor => Block(LogicalBorders, &[BorderTopColor, BorderBottomColor]),
539    BorderBlockWidth => Block(LogicalBorders, &[BorderTopWidth, BorderBottomWidth]),
540    BorderBlockStyle => Block(LogicalBorders, &[BorderTopStyle, BorderBottomStyle]),
541
542    BorderInline => Block(LogicalBorders, &[BorderLeft, BorderRight]),
543    BorderInlineColor => Block(LogicalBorders, &[BorderLeftColor, BorderRightColor]),
544    BorderInlineWidth => Block(LogicalBorders, &[BorderLeftWidth, BorderRightWidth]),
545    BorderInlineStyle => Block(LogicalBorders, &[BorderLeftStyle, BorderRightStyle]),
546
547    // Not worth using vendor prefixes for these since border-radius is supported
548    // everywhere custom properties (which are used to polyfill logical properties) are.
549    BorderStartStartRadius => Inline(
550      LogicalBorders,
551      &[BorderTopLeftRadius(VendorPrefix::None)],
552      &[BorderTopRightRadius(VendorPrefix::None)],
553    ),
554    BorderStartEndRadius => Inline(
555      LogicalBorders,
556      &[BorderTopRightRadius(VendorPrefix::None)],
557      &[BorderTopLeftRadius(VendorPrefix::None)],
558    ),
559    BorderEndStartRadius => Inline(
560      LogicalBorders,
561      &[BorderBottomLeftRadius(VendorPrefix::None)],
562      &[BorderBottomRightRadius(VendorPrefix::None)],
563    ),
564    BorderEndEndRadius => Inline(
565      LogicalBorders,
566      &[BorderBottomRightRadius(VendorPrefix::None)],
567      &[BorderBottomLeftRadius(VendorPrefix::None)],
568    ),
569
570    _ => None,
571  }
572}