Bug 1834876 - Part 2: Resolve starting style if we don't have before-change style. r=layout-reviewers,firefox-style-system-reviewers,emilio

Per spec, we define starting style for an element as the after-change style
with @starting-style rules applied in addition.

If an element does not have a before-change style for a given style change
event, the starting style is used instead of the before-change style to
compare with the after-change style to start transitions.

The basic idea in this patch is:
1. We add a flag to indicate if this element may have starting style. We
   set this flag during its full matching, and store this flag in the
   element data.
2. So during process animations, we check this flag, if this element may
   have starting style and specifies transitions, we resolve the
   starting style. Use it as the before-change style.

The implmentation in process_animations() and tests are in the following
patches.

Differential Revision: https://phabricator.services.mozilla.com/D208570
This commit is contained in:
Boris Chiou 2024-04-30 05:06:53 +00:00
parent 4c920b2d33
commit d975b03929
11 changed files with 243 additions and 28 deletions

View file

@ -70,6 +70,19 @@ impl VisitedHandlingMode {
}
}
/// The mode to use whether we should matching rules inside @starting-style.
/// https://drafts.csswg.org/css-transitions-2/#starting-style
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum IncludeStartingStyle {
/// All without rules inside @starting-style. This is for the most common case because the
/// primary/pseudo styles doesn't use rules inside @starting-style.
No,
/// Get the starting style. The starting style for an element as the after-change style with
/// @starting-style rules applied in addition. In other words, this matches all rules,
/// including rules inside @starting-style.
Yes,
}
/// Whether we need to set selector invalidation flags on elements for this
/// match request.
#[derive(Clone, Copy, Debug, PartialEq)]
@ -191,6 +204,12 @@ where
/// Controls how matching for links is handled.
visited_handling: VisitedHandlingMode,
/// Controls if we should match rules in @starting-style.
pub include_starting_style: IncludeStartingStyle,
/// Whether there are any rules inside @starting-style.
pub has_starting_style: bool,
/// The current nesting level of selectors that we're matching.
nesting_level: usize,
@ -239,6 +258,7 @@ where
bloom_filter,
selector_caches,
VisitedHandlingMode::AllLinksUnvisited,
IncludeStartingStyle::No,
quirks_mode,
needs_selector_flags,
matching_for_invalidation,
@ -251,6 +271,7 @@ where
bloom_filter: Option<&'a BloomFilter>,
selector_caches: &'a mut SelectorCaches,
visited_handling: VisitedHandlingMode,
include_starting_style: IncludeStartingStyle,
quirks_mode: QuirksMode,
needs_selector_flags: NeedsSelectorFlags,
matching_for_invalidation: MatchingForInvalidation,
@ -259,6 +280,8 @@ where
matching_mode,
bloom_filter,
visited_handling,
include_starting_style,
has_starting_style: false,
quirks_mode,
classes_and_ids_case_sensitivity: quirks_mode.classes_and_ids_case_sensitivity(),
needs_selector_flags,

View file

@ -45,6 +45,9 @@ bitflags! {
/// The former gives us stronger transitive guarantees that allows us to
/// apply the style sharing cache to cousins.
const PRIMARY_STYLE_REUSED_VIA_RULE_NODE = 1 << 2;
/// Whether this element may have matched rules inside @starting-style.
const MAY_HAVE_STARTING_STYLE = 1 << 3;
}
}
@ -344,22 +347,28 @@ impl ElementData {
let reused_via_rule_node = self
.flags
.contains(ElementDataFlags::PRIMARY_STYLE_REUSED_VIA_RULE_NODE);
let may_have_starting_style = self
.flags
.contains(ElementDataFlags::MAY_HAVE_STARTING_STYLE);
PrimaryStyle {
style: ResolvedStyle(self.styles.primary().clone()),
reused_via_rule_node,
may_have_starting_style,
}
}
/// Sets a new set of styles, returning the old ones.
pub fn set_styles(&mut self, new_styles: ResolvedElementStyles) -> ElementStyles {
if new_styles.primary.reused_via_rule_node {
self.flags
.insert(ElementDataFlags::PRIMARY_STYLE_REUSED_VIA_RULE_NODE);
} else {
self.flags
.remove(ElementDataFlags::PRIMARY_STYLE_REUSED_VIA_RULE_NODE);
}
self.flags.set(
ElementDataFlags::PRIMARY_STYLE_REUSED_VIA_RULE_NODE,
new_styles.primary.reused_via_rule_node,
);
self.flags.set(
ElementDataFlags::MAY_HAVE_STARTING_STYLE,
new_styles.primary.may_have_starting_style,
);
mem::replace(&mut self.styles, new_styles.into())
}
@ -542,4 +551,11 @@ impl ElementData {
n
}
/// Returns true if this element data may need to compute the starting style for CSS
/// transitions.
#[inline]
pub fn may_have_starting_style(&self) -> bool {
self.flags.contains(ElementDataFlags::MAY_HAVE_STARTING_STYLE)
}
}

