diff --git a/components/layout_thread/dom_wrapper.rs b/components/layout_thread/dom_wrapper.rs index 1ec4c92fcf0..5eb057cdc4f 100644 --- a/components/layout_thread/dom_wrapper.rs +++ b/components/layout_thread/dom_wrapper.rs @@ -584,8 +584,13 @@ impl<'le> TElement for ServoLayoutElement<'le> { false } - fn has_css_animations(&self) -> bool { - unreachable!("this should be only called on gecko"); + fn has_css_animations(&self, context: &SharedStyleContext) -> bool { + context + .animation_states + .read() + .get(&self.as_node().opaque()) + .map(|set| set.has_active_animation()) + .unwrap_or(false) } fn has_css_transitions(&self) -> bool { diff --git a/components/layout_thread/lib.rs b/components/layout_thread/lib.rs index 920345cbd47..2217bcaf621 100644 --- a/components/layout_thread/lib.rs +++ b/components/layout_thread/lib.rs @@ -610,7 +610,13 @@ impl LayoutThread { origin: ImmutableOrigin, animation_timeline_value: f64, animation_states: ServoArc>>, + stylesheets_changed: bool, ) -> LayoutContext<'a> { + let traversal_flags = match stylesheets_changed { + true => TraversalFlags::ForCSSRuleChanges, + false => TraversalFlags::empty(), + }; + LayoutContext { id: self.id, origin, @@ -622,7 +628,7 @@ impl LayoutThread { animation_states, registered_speculative_painters: &self.registered_painters, current_time_for_animations: animation_timeline_value, - traversal_flags: TraversalFlags::empty(), + traversal_flags, snapshot_map: snapshot_map, }, image_cache: self.image_cache.clone(), @@ -1405,6 +1411,7 @@ impl LayoutThread { origin, data.animation_timeline_value, data.animations.clone(), + data.stylesheets_changed, ); let pool; diff --git a/components/layout_thread_2020/dom_wrapper.rs b/components/layout_thread_2020/dom_wrapper.rs index 15e19ee73a4..456385ab324 100644 --- a/components/layout_thread_2020/dom_wrapper.rs +++ b/components/layout_thread_2020/dom_wrapper.rs @@ -592,8 +592,13 @@ impl<'le> TElement for ServoLayoutElement<'le> { false } - fn has_css_animations(&self) -> bool { - unreachable!("this should be only called on gecko"); + fn has_css_animations(&self, context: &SharedStyleContext) -> bool { + context + .animation_states + .read() + .get(&self.as_node().opaque()) + .map(|set| set.has_active_animation()) + .unwrap_or(false) } fn has_css_transitions(&self) -> bool { diff --git a/components/layout_thread_2020/lib.rs b/components/layout_thread_2020/lib.rs index e459adf0dff..fd0d22fc67b 100644 --- a/components/layout_thread_2020/lib.rs +++ b/components/layout_thread_2020/lib.rs @@ -574,7 +574,13 @@ impl LayoutThread { origin: ImmutableOrigin, animation_timeline_value: f64, animation_states: ServoArc>>, + stylesheets_changed: bool, ) -> LayoutContext<'a> { + let traversal_flags = match stylesheets_changed { + true => TraversalFlags::ForCSSRuleChanges, + false => TraversalFlags::empty(), + }; + LayoutContext { id: self.id, origin, @@ -586,7 +592,7 @@ impl LayoutThread { animation_states, registered_speculative_painters: &self.registered_painters, current_time_for_animations: animation_timeline_value, - traversal_flags: TraversalFlags::empty(), + traversal_flags, snapshot_map: snapshot_map, }, image_cache: self.image_cache.clone(), @@ -1064,6 +1070,7 @@ impl LayoutThread { origin, data.animation_timeline_value, data.animations.clone(), + data.stylesheets_changed, ); let dirty_root = unsafe { diff --git a/components/style/animation.rs b/components/style/animation.rs index a0d684f1a4b..c54253ed93c 100644 --- a/components/style/animation.rs +++ b/components/style/animation.rs @@ -17,7 +17,7 @@ use crate::properties::longhands::animation_fill_mode::computed_value::single_va use crate::properties::longhands::animation_play_state::computed_value::single_value::T as AnimationPlayState; use crate::properties::LonghandIdSet; use crate::properties::{self, CascadeMode, ComputedValues, LonghandId}; -use crate::stylesheets::keyframes_rule::{KeyframesAnimation, KeyframesStep, KeyframesStepValue}; +use crate::stylesheets::keyframes_rule::{KeyframesStep, KeyframesStepValue}; use crate::stylesheets::Origin; use crate::values::animated::{Animate, Procedure}; use crate::values::computed::Time; @@ -172,6 +172,103 @@ pub enum KeyframesIterationState { Finite(f64, f64), } +/// A single computed keyframe for a CSS Animation. +#[derive(Clone, MallocSizeOf)] +struct ComputedKeyframeStep { + step: KeyframesStep, + + #[ignore_malloc_size_of = "ComputedValues"] + style: Arc, + + timing_function: TimingFunction, +} + +impl ComputedKeyframeStep { + fn generate_for_keyframes( + element: E, + steps: &[KeyframesStep], + context: &SharedStyleContext, + base_style: &Arc, + font_metrics_provider: &dyn FontMetricsProvider, + default_timing_function: TimingFunction, + ) -> Vec + where + E: TElement, + { + let mut previous_style = base_style.clone(); + steps + .iter() + .cloned() + .map(|step| match step.value { + KeyframesStepValue::ComputedValues => ComputedKeyframeStep { + step, + style: base_style.clone(), + timing_function: default_timing_function, + }, + KeyframesStepValue::Declarations { + block: ref declarations, + } => { + let guard = declarations.read_with(context.guards.author); + + let iter = || { + // It's possible to have !important properties in keyframes + // so we have to filter them out. + // See the spec issue https://github.com/w3c/csswg-drafts/issues/1824 + // Also we filter our non-animatable properties. + guard + .normal_declaration_iter() + .filter(|declaration| declaration.is_animatable()) + .map(|decl| (decl, Origin::Author)) + }; + + // This currently ignores visited styles, which seems acceptable, + // as existing browsers don't appear to animate visited styles. + // + // TODO(mrobinson): We shouldn't be calling `apply_declarations` + // here because it doesn't really produce the correct values (for + // instance for keyframes that are missing animating properties). + // Instead we should do something like what Gecko does in + // Servo_StyleSet_GetKeyframesForName. + let computed_style = properties::apply_declarations::( + context.stylist.device(), + /* pseudo = */ None, + previous_style.rules(), + &context.guards, + iter, + Some(&previous_style), + Some(&previous_style), + Some(&previous_style), + font_metrics_provider, + CascadeMode::Unvisited { + visited_rules: None, + }, + context.quirks_mode(), + /* rule_cache = */ None, + &mut Default::default(), + Some(element), + ); + + // NB: The spec says that the timing function can be overwritten + // from the keyframe style. `animation_timing_function` can never + // be empty, always has at least the default value (`ease`). + let timing_function = if step.declared_timing_function { + computed_style.get_box().animation_timing_function_at(0) + } else { + default_timing_function + }; + + previous_style = computed_style.clone(); + ComputedKeyframeStep { + step, + style: computed_style, + timing_function, + } + }, + }) + .collect() + } +} + /// A CSS Animation #[derive(Clone, MallocSizeOf)] pub struct Animation { @@ -181,8 +278,11 @@ pub struct Animation { /// The name of this animation as defined by the style. pub name: Atom, - /// The internal animation from the style system. - pub keyframes_animation: KeyframesAnimation, + /// The properties that change in this animation. + properties_changed: LonghandIdSet, + + /// The computed style for each keyframe of this animation. + computed_steps: Vec, /// The time this animation started at, which is the current value of the animation /// timeline when this animation was created plus any animation delay. @@ -377,14 +477,7 @@ impl Animation { /// Update the given style to reflect the values specified by this `Animation` /// at the time provided by the given `SharedStyleContext`. - fn update_style( - &self, - context: &SharedStyleContext, - style: &mut Arc, - font_metrics_provider: &dyn FontMetricsProvider, - ) where - E: TElement, - { + fn update_style(&self, context: &SharedStyleContext, style: &mut Arc) { let duration = self.duration; let started_at = self.started_at; @@ -396,7 +489,7 @@ impl Animation { AnimationState::Canceled => return, }; - debug_assert!(!self.keyframes_animation.steps.is_empty()); + debug_assert!(!self.computed_steps.is_empty()); let mut total_progress = (now - started_at) / duration; if total_progress < 0. && @@ -417,34 +510,34 @@ impl Animation { // Get the indices of the previous (from) keyframe and the next (to) keyframe. let next_keyframe_index; let prev_keyframe_index; + let num_steps = self.computed_steps.len(); + debug_assert!(num_steps > 0); match self.current_direction { AnimationDirection::Normal => { next_keyframe_index = self - .keyframes_animation - .steps + .computed_steps .iter() - .position(|step| total_progress as f32 <= step.start_percentage.0); + .position(|step| total_progress as f32 <= step.step.start_percentage.0); prev_keyframe_index = next_keyframe_index .and_then(|pos| if pos != 0 { Some(pos - 1) } else { None }) .unwrap_or(0); }, AnimationDirection::Reverse => { next_keyframe_index = self - .keyframes_animation - .steps + .computed_steps .iter() .rev() - .position(|step| total_progress as f32 <= 1. - step.start_percentage.0) - .map(|pos| self.keyframes_animation.steps.len() - pos - 1); + .position(|step| total_progress as f32 <= 1. - step.step.start_percentage.0) + .map(|pos| num_steps - pos - 1); prev_keyframe_index = next_keyframe_index .and_then(|pos| { - if pos != self.keyframes_animation.steps.len() - 1 { + if pos != num_steps - 1 { Some(pos + 1) } else { None } }) - .unwrap_or(self.keyframes_animation.steps.len() - 1) + .unwrap_or(num_steps - 1) }, _ => unreachable!(), } @@ -454,87 +547,48 @@ impl Animation { prev_keyframe_index, next_keyframe_index ); - let prev_keyframe = &self.keyframes_animation.steps[prev_keyframe_index]; + let prev_keyframe = &self.computed_steps[prev_keyframe_index]; let next_keyframe = match next_keyframe_index { - Some(target) => &self.keyframes_animation.steps[target], + Some(index) => &self.computed_steps[index], None => return, }; let update_with_single_keyframe_style = |style, computed_style: &Arc| { let mutable_style = Arc::make_mut(style); - for property in self - .keyframes_animation - .properties_changed - .iter() - .filter_map(|longhand| { - AnimationValue::from_computed_values(longhand, &**computed_style) - }) - { + for property in self.properties_changed.iter().filter_map(|longhand| { + AnimationValue::from_computed_values(longhand, &**computed_style) + }) { property.set_in_style_for_servo(mutable_style); } }; // TODO: How could we optimise it? Is it such a big deal? - let prev_keyframe_style = compute_style_for_animation_step::( - context, - prev_keyframe, - style, - &self.cascade_style, - font_metrics_provider, - ); + let prev_keyframe_style = &prev_keyframe.style; + let next_keyframe_style = &next_keyframe.style; if total_progress <= 0.0 { update_with_single_keyframe_style(style, &prev_keyframe_style); return; } - let next_keyframe_style = compute_style_for_animation_step::( - context, - next_keyframe, - &prev_keyframe_style, - &self.cascade_style, - font_metrics_provider, - ); if total_progress >= 1.0 { update_with_single_keyframe_style(style, &next_keyframe_style); return; } let relative_timespan = - (next_keyframe.start_percentage.0 - prev_keyframe.start_percentage.0).abs(); + (next_keyframe.step.start_percentage.0 - prev_keyframe.step.start_percentage.0).abs(); let relative_duration = relative_timespan as f64 * duration; let last_keyframe_ended_at = match self.current_direction { AnimationDirection::Normal => { - self.started_at + (duration * prev_keyframe.start_percentage.0 as f64) + self.started_at + (duration * prev_keyframe.step.start_percentage.0 as f64) }, AnimationDirection::Reverse => { - self.started_at + (duration * (1. - prev_keyframe.start_percentage.0 as f64)) + self.started_at + (duration * (1. - prev_keyframe.step.start_percentage.0 as f64)) }, _ => unreachable!(), }; let relative_progress = (now - last_keyframe_ended_at) / relative_duration; - // NB: The spec says that the timing function can be overwritten - // from the keyframe style. - let timing_function = if prev_keyframe.declared_timing_function { - // NB: animation_timing_function can never be empty, always has - // at least the default value (`ease`). - prev_keyframe_style - .get_box() - .animation_timing_function_at(0) - } else { - // TODO(mrobinson): It isn't optimal to have to walk this list every - // time. Perhaps this should be stored in the animation. - let index = match style - .get_box() - .animation_name_iter() - .position(|animation_name| Some(&self.name) == animation_name.as_atom()) - { - Some(index) => index, - None => return warn!("Tried to update a style with a cancelled animation."), - }; - style.get_box().animation_timing_function_mod(index) - }; - let mut new_style = (**style).clone(); let mut update_style_for_longhand = |longhand| { let from = AnimationValue::from_computed_values(longhand, &prev_keyframe_style)?; @@ -542,14 +596,14 @@ impl Animation { PropertyAnimation { from, to, - timing_function, + timing_function: prev_keyframe.timing_function, duration: relative_duration as f64, } .update(&mut new_style, relative_progress); None::<()> }; - for property in self.keyframes_animation.properties_changed.iter() { + for property in self.properties_changed.iter() { update_style_for_longhand(property); } @@ -724,16 +778,13 @@ impl ElementAnimationSet { } } - pub(crate) fn apply_active_animations( + pub(crate) fn apply_active_animations( &mut self, context: &SharedStyleContext, style: &mut Arc, - font_metrics: &dyn crate::font_metrics::FontMetricsProvider, - ) where - E: TElement, - { + ) { for animation in &self.animations { - animation.update_style::(context, style, font_metrics); + animation.update_style(context, style); } for transition in &self.transitions { @@ -778,13 +829,18 @@ impl ElementAnimationSet { .count() } - fn has_active_transition_or_animation(&self) -> bool { + /// If this `ElementAnimationSet` has any any active animations. + pub fn has_active_animation(&self) -> bool { self.animations .iter() - .any(|animation| animation.state != AnimationState::Canceled) || - self.transitions - .iter() - .any(|transition| transition.state != AnimationState::Canceled) + .any(|animation| animation.state != AnimationState::Canceled) + } + + /// If this `ElementAnimationSet` has any any active transitions. + pub fn has_active_transition(&self) -> bool { + self.transitions + .iter() + .any(|transition| transition.state != AnimationState::Canceled) } /// Update our animations given a new style, canceling or starting new animations @@ -794,6 +850,7 @@ impl ElementAnimationSet { element: E, context: &SharedStyleContext, new_style: &Arc, + font_metrics: &dyn crate::font_metrics::FontMetricsProvider, ) where E: TElement, { @@ -803,21 +860,18 @@ impl ElementAnimationSet { } } - maybe_start_animations(element, &context, &new_style, self); + maybe_start_animations(element, &context, &new_style, self, font_metrics); } /// Update our transitions given a new style, canceling or starting new animations /// when appropriate. - pub fn update_transitions_for_new_style( + pub fn update_transitions_for_new_style( &mut self, context: &SharedStyleContext, opaque_node: OpaqueNode, old_style: Option<&Arc>, after_change_style: &Arc, - font_metrics: &dyn crate::font_metrics::FontMetricsProvider, - ) where - E: TElement, - { + ) { // If this is the first style, we don't trigger any transitions and we assume // there were no previously triggered transitions. let mut before_change_style = match old_style { @@ -829,9 +883,9 @@ impl ElementAnimationSet { // See https://drafts.csswg.org/css-transitions/#starting. We need to clone the // style because this might still be a reference to the original `old_style` and // we want to preserve that so that we can later properly calculate restyle damage. - if self.has_active_transition_or_animation() { + if self.has_active_transition() || self.has_active_animation() { before_change_style = before_change_style.clone(); - self.apply_active_animations::(context, &mut before_change_style, font_metrics); + self.apply_active_animations(context, &mut before_change_style); } let transitioning_properties = start_transitions_if_applicable( @@ -961,55 +1015,6 @@ pub fn start_transitions_if_applicable( properties_that_transition } -fn compute_style_for_animation_step( - context: &SharedStyleContext, - step: &KeyframesStep, - previous_style: &ComputedValues, - style_from_cascade: &Arc, - font_metrics_provider: &dyn FontMetricsProvider, -) -> Arc -where - E: TElement, -{ - match step.value { - KeyframesStepValue::ComputedValues => style_from_cascade.clone(), - KeyframesStepValue::Declarations { - block: ref declarations, - } => { - let guard = declarations.read_with(context.guards.author); - - // This currently ignores visited styles, which seems acceptable, - // as existing browsers don't appear to animate visited styles. - let computed = properties::apply_declarations::( - context.stylist.device(), - /* pseudo = */ None, - previous_style.rules(), - &context.guards, - // It's possible to have !important properties in keyframes - // so we have to filter them out. - // See the spec issue https://github.com/w3c/csswg-drafts/issues/1824 - // Also we filter our non-animatable properties. - guard - .normal_declaration_iter() - .filter(|declaration| declaration.is_animatable()) - .map(|decl| (decl, Origin::Author)), - Some(previous_style), - Some(previous_style), - Some(previous_style), - font_metrics_provider, - CascadeMode::Unvisited { - visited_rules: None, - }, - context.quirks_mode(), - /* rule_cache = */ None, - &mut Default::default(), - /* element = */ None, - ); - computed - }, - } -} - /// Triggers animations for a given node looking at the animation property /// values. pub fn maybe_start_animations( @@ -1017,6 +1022,7 @@ pub fn maybe_start_animations( context: &SharedStyleContext, new_style: &Arc, animation_state: &mut ElementAnimationSet, + font_metrics_provider: &dyn FontMetricsProvider, ) where E: TElement, { @@ -1033,7 +1039,7 @@ pub fn maybe_start_animations( continue; } - let anim = match context.stylist.get_animation(name, element) { + let keyframe_animation = match context.stylist.get_animation(name, element) { Some(animation) => animation, None => continue, }; @@ -1044,7 +1050,7 @@ pub fn maybe_start_animations( // without submitting it to the compositor, since both the first and // the second keyframes would be synthetised from the computed // values. - if anim.steps.is_empty() { + if keyframe_animation.steps.is_empty() { continue; } @@ -1071,10 +1077,20 @@ pub fn maybe_start_animations( AnimationPlayState::Running => AnimationState::Pending, }; + let computed_steps = ComputedKeyframeStep::generate_for_keyframes::( + element, + &keyframe_animation.steps, + context, + new_style, + font_metrics_provider, + new_style.get_box().animation_timing_function_mod(i), + ); + let new_animation = Animation { node: element.as_node().opaque(), name: name.clone(), - keyframes_animation: anim.clone(), + properties_changed: keyframe_animation.properties_changed, + computed_steps, started_at: animation_start, duration: duration as f64, fill_mode: box_style.animation_fill_mode_mod(i), diff --git a/components/style/dom.rs b/components/style/dom.rs index c1c1f74ef68..de37f7d6536 100644 --- a/components/style/dom.rs +++ b/components/style/dom.rs @@ -8,10 +8,9 @@ #![deny(missing_docs)] use crate::applicable_declarations::ApplicableDeclarationBlock; +use crate::context::SharedStyleContext; #[cfg(feature = "gecko")] -use crate::context::PostAnimationTasks; -#[cfg(feature = "gecko")] -use crate::context::UpdateAnimationsTasks; +use crate::context::{PostAnimationTasks, UpdateAnimationsTasks}; use crate::data::ElementData; use crate::element_state::ElementState; use crate::font_metrics::FontMetricsProvider; @@ -749,7 +748,7 @@ pub trait TElement: fn has_animations(&self) -> bool; /// Returns true if the element has a CSS animation. - fn has_css_animations(&self) -> bool; + fn has_css_animations(&self, context: &SharedStyleContext) -> bool; /// Returns true if the element has a CSS transition (including running transitions and /// completed transitions). diff --git a/components/style/gecko/wrapper.rs b/components/style/gecko/wrapper.rs index 62af71f212b..ade1909300e 100644 --- a/components/style/gecko/wrapper.rs +++ b/components/style/gecko/wrapper.rs @@ -1516,7 +1516,7 @@ impl<'le> TElement for GeckoElement<'le> { self.may_have_animations() && unsafe { Gecko_ElementHasAnimations(self.0) } } - fn has_css_animations(&self) -> bool { + fn has_css_animations(&self, _: &SharedStyleContext) -> bool { self.may_have_animations() && unsafe { Gecko_ElementHasCSSAnimations(self.0) } } diff --git a/components/style/matching.rs b/components/style/matching.rs index e8c6e55520f..706d1fc1fb7 100644 --- a/components/style/matching.rs +++ b/components/style/matching.rs @@ -233,7 +233,6 @@ trait PrivateMatchMethods: TElement { Some(style.0) } - #[cfg(feature = "gecko")] fn needs_animations_update( &self, context: &mut StyleContext, @@ -243,7 +242,7 @@ trait PrivateMatchMethods: TElement { let new_box_style = new_style.get_box(); let new_style_specifies_animations = new_box_style.specifies_animations(); - let has_animations = self.has_css_animations(); + let has_animations = self.has_css_animations(&context.shared); if !new_style_specifies_animations && !has_animations { return false; } @@ -439,37 +438,53 @@ trait PrivateMatchMethods: TElement { ) { use crate::animation::AnimationState; + // We need to call this before accessing the `ElementAnimationSet` from the + // map because this call will do a RwLock::read(). + let needs_animations_update = + self.needs_animations_update(context, old_values.as_ref().map(|s| &**s), new_values); + let this_opaque = self.as_node().opaque(); let shared_context = context.shared; - let mut animation_states = shared_context.animation_states.write(); - let mut animation_state = animation_states.remove(&this_opaque).unwrap_or_default(); + let mut animation_set = shared_context + .animation_states + .write() + .remove(&this_opaque) + .unwrap_or_default(); - animation_state.update_animations_for_new_style(*self, &shared_context, &new_values); + // Starting animations is expensive, because we have to recalculate the style + // for all the keyframes. We only want to do this if we think that there's a + // chance that the animations really changed. + if needs_animations_update { + animation_set.update_animations_for_new_style::( + *self, + &shared_context, + &new_values, + &context.thread_local.font_metrics_provider, + ); + } - animation_state.update_transitions_for_new_style::( + animation_set.update_transitions_for_new_style( &shared_context, this_opaque, old_values.as_ref(), new_values, - &context.thread_local.font_metrics_provider, ); - animation_state.apply_active_animations::( - shared_context, - new_values, - &context.thread_local.font_metrics_provider, - ); + animation_set.apply_active_animations(shared_context, new_values); // We clear away any finished transitions, but retain animations, because they // might still be used for proper calculation of `animation-fill-mode`. - animation_state + animation_set .transitions .retain(|transition| transition.state != AnimationState::Finished); // If the ElementAnimationSet is empty, and don't store it in order to // save memory and to avoid extra processing later. - if !animation_state.is_empty() { - animation_states.insert(this_opaque, animation_state); + if !animation_set.is_empty() { + shared_context + .animation_states + .write() + .insert(this_opaque, animation_set); } } diff --git a/components/style/properties/properties.mako.rs b/components/style/properties/properties.mako.rs index 4bebe0040e7..a098534793f 100644 --- a/components/style/properties/properties.mako.rs +++ b/components/style/properties/properties.mako.rs @@ -2828,10 +2828,27 @@ pub mod style_structs { /// Returns whether there are any transitions specified. #[cfg(feature = "servo")] pub fn specifies_transitions(&self) -> bool { + // TODO(mrobinson): This should check the combined duration and not just + // the duration. self.transition_duration_iter() .take(self.transition_property_count()) .any(|t| t.seconds() > 0.) } + + /// Returns true if animation properties are equal between styles, but without + /// considering keyframe data. + #[cfg(feature = "servo")] + pub fn animations_equals(&self, other: &Self) -> bool { + self.animation_name_iter().eq(other.animation_name_iter()) && + self.animation_delay_iter().eq(other.animation_delay_iter()) && + self.animation_direction_iter().eq(other.animation_direction_iter()) && + self.animation_duration_iter().eq(other.animation_duration_iter()) && + self.animation_fill_mode_iter().eq(other.animation_fill_mode_iter()) && + self.animation_iteration_count_iter().eq(other.animation_iteration_count_iter()) && + self.animation_play_state_iter().eq(other.animation_play_state_iter()) && + self.animation_timing_function_iter().eq(other.animation_timing_function_iter()) + } + % elif style_struct.name == "Column": /// Whether this is a multicol style. #[cfg(feature = "servo")] @@ -2924,6 +2941,12 @@ impl ComputedValues { self.pseudo.as_ref() } + /// Returns true if this is the style for a pseudo-element. + #[cfg(feature = "servo")] + pub fn is_pseudo_style(&self) -> bool { + self.pseudo().is_some() + } + /// Returns whether this style's display value is equal to contents. pub fn is_display_contents(&self) -> bool { self.get_box().clone_display().is_contents()