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