/* 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 properties have changed.
reset_only: bool, /// Whether custom properties have changed.
custom_properties_changed: bool,
},
}
/// 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,
);
} 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());
#[inline] fn requires_animation_update_for_scroll_self(
old: &ComputedValues,
new: &ComputedValues,
) -> bool { // Need to specifically take care of `animation-timeline: scroll(self)` - unlike other values, it can become inactive. // When we switch in and out of being scrollable, we should make sure to perform the animation update. // Specifying scroll in any axis makes the other axis scrollable [1], so we need to update on either axis changing. // This does not apply to `scroll(root)`, since the viewport scroller is always available, or `scroll(nearest)`, // which will go up to root. // [1]: https://drafts.csswg.org/css-overflow/#propdef-overflow let scrollable_changed = old.clone_overflow_x().is_scrollable()
!= new.clone_overflow_x().is_scrollable()
|| old.clone_overflow_y().is_scrollable() != new.clone_overflow_y().is_scrollable(); if !scrollable_changed { returnfalse;
}
new.get_ui().animation_timeline_iter().any(|timeline| { let scroll_function = match timeline {
GenericAnimationTimeline::Scroll(ref sf) => sf,
_ => returnfalse,
}; if scroll_function.scroller != Scroller::SelfElement { returnfalse;
} true
})
}
/// If there is no transition rule in the ComputedValues, it returns None. fn after_change_style(
&self,
context: &mut StyleContext<Self>,
primary_style: &Arc<ComputedValues>,
) -> Option<Arc<ComputedValues>> { // Actually `PseudoElementResolution` doesn't really matter.
StyleResolverForElement::new(
*self,
context,
RuleInclusion::All,
PseudoElementResolution::IfApplicable,
)
.after_change_style(primary_style)
}
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;
}
#[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: // If there is no transitions specified we don't have to resolve starting style. let new_primary = new_styles.primary_style(); if !new_primary.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_primary.is_display_property_changed_from_none(old_values.map(|s| &**s))
{ return None;
}
let starting_style = resolver.resolve_starting_style(new_primary)?; 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 = starting_values.as_ref().or(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;
}
// 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,
);
// 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);
letmut children_hint = RestyleHint::empty(); if old_values.flags.maybe_inherited() != new_values.flags.maybe_inherited() { // Even if the styles are otherwise equal, we need to cascade the children in order to // ensure the correct propagation of inherited computed value flags.
debug!( " > flags changed: {:?} != {:?}",
old_values.flags, new_values.flags
);
children_hint |= RestyleHint::RECASCADE_SELF;
} elseif old_values.effective_zoom != new_values.effective_zoom { // Similarly, even if styles are equal, we need to propagate zoom changes.
debug!( " > zoom changed: {:?} != {:?}",
old_values.effective_zoom, new_values.effective_zoom
);
children_hint |= RestyleHint::RECASCADE_SELF;
}
let new_container_name = new_values.clone_container_name(); if new_container_name != old_values.clone_container_name() { // If we're becoming or stopped to become a named container, we need to potentially // restyle children.
children_hint |= RestyleHint::RESTYLE_IF_AFFECTED_BY_NAMED_STYLE_CONTAINER;
} elseif custom_properties_changed { // Custom property changes affect style queries. How specifically depends on whether // we're a named container (more expensive, need to check the subtree) or not.
children_hint |= if !new_container_name.is_none() {
RestyleHint::RESTYLE_IF_AFFECTED_BY_NAMED_STYLE_CONTAINER
} else {
RestyleHint::RESTYLE_IF_AFFECTED_BY_STYLE_QUERIES
};
}
if reset_only { // If only reset properties changed, we _might_ need to unconditionally restyle, but // most likely we can get away with stopping the cascade at the next level, if our // children don't inherit reset properties.
children_hint |= if need_to_unconditionally_recascade_for_reset_change(old_values, new_values) {
RestyleHint::RECASCADE_SELF
} else {
RestyleHint::RECASCADE_SELF_IF_INHERIT_RESET_STYLE
};
} else { // If inherited properties changed, we need to cascade our children.
children_hint |= RestyleHint::RECASCADE_SELF;
}
children_hint
}
}
/// Whether we need to recascade children for a change in non-inherited properties. fn need_to_unconditionally_recascade_for_reset_change(
old_values: &ComputedValues,
new_values: &ComputedValues,
) -> bool { 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 { returntrue;
} // 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() { returntrue;
} // 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() { returntrue;
} // 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() { returntrue;
}
}
// 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.contains(AlignFlags::LEGACY);
let is_legacy_justify_items = new_justify_items.computed.contains(AlignFlags::LEGACY);
if is_legacy_justify_items != was_legacy_justify_items { returntrue;
}
if was_legacy_justify_items && old_justify_items.computed != new_justify_items.computed { returntrue;
}
}
false
}
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;
}
}
}
/// Rather than comparing the resolved line-height, which can be expensive to compute /// as it involves locking and font metrics access, we consider that line-height may have /// changed if the font-size or line-height property itself has changed, or if the value /// is 'normal' and one of the properties that affects font selection (family, style, /// weight, stretch) has changed. fn line_height_likely_changed(
old_style: Option<&Arc<ComputedValues>>,
new_style: &Arc<ComputedValues>,
) -> bool { let old_line_height = old_style.map(|s| s.get_font().clone_line_height()); let new_line_height = new_style.get_font().clone_line_height(); // Return true if the old value was missing, or if the computed values are different. if old_line_height.is_none_or(|lh| lh != new_line_height) { returntrue;
} // If the value isn't `normal`, it doesn't depend on font metrics: return false. if !new_line_height.is_normal() { returnfalse;
} // Check the font-selection properties, which could affect metrics used to resolve // `normal` line-height.
macro_rules! font_property_changed {
($getter: ident) => {
old_style
.map(|s| s.get_font().$getter())
.is_none_or(|v| v != new_style.get_font().$getter())
};
}
font_property_changed!(clone_font_family)
|| font_property_changed!(clone_font_style)
|| font_property_changed!(clone_font_weight)
|| font_property_changed!(clone_font_stretch)
}
/// 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,
) -> RestyleHint { self.process_animations(
context,
&mut data.styles,
&mut new_styles,
important_rules_changed,
);
// 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 child_restyle_hint = RestyleHint::empty(); let is_root = new_primary_style
.flags
.contains(ComputedValueFlags::IS_ROOT_ELEMENT_STYLE);
let device = context.shared.stylist.device(); let new_font_size = new_primary_style.get_font().clone_font_size(); let new_container_type = new_primary_style.clone_container_type();
let old_style = old_styles.primary.as_ref(); let old_font_size = old_style.map(|s| s.get_font().clone_font_size()); let font_size_changed = old_font_size.is_none_or(|fs| fs != new_font_size);
let line_height_likely_changed =
font_size_changed || Self::line_height_likely_changed(old_style, new_primary_style);
// Update root font-relative units. If any of these unit values changed // since last time, ensure that we recascade the entire tree. if is_root {
debug_assert!(self.owner_doc_matches_for_testing(device));
device.set_root_style(new_primary_style);
// Update root font size for rem units if font_size_changed { let size = new_font_size.computed_size();
device.set_root_font_size(new_primary_style.effective_zoom.unzoom(size.px()));
}
// Update root line height for rlh units if line_height_likely_changed { let new_line_height = device
.calc_line_height(
&new_primary_style.get_font(),
new_primary_style.writing_mode,
None,
)
.0;
device.set_root_line_height(
new_primary_style
.effective_zoom
.unzoom(new_line_height.px()),
);
}
// Update root font metrics for rcap, rch, rex, ric units. Since querying // font metrics can be an expensive call, they are only updated if these // units are used in the document. if device.used_root_font_metrics() && device.update_root_font_metrics() {
child_restyle_hint |= RestyleHint::RESTYLE_IF_AFFECTED_BY_ANCESTOR_FONT;
}
}
if font_size_changed || line_height_likely_changed {
child_restyle_hint |= RestyleHint::RESTYLE_IF_AFFECTED_BY_ANCESTOR_FONT;
}
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 RestyleHint::RECASCADE_SELF;
}
// Also, don't do anything if there was no style. let old_primary_style = match old_styles.primary {
Some(s) => s,
None => return RestyleHint::RECASCADE_SELF,
};
let old_container_type = old_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`.
child_restyle_hint |= RestyleHint::restyle_subtree();
} 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`.
child_restyle_hint |= RestyleHint::restyle_subtree();
}
if data.styles.pseudos.is_empty() && old_styles.pseudos.is_empty() { // This is the common case; no need to examine pseudos here. return child_restyle_hint;
}
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 child_restyle_hint;
}
},
}
}
child_restyle_hint
}
/// 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())); #[cfg(feature = "gecko")]
{
RestyleDamage::compute_style_difference(old_values, new_values)
} #[cfg(feature = "servo")]
{
RestyleDamage::compute_style_difference::<Self>(old_values, new_values)
}
}
}
impl<E: TElement> MatchMethods for E {}
Messung V0.5 in Prozent
¤ Dauer der Verarbeitung: 0.29 Sekunden
(vorverarbeitet am 2026-10-01)
¤
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.