Bug 1646811 - servo: Add animation and transition support for pseudo-elements.

This change extends the DocumentAnimationSet to hold animations for
pseudo-elements. Since pseudo-elements in Servo are not in the DOM like
in Gecko, they need to be handled a bit carefully in stylo.  When a
pseudo-element has an animation, recascade the style. Finally, this
change passes the pseudo-element string properly to animation events.

Fixes: #10316

Depends on D80242

Differential Revision: https://phabricator.services.mozilla.com/D80243
This commit is contained in:
Martin Robinson 2020-06-18 18:14:05 +00:00
parent d406afe4e6
commit 0346b7de29
6 changed files with 286 additions and 55 deletions

View File

@ -19,6 +19,7 @@ use crate::properties::{
PropertyDeclarationId,
};
use crate::rule_tree::CascadeLevel;
use crate::selector_parser::PseudoElement;
use crate::shared_lock::{Locked, SharedRwLock};
use crate::style_resolver::StyleResolverForElement;
use crate::stylesheets::keyframes_rule::{KeyframesAnimation, KeyframesStep, KeyframesStepValue};
@ -1138,7 +1139,39 @@ impl ElementAnimationSet {
#[derive(Clone, Debug, Eq, Hash, MallocSizeOf, PartialEq)]
/// A key that is used to identify nodes in the `DocumentAnimationSet`.
pub struct AnimationSetKey(pub OpaqueNode);
pub struct AnimationSetKey {
/// The node for this `AnimationSetKey`.
pub node: OpaqueNode,
/// The pseudo element for this `AnimationSetKey`. If `None` this key will
/// refer to the main content for its node.
pub pseudo_element: Option<PseudoElement>,
}
impl AnimationSetKey {
/// Create a new key given a node and optional pseudo element.
pub fn new(node: OpaqueNode, pseudo_element: Option<PseudoElement>) -> Self {
AnimationSetKey {
node,
pseudo_element,
}
}
/// Create a new key for the main content of this node.
pub fn new_for_non_pseudo(node: OpaqueNode) -> Self {
AnimationSetKey {
node,
pseudo_element: None,
}
}
/// Create a new key for given node and pseudo element.
pub fn new_for_pseudo(node: OpaqueNode, pseudo_element: PseudoElement) -> Self {
AnimationSetKey {
node,
pseudo_element: Some(pseudo_element),
}
}
}
#[derive(Clone, Debug, Default, MallocSizeOf)]
/// A set of animations for a document.
@ -1154,8 +1187,7 @@ impl DocumentAnimationSet {
self.sets
.read()
.get(key)
.map(|set| set.has_active_animation())
.unwrap_or(false)
.map_or(false, |set| set.has_active_animation())
}
/// Return whether or not the provided node has active CSS transitions.
@ -1163,8 +1195,7 @@ impl DocumentAnimationSet {
self.sets
.read()
.get(key)
.map(|set| set.has_active_transition())
.unwrap_or(false)
.map_or(false, |set| set.has_active_transition())
}
/// Return a locked PropertyDeclarationBlock with animation values for the given
@ -1202,6 +1233,58 @@ impl DocumentAnimationSet {
Arc::new(shared_lock.wrap(block))
})
}
/// Get all the animation declarations for the given key, returning an empty
/// `AnimationAndTransitionDeclarations` if there are no animations.
pub(crate) fn get_all_declarations(
&self,
key: &AnimationSetKey,
time: f64,
shared_lock: &SharedRwLock,
) -> AnimationAndTransitionDeclarations {
let sets = self.sets.read();
let set = match sets.get(key) {
Some(set) => set,
None => return Default::default(),
};
let animations = set.get_value_map_for_active_animations(time).map(|map| {
let block = PropertyDeclarationBlock::from_animation_value_map(&map);
Arc::new(shared_lock.wrap(block))
});
let transitions = set.get_value_map_for_active_transitions(time).map(|map| {
let block = PropertyDeclarationBlock::from_animation_value_map(&map);
Arc::new(shared_lock.wrap(block))
});
AnimationAndTransitionDeclarations {
animations,
transitions,
}
}
/// Cancel all animations for set at the given key.
pub(crate) fn cancel_all_animations_for_key(&self, key: &AnimationSetKey) {
if let Some(set) = self.sets.write().get_mut(key) {
set.cancel_all_animations();
}
}
}
/// A set of property declarations for a node, including animations and
/// transitions.
#[derive(Default)]
pub struct AnimationAndTransitionDeclarations {
/// Declarations for animations.
pub animations: Option<Arc<Locked<PropertyDeclarationBlock>>>,
/// Declarations for transitions.
pub transitions: Option<Arc<Locked<PropertyDeclarationBlock>>>,
}
impl AnimationAndTransitionDeclarations {
/// Whether or not this `AnimationAndTransitionDeclarations` is empty.
pub(crate) fn is_empty(&self) -> bool {
self.animations.is_none() && self.transitions.is_none()
}
}
/// Kick off any new transitions for this node and return all of the properties that are

