blob: 8d6a66298598a77c52eaa2f46558e1b696fca693 [file] [log] [blame] [edit]
//! CSS properties related to transitions.
use super::{Property, PropertyId};
use crate::compat;
use crate::context::PropertyHandlerContext;
use crate::declaration::{DeclarationBlock, DeclarationList};
use crate::error::{ParserError, PrinterError};
use crate::macros::define_list_shorthand;
use crate::prefixes::Feature;
use crate::printer::Printer;
use crate::properties::masking::get_webkit_mask_property;
use crate::traits::{Parse, PropertyHandler, Shorthand, ToCss, Zero};
use crate::values::ident::CustomIdent;
use crate::values::{easing::EasingFunction, time::Time};
use crate::vendor_prefix::VendorPrefix;
#[cfg(feature = "visitor")]
use crate::visitor::Visit;
use cssparser::*;
use itertools::izip;
use smallvec::SmallVec;
define_list_shorthand! {
/// A value for the [transition](https://www.w3.org/TR/2018/WD-css-transitions-1-20181011/#transition-shorthand-property) property.
pub struct Transition<'i>(VendorPrefix) {
/// The property to transition.
#[cfg_attr(feature = "serde", serde(borrow))]
property: TransitionProperty(PropertyId<'i>, VendorPrefix),
/// The duration of the transition.
duration: TransitionDuration(Time, VendorPrefix),
/// The delay before the transition starts.
delay: TransitionDelay(Time, VendorPrefix),
/// The easing function for the transition.
timing_function: TransitionTimingFunction(EasingFunction, VendorPrefix),
}
}
impl<'i> Parse<'i> for Transition<'i> {
fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
let mut property = None;
let mut duration = None;
let mut delay = None;
let mut timing_function = None;
loop {
if duration.is_none() {
if let Ok(value) = input.try_parse(Time::parse) {
duration = Some(value);
continue;
}
}
if timing_function.is_none() {
if let Ok(value) = input.try_parse(EasingFunction::parse) {
timing_function = Some(value);
continue;
}
}
if delay.is_none() {
if let Ok(value) = input.try_parse(Time::parse) {
delay = Some(value);
continue;
}
}
if property.is_none() {
if let Ok(value) = input.try_parse(PropertyId::parse) {
property = Some(value);
continue;
}
}
break;
}
Ok(Transition {
property: property.unwrap_or(PropertyId::All),
duration: duration.unwrap_or(Time::Seconds(0.0)),
delay: delay.unwrap_or(Time::Seconds(0.0)),
timing_function: timing_function.unwrap_or(EasingFunction::Ease),
})
}
}
impl<'i> ToCss for Transition<'i> {
fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
where
W: std::fmt::Write,
{
self.property.to_css(dest)?;
if !self.duration.is_zero() || !self.delay.is_zero() {
dest.write_char(' ')?;
self.duration.to_css(dest)?;
}
if !self.timing_function.is_ease() {
dest.write_char(' ')?;
self.timing_function.to_css(dest)?;
}
if !self.delay.is_zero() {
dest.write_char(' ')?;
self.delay.to_css(dest)?;
}
Ok(())
}
}
/// A value for the [view-transition-name](https://drafts.csswg.org/css-view-transitions-1/#view-transition-name-prop) property.
#[derive(Debug, Clone, PartialEq, Default, Parse, ToCss)]
#[cfg_attr(feature = "visitor", derive(Visit))]
#[cfg_attr(
feature = "serde",
derive(serde::Serialize, serde::Deserialize),
serde(rename_all = "kebab-case")
)]
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
pub enum ViewTransitionName<'i> {
/// The element will not participate independently in a view transition.
#[default]
None,
/// The `auto` keyword.
Auto,
/// A custom name.
#[cfg_attr(feature = "serde", serde(borrow, untagged))]
Custom(CustomIdent<'i>),
}
/// A value for the [view-transition-group](https://drafts.csswg.org/css-view-transitions-2/#view-transition-group-prop) property.
#[derive(Debug, Clone, PartialEq, Default, Parse, ToCss)]
#[cfg_attr(feature = "visitor", derive(Visit))]
#[cfg_attr(
feature = "serde",
derive(serde::Serialize, serde::Deserialize),
serde(rename_all = "kebab-case")
)]
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
pub enum ViewTransitionGroup<'i> {
/// The `normal` keyword.
#[default]
Normal,
/// The `contain` keyword.
Contain,
/// The `nearest` keyword.
Nearest,
/// A custom group.
#[cfg_attr(feature = "serde", serde(borrow, untagged))]
Custom(CustomIdent<'i>),
}
#[derive(Default)]
pub(crate) struct TransitionHandler<'i> {
properties: Option<(SmallVec<[PropertyId<'i>; 1]>, VendorPrefix)>,
durations: Option<(SmallVec<[Time; 1]>, VendorPrefix)>,
delays: Option<(SmallVec<[Time; 1]>, VendorPrefix)>,
timing_functions: Option<(SmallVec<[EasingFunction; 1]>, VendorPrefix)>,
has_any: bool,
}
impl<'i> PropertyHandler<'i> for TransitionHandler<'i> {
fn handle_property(
&mut self,
property: &Property<'i>,
dest: &mut DeclarationList<'i>,
context: &mut PropertyHandlerContext<'i, '_>,
) -> bool {
use Property::*;
macro_rules! maybe_flush {
($prop: ident, $val: expr, $vp: ident) => {{
// If two vendor prefixes for the same property have different
// values, we need to flush what we have immediately to preserve order.
if let Some((val, prefixes)) = &self.$prop {
if val != $val && !prefixes.contains(*$vp) {
self.flush(dest, context);
}
}
}};
}
macro_rules! property {
($feature: ident, $prop: ident, $val: expr, $vp: ident) => {{
maybe_flush!($prop, $val, $vp);
// Otherwise, update the value and add the prefix.
if let Some((val, prefixes)) = &mut self.$prop {
*val = $val.clone();
*prefixes |= *$vp;
*prefixes = context.targets.prefixes(*prefixes, Feature::$feature);
} else {
let prefixes = context.targets.prefixes(*$vp, Feature::$feature);
self.$prop = Some(($val.clone(), prefixes));
self.has_any = true;
}
}};
}
match property {
TransitionProperty(val, vp) => {
let merged_values = merge_properties(val.iter());
property!(TransitionProperty, properties, &merged_values, vp);
}
TransitionDuration(val, vp) => property!(TransitionDuration, durations, val, vp),
TransitionDelay(val, vp) => property!(TransitionDelay, delays, val, vp),
TransitionTimingFunction(val, vp) => property!(TransitionTimingFunction, timing_functions, val, vp),
Transition(val, vp) => {
let properties: SmallVec<[PropertyId; 1]> = merge_properties(val.iter().map(|b| &b.property));
maybe_flush!(properties, &properties, vp);
let durations: SmallVec<[Time; 1]> = val.iter().map(|b| b.duration.clone()).collect();
maybe_flush!(durations, &durations, vp);
let delays: SmallVec<[Time; 1]> = val.iter().map(|b| b.delay.clone()).collect();
maybe_flush!(delays, &delays, vp);
let timing_functions: SmallVec<[EasingFunction; 1]> =
val.iter().map(|b| b.timing_function.clone()).collect();
maybe_flush!(timing_functions, &timing_functions, vp);
property!(TransitionProperty, properties, &properties, vp);
property!(TransitionDuration, durations, &durations, vp);
property!(TransitionDelay, delays, &delays, vp);
property!(TransitionTimingFunction, timing_functions, &timing_functions, vp);
}
Unparsed(val) if is_transition_property(&val.property_id) => {
self.flush(dest, context);
dest.push(Property::Unparsed(
val.get_prefixed(context.targets, Feature::Transition),
));
}
_ => return false,
}
true
}
fn finalize(&mut self, dest: &mut DeclarationList<'i>, context: &mut PropertyHandlerContext<'i, '_>) {
self.flush(dest, context);
}
}
impl<'i> TransitionHandler<'i> {
fn flush(&mut self, dest: &mut DeclarationList<'i>, context: &mut PropertyHandlerContext<'i, '_>) {
if !self.has_any {
return;
}
self.has_any = false;
let mut properties = std::mem::take(&mut self.properties);
let mut durations = std::mem::take(&mut self.durations);
let mut delays = std::mem::take(&mut self.delays);
let mut timing_functions = std::mem::take(&mut self.timing_functions);
let rtl_properties = if let Some((properties, _)) = &mut properties {
expand_properties(properties, context)
} else {
None
};
if let (
Some((properties, property_prefixes)),
Some((durations, duration_prefixes)),
Some((delays, delay_prefixes)),
Some((timing_functions, timing_prefixes)),
) = (&mut properties, &mut durations, &mut delays, &mut timing_functions)
{
// Find the intersection of prefixes with the same value.
// Remove that from the prefixes of each of the properties. The remaining
// prefixes will be handled by outputting individual properties below.
let intersection = *property_prefixes & *duration_prefixes & *delay_prefixes & *timing_prefixes;
if !intersection.is_empty() {
macro_rules! get_transitions {
($properties: ident) => {{
// transition-property determines the number of transitions. The values of other
// properties are repeated to match this length.
let mut transitions = SmallVec::with_capacity($properties.len());
let mut durations_iter = durations.iter().cycle().cloned();
let mut delays_iter = delays.iter().cycle().cloned();
let mut timing_iter = timing_functions.iter().cycle().cloned();
for property_id in $properties {
let duration = durations_iter.next().unwrap_or(Time::Seconds(0.0));
let delay = delays_iter.next().unwrap_or(Time::Seconds(0.0));
let timing_function = timing_iter.next().unwrap_or(EasingFunction::Ease);
let transition = Transition {
property: property_id.clone(),
duration,
delay,
timing_function,
};
// Expand vendor prefixes into multiple transitions.
for p in property_id.prefix().or_none() {
let mut t = transition.clone();
t.property = property_id.with_prefix(p);
transitions.push(t);
}
}
transitions
}};
}
let transitions: SmallVec<[Transition; 1]> = get_transitions!(properties);
if let Some(rtl_properties) = &rtl_properties {
let rtl_transitions = get_transitions!(rtl_properties);
context.add_logical_rule(
Property::Transition(transitions, intersection),
Property::Transition(rtl_transitions, intersection),
);
} else {
dest.push(Property::Transition(transitions.clone(), intersection));
}
property_prefixes.remove(intersection);
duration_prefixes.remove(intersection);
delay_prefixes.remove(intersection);
timing_prefixes.remove(intersection);
}
}
if let Some((properties, prefix)) = properties {
if !prefix.is_empty() {
if let Some(rtl_properties) = rtl_properties {
context.add_logical_rule(
Property::TransitionProperty(properties, prefix),
Property::TransitionProperty(rtl_properties, prefix),
);
} else {
dest.push(Property::TransitionProperty(properties, prefix));
}
}
}
if let Some((durations, prefix)) = durations {
if !prefix.is_empty() {
dest.push(Property::TransitionDuration(durations, prefix));
}
}
if let Some((delays, prefix)) = delays {
if !prefix.is_empty() {
dest.push(Property::TransitionDelay(delays, prefix));
}
}
if let Some((timing_functions, prefix)) = timing_functions {
if !prefix.is_empty() {
dest.push(Property::TransitionTimingFunction(timing_functions, prefix));
}
}
self.reset();
}
fn reset(&mut self) {
self.properties = None;
self.durations = None;
self.delays = None;
self.timing_functions = None;
}
}
#[inline]
fn is_transition_property(property_id: &PropertyId) -> bool {
match property_id {
PropertyId::TransitionProperty(_)
| PropertyId::TransitionDuration(_)
| PropertyId::TransitionDelay(_)
| PropertyId::TransitionTimingFunction(_)
| PropertyId::Transition(_) => true,
_ => false,
}
}
fn merge_properties<'i: 'a, 'a>(val: impl Iterator<Item = &'a PropertyId<'i>>) -> SmallVec<[PropertyId<'i>; 1]> {
let mut merged_values = SmallVec::<[PropertyId<'_>; 1]>::with_capacity(val.size_hint().1.unwrap_or(1));
for p in val {
let without_prefix = p.with_prefix(VendorPrefix::empty());
if let Some(idx) = merged_values
.iter()
.position(|c| c.with_prefix(VendorPrefix::empty()) == without_prefix)
{
merged_values[idx].add_prefix(p.prefix());
} else {
merged_values.push(p.clone());
}
}
merged_values
}
fn expand_properties<'i>(
properties: &mut SmallVec<[PropertyId<'i>; 1]>,
context: &mut PropertyHandlerContext,
) -> Option<SmallVec<[PropertyId<'i>; 1]>> {
let mut rtl_properties: Option<SmallVec<[PropertyId; 1]>> = None;
let mut i = 0;
macro_rules! replace {
($properties: ident, $props: ident) => {
$properties[i] = $props[0].clone();
if $props.len() > 1 {
$properties.insert_many(i + 1, $props[1..].into_iter().cloned());
}
};
}
// Expand logical properties in place.
while i < properties.len() {
match get_logical_properties(&properties[i]) {
LogicalPropertyId::Block(feature, props) if context.should_compile_logical(feature) => {
replace!(properties, props);
if let Some(rtl_properties) = &mut rtl_properties {
replace!(rtl_properties, props);
}
i += props.len();
}
LogicalPropertyId::Inline(feature, ltr, rtl) if context.should_compile_logical(feature) => {
// Clone properties to create RTL version only when needed.
if rtl_properties.is_none() {
rtl_properties = Some(properties.clone());
}
replace!(properties, ltr);
if let Some(rtl_properties) = &mut rtl_properties {
replace!(rtl_properties, rtl);
}
i += ltr.len();
}
_ => {
// Expand vendor prefixes for targets.
properties[i].set_prefixes_for_targets(context.targets);
// Expand mask properties, which use different vendor-prefixed names.
if let Some(property_id) = get_webkit_mask_property(&properties[i]) {
if context
.targets
.prefixes(VendorPrefix::None, Feature::MaskBorder)
.contains(VendorPrefix::WebKit)
{
properties.insert(i, property_id);
i += 1;
}
}
if let Some(rtl_properties) = &mut rtl_properties {
rtl_properties[i].set_prefixes_for_targets(context.targets);
if let Some(property_id) = get_webkit_mask_property(&rtl_properties[i]) {
if context
.targets
.prefixes(VendorPrefix::None, Feature::MaskBorder)
.contains(VendorPrefix::WebKit)
{
rtl_properties.insert(i, property_id);
}
}
}
i += 1;
}
}
}
rtl_properties
}
enum LogicalPropertyId {
None,
Block(compat::Feature, &'static [PropertyId<'static>]),
Inline(
compat::Feature,
&'static [PropertyId<'static>],
&'static [PropertyId<'static>],
),
}
#[inline]
fn get_logical_properties(property_id: &PropertyId) -> LogicalPropertyId {
use compat::Feature::*;
use LogicalPropertyId::*;
use PropertyId::*;
match property_id {
BlockSize => Block(LogicalSize, &[Height]),
InlineSize => Inline(LogicalSize, &[Width], &[Height]),
MinBlockSize => Block(LogicalSize, &[MinHeight]),
MaxBlockSize => Block(LogicalSize, &[MaxHeight]),
MinInlineSize => Inline(LogicalSize, &[MinWidth], &[MinHeight]),
MaxInlineSize => Inline(LogicalSize, &[MaxWidth], &[MaxHeight]),
InsetBlockStart => Block(LogicalInset, &[Top]),
InsetBlockEnd => Block(LogicalInset, &[Bottom]),
InsetInlineStart => Inline(LogicalInset, &[Left], &[Right]),
InsetInlineEnd => Inline(LogicalInset, &[Right], &[Left]),
InsetBlock => Block(LogicalInset, &[Top, Bottom]),
InsetInline => Block(LogicalInset, &[Left, Right]),
Inset => Block(LogicalInset, &[Top, Bottom, Left, Right]),
MarginBlockStart => Block(LogicalMargin, &[MarginTop]),
MarginBlockEnd => Block(LogicalMargin, &[MarginBottom]),
MarginInlineStart => Inline(LogicalMargin, &[MarginLeft], &[MarginRight]),
MarginInlineEnd => Inline(LogicalMargin, &[MarginRight], &[MarginLeft]),
MarginBlock => Block(LogicalMargin, &[MarginTop, MarginBottom]),
MarginInline => Block(LogicalMargin, &[MarginLeft, MarginRight]),
PaddingBlockStart => Block(LogicalPadding, &[PaddingTop]),
PaddingBlockEnd => Block(LogicalPadding, &[PaddingBottom]),
PaddingInlineStart => Inline(LogicalPadding, &[PaddingLeft], &[PaddingRight]),
PaddingInlineEnd => Inline(LogicalPadding, &[PaddingRight], &[PaddingLeft]),
PaddingBlock => Block(LogicalPadding, &[PaddingTop, PaddingBottom]),
PaddingInline => Block(LogicalPadding, &[PaddingLeft, PaddingRight]),
BorderBlockStart => Block(LogicalBorders, &[BorderTop]),
BorderBlockStartWidth => Block(LogicalBorders, &[BorderTopWidth]),
BorderBlockStartColor => Block(LogicalBorders, &[BorderTopColor]),
BorderBlockStartStyle => Block(LogicalBorders, &[BorderTopStyle]),
BorderBlockEnd => Block(LogicalBorders, &[BorderBottom]),
BorderBlockEndWidth => Block(LogicalBorders, &[BorderBottomWidth]),
BorderBlockEndColor => Block(LogicalBorders, &[BorderBottomColor]),
BorderBlockEndStyle => Block(LogicalBorders, &[BorderBottomStyle]),
BorderInlineStart => Inline(LogicalBorders, &[BorderLeft], &[BorderRight]),
BorderInlineStartWidth => Inline(LogicalBorders, &[BorderLeftWidth], &[BorderRightWidth]),
BorderInlineStartColor => Inline(LogicalBorders, &[BorderLeftColor], &[BorderRightColor]),
BorderInlineStartStyle => Inline(LogicalBorders, &[BorderLeftStyle], &[BorderRightStyle]),
BorderInlineEnd => Inline(LogicalBorders, &[BorderRight], &[BorderLeft]),
BorderInlineEndWidth => Inline(LogicalBorders, &[BorderRightWidth], &[BorderLeftWidth]),
BorderInlineEndColor => Inline(LogicalBorders, &[BorderRightColor], &[BorderLeftColor]),
BorderInlineEndStyle => Inline(LogicalBorders, &[BorderRightStyle], &[BorderLeftStyle]),
BorderBlock => Block(LogicalBorders, &[BorderTop, BorderBottom]),
BorderBlockColor => Block(LogicalBorders, &[BorderTopColor, BorderBottomColor]),
BorderBlockWidth => Block(LogicalBorders, &[BorderTopWidth, BorderBottomWidth]),
BorderBlockStyle => Block(LogicalBorders, &[BorderTopStyle, BorderBottomStyle]),
BorderInline => Block(LogicalBorders, &[BorderLeft, BorderRight]),
BorderInlineColor => Block(LogicalBorders, &[BorderLeftColor, BorderRightColor]),
BorderInlineWidth => Block(LogicalBorders, &[BorderLeftWidth, BorderRightWidth]),
BorderInlineStyle => Block(LogicalBorders, &[BorderLeftStyle, BorderRightStyle]),
// Not worth using vendor prefixes for these since border-radius is supported
// everywhere custom properties (which are used to polyfill logical properties) are.
BorderStartStartRadius => Inline(
LogicalBorders,
&[BorderTopLeftRadius(VendorPrefix::None)],
&[BorderTopRightRadius(VendorPrefix::None)],
),
BorderStartEndRadius => Inline(
LogicalBorders,
&[BorderTopRightRadius(VendorPrefix::None)],
&[BorderTopLeftRadius(VendorPrefix::None)],
),
BorderEndStartRadius => Inline(
LogicalBorders,
&[BorderBottomLeftRadius(VendorPrefix::None)],
&[BorderBottomRightRadius(VendorPrefix::None)],
),
BorderEndEndRadius => Inline(
LogicalBorders,
&[BorderBottomRightRadius(VendorPrefix::None)],
&[BorderBottomLeftRadius(VendorPrefix::None)],
),
_ => None,
}
}