View file

@ -14,8 +14,8 @@ use crate::invalidation::element::state_and_attributes;
use crate::stylist::CascadeData;
use dom::DocumentState;
use selectors::matching::{
MatchingContext, MatchingForInvalidation, MatchingMode, NeedsSelectorFlags, QuirksMode,
SelectorCaches, VisitedHandlingMode,
IncludeStartingStyle, MatchingContext, MatchingForInvalidation, MatchingMode,
NeedsSelectorFlags, QuirksMode, SelectorCaches, VisitedHandlingMode,
};
/// A struct holding the members necessary to invalidate document state
@ -59,6 +59,7 @@ impl<'a, 'b, E: TElement, I> DocumentStateInvalidationProcessor<'a, 'b, E, I> {
None,
selector_caches,
VisitedHandlingMode::AllLinksVisitedAndUnvisited,
IncludeStartingStyle::No,
quirks_mode,
NeedsSelectorFlags::No,
MatchingForInvalidation::No,

View file

@ -26,8 +26,8 @@ use dom::ElementState;
use fxhash::FxHashMap;
use selectors::matching::{
matches_compound_selector_from, matches_selector, CompoundSelectorMatchingResult,
ElementSelectorFlags, MatchingContext, MatchingForInvalidation, MatchingMode,
NeedsSelectorFlags, QuirksMode, SelectorCaches, VisitedHandlingMode,
ElementSelectorFlags, IncludeStartingStyle, MatchingContext, MatchingForInvalidation,
MatchingMode, NeedsSelectorFlags, QuirksMode, SelectorCaches, VisitedHandlingMode,
};
use selectors::parser::{Combinator, SelectorKey};
use selectors::OpaqueElement;
@ -822,6 +822,7 @@ where
None,
&mut selector_caches,
VisitedHandlingMode::AllLinksVisitedAndUnvisited,
IncludeStartingStyle::No,
self.quirks_mode,
NeedsSelectorFlags::No,
MatchingForInvalidation::Yes,
@ -1032,6 +1033,7 @@ where
None,
selector_caches,
VisitedHandlingMode::AllLinksVisitedAndUnvisited,
IncludeStartingStyle::No,
quirks_mode,
NeedsSelectorFlags::No,
MatchingForInvalidation::Yes,

View file

@ -24,7 +24,8 @@ use dom::ElementState;
use selectors::attr::CaseSensitivity;
use selectors::kleene_value::KleeneValue;
use selectors::matching::{
matches_selector_kleene, MatchingContext, MatchingForInvalidation, MatchingMode, NeedsSelectorFlags, SelectorCaches, VisitedHandlingMode
matches_selector_kleene, IncludeStartingStyle, MatchingContext, MatchingForInvalidation,
MatchingMode, NeedsSelectorFlags, SelectorCaches, VisitedHandlingMode
};
use smallvec::SmallVec;
@ -73,6 +74,7 @@ impl<'a, 'b: 'a, E: TElement + 'b> StateAndAttrInvalidationProcessor<'a, 'b, E>
None,
selector_caches,
VisitedHandlingMode::AllLinksVisitedAndUnvisited,
IncludeStartingStyle::No,
shared_context.quirks_mode(),
NeedsSelectorFlags::No,
MatchingForInvalidation::Yes,

View file

@ -21,8 +21,10 @@ use crate::properties::PropertyDeclarationBlock;
use crate::rule_tree::{CascadeLevel, StrongRuleNode};
use crate::selector_parser::{PseudoElement, RestyleDamage};
use crate::shared_lock::Locked;
use crate::style_resolver::ResolvedElementStyles;
use crate::style_resolver::{PseudoElementResolution, StyleResolverForElement};
use crate::style_resolver::{PseudoElementResolution, ResolvedElementStyles};
#[cfg(feature = "gecko")]
use crate::style_resolver::ResolvedStyle;
use crate::style_resolver::StyleResolverForElement;
use crate::stylesheets::layer_rule::LayerOrder;
use crate::stylist::RuleInclusion;
use crate::traversal_flags::TraversalFlags;
@ -384,6 +386,96 @@ trait PrivateMatchMethods: TElement {
}
}
#[cfg(feature = "gecko")]
fn resolve_starting_style(
&self,
context: &mut StyleContext<Self>,
) -> ResolvedStyle {
use selectors::matching::IncludeStartingStyle;
// Compute after-change style for the parent and the layout parent.
// Per spec, starting style inherits from the parents after-change style just like
// after-change style does.
let parent_el = self.inheritance_parent();
let parent_data = parent_el.as_ref().and_then(|e| e.borrow_data());
let parent_style = parent_data.as_ref().map(|d| d.styles.primary());
let parent_after_change_style =
parent_style.and_then(|s| self.after_change_style(context, s));
let parent_values = parent_after_change_style
.as_ref()
.or(parent_style)
.map(|x| &**x);
let mut layout_parent_el = parent_el.clone();
let layout_parent_data;
let layout_parent_after_change_style;
let layout_parent_values = if parent_style.map_or(false, |s| s.is_display_contents()) {
layout_parent_el = Some(layout_parent_el.unwrap().layout_parent());
layout_parent_data = layout_parent_el.as_ref().unwrap().borrow_data().unwrap();
let layout_parent_style = Some(layout_parent_data.styles.primary());
layout_parent_after_change_style =
layout_parent_style.and_then(|s| self.after_change_style(context, s));
layout_parent_after_change_style
.as_ref()
.or(layout_parent_style)
.map(|x| &**x)
} else {
parent_values
};
// Note: Basically, we have to remove transition rules because the starting style for an
// element is the after-change style with @starting-style rules applied in addition.
// However, we expect there is no transition rules for this element when calling this
// function because we do this only when we don't have before-change style and it's
// unlikely to have running transitions on this element.
let mut resolver = StyleResolverForElement::new(
*self,
context,
RuleInclusion::All,
PseudoElementResolution::IfApplicable,
);
resolver
.resolve_primary_style(
parent_values,
layout_parent_values,
IncludeStartingStyle::Yes,
)
.style
}
#[cfg(feature = "gecko")]
fn maybe_resolve_starting_style(
&self,
context: &mut StyleContext<Self>,
old_styles: &ElementStyles,
new_styles: &ResolvedElementStyles,
) -> Option<Arc<ComputedValues>> {
// For both cases:
// 1. If we didn't see any starting-style rules for this given element during full matching.
// 2. If there is no transitions specified.
// We don't have to resolve starting style.
if !new_styles.may_have_starting_style()
|| !new_styles.primary_style().get_ui().specifies_transitions()
{
return None;
}
// If we don't have before-change-style, we don't have to resolve starting style.
// FIXME: we may have to resolve starting style if the old style is display:none and the new
// style change the display property. We will have a tentative solution in the following
// patches.
if old_styles.primary.is_some() {
return None;
}
let starting_style = self.resolve_starting_style(context);
if starting_style.style().clone_display().is_none() {
return None;
}
Some(starting_style.0)
}
#[cfg(feature = "gecko")]
fn process_animations(
&self,
@ -395,18 +487,21 @@ trait PrivateMatchMethods: TElement {
) {
use crate::context::UpdateAnimationsTasks;
let new_values = new_styles.primary_style_mut();
let old_values = &old_styles.primary;
if context.shared.traversal_flags.for_animation_only() {
self.handle_display_change_for_smil_if_needed(
context,
old_values.as_deref(),
new_values,
new_styles.primary_style(),
restyle_hint,
);
return;
}
// TODO: Use this in the following patches.
let _starting_styles = self.maybe_resolve_starting_style(context, old_styles, new_styles);
let new_values = new_styles.primary_style_mut();
// Bug 868975: These steps should examine and update the visited styles
// in addition to the unvisited styles.

View file

@ -330,6 +330,10 @@ impl SelectorMap<Rule> {
) where
E: TElement,
{
use selectors::matching::IncludeStartingStyle;
let include_starting_style =
matches!(matching_context.include_starting_style, IncludeStartingStyle::Yes);
for rule in rules {
if !matches_selector(
&rule.selector,
@ -352,6 +356,17 @@ impl SelectorMap<Rule> {
}
}
if rule.is_starting_style {
// Set this flag if there are any rules inside @starting-style. This flag is for
// optimization to avoid any redundant resolution of starting style if the author
// doesn't specify for this element.
matching_context.has_starting_style = true;
if !include_starting_style {
continue;
}
}
matching_rules.push(rule.to_applicable_declaration_block(cascade_level, cascade_data));
}
}

View file

@ -17,7 +17,8 @@ use crate::selector_parser::{PseudoElement, SelectorImpl};
use crate::stylist::RuleInclusion;
use log::Level::Trace;
use selectors::matching::{
MatchingContext, MatchingForInvalidation, MatchingMode, NeedsSelectorFlags, VisitedHandlingMode,
IncludeStartingStyle, MatchingContext, MatchingForInvalidation, MatchingMode,
NeedsSelectorFlags, VisitedHandlingMode,
};
use servo_arc::Arc;
@ -47,11 +48,20 @@ where
struct MatchingResults {
rule_node: StrongRuleNode,
flags: ComputedValueFlags,
has_starting_style: bool,
}
/// A style returned from the resolver machinery.
pub struct ResolvedStyle(pub Arc<ComputedValues>);
impl ResolvedStyle {
/// Convenience accessor for the style.
#[inline]
pub fn style(&self) -> &ComputedValues {
&*self.0
}
}
/// The primary style of an element or an element-backed pseudo-element.
pub struct PrimaryStyle {
/// The style itself.
@ -59,6 +69,11 @@ pub struct PrimaryStyle {
/// Whether the style was reused from another element via the rule node (see
/// `StyleSharingCache::lookup_by_rules`).
pub reused_via_rule_node: bool,
/// The element may have matched rules inside @starting-style.
/// Basically, we don't apply @starting-style rules to |style|. This is a sugar to let us know
/// if we should resolve the element again for starting style, which is the after-change style
/// with @starting-style rules applied in addition.
pub may_have_starting_style: bool,
}
/// A set of style returned from the resolver machinery.
@ -79,6 +94,12 @@ impl ResolvedElementStyles {
pub fn primary_style_mut(&mut self) -> &mut Arc<ComputedValues> {
&mut self.primary.style.0
}
/// Returns true if this element may have starting style rules.
#[inline]
pub fn may_have_starting_style(&self) -> bool {
self.primary.may_have_starting_style
}
}
impl PrimaryStyle {
@ -186,16 +207,22 @@ where
&mut self,
parent_style: Option<&ComputedValues>,
layout_parent_style: Option<&ComputedValues>,
include_starting_style: IncludeStartingStyle,
) -> PrimaryStyle {
let primary_results = self.match_primary(VisitedHandlingMode::AllLinksUnvisited);
let primary_results = self.match_primary(
VisitedHandlingMode::AllLinksUnvisited,
include_starting_style,
);
let inside_link = parent_style.map_or(false, |s| s.visited_style().is_some());
let visited_rules = if self.context.shared.visited_styles_enabled &&
(inside_link || self.element.is_link())
{
let visited_matching_results =
self.match_primary(VisitedHandlingMode::RelevantLinkVisited);
let visited_matching_results = self.match_primary(
VisitedHandlingMode::RelevantLinkVisited,
IncludeStartingStyle::No,
);
Some(visited_matching_results.rule_node)
} else {
None
@ -209,6 +236,7 @@ where
},
parent_style,
layout_parent_style,
primary_results.has_starting_style,
)
}
@ -217,6 +245,7 @@ where
inputs: CascadeInputs,
parent_style: Option<&ComputedValues>,
layout_parent_style: Option<&ComputedValues>,
may_have_starting_style: bool,
) -> PrimaryStyle {
// Before doing the cascade, check the sharing cache and see if we can
// reuse the style via rule node identity.
@ -253,6 +282,7 @@ where
/* pseudo = */ None,
),
reused_via_rule_node: false,
may_have_starting_style,
}
}
@ -262,7 +292,11 @@ where
parent_style: Option<&ComputedValues>,
layout_parent_style: Option<&ComputedValues>,
) -> ResolvedElementStyles {
let primary_style = self.resolve_primary_style(parent_style, layout_parent_style);
let primary_style = self.resolve_primary_style(
parent_style,
layout_parent_style,
IncludeStartingStyle::No,
);
let mut pseudo_styles = EagerPseudoStyles::default();
@ -375,10 +409,15 @@ where
pub fn cascade_styles_with_default_parents(
&mut self,
inputs: ElementCascadeInputs,
may_have_starting_style: bool,
) -> ResolvedElementStyles {
with_default_parent_styles(self.element, move |parent_style, layout_parent_style| {
let primary_style =
self.cascade_primary_style(inputs.primary, parent_style, layout_parent_style);
let primary_style = self.cascade_primary_style(
inputs.primary,
parent_style,
layout_parent_style,
may_have_starting_style,
);
let mut pseudo_styles = EagerPseudoStyles::default();
if let Some(mut pseudo_array) = inputs.pseudos.into_array() {
@ -427,6 +466,7 @@ where
let MatchingResults {
rule_node,
mut flags,
has_starting_style: _,
} = self.match_pseudo(
&originating_element_style.style.0,
pseudo,
@ -459,7 +499,11 @@ where
))
}
fn match_primary(&mut self, visited_handling: VisitedHandlingMode) -> MatchingResults {
fn match_primary(
&mut self,
visited_handling: VisitedHandlingMode,
include_starting_style: IncludeStartingStyle,
) -> MatchingResults {
debug!(
"Match primary for {:?}, visited: {:?}",
self.element, visited_handling
@ -473,6 +517,7 @@ where
Some(bloom_filter),
selector_caches,
visited_handling,
include_starting_style,
self.context.shared.quirks_mode(),
NeedsSelectorFlags::Yes,
MatchingForInvalidation::No,
@ -512,6 +557,7 @@ where
MatchingResults {
rule_node,
flags: matching_context.extra_data.cascade_input_flags,
has_starting_style: matching_context.has_starting_style,
}
}
@ -550,6 +596,7 @@ where
Some(bloom_filter),
selector_caches,
visited_handling,
IncludeStartingStyle::No,
self.context.shared.quirks_mode(),
NeedsSelectorFlags::Yes,
MatchingForInvalidation::No,
@ -580,6 +627,7 @@ where
Some(MatchingResults {
rule_node,
flags: matching_context.extra_data.cascade_input_flags,
has_starting_style: false, // We don't care.
})
}
}

View file

@ -1263,6 +1263,7 @@ impl Stylist {
None,
&mut selector_caches,
VisitedHandlingMode::RelevantLinkVisited,
selectors::matching::IncludeStartingStyle::No,
self.quirks_mode,
needs_selector_flags,
MatchingForInvalidation::No,

View file

@ -350,7 +350,11 @@ where
rule_inclusion,
PseudoElementResolution::IfApplicable,
)
.resolve_primary_style(style.as_deref(), layout_parent_style.as_deref());
.resolve_primary_style(
style.as_deref(),
layout_parent_style.as_deref(),
selectors::matching::IncludeStartingStyle::No,
);
let is_display_contents = primary_style.style().is_display_contents();
@ -639,7 +643,10 @@ where
PseudoElementResolution::IfApplicable,
);
resolver.cascade_styles_with_default_parents(cascade_inputs)
resolver.cascade_styles_with_default_parents(
cascade_inputs,
data.may_have_starting_style(),
)
},
CascadeOnly => {
// Skipping full matching, load cascade inputs from previous values.
@ -653,7 +660,10 @@ where
PseudoElementResolution::IfApplicable,
);
resolver.cascade_styles_with_default_parents(cascade_inputs)
resolver.cascade_styles_with_default_parents(
cascade_inputs,
data.may_have_starting_style(),
)
};
// Insert into the cache, but only if this style isn't reused from a

View file

@ -2587,7 +2587,8 @@ pub extern "C" fn Servo_StyleRule_SelectorMatchesElement(
relevant_link_visited: bool,
) -> bool {
use selectors::matching::{
matches_selector, MatchingContext, MatchingMode, NeedsSelectorFlags, VisitedHandlingMode,
matches_selector, IncludeStartingStyle, MatchingContext, MatchingMode, NeedsSelectorFlags,
VisitedHandlingMode,
};
let selectors = desugared_selector_list(rules);
let Some(selector) = selectors.slice().get(index as usize) else {
@ -2628,6 +2629,7 @@ pub extern "C" fn Servo_StyleRule_SelectorMatchesElement(
/* bloom_filter = */ None,
&mut selector_caches,
visited_mode,
IncludeStartingStyle::No,
quirks_mode,
NeedsSelectorFlags::No,
MatchingForInvalidation::No,