View File

@ -750,12 +750,23 @@ pub trait TElement:
/// or are scheduled to do so in the future.
fn has_animations(&self, context: &SharedStyleContext) -> bool;
/// Returns true if the element has a CSS animation.
fn has_css_animations(&self, context: &SharedStyleContext) -> bool;
/// Returns true if the element has a CSS animation. The `context` and `pseudo_element`
/// arguments are only used by Servo, since it stores animations globally and pseudo-elements
/// are not in the DOM.
fn has_css_animations(
&self,
context: &SharedStyleContext,
pseudo_element: Option<PseudoElement>,
) -> bool;
/// Returns true if the element has a CSS transition (including running transitions and
/// completed transitions).
fn has_css_transitions(&self, context: &SharedStyleContext) -> bool;
/// completed transitions). The `context` and `pseudo_element` arguments are only used
/// by Servo, since it stores animations globally and pseudo-elements are not in the DOM.
fn has_css_transitions(
&self,
context: &SharedStyleContext,
pseudo_element: Option<PseudoElement>,
) -> bool;
/// Returns true if the element has animation restyle hints.
fn has_animation_restyle_hints(&self) -> bool {

View File

@ -1520,11 +1520,11 @@ impl<'le> TElement for GeckoElement<'le> {
self.may_have_animations() && unsafe { Gecko_ElementHasAnimations(self.0) }
}
fn has_css_animations(&self, _: &SharedStyleContext) -> bool {
fn has_css_animations(&self, _: &SharedStyleContext, _: Option<PseudoElement>) -> bool {
self.may_have_animations() && unsafe { Gecko_ElementHasCSSAnimations(self.0) }
}
fn has_css_transitions(&self, _: &SharedStyleContext) -> bool {
fn has_css_transitions(&self, _: &SharedStyleContext, _: Option<PseudoElement>) -> bool {
self.may_have_animations() && unsafe { Gecko_ElementHasCSSTransitions(self.0) }
}

View File

@ -10,7 +10,7 @@
use crate::computed_value_flags::ComputedValueFlags;
use crate::context::{CascadeInputs, ElementCascadeInputs, QuirksMode, SelectorFlagsMap};
use crate::context::{SharedStyleContext, StyleContext};
use crate::data::ElementData;
use crate::data::{ElementData, ElementStyles};
use crate::dom::TElement;
#[cfg(feature = "servo")]
use crate::dom::TNode;
@ -90,13 +90,13 @@ enum CascadeVisitedMode {
trait PrivateMatchMethods: TElement {
fn replace_single_rule_node(
context: &mut StyleContext<Self>,
context: &SharedStyleContext,
level: CascadeLevel,
pdb: Option<ArcBorrow<Locked<PropertyDeclarationBlock>>>,
path: &mut StrongRuleNode,
) -> bool {
let stylist = &context.shared.stylist;
let guards = &context.shared.guards;
let stylist = &context.stylist;
let guards = &context.guards;
let mut important_rules_changed = false;
let new_node = stylist.rule_tree().update_rule_at_level(
@ -143,13 +143,13 @@ trait PrivateMatchMethods: TElement {
if replacements.contains(RestyleHint::RESTYLE_STYLE_ATTRIBUTE) {
let style_attribute = self.style_attribute();
result |= Self::replace_single_rule_node(
context,
context.shared,
CascadeLevel::same_tree_author_normal(),
style_attribute,
primary_rules,
);
result |= Self::replace_single_rule_node(
context,
context.shared,
CascadeLevel::same_tree_author_important(),
style_attribute,
primary_rules,
@ -170,7 +170,7 @@ trait PrivateMatchMethods: TElement {
if replacements.contains(RestyleHint::RESTYLE_SMIL) {
Self::replace_single_rule_node(
context,
context.shared,
CascadeLevel::SMILOverride,
self.smil_override(),
primary_rules,
@ -179,7 +179,7 @@ trait PrivateMatchMethods: TElement {
if replacements.contains(RestyleHint::RESTYLE_CSS_TRANSITIONS) {
Self::replace_single_rule_node(
context,
context.shared,
CascadeLevel::Transitions,
self.transition_rule(&context.shared)
.as_ref()
@ -190,7 +190,7 @@ trait PrivateMatchMethods: TElement {
if replacements.contains(RestyleHint::RESTYLE_CSS_ANIMATIONS) {
Self::replace_single_rule_node(
context,
context.shared,
CascadeLevel::Animations,
self.animation_rule(&context.shared)
.as_ref()
@ -245,11 +245,12 @@ trait PrivateMatchMethods: TElement {
context: &mut StyleContext<Self>,
old_style: Option<&ComputedValues>,
new_style: &ComputedValues,
pseudo_element: Option<PseudoElement>,
) -> bool {
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(&context.shared);
let has_animations = self.has_css_animations(&context.shared, pseudo_element);
if !new_style_specifies_animations && !has_animations {
return false;
}
@ -326,6 +327,7 @@ trait PrivateMatchMethods: TElement {
context: &StyleContext<Self>,
old_style: Option<&ComputedValues>,
new_style: &ComputedValues,
pseudo_element: Option<PseudoElement>,
) -> bool {
let old_style = match old_style {
Some(v) => v,
@ -333,7 +335,9 @@ trait PrivateMatchMethods: TElement {
};
let new_box_style = new_style.get_box();
if !self.has_css_transitions(context.shared) && !new_box_style.specifies_transitions() {
if !self.has_css_transitions(context.shared, pseudo_element) &&
!new_box_style.specifies_transitions()
{
return false;
}
@ -379,7 +383,7 @@ trait PrivateMatchMethods: TElement {
fn process_animations(
&self,
context: &mut StyleContext<Self>,
old_values: &mut Option<Arc<ComputedValues>>,
old_styles: &mut ElementStyles,
new_styles: &mut ResolvedElementStyles,
restyle_hint: RestyleHint,
important_rules_changed: bool,
@ -401,7 +405,12 @@ trait PrivateMatchMethods: TElement {
// in addition to the unvisited styles.
let mut tasks = UpdateAnimationsTasks::empty();
if self.needs_animations_update(context, old_values.as_ref().map(|s| &**s), new_values) {
if self.needs_animations_update(
context,
old_values.as_ref().map(|s| &**s),
new_values,
/* pseudo_element = */ None,
) {
tasks.insert(UpdateAnimationsTasks::CSS_ANIMATIONS);
}
@ -409,6 +418,7 @@ trait PrivateMatchMethods: TElement {
context,
old_values.as_ref().map(|s| &**s),
new_values,
/* pseudo_element = */ None,
) {
let after_change_style = if self.has_css_transitions(context.shared) {
self.after_change_style(context, new_values)
@ -467,57 +477,149 @@ trait PrivateMatchMethods: TElement {
fn process_animations(
&self,
context: &mut StyleContext<Self>,
old_values: &mut Option<Arc<ComputedValues>>,
old_styles: &mut ElementStyles,
new_resolved_styles: &mut ResolvedElementStyles,
_restyle_hint: RestyleHint,
_important_rules_changed: bool,
) {
if !self.process_animations_for_style(
use crate::animation::AnimationSetKey;
use crate::dom::TDocument;
let style_changed = self.process_animations_for_style(
context,
old_values,
&mut old_styles.primary,
new_resolved_styles.primary_style_mut(),
) {
/* pseudo_element = */ None,
);
// If we have modified animation or transitions, we recascade style for this node.
if style_changed {
let mut rule_node = new_resolved_styles.primary_style().rules().clone();
let declarations = context.shared.animations.get_all_declarations(
&AnimationSetKey::new_for_non_pseudo(self.as_node().opaque()),
context.shared.current_time_for_animations,
self.as_node().owner_doc().shared_lock(),
);
Self::replace_single_rule_node(
&context.shared,
CascadeLevel::Transitions,
declarations.transitions.as_ref().map(|a| a.borrow_arc()),
&mut rule_node,
);
Self::replace_single_rule_node(
&context.shared,
CascadeLevel::Animations,
declarations.animations.as_ref().map(|a| a.borrow_arc()),
&mut rule_node,
);
if rule_node != *new_resolved_styles.primary_style().rules() {
let inputs = CascadeInputs {
rules: Some(rule_node),
visited_rules: new_resolved_styles.primary_style().visited_rules().cloned(),
};
new_resolved_styles.primary.style = StyleResolverForElement::new(
*self,
context,
RuleInclusion::All,
PseudoElementResolution::IfApplicable,
)
.cascade_style_and_visited_with_default_parents(inputs);
}
}
self.process_animations_for_pseudo(
context,
old_styles,
new_resolved_styles,
PseudoElement::Before,
);
self.process_animations_for_pseudo(
context,
old_styles,
new_resolved_styles,
PseudoElement::After,
);
}
#[cfg(feature = "servo")]
fn process_animations_for_pseudo(
&self,
context: &mut StyleContext<Self>,
old_styles: &mut ElementStyles,
new_resolved_styles: &mut ResolvedElementStyles,
pseudo_element: PseudoElement,
) {
use crate::animation::AnimationSetKey;
use crate::dom::TDocument;
let key = AnimationSetKey::new_for_pseudo(self.as_node().opaque(), pseudo_element.clone());
let mut style = match new_resolved_styles.pseudos.get(&pseudo_element) {
Some(style) => Arc::clone(style),
None => {
context
.shared
.animations
.cancel_all_animations_for_key(&key);
return;
},
};
let mut old_style = old_styles.pseudos.get(&pseudo_element).cloned();
self.process_animations_for_style(
context,
&mut old_style,
&mut style,
Some(pseudo_element.clone()),
);
let declarations = context.shared.animations.get_all_declarations(
&key,
context.shared.current_time_for_animations,
self.as_node().owner_doc().shared_lock(),
);
if declarations.is_empty() {
return;
}
// If we have modified animation or transitions, we recascade style for this node.
let mut rule_node = new_resolved_styles.primary_style().rules().clone();
let mut rule_node = style.rules().clone();
Self::replace_single_rule_node(
context,
&context.shared,
CascadeLevel::Transitions,
self.transition_rule(&context.shared)
.as_ref()
.map(|a| a.borrow_arc()),
declarations.transitions.as_ref().map(|a| a.borrow_arc()),
&mut rule_node,
);
Self::replace_single_rule_node(
context,
&context.shared,
CascadeLevel::Animations,
self.animation_rule(&context.shared)
.as_ref()
.map(|a| a.borrow_arc()),
declarations.animations.as_ref().map(|a| a.borrow_arc()),
&mut rule_node,
);
// If these animations haven't modified the rule now, we can just exit early.
if rule_node == *new_resolved_styles.primary_style().rules() {
if rule_node == *style.rules() {
return;
}
let inputs = CascadeInputs {
rules: Some(rule_node),
visited_rules: new_resolved_styles.primary_style().visited_rules().cloned(),
visited_rules: style.visited_rules().cloned(),
};
let style = StyleResolverForElement::new(
let new_style = StyleResolverForElement::new(
*self,
context,
RuleInclusion::All,
PseudoElementResolution::IfApplicable,
)
.cascade_style_and_visited_with_default_parents(inputs);
.cascade_style_and_visited_for_pseudo_with_default_parents(
inputs,
&pseudo_element,
&new_resolved_styles.primary,
);
new_resolved_styles.primary.style = style;
new_resolved_styles
.pseudos
.set(&pseudo_element, new_style.0);
}
#[cfg(feature = "servo")]
@ -526,17 +628,24 @@ trait PrivateMatchMethods: TElement {
context: &mut StyleContext<Self>,
old_values: &mut Option<Arc<ComputedValues>>,
new_values: &mut Arc<ComputedValues>,
pseudo_element: Option<PseudoElement>,
) -> bool {
use crate::animation::{AnimationSetKey, 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 needs_animations_update = self.needs_animations_update(
context,
old_values.as_ref().map(|s| &**s),
new_values,
pseudo_element,
);
let might_need_transitions_update = self.might_need_transitions_update(
context,
old_values.as_ref().map(|s| &**s),
new_values,
pseudo_element,
);
let mut after_change_style = None;
@ -544,7 +653,7 @@ trait PrivateMatchMethods: TElement {
after_change_style = self.after_change_style(context, new_values);
}
let key = AnimationSetKey(self.as_node().opaque());
let key = AnimationSetKey::new(self.as_node().opaque(), pseudo_element);
let shared_context = context.shared;
let mut animation_set = shared_context
.animations
@ -764,7 +873,7 @@ pub trait MatchMethods: TElement {
self.process_animations(
context,
&mut data.styles.primary,
&mut data.styles,
&mut new_styles,
data.hint,
important_rules_changed,

View File

@ -29,7 +29,7 @@ use style_traits::{ParseError, StyleParseErrorKind};
/// A pseudo-element, both public and private.
///
/// NB: If you add to this list, be sure to update `each_simple_pseudo_element` too.
#[derive(Clone, Debug, Eq, Hash, MallocSizeOf, PartialEq, ToShmem)]
#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, MallocSizeOf, PartialEq, Serialize, ToShmem)]
#[allow(missing_docs)]
#[repr(usize)]
pub enum PseudoElement {

View File

@ -118,6 +118,17 @@ where
)
}
fn layout_parent_style_for_pseudo<'a>(
primary_style: &'a PrimaryStyle,
layout_parent_style: Option<&'a ComputedValues>,
) -> Option<&'a ComputedValues> {
if primary_style.style().is_display_contents() {
layout_parent_style
} else {
Some(primary_style.style())
}
}
fn eager_pseudo_is_definitely_not_generated(
pseudo: &PseudoElement,
style: &ComputedValues,
@ -246,11 +257,8 @@ where
let mut pseudo_styles = EagerPseudoStyles::default();
if !self.element.is_pseudo_element() {
let layout_parent_style_for_pseudo = if primary_style.style().is_display_contents() {
layout_parent_style
} else {
Some(primary_style.style())
};
let layout_parent_style_for_pseudo =
layout_parent_style_for_pseudo(&primary_style, layout_parent_style);
SelectorImpl::each_eagerly_cascaded_pseudo_element(|pseudo| {
let pseudo_style = self.resolve_pseudo_style(
&pseudo,
@ -298,6 +306,26 @@ where
})
}
/// Cascade a set of rules for pseudo element, using the default parent for inheritance.
pub(crate) fn cascade_style_and_visited_for_pseudo_with_default_parents(
&mut self,
inputs: CascadeInputs,
pseudo: &PseudoElement,
primary_style: &PrimaryStyle,
) -> ResolvedStyle {
with_default_parent_styles(self.element, |_, layout_parent_style| {
let layout_parent_style_for_pseudo =
layout_parent_style_for_pseudo(primary_style, layout_parent_style);
self.cascade_style_and_visited(
inputs,
Some(primary_style.style()),
layout_parent_style_for_pseudo,
Some(pseudo),
)
})
}
fn cascade_style_and_visited(
&mut self,
inputs: CascadeInputs,