commit c64696917cdda600e5b3ad04dfbb9c2dd6fa24b6
parent 25f525fca85324e9c9b29e59de16a6ce81df9118
Author: Diego Escalante <descalante@mozilla.com>
Date: Tue, 14 Oct 2025 12:36:14 +0000
Bug 1986741 - Add special invalidation handling for :scope selectors inside of :not(). r=dshin,firefox-style-system-reviewers
Differential Revision: https://phabricator.services.mozilla.com/D267685
Diffstat:
2 files changed, 186 insertions(+), 55 deletions(-)
diff --git a/servo/components/style/invalidation/element/invalidator.rs b/servo/components/style/invalidation/element/invalidator.rs
@@ -13,8 +13,8 @@ use crate::invalidation::element::invalidation_map::{
};
use selectors::matching::matches_compound_selector_from;
use selectors::matching::{CompoundSelectorMatchingResult, MatchingContext};
-use selectors::parser::{Combinator, Component};
-use selectors::OpaqueElement;
+use selectors::parser::{Combinator, Component, Selector, SelectorVisitor};
+use selectors::{OpaqueElement, SelectorImpl};
use smallvec::{smallvec, SmallVec};
use std::fmt;
use std::fmt::Write;
@@ -284,6 +284,12 @@ pub struct Invalidation<'a> {
/// this one if the generated invalidation is effective for all the siblings
/// or descendants after us.
matched_by_any_previous: bool,
+ /// Whether this incalidation should always be pushed to next invalidations.
+ ///
+ /// This is useful for overriding invalidations we would otherwise skip.
+ /// e.g @scope(.a){:not(:scope)} where we would need the :not(:scope)
+ /// invalidation to traverse down for all children of the scope root
+ always_effective_for_next: bool,
}
impl<'a> Invalidation<'a> {
@@ -308,6 +314,7 @@ impl<'a> Invalidation<'a> {
// + 1 to go past the combinator.
offset: dependency.selector.len() + 1 - dependency.selector_offset,
matched_by_any_previous: false,
+ always_effective_for_next: false,
}
}
@@ -335,13 +342,24 @@ impl<'a> Invalidation<'a> {
scope,
offset: dependency.selector.len() - compound_offset,
matched_by_any_previous: false,
+ always_effective_for_next: true,
}
}
+ /// Return the combinator to the right of the currently invalidating compound
+ /// Useful for determining whether this invalidation should be pushed to
+ /// sibling or descendant invalidations.
+ pub fn combinator_to_right(&self) -> Combinator {
+ debug_assert_ne!(self.dependency.selector_offset, 0);
+ self.dependency
+ .selector
+ .combinator_at_match_order(self.dependency.selector.len() - self.offset)
+ }
+
/// Whether this invalidation is effective for the next sibling or
/// descendant after us.
fn effective_for_next(&self) -> bool {
- if self.offset == 0 {
+ if self.offset == 0 || self.always_effective_for_next {
return true;
}
@@ -384,6 +402,126 @@ impl<'a> Invalidation<'a> {
}
}
+
+/// A struct that visits a selector and determines if there is a `:scope`
+/// component nested withing a negation. eg. :not(:scope)
+struct NegationScopeVisitor {
+ /// Have we found a negation list yet
+ in_negation: bool,
+ /// Have we found a :scope inside a negation yet
+ found_scope_in_negation: bool,
+}
+
+impl NegationScopeVisitor {
+ /// Create a new NegationScopeVisitor
+ fn new() -> Self {
+ Self {
+ in_negation: false,
+ found_scope_in_negation: false,
+ }
+ }
+
+ fn traverse_selector(
+ mut self,
+ selector: &Selector<<NegationScopeVisitor as SelectorVisitor>::Impl>,
+ ) -> bool {
+ selector.visit(&mut self);
+ self.found_scope_in_negation
+ }
+
+ /// Traverse all the next dependencies in an outer dependency until we reach
+ /// 1. :not(* :scope *)
+ /// 2. a scope or relative dependency
+ /// 3. the end of the chain of dependencies
+ /// Return whether or not we encountered :not(* :scope *)
+ fn traverse_dependency(mut self, dependency: &Dependency) -> bool {
+ if dependency.next.is_none()
+ || !matches!(
+ dependency.invalidation_kind(),
+ DependencyInvalidationKind::Normal(..)
+ )
+ {
+ return dependency.selector.visit(&mut self);
+ }
+
+ let nested_visitor = Self {
+ in_negation: self.in_negation,
+ found_scope_in_negation: false,
+ };
+ dependency.selector.visit(&mut self);
+ // Has to be normal dependency and next.is_some()
+ nested_visitor.traverse_dependency(&dependency.next.as_ref().unwrap().slice()[0])
+ }
+}
+
+impl SelectorVisitor for NegationScopeVisitor {
+ type Impl = crate::selector_parser::SelectorImpl;
+
+ fn visit_attribute_selector(
+ &mut self,
+ _namespace: &selectors::attr::NamespaceConstraint<
+ &<Self::Impl as SelectorImpl>::NamespaceUrl,
+ >,
+ _local_name: &<Self::Impl as SelectorImpl>::LocalName,
+ _local_name_lower: &<Self::Impl as SelectorImpl>::LocalName,
+ ) -> bool {
+ true
+ }
+
+ fn visit_simple_selector(&mut self, component: &Component<Self::Impl>) -> bool {
+ if self.in_negation {
+ match component {
+ Component::Scope => {
+ self.found_scope_in_negation = true;
+ },
+ _ => {},
+ }
+ }
+ true
+ }
+
+ fn visit_relative_selector_list(
+ &mut self,
+ _list: &[selectors::parser::RelativeSelector<Self::Impl>],
+ ) -> bool {
+ true
+ }
+
+ fn visit_selector_list(
+ &mut self,
+ list_kind: selectors::visitor::SelectorListKind,
+ list: &[selectors::parser::Selector<Self::Impl>],
+ ) -> bool {
+ for nested in list {
+ let nested_visitor = Self {
+ in_negation: list_kind.in_negation(),
+ found_scope_in_negation: false,
+ };
+
+ self.found_scope_in_negation |= nested_visitor.traverse_selector(nested);
+ }
+ true
+ }
+
+ fn visit_complex_selector(&mut self, _combinator_to_right: Option<Combinator>) -> bool {
+ true
+ }
+}
+
+/// Determines if we can find a selector in the form of :not(:scope)
+/// anywhere down the chain of dependencies.
+pub fn any_next_has_scope_in_negation(dependency: &Dependency) -> bool {
+ let next = match dependency.next.as_ref() {
+ None => return false,
+ Some(l) => l,
+ };
+
+ next.slice().iter().any(|dep| {
+ let visitor = NegationScopeVisitor::new();
+ visitor.traverse_dependency(dep)
+ })
+}
+
impl<'a> fmt::Debug for Invalidation<'a> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
use cssparser::ToCss;
@@ -942,20 +1080,26 @@ where
let mut next_dependencies: SmallVec<[&Dependency; 1]> = SmallVec::new();
while let Some(dependency) = to_process.pop() {
- if dependency.invalidation_kind()
- == DependencyInvalidationKind::Scope(ScopeDependencyInvalidationKind::ScopeEnd)
+ if let DependencyInvalidationKind::Scope(scope_kind) =
+ dependency.invalidation_kind()
{
- let invalidations = note_scope_dependency_force_at_subject(
- dependency,
- invalidation.host,
- invalidation.scope,
- false,
- );
- for invalidation in invalidations{
- descendant_invalidations.dom_descendants.push(invalidation);
+ let force_add = any_next_has_scope_in_negation(dependency);
+ if scope_kind == ScopeDependencyInvalidationKind::ScopeEnd || force_add {
+ let invalidations = note_scope_dependency_force_at_subject(
+ dependency,
+ invalidation.host,
+ invalidation.scope,
+ force_add,
+ );
+
+ for invalidation in invalidations {
+ descendant_invalidations.dom_descendants.push(invalidation);
+ }
+
+ continue;
}
- continue;
}
+
match dependency.next {
None => {
result.invalidated_self = true;
@@ -1066,10 +1210,9 @@ where
matched: false,
}
},
- CompoundSelectorMatchingResult::FullyMatched => self.handle_fully_matched(
- invalidation,
- descendant_invalidations,
- ),
+ CompoundSelectorMatchingResult::FullyMatched => {
+ self.handle_fully_matched(invalidation, descendant_invalidations)
+ },
CompoundSelectorMatchingResult::Matched {
next_combinator_offset,
} => (
@@ -1083,6 +1226,7 @@ where
scope: invalidation.scope,
offset: next_combinator_offset + 1,
matched_by_any_previous: false,
+ always_effective_for_next: false,
}],
),
};
@@ -1243,8 +1387,7 @@ pub fn note_scope_dependency_force_at_subject<'selectors>(
scope: Option<OpaqueElement>,
traversed_non_subject: bool,
) -> Vec<Invalidation<'selectors>> {
- let mut invalidations: Vec<Invalidation> =
- Vec::new();
+ let mut invalidations: Vec<Invalidation> = Vec::new();
if let Some(next) = dependency.next.as_ref() {
for dep in next.slice() {
if dep.selector_offset == 0 && !traversed_non_subject {
@@ -1252,18 +1395,16 @@ pub fn note_scope_dependency_force_at_subject<'selectors>(
}
if dep.next.is_some() {
- invalidations
- .extend(
- note_scope_dependency_force_at_subject(
- dep,
- current_host,
- scope,
- true,
- )
- );
+ invalidations.extend(note_scope_dependency_force_at_subject(
+ dep,
+ current_host,
+ scope,
+ // Force add from now on because we
+ // passed through a non-subject compound
+ true,
+ ));
} else {
- let invalidation =
- Invalidation::new_subject_invalidation(dep, current_host, scope);
+ let invalidation = Invalidation::new_subject_invalidation(dep, current_host, scope);
invalidations.push(invalidation);
}
diff --git a/servo/components/style/invalidation/element/state_and_attributes.rs b/servo/components/style/invalidation/element/state_and_attributes.rs
@@ -11,8 +11,7 @@ use crate::dom::{TElement, TNode};
use crate::invalidation::element::element_wrapper::{ElementSnapshot, ElementWrapper};
use crate::invalidation::element::invalidation_map::*;
use crate::invalidation::element::invalidator::{
- note_scope_dependency_force_at_subject, DescendantInvalidationLists,
- InvalidationVector, SiblingTraversalMap,
+ any_next_has_scope_in_negation, note_scope_dependency_force_at_subject, DescendantInvalidationLists, InvalidationVector, SiblingTraversalMap
};
use crate::invalidation::element::invalidator::{Invalidation, InvalidationProcessor};
use crate::invalidation::element::restyle_hints::RestyleHint;
@@ -592,15 +591,18 @@ where
if let DependencyInvalidationKind::Scope(scope_kind) = invalidation_kind {
if dependency.selector_offset == 0 {
- if scope_kind == ScopeDependencyInvalidationKind::ScopeEnd {
+ let force_add = any_next_has_scope_in_negation(dependency);
+ if scope_kind == ScopeDependencyInvalidationKind::ScopeEnd || force_add {
let invalidations = note_scope_dependency_force_at_subject(
dependency,
self.matching_context.current_host.clone(),
self.matching_context.scope_element,
- false,
+ force_add,
);
for invalidation in invalidations {
- self.descendant_invalidations.dom_descendants.push(invalidation);
+ self.descendant_invalidations
+ .dom_descendants
+ .push(invalidation);
}
self.invalidates_self = true;
} else if let Some(ref next) = dependency.next {
@@ -608,25 +610,8 @@ where
self.scan_dependency(dep, true);
}
}
- } else {
- let invalidation = Invalidation::new(
- &dependency,
- self.matching_context.current_host.clone(),
- None,
- );
-
- let combinator = dependency
- .selector
- .combinator_at_match_order(dependency.selector_offset - 1);
- if combinator.is_sibling() {
- self.sibling_invalidations.push(invalidation);
- } else {
- self.descendant_invalidations
- .dom_descendants
- .push(invalidation);
- }
+ return;
}
- return;
}
debug_assert_ne!(dependency.selector_offset, 0);
@@ -678,7 +663,12 @@ pub(crate) fn push_invalidation<'a>(
DependencyInvalidationKind::FullSelector => unreachable!(),
DependencyInvalidationKind::Relative(_) => unreachable!(),
DependencyInvalidationKind::Scope(_) => {
- descendant_invalidations.dom_descendants.push(invalidation);
+ let combinator = invalidation.combinator_to_right();
+ if combinator.is_sibling() {
+ sibling_invalidations.push(invalidation);
+ } else {
+ descendant_invalidations.dom_descendants.push(invalidation);
+ }
true
},
DependencyInvalidationKind::Normal(kind) => match kind {