/* This Source Code Form is subject to the terms of the Mozilla Public *License,v.2.0.IfacopyoftheMPLwasnotdistributedwiththis
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
//! High-level interface to CSS selector matching.
/// Represents the result of comparing an element's old and new style. #[derive(Debug)] pubstruct StyleDifference { /// The resulting damage. pub damage: RestyleDamage,
/// Whether any styles changed. pub change: StyleChange,
}
/// Represents whether or not the style of an element has changed. #[derive(Clone, Copy, Debug)] pubenum StyleChange { /// The style hasn't changed.
Unchanged, /// The style has changed.
Changed { /// Whether only reset structs changed.
reset_only: bool,
},
}
/// Whether or not newly computed values for an element need to be cascaded to /// children (or children might need to be re-matched, e.g., for container /// queries). #[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)] pubenum ChildRestyleRequirement { /// Old and new computed values were the same, or we otherwise know that /// we won't bother recomputing style for children, so we can skip cascading /// the new values into child elements.
CanSkipCascade = 0, /// The same as `MustCascadeChildren`, but we only need to actually /// recascade if the child inherits any explicit reset style.
MustCascadeChildrenIfInheritResetStyle = 1, /// Old and new computed values were different, so we must cascade the /// new values to children.
MustCascadeChildren = 2, /// The same as `MustCascadeChildren`, but for the entire subtree. This is /// used to handle root font-size updates needing to recascade the whole /// document.
MustCascadeDescendants = 3, /// We need to re-match the whole subttree. This is used to handle container /// query relative unit changes for example. Container query size changes /// also trigger re-match, but after layout.
MustMatchDescendants = 4,
}
/// Determines which styles are being cascaded currently. #[derive(Clone, Copy, Debug, Eq, PartialEq)] enum CascadeVisitedMode { /// Cascade the regular, unvisited styles.
Unvisited, /// Cascade the styles used when an element's relevant link is visited. A /// "relevant link" is the element being matched if it is a link or the /// nearest ancestor link.
Visited,
}
/// Updates the rule nodes without re-running selector matching, using just /// the rule tree, for a specific visited mode. /// /// Returns true if an !important rule was replaced. fn replace_rules_internal(
&self,
replacements: RestyleHint,
context: &mut StyleContext<Self>,
cascade_visited: CascadeVisitedMode,
cascade_inputs: &mut ElementCascadeInputs,
) -> bool {
debug_assert!(
replacements.intersects(RestyleHint::replacements()) &&
(replacements & !RestyleHint::replacements()).is_empty()
);
let primary_rules = match cascade_visited {
CascadeVisitedMode::Unvisited => cascade_inputs.primary.rules.as_mut(),
CascadeVisitedMode::Visited => cascade_inputs.primary.visited_rules.as_mut(),
};
let primary_rules = match primary_rules {
Some(r) => r,
None => returnfalse,
};
if !context.shared.traversal_flags.for_animation_only() { letmut result = false; if replacements.contains(RestyleHint::RESTYLE_STYLE_ATTRIBUTE) { let style_attribute = self.style_attribute();
result |= Self::replace_single_rule_node(
context.shared,
CascadeLevel::same_tree_author_normal(),
LayerOrder::style_attribute(),
style_attribute,
primary_rules,
);
result |= Self::replace_single_rule_node(
context.shared,
CascadeLevel::same_tree_author_important(),
LayerOrder::style_attribute(),
style_attribute,
primary_rules,
); // FIXME(emilio): Still a hack! self.unset_dirty_style_attribute();
} return result;
}
// Animation restyle hints are processed prior to other restyle // hints in the animation-only traversal. // // Non-animation restyle hints will be processed in a subsequent // normal traversal. if replacements.intersects(RestyleHint::for_animations()) {
debug_assert!(context.shared.traversal_flags.for_animation_only());
let has_animations = self.has_css_animations(&context.shared, pseudo_element); if !new_style_specifies_animations && !has_animations { returnfalse;
}
let old_style = match old_style {
Some(old) => old, // If we have no old style but have animations, we may be a // pseudo-element which was re-created without style changes. // // This can happen when we reframe the pseudo-element without // restyling it (due to content insertion on a flex container or // such, for example). See bug 1564366. // // FIXME(emilio): The really right fix for this is keeping the // pseudo-element itself around on reframes, but that's a bit // harder. If we do that we can probably remove quite a lot of the // EffectSet complexity though, since right now it's stored on the // parent element for pseudo-elements given we need to keep it // around...
None => { return new_style_specifies_animations || new_style.is_pseudo_style();
},
};
let old_ui_style = old_style.get_ui();
let keyframes_could_have_changed = context
.shared
.traversal_flags
.contains(TraversalFlags::ForCSSRuleChanges);
// If the traversal is triggered due to changes in CSS rules changes, we // need to try to update all CSS animations on the element if the // element has or will have CSS animation style regardless of whether // the animation is running or not. // // TODO: We should check which @keyframes were added/changed/deleted and // update only animations corresponding to those @keyframes. if keyframes_could_have_changed { returntrue;
}
// If the animations changed, well... if !old_ui_style.animations_equals(new_ui_style) { returntrue;
}
let old_display = old_style.clone_display(); let new_display = new_style.clone_display();
// If we were display: none, we may need to trigger animations. if old_display == Display::None && new_display != Display::None { return new_style_specifies_animations;
}
// If we are becoming display: none, we may need to stop animations. if old_display != Display::None && new_display == Display::None { return has_animations;
}
// We might need to update animations if writing-mode or direction // changed, and any of the animations contained logical properties. // // We may want to be more granular, but it's probably not worth it. if new_style.writing_mode != old_style.writing_mode { return has_animations;
}
if !self.has_css_transitions(context.shared, pseudo_element) &&
!new_style.get_ui().specifies_transitions()
{ returnfalse;
}
if old_style.clone_display().is_none() { returnfalse;
}
returntrue;
}
/// Create a SequentialTask for resolving descendants in a SMIL display /// property animation if the display property changed from none. #[cfg(feature = "gecko")] fn handle_display_change_for_smil_if_needed(
&self,
context: &mut StyleContext<Self>,
old_values: Option<&ComputedValues>,
new_values: &ComputedValues,
restyle_hints: RestyleHint,
) { usecrate::context::PostAnimationTasks;
if !restyle_hints.intersects(RestyleHint::RESTYLE_SMIL) { return;
}
if new_values.is_display_property_changed_from_none(old_values) { // When display value is changed from none to other, we need to // traverse descendant elements in a subsequent normal // traversal (we can't traverse them in this animation-only restyle // since we have no way to know whether the decendants // need to be traversed at the beginning of the animation-only // restyle). let task = crate::context::SequentialTask::process_post_animation(
*self,
PostAnimationTasks::DISPLAY_CHANGED_FROM_NONE_FOR_SMIL,
);
context.thread_local.tasks.push(task);
}
}
#[cfg(feature = "gecko")] fn maybe_resolve_starting_style(
&self,
context: &mut StyleContext<Self>,
old_values: Option<&Arc<ComputedValues>>,
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;
}
// We resolve starting style only if we don't have before-change-style, or we change from // display:none. if old_values.is_some() &&
!new_styles
.primary_style()
.is_display_property_changed_from_none(old_values.map(|s| &**s))
{ return None;
}
// 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 or we change // from display:none. In these cases, it's unlikely to have running transitions on this // element. letmut resolver = StyleResolverForElement::new(
*self,
context,
RuleInclusion::All,
PseudoElementResolution::IfApplicable,
);
let starting_style = resolver.resolve_starting_style().style; if starting_style.style().clone_display().is_none() { return None;
}
Some(starting_style.0)
}
/// Handle CSS Transitions. Returns None if we don't need to update transitions. And it returns /// the before-change style per CSS Transitions spec. /// /// Note: The before-change style could be the computed values of all properties on the element /// as of the previous style change event, or the starting style if we don't have the valid /// before-change style there. #[cfg(feature = "gecko")] fn process_transitions(
&self,
context: &mut StyleContext<Self>,
old_values: Option<&Arc<ComputedValues>>,
new_styles: &mut ResolvedElementStyles,
) -> Option<Arc<ComputedValues>> { let starting_values = self.maybe_resolve_starting_style(context, old_values, new_styles); let before_change_or_starting = if starting_values.is_some() {
starting_values.as_ref()
} else {
old_values
}; let new_values = new_styles.primary_style_mut();
// In order to avoid creating a SequentialTask for transitions which // may not be updated, we check it per property to make sure Gecko // side will really update transition. if !self.needs_transitions_update(
before_change_or_starting.unwrap(),
after_change_style.as_ref().unwrap_or(&new_values),
) { return None;
}
// If we have modified animation or transitions, we recascade style for this node. if style_changed { letmut 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(),
};
// 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_deref(),
new_values,
pseudo_element,
);
let might_need_transitions_update = self.might_need_transitions_update(
context,
old_values.as_deref(),
new_values,
pseudo_element,
);
let key = AnimationSetKey::new(self.as_node().opaque(), pseudo_element); let shared_context = context.shared; letmut animation_set = shared_context
.animations
.sets
.write()
.remove(&key)
.unwrap_or_default();
// 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 { letmut resolver = StyleResolverForElement::new(
*self,
context,
RuleInclusion::All,
PseudoElementResolution::IfApplicable,
);
// We clear away any finished transitions, but retain animations, because they // might still be used for proper calculation of `animation-fill-mode`. This // should change the computed values in the style, so we don't need to mark // this set as dirty.
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. let changed_animations = animation_set.dirty; if !animation_set.is_empty() {
animation_set.dirty = false;
shared_context
.animations
.sets
.write()
.insert(key, animation_set);
}
let difference = self.compute_style_difference(old_values, new_values, pseudo);
*damage |= difference.damage;
debug!(" > style difference: {:?}", difference);
// We need to cascade the children in order to ensure the correct // propagation of inherited computed value flags. if old_values.flags.maybe_inherited() != new_values.flags.maybe_inherited() {
debug!( " > flags changed: {:?} != {:?}",
old_values.flags, new_values.flags
); return ChildRestyleRequirement::MustCascadeChildren;
}
if old_values.effective_zoom != new_values.effective_zoom { // Zoom changes need to get propagated to children.
debug!( " > zoom changed: {:?} != {:?}",
old_values.effective_zoom, new_values.effective_zoom
); return ChildRestyleRequirement::MustCascadeChildren;
}
match difference.change {
StyleChange::Unchanged => return ChildRestyleRequirement::CanSkipCascade,
StyleChange::Changed { reset_only } => { // If inherited properties changed, the best we can do is // cascade the children. if !reset_only { return ChildRestyleRequirement::MustCascadeChildren;
}
},
}
let old_display = old_values.clone_display(); let new_display = new_values.clone_display();
if old_display != new_display { // If we used to be a display: none element, and no longer are, our // children need to be restyled because they're unstyled. if old_display == Display::None { return ChildRestyleRequirement::MustCascadeChildren;
} // Blockification of children may depend on our display value, // so we need to actually do the recascade. We could potentially // do better, but it doesn't seem worth it. if old_display.is_item_container() != new_display.is_item_container() { return ChildRestyleRequirement::MustCascadeChildren;
} // We may also need to blockify and un-blockify descendants if our // display goes from / to display: contents, since the "layout // parent style" changes. if old_display.is_contents() || new_display.is_contents() { return ChildRestyleRequirement::MustCascadeChildren;
} // Line break suppression may also be affected if the display // type changes from ruby to non-ruby. #[cfg(feature = "gecko")]
{ if old_display.is_ruby_type() != new_display.is_ruby_type() { return ChildRestyleRequirement::MustCascadeChildren;
}
}
}
// Children with justify-items: auto may depend on our // justify-items property value. // // Similarly, we could potentially do better, but this really // seems not common enough to care about. #[cfg(feature = "gecko")]
{ usecrate::values::specified::align::AlignFlags;
let old_justify_items = old_values.get_position().clone_justify_items(); let new_justify_items = new_values.get_position().clone_justify_items();
let was_legacy_justify_items =
old_justify_items.computed.0.contains(AlignFlags::LEGACY);
let is_legacy_justify_items = new_justify_items.computed.0.contains(AlignFlags::LEGACY);
if is_legacy_justify_items != was_legacy_justify_items { return ChildRestyleRequirement::MustCascadeChildren;
}
#[cfg(feature = "servo")]
{ // We may need to set or propagate the CAN_BE_FRAGMENTED bit // on our children. if old_values.is_multicol() != new_values.is_multicol() { return ChildRestyleRequirement::MustCascadeChildren;
}
}
// We could prove that, if our children don't inherit reset // properties, we can stop the cascade.
ChildRestyleRequirement::MustCascadeChildrenIfInheritResetStyle
}
}
impl<E: TElement> PrivateMatchMethods for E {}
/// The public API that elements expose for selector matching. pubtrait MatchMethods: TElement { /// Returns the closest parent element that doesn't have a display: contents /// style (and thus generates a box). /// /// This is needed to correctly handle blockification of flex and grid /// items. /// /// Returns itself if the element has no parent. In practice this doesn't /// happen because the root element is blockified per spec, but it could /// happen if we decide to not blockify for roots of disconnected subtrees, /// which is a kind of dubious behavior. fn layout_parent(&self) -> Self { letmut current = self.clone(); loop {
current = match current.traversal_parent() {
Some(el) => el,
None => return current,
};
let is_display_contents = current
.borrow_data()
.unwrap()
.styles
.primary()
.is_display_contents();
if !is_display_contents { return current;
}
}
}
/// Updates the styles with the new ones, diffs them, and stores the restyle /// damage. fn finish_restyle(
&self,
context: &mut StyleContext<Self>,
data: &mut ElementData, mut new_styles: ResolvedElementStyles,
important_rules_changed: bool,
) -> ChildRestyleRequirement { use std::cmp;
// First of all, update the styles. let old_styles = data.set_styles(new_styles);
let new_primary_style = data.styles.primary.as_ref().unwrap();
letmut restyle_requirement = ChildRestyleRequirement::CanSkipCascade; let is_root = new_primary_style
.flags
.contains(ComputedValueFlags::IS_ROOT_ELEMENT_STYLE); let is_container = !new_primary_style
.get_box()
.clone_container_type()
.is_normal(); if is_root || is_container { let device = context.shared.stylist.device(); let old_style = old_styles.primary.as_ref(); let new_font_size = new_primary_style.get_font().clone_font_size(); let old_font_size = old_style.map(|s| s.get_font().clone_font_size());
if old_font_size != Some(new_font_size) { if is_root {
debug_assert!(self.owner_doc_matches_for_testing(device)); let size = new_font_size.computed_size();
device.set_root_font_size(new_primary_style.effective_zoom.unzoom(size.px())); if device.used_root_font_size() { // If the root font-size changed since last time, and something // in the document did use rem units, ensure we recascade the // entire tree.
restyle_requirement = ChildRestyleRequirement::MustCascadeDescendants;
}
}
if is_container && old_font_size.is_some() { // TODO(emilio): Maybe only do this if we were matched // against relative font sizes? // Also, maybe we should do this as well for font-family / // etc changes (for ex/ch/ic units to work correctly)? We // should probably do the optimization mentioned above if // so.
restyle_requirement = ChildRestyleRequirement::MustMatchDescendants;
}
}
// For line-height, we want the fully resolved value, as `normal` also depends on other // font properties. let new_line_height = device
.calc_line_height(
&new_primary_style.get_font(),
new_primary_style.writing_mode,
None,
)
.0; let old_line_height = old_style.map(|s| {
device
.calc_line_height(&s.get_font(), s.writing_mode, None)
.0
});
if old_line_height != Some(new_line_height) { if is_root {
debug_assert!(self.owner_doc_matches_for_testing(device));
device.set_root_line_height(
new_primary_style
.effective_zoom
.unzoom(new_line_height.px()),
); if device.used_root_line_height() {
restyle_requirement = std::cmp::max(
restyle_requirement,
ChildRestyleRequirement::MustCascadeDescendants,
);
}
}
if context.shared.stylist.quirks_mode() == QuirksMode::Quirks { ifself.is_html_document_body_element() { // NOTE(emilio): We _could_ handle dynamic changes to it if it // changes and before we reach our children the cascade stops, // but we don't track right now whether we use the document body // color, and nobody else handles that properly anyway. let device = context.shared.stylist.device();
// Needed for the "inherit from body" quirk. let text_color = new_primary_style.get_inherited_text().clone_color();
device.set_body_text_color(text_color);
}
}
// Don't accumulate damage if we're in the final animation traversal. if context
.shared
.traversal_flags
.contains(TraversalFlags::FinalAnimationTraversal)
{ return ChildRestyleRequirement::MustCascadeChildren;
}
// Also, don't do anything if there was no style. let old_primary_style = match old_styles.primary {
Some(s) => s,
None => return ChildRestyleRequirement::MustCascadeChildren,
};
let old_container_type = old_primary_style.clone_container_type(); let new_container_type = new_primary_style.clone_container_type(); if old_container_type != new_container_type && !new_container_type.is_size_container_type()
{ // Stopped being a size container. Re-evaluate container queries and units on all our descendants. // Changes into and between different size containment is handled in `UpdateContainerQueryStyles`.
restyle_requirement = ChildRestyleRequirement::MustMatchDescendants;
} elseif old_container_type.is_size_container_type() &&
!old_primary_style.is_display_contents() &&
new_primary_style.is_display_contents()
{ // Also re-evaluate when a container gets 'display: contents', since size queries will now evaluate to unknown. // Other displays like 'inline' will keep generating a box, so they are handled in `UpdateContainerQueryStyles`.
restyle_requirement = ChildRestyleRequirement::MustMatchDescendants;
}
if data.styles.pseudos.is_empty() && old_styles.pseudos.is_empty() { // This is the common case; no need to examine pseudos here. return restyle_requirement;
}
let pseudo_styles = old_styles
.pseudos
.as_array()
.iter()
.zip(data.styles.pseudos.as_array().iter());
for (i, (old, new)) in pseudo_styles.enumerate() { match (old, new) {
(&Some(ref old), &Some(ref new)) => { self.accumulate_damage_for(
context.shared,
&mut data.damage,
old,
new,
Some(&PseudoElement::from_eager_index(i)),
);
},
(&None, &None) => {},
_ => { // It's possible that we're switching from not having // ::before/::after at all to having styles for them but not // actually having a useful pseudo-element. Check for that // case. let pseudo = PseudoElement::from_eager_index(i); let new_pseudo_should_exist =
new.as_ref().map_or(false, |s| pseudo.should_exist(s)); let old_pseudo_should_exist =
old.as_ref().map_or(false, |s| pseudo.should_exist(s)); if new_pseudo_should_exist != old_pseudo_should_exist {
data.damage |= RestyleDamage::reconstruct(); return restyle_requirement;
}
},
}
}
restyle_requirement
}
/// Updates the rule nodes without re-running selector matching, using just /// the rule tree. /// /// Returns true if an !important rule was replaced. fn replace_rules(
&self,
replacements: RestyleHint,
context: &mut StyleContext<Self>,
cascade_inputs: &mut ElementCascadeInputs,
) -> bool { letmut result = false;
result |= self.replace_rules_internal(
replacements,
context,
CascadeVisitedMode::Unvisited,
cascade_inputs,
);
result |= self.replace_rules_internal(
replacements,
context,
CascadeVisitedMode::Visited,
cascade_inputs,
);
result
}
/// Given the old and new style of this element, and whether it's a /// pseudo-element, compute the restyle damage used to determine which /// kind of layout or painting operations we'll need. fn compute_style_difference(
&self,
old_values: &ComputedValues,
new_values: &ComputedValues,
pseudo: Option<&PseudoElement>,
) -> StyleDifference {
debug_assert!(pseudo.map_or(true, |p| p.is_eager()));
RestyleDamage::compute_style_difference(old_values, new_values)
}
}
impl<E: TElement> MatchMethods for E {}
Messung V0.5 in Prozent
¤ Dauer der Verarbeitung: 0.25 Sekunden
(vorverarbeitet am 2026-06-18)
¤
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.