diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
commit | 26a029d407be480d791972afb5975cf62c9360a6 (patch) | |
tree | f435a8308119effd964b339f76abb83a57c29483 /servo/components/style/invalidation | |
parent | Initial commit. (diff) | |
download | firefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz firefox-26a029d407be480d791972afb5975cf62c9360a6.zip |
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'servo/components/style/invalidation')
12 files changed, 5928 insertions, 0 deletions
diff --git a/servo/components/style/invalidation/element/document_state.rs b/servo/components/style/invalidation/element/document_state.rs new file mode 100644 index 0000000000..0b846510d8 --- /dev/null +++ b/servo/components/style/invalidation/element/document_state.rs @@ -0,0 +1,154 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! An invalidation processor for style changes due to document state changes. + +use crate::dom::TElement; +use crate::invalidation::element::invalidation_map::Dependency; +use crate::invalidation::element::invalidator::{ + DescendantInvalidationLists, InvalidationVector, SiblingTraversalMap, +}; +use crate::invalidation::element::invalidator::{Invalidation, InvalidationProcessor}; +use crate::invalidation::element::state_and_attributes; +use crate::stylist::CascadeData; +use dom::DocumentState; +use selectors::matching::{ + MatchingContext, MatchingForInvalidation, MatchingMode, NeedsSelectorFlags, QuirksMode, + SelectorCaches, VisitedHandlingMode, +}; + +/// A struct holding the members necessary to invalidate document state +/// selectors. +#[derive(Debug)] +pub struct InvalidationMatchingData { + /// The document state that has changed, which makes it always match. + pub document_state: DocumentState, +} + +impl Default for InvalidationMatchingData { + #[inline(always)] + fn default() -> Self { + Self { + document_state: DocumentState::empty(), + } + } +} + +/// An invalidation processor for style changes due to state and attribute +/// changes. +pub struct DocumentStateInvalidationProcessor<'a, 'b, E: TElement, I> { + rules: I, + matching_context: MatchingContext<'a, E::Impl>, + traversal_map: SiblingTraversalMap<E>, + document_states_changed: DocumentState, + _marker: std::marker::PhantomData<&'b ()>, +} + +impl<'a, 'b, E: TElement, I> DocumentStateInvalidationProcessor<'a, 'b, E, I> { + /// Creates a new DocumentStateInvalidationProcessor. + #[inline] + pub fn new( + rules: I, + document_states_changed: DocumentState, + selector_caches: &'a mut SelectorCaches, + quirks_mode: QuirksMode, + ) -> Self { + let mut matching_context = MatchingContext::<'a, E::Impl>::new_for_visited( + MatchingMode::Normal, + None, + selector_caches, + VisitedHandlingMode::AllLinksVisitedAndUnvisited, + quirks_mode, + NeedsSelectorFlags::No, + MatchingForInvalidation::No, + ); + + matching_context.extra_data.invalidation_data.document_state = document_states_changed; + + Self { + rules, + document_states_changed, + matching_context, + traversal_map: SiblingTraversalMap::default(), + _marker: std::marker::PhantomData, + } + } +} + +impl<'a, 'b, E, I> InvalidationProcessor<'b, 'a, E> + for DocumentStateInvalidationProcessor<'a, 'b, E, I> +where + E: TElement, + I: Iterator<Item = &'b CascadeData>, +{ + fn check_outer_dependency(&mut self, _: &Dependency, _: E) -> bool { + debug_assert!( + false, + "how, we should only have parent-less dependencies here!" + ); + true + } + + fn collect_invalidations( + &mut self, + _element: E, + self_invalidations: &mut InvalidationVector<'b>, + _descendant_invalidations: &mut DescendantInvalidationLists<'b>, + _sibling_invalidations: &mut InvalidationVector<'b>, + ) -> bool { + for cascade_data in &mut self.rules { + let map = cascade_data.invalidation_map(); + for dependency in &map.document_state_selectors { + if !dependency.state.intersects(self.document_states_changed) { + continue; + } + + // We pass `None` as a scope, as document state selectors aren't + // affected by the current scope. + // + // FIXME(emilio): We should really pass the relevant host for + // self.rules, so that we invalidate correctly if the selector + // happens to have something like :host(:-moz-window-inactive) + // for example. + self_invalidations.push(Invalidation::new( + &dependency.dependency, + /* scope = */ None, + )); + } + } + + false + } + + fn matching_context(&mut self) -> &mut MatchingContext<'a, E::Impl> { + &mut self.matching_context + } + + fn sibling_traversal_map(&self) -> &SiblingTraversalMap<E> { + &self.traversal_map + } + + fn recursion_limit_exceeded(&mut self, _: E) { + unreachable!("We don't run document state invalidation with stack limits") + } + + fn should_process_descendants(&mut self, element: E) -> bool { + match element.borrow_data() { + Some(d) => state_and_attributes::should_process_descendants(&d), + None => false, + } + } + + fn invalidated_descendants(&mut self, element: E, child: E) { + state_and_attributes::invalidated_descendants(element, child) + } + + fn invalidated_self(&mut self, element: E) { + state_and_attributes::invalidated_self(element); + } + + fn invalidated_sibling(&mut self, sibling: E, of: E) { + state_and_attributes::invalidated_sibling(sibling, of); + } +} diff --git a/servo/components/style/invalidation/element/element_wrapper.rs b/servo/components/style/invalidation/element/element_wrapper.rs new file mode 100644 index 0000000000..e17afd7774 --- /dev/null +++ b/servo/components/style/invalidation/element/element_wrapper.rs @@ -0,0 +1,388 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! A wrapper over an element and a snapshot, that allows us to selector-match +//! against a past state of the element. + +use crate::dom::TElement; +use crate::selector_parser::{AttrValue, NonTSPseudoClass, PseudoElement, SelectorImpl}; +use crate::selector_parser::{Snapshot, SnapshotMap}; +use crate::values::AtomIdent; +use crate::{CaseSensitivityExt, LocalName, Namespace, WeakAtom}; +use dom::ElementState; +use selectors::attr::{AttrSelectorOperation, CaseSensitivity, NamespaceConstraint}; +use selectors::bloom::BloomFilter; +use selectors::matching::{ElementSelectorFlags, MatchingContext}; +use selectors::{Element, OpaqueElement}; +use std::cell::Cell; +use std::fmt; + +/// In order to compute restyle hints, we perform a selector match against a +/// list of partial selectors whose rightmost simple selector may be sensitive +/// to the thing being changed. We do this matching twice, once for the element +/// as it exists now and once for the element as it existed at the time of the +/// last restyle. If the results of the selector match differ, that means that +/// the given partial selector is sensitive to the change, and we compute a +/// restyle hint based on its combinator. +/// +/// In order to run selector matching against the old element state, we generate +/// a wrapper for the element which claims to have the old state. This is the +/// ElementWrapper logic below. +/// +/// Gecko does this differently for element states, and passes a mask called +/// mStateMask, which indicates the states that need to be ignored during +/// selector matching. This saves an ElementWrapper allocation and an additional +/// selector match call at the expense of additional complexity inside the +/// selector matching logic. This only works for boolean states though, so we +/// still need to take the ElementWrapper approach for attribute-dependent +/// style. So we do it the same both ways for now to reduce complexity, but it's +/// worth measuring the performance impact (if any) of the mStateMask approach. +pub trait ElementSnapshot: Sized { + /// The state of the snapshot, if any. + fn state(&self) -> Option<ElementState>; + + /// If this snapshot contains attribute information. + fn has_attrs(&self) -> bool; + + /// Gets the attribute information of the snapshot as a string. + /// + /// Only for debugging purposes. + fn debug_list_attributes(&self) -> String { + String::new() + } + + /// The ID attribute per this snapshot. Should only be called if + /// `has_attrs()` returns true. + fn id_attr(&self) -> Option<&WeakAtom>; + + /// Whether this snapshot contains the class `name`. Should only be called + /// if `has_attrs()` returns true. + fn has_class(&self, name: &AtomIdent, case_sensitivity: CaseSensitivity) -> bool; + + /// Whether this snapshot represents the part named `name`. Should only be + /// called if `has_attrs()` returns true. + fn is_part(&self, name: &AtomIdent) -> bool; + + /// See Element::imported_part. + fn imported_part(&self, name: &AtomIdent) -> Option<AtomIdent>; + + /// A callback that should be called for each class of the snapshot. Should + /// only be called if `has_attrs()` returns true. + fn each_class<F>(&self, _: F) + where + F: FnMut(&AtomIdent); + + /// The `xml:lang=""` or `lang=""` attribute value per this snapshot. + fn lang_attr(&self) -> Option<AttrValue>; +} + +/// A simple wrapper over an element and a snapshot, that allows us to +/// selector-match against a past state of the element. +#[derive(Clone)] +pub struct ElementWrapper<'a, E> +where + E: TElement, +{ + element: E, + cached_snapshot: Cell<Option<&'a Snapshot>>, + snapshot_map: &'a SnapshotMap, +} + +impl<'a, E> ElementWrapper<'a, E> +where + E: TElement, +{ + /// Trivially constructs an `ElementWrapper`. + pub fn new(el: E, snapshot_map: &'a SnapshotMap) -> Self { + ElementWrapper { + element: el, + cached_snapshot: Cell::new(None), + snapshot_map: snapshot_map, + } + } + + /// Gets the snapshot associated with this element, if any. + pub fn snapshot(&self) -> Option<&'a Snapshot> { + if !self.element.has_snapshot() { + return None; + } + + if let Some(s) = self.cached_snapshot.get() { + return Some(s); + } + + let snapshot = self.snapshot_map.get(&self.element); + debug_assert!(snapshot.is_some(), "has_snapshot lied!"); + + self.cached_snapshot.set(snapshot); + + snapshot + } + + /// Returns the states that have changed since the element was snapshotted. + pub fn state_changes(&self) -> ElementState { + let snapshot = match self.snapshot() { + Some(s) => s, + None => return ElementState::empty(), + }; + + match snapshot.state() { + Some(state) => state ^ self.element.state(), + None => ElementState::empty(), + } + } + + /// Returns the value of the `xml:lang=""` (or, if appropriate, `lang=""`) + /// attribute from this element's snapshot or the closest ancestor + /// element snapshot with the attribute specified. + fn get_lang(&self) -> Option<AttrValue> { + let mut current = self.clone(); + loop { + let lang = match self.snapshot() { + Some(snapshot) if snapshot.has_attrs() => snapshot.lang_attr(), + _ => current.element.lang_attr(), + }; + if lang.is_some() { + return lang; + } + current = current.parent_element()?; + } + } +} + +impl<'a, E> fmt::Debug for ElementWrapper<'a, E> +where + E: TElement, +{ + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + // Ignore other fields for now, can change later if needed. + self.element.fmt(f) + } +} + +impl<'a, E> Element for ElementWrapper<'a, E> +where + E: TElement, +{ + type Impl = SelectorImpl; + + fn match_non_ts_pseudo_class( + &self, + pseudo_class: &NonTSPseudoClass, + context: &mut MatchingContext<Self::Impl>, + ) -> bool { + // Some pseudo-classes need special handling to evaluate them against + // the snapshot. + match *pseudo_class { + // For :link and :visited, we don't actually want to test the + // element state directly. + // + // Instead, we use the `visited_handling` to determine if they + // match. + NonTSPseudoClass::Link => { + return self.is_link() && context.visited_handling().matches_unvisited(); + }, + NonTSPseudoClass::Visited => { + return self.is_link() && context.visited_handling().matches_visited(); + }, + + #[cfg(feature = "gecko")] + NonTSPseudoClass::MozTableBorderNonzero => { + if let Some(snapshot) = self.snapshot() { + if snapshot.has_other_pseudo_class_state() { + return snapshot.mIsTableBorderNonzero(); + } + } + }, + + #[cfg(feature = "gecko")] + NonTSPseudoClass::MozSelectListBox => { + if let Some(snapshot) = self.snapshot() { + if snapshot.has_other_pseudo_class_state() { + return snapshot.mIsSelectListBox(); + } + } + }, + + // :lang() needs to match using the closest ancestor xml:lang="" or + // lang="" attribtue from snapshots. + NonTSPseudoClass::Lang(ref lang_arg) => { + return self + .element + .match_element_lang(Some(self.get_lang()), lang_arg); + }, + + _ => {}, + } + + let flag = pseudo_class.state_flag(); + if flag.is_empty() { + return self + .element + .match_non_ts_pseudo_class(pseudo_class, context); + } + match self.snapshot().and_then(|s| s.state()) { + Some(snapshot_state) => snapshot_state.intersects(flag), + None => self + .element + .match_non_ts_pseudo_class(pseudo_class, context), + } + } + + fn apply_selector_flags(&self, _flags: ElementSelectorFlags) { + debug_assert!(false, "Shouldn't need selector flags for invalidation"); + } + + fn match_pseudo_element( + &self, + pseudo_element: &PseudoElement, + context: &mut MatchingContext<Self::Impl>, + ) -> bool { + self.element.match_pseudo_element(pseudo_element, context) + } + + fn is_link(&self) -> bool { + match self.snapshot().and_then(|s| s.state()) { + Some(state) => state.intersects(ElementState::VISITED_OR_UNVISITED), + None => self.element.is_link(), + } + } + + fn opaque(&self) -> OpaqueElement { + self.element.opaque() + } + + fn parent_element(&self) -> Option<Self> { + let parent = self.element.parent_element()?; + Some(Self::new(parent, self.snapshot_map)) + } + + fn parent_node_is_shadow_root(&self) -> bool { + self.element.parent_node_is_shadow_root() + } + + fn containing_shadow_host(&self) -> Option<Self> { + let host = self.element.containing_shadow_host()?; + Some(Self::new(host, self.snapshot_map)) + } + + fn prev_sibling_element(&self) -> Option<Self> { + let sibling = self.element.prev_sibling_element()?; + Some(Self::new(sibling, self.snapshot_map)) + } + + fn next_sibling_element(&self) -> Option<Self> { + let sibling = self.element.next_sibling_element()?; + Some(Self::new(sibling, self.snapshot_map)) + } + + fn first_element_child(&self) -> Option<Self> { + let child = self.element.first_element_child()?; + Some(Self::new(child, self.snapshot_map)) + } + + #[inline] + fn is_html_element_in_html_document(&self) -> bool { + self.element.is_html_element_in_html_document() + } + + #[inline] + fn is_html_slot_element(&self) -> bool { + self.element.is_html_slot_element() + } + + #[inline] + fn has_local_name( + &self, + local_name: &<Self::Impl as ::selectors::SelectorImpl>::BorrowedLocalName, + ) -> bool { + self.element.has_local_name(local_name) + } + + #[inline] + fn has_namespace( + &self, + ns: &<Self::Impl as ::selectors::SelectorImpl>::BorrowedNamespaceUrl, + ) -> bool { + self.element.has_namespace(ns) + } + + #[inline] + fn is_same_type(&self, other: &Self) -> bool { + self.element.is_same_type(&other.element) + } + + fn attr_matches( + &self, + ns: &NamespaceConstraint<&Namespace>, + local_name: &LocalName, + operation: &AttrSelectorOperation<&AttrValue>, + ) -> bool { + match self.snapshot() { + Some(snapshot) if snapshot.has_attrs() => { + snapshot.attr_matches(ns, local_name, operation) + }, + _ => self.element.attr_matches(ns, local_name, operation), + } + } + + fn has_id(&self, id: &AtomIdent, case_sensitivity: CaseSensitivity) -> bool { + match self.snapshot() { + Some(snapshot) if snapshot.has_attrs() => snapshot + .id_attr() + .map_or(false, |atom| case_sensitivity.eq_atom(&atom, id)), + _ => self.element.has_id(id, case_sensitivity), + } + } + + fn is_part(&self, name: &AtomIdent) -> bool { + match self.snapshot() { + Some(snapshot) if snapshot.has_attrs() => snapshot.is_part(name), + _ => self.element.is_part(name), + } + } + + fn imported_part(&self, name: &AtomIdent) -> Option<AtomIdent> { + match self.snapshot() { + Some(snapshot) if snapshot.has_attrs() => snapshot.imported_part(name), + _ => self.element.imported_part(name), + } + } + + fn has_class(&self, name: &AtomIdent, case_sensitivity: CaseSensitivity) -> bool { + match self.snapshot() { + Some(snapshot) if snapshot.has_attrs() => snapshot.has_class(name, case_sensitivity), + _ => self.element.has_class(name, case_sensitivity), + } + } + + fn is_empty(&self) -> bool { + self.element.is_empty() + } + + fn is_root(&self) -> bool { + self.element.is_root() + } + + fn is_pseudo_element(&self) -> bool { + self.element.is_pseudo_element() + } + + fn pseudo_element_originating_element(&self) -> Option<Self> { + self.element + .pseudo_element_originating_element() + .map(|e| ElementWrapper::new(e, self.snapshot_map)) + } + + fn assigned_slot(&self) -> Option<Self> { + self.element + .assigned_slot() + .map(|e| ElementWrapper::new(e, self.snapshot_map)) + } + + fn add_element_unique_hashes(&self, _filter: &mut BloomFilter) -> bool { + // Should not be relevant in the context of checking past elements in invalidation. + false + } +} diff --git a/servo/components/style/invalidation/element/invalidation_map.rs b/servo/components/style/invalidation/element/invalidation_map.rs new file mode 100644 index 0000000000..cb03862740 --- /dev/null +++ b/servo/components/style/invalidation/element/invalidation_map.rs @@ -0,0 +1,1425 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Code for invalidations due to state or attribute changes. + +use crate::context::QuirksMode; +use crate::selector_map::{ + MaybeCaseInsensitiveHashMap, PrecomputedHashMap, SelectorMap, SelectorMapEntry, +}; +use crate::selector_parser::{NonTSPseudoClass, SelectorImpl}; +use crate::AllocErr; +use crate::{Atom, LocalName, Namespace, ShrinkIfNeeded}; +use dom::{DocumentState, ElementState}; +use selectors::attr::NamespaceConstraint; +use selectors::parser::{ + Combinator, Component, RelativeSelector, RelativeSelectorCombinatorCount, + RelativeSelectorMatchHint, +}; +use selectors::parser::{Selector, SelectorIter}; +use selectors::visitor::{SelectorListKind, SelectorVisitor}; +use servo_arc::Arc; +use smallvec::SmallVec; + +/// Mapping between (partial) CompoundSelectors (and the combinator to their +/// right) and the states and attributes they depend on. +/// +/// In general, for all selectors in all applicable stylesheets of the form: +/// +/// |a _ b _ c _ d _ e| +/// +/// Where: +/// * |b| and |d| are simple selectors that depend on state (like :hover) or +/// attributes (like [attr...], .foo, or #foo). +/// * |a|, |c|, and |e| are arbitrary simple selectors that do not depend on +/// state or attributes. +/// +/// We generate a Dependency for both |a _ b:X _| and |a _ b:X _ c _ d:Y _|, +/// even though those selectors may not appear on their own in any stylesheet. +/// This allows us to quickly scan through the dependency sites of all style +/// rules and determine the maximum effect that a given state or attribute +/// change may have on the style of elements in the document. +#[derive(Clone, Debug, MallocSizeOf)] +pub struct Dependency { + /// The dependency selector. + #[ignore_malloc_size_of = "CssRules have primary refs, we measure there"] + pub selector: Selector<SelectorImpl>, + + /// The offset into the selector that we should match on. + pub selector_offset: usize, + + /// The parent dependency for an ancestor selector. For example, consider + /// the following: + /// + /// .foo .bar:where(.baz span) .qux + /// ^ ^ ^ + /// A B C + /// + /// We'd generate: + /// + /// * One dependency for .qux (offset: 0, parent: None) + /// * One dependency for .baz pointing to B with parent being a + /// dependency pointing to C. + /// * One dependency from .bar pointing to C (parent: None) + /// * One dependency from .foo pointing to A (parent: None) + /// + #[ignore_malloc_size_of = "Arc"] + pub parent: Option<Arc<Dependency>>, + + /// What kind of relative selector invalidation this generates. + /// None if this dependency is not within a relative selector. + relative_kind: Option<RelativeDependencyInvalidationKind>, +} + +impl SelectorMapEntry for Dependency { + fn selector(&self) -> SelectorIter<SelectorImpl> { + self.selector.iter_from(self.selector_offset) + } +} + +/// The kind of elements down the tree this dependency may affect. +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, MallocSizeOf)] +pub enum NormalDependencyInvalidationKind { + /// This dependency may affect the element that changed itself. + Element, + /// This dependency affects the style of the element itself, and also the + /// style of its descendants. + /// + /// TODO(emilio): Each time this feels more of a hack for eager pseudos... + ElementAndDescendants, + /// This dependency may affect descendants down the tree. + Descendants, + /// This dependency may affect siblings to the right of the element that + /// changed. + Siblings, + /// This dependency may affect slotted elements of the element that changed. + SlottedElements, + /// This dependency may affect parts of the element that changed. + Parts, +} + +/// The kind of elements up the tree this relative selector dependency may +/// affect. Because this travels upwards, it's not viable for parallel subtree +/// traversal, and is handled separately. +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, MallocSizeOf)] +pub enum RelativeDependencyInvalidationKind { + /// This dependency may affect relative selector anchors for ancestors. + Ancestors, + /// This dependency may affect a relative selector anchor for the parent. + Parent, + /// This dependency may affect a relative selector anchor for the previous sibling. + PrevSibling, + /// This dependency may affect relative selector anchors for ancestors' previous siblings. + AncestorPrevSibling, + /// This dependency may affect relative selector anchors for earlier siblings. + EarlierSibling, + /// This dependency may affect relative selector anchors for ancestors' earlier siblings. + AncestorEarlierSibling, +} + +/// Invalidation kind merging normal and relative dependencies. +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, MallocSizeOf)] +pub enum DependencyInvalidationKind { + /// This dependency is a normal dependency. + Normal(NormalDependencyInvalidationKind), + /// This dependency is a relative dependency. + Relative(RelativeDependencyInvalidationKind), +} + +impl Dependency { + /// Creates a dummy dependency to invalidate the whole selector. + /// + /// This is necessary because document state invalidation wants to + /// invalidate all elements in the document. + /// + /// The offset is such as that Invalidation::new(self) returns a zero + /// offset. That is, it points to a virtual "combinator" outside of the + /// selector, so calling combinator() on such a dependency will panic. + pub fn for_full_selector_invalidation(selector: Selector<SelectorImpl>) -> Self { + Self { + selector_offset: selector.len() + 1, + selector, + parent: None, + relative_kind: None, + } + } + + /// Returns the combinator to the right of the partial selector this + /// dependency represents. + /// + /// TODO(emilio): Consider storing inline if it helps cache locality? + fn combinator(&self) -> Option<Combinator> { + if self.selector_offset == 0 { + return None; + } + + Some( + self.selector + .combinator_at_match_order(self.selector_offset - 1), + ) + } + + /// The kind of normal invalidation that this would generate. The dependency + /// in question must be a normal dependency. + pub fn normal_invalidation_kind(&self) -> NormalDependencyInvalidationKind { + debug_assert!( + self.relative_kind.is_none(), + "Querying normal invalidation kind on relative dependency." + ); + match self.combinator() { + None => NormalDependencyInvalidationKind::Element, + Some(Combinator::Child) | Some(Combinator::Descendant) => { + NormalDependencyInvalidationKind::Descendants + }, + Some(Combinator::LaterSibling) | Some(Combinator::NextSibling) => { + NormalDependencyInvalidationKind::Siblings + }, + // TODO(emilio): We could look at the selector itself to see if it's + // an eager pseudo, and return only Descendants here if not. + Some(Combinator::PseudoElement) => { + NormalDependencyInvalidationKind::ElementAndDescendants + }, + Some(Combinator::SlotAssignment) => NormalDependencyInvalidationKind::SlottedElements, + Some(Combinator::Part) => NormalDependencyInvalidationKind::Parts, + } + } + + /// The kind of invalidation that this would generate. + pub fn invalidation_kind(&self) -> DependencyInvalidationKind { + if let Some(kind) = self.relative_kind { + return DependencyInvalidationKind::Relative(kind); + } + DependencyInvalidationKind::Normal(self.normal_invalidation_kind()) + } + + /// Is the combinator to the right of this dependency's compound selector + /// the next sibling combinator? This matters for insertion/removal in between + /// two elements connected through next sibling, e.g. `.foo:has(> .a + .b)` + /// where an element gets inserted between `.a` and `.b`. + pub fn right_combinator_is_next_sibling(&self) -> bool { + if self.selector_offset == 0 { + return false; + } + matches!( + self.selector + .combinator_at_match_order(self.selector_offset - 1), + Combinator::NextSibling + ) + } + + /// Is this dependency's compound selector a single compound in `:has` + /// with the next sibling relative combinator i.e. `:has(> .foo)`? + /// This matters for insertion between an anchor and an element + /// connected through next sibling, e.g. `.a:has(> .b)`. + pub fn dependency_is_relative_with_single_next_sibling(&self) -> bool { + match self.invalidation_kind() { + DependencyInvalidationKind::Normal(_) => false, + DependencyInvalidationKind::Relative(kind) => { + kind == RelativeDependencyInvalidationKind::PrevSibling + }, + } + } +} + +/// The same, but for state selectors, which can track more exactly what state +/// do they track. +#[derive(Clone, Debug, MallocSizeOf)] +pub struct StateDependency { + /// The other dependency fields. + pub dep: Dependency, + /// The state this dependency is affected by. + pub state: ElementState, +} + +impl SelectorMapEntry for StateDependency { + fn selector(&self) -> SelectorIter<SelectorImpl> { + self.dep.selector() + } +} + +/// The same, but for document state selectors. +#[derive(Clone, Debug, MallocSizeOf)] +pub struct DocumentStateDependency { + /// We track `Dependency` even though we don't need to track an offset, + /// since when it changes it changes for the whole document anyway. + #[cfg_attr( + feature = "gecko", + ignore_malloc_size_of = "CssRules have primary refs, we measure there" + )] + #[cfg_attr(feature = "servo", ignore_malloc_size_of = "Arc")] + pub dependency: Dependency, + /// The state this dependency is affected by. + pub state: DocumentState, +} + +/// Dependency mapping for classes or IDs. +pub type IdOrClassDependencyMap = MaybeCaseInsensitiveHashMap<Atom, SmallVec<[Dependency; 1]>>; +/// Dependency mapping for non-tree-strctural pseudo-class states. +pub type StateDependencyMap = SelectorMap<StateDependency>; +/// Dependency mapping for local names. +pub type LocalNameDependencyMap = PrecomputedHashMap<LocalName, SmallVec<[Dependency; 1]>>; + +/// A map where we store invalidations. +/// +/// This is slightly different to a SelectorMap, in the sense of that the same +/// selector may appear multiple times. +/// +/// In particular, we want to lookup as few things as possible to get the fewer +/// selectors the better, so this looks up by id, class, or looks at the list of +/// state/other attribute affecting selectors. +#[derive(Clone, Debug, MallocSizeOf)] +pub struct InvalidationMap { + /// A map from a given class name to all the selectors with that class + /// selector. + pub class_to_selector: IdOrClassDependencyMap, + /// A map from a given id to all the selectors with that ID in the + /// stylesheets currently applying to the document. + pub id_to_selector: IdOrClassDependencyMap, + /// A map of all the state dependencies. + pub state_affecting_selectors: StateDependencyMap, + /// A list of document state dependencies in the rules we represent. + pub document_state_selectors: Vec<DocumentStateDependency>, + /// A map of other attribute affecting selectors. + pub other_attribute_affecting_selectors: LocalNameDependencyMap, +} + +/// Tree-structural pseudoclasses that we care about for (Relative selector) invalidation. +/// Specifically, we need to store information on ones that don't generate the inner selector. +#[derive(Clone, Copy, Debug, MallocSizeOf)] +pub struct TSStateForInvalidation(u8); + +bitflags! { + impl TSStateForInvalidation : u8 { + /// :empty + const EMPTY = 1 << 0; + /// :nth etc, without of. + const NTH = 1 << 1; + /// "Simple" edge child selectors, like :first-child, :last-child, etc. + /// Excludes :*-of-type. + const NTH_EDGE = 1 << 2; + } +} + +/// Dependency for tree-structural pseudo-classes. +#[derive(Clone, Debug, MallocSizeOf)] +pub struct TSStateDependency { + /// The other dependency fields. + pub dep: Dependency, + /// The state this dependency is affected by. + pub state: TSStateForInvalidation, +} + +impl SelectorMapEntry for TSStateDependency { + fn selector(&self) -> SelectorIter<SelectorImpl> { + self.dep.selector() + } +} + +/// Dependency mapping for tree-structural pseudo-class states. +pub type TSStateDependencyMap = SelectorMap<TSStateDependency>; +/// Dependency mapping for * selectors. +pub type AnyDependencyMap = SmallVec<[Dependency; 1]>; + +/// A map to store all relative selector invalidations. +/// This keeps a lot more data than the usual map, because any change can generate +/// upward traversals that need to be handled separately. +#[derive(Clone, Debug, MallocSizeOf)] +pub struct RelativeSelectorInvalidationMap { + /// Portion common to the normal invalidation map, except that this is for relative selectors and their inner selectors. + pub map: InvalidationMap, + /// A map for a given tree-structural pseudo-class to all the relative selector dependencies with that type. + pub ts_state_to_selector: TSStateDependencyMap, + /// A map from a given type name to all the relative selector dependencies with that type. + pub type_to_selector: LocalNameDependencyMap, + /// All relative selector dependencies that specify `*`. + pub any_to_selector: AnyDependencyMap, + /// Flag indicating if any relative selector is used. + pub used: bool, + /// Flag indicating if invalidating a relative selector requires ancestor traversal. + pub needs_ancestors_traversal: bool, +} + +impl RelativeSelectorInvalidationMap { + /// Creates an empty `InvalidationMap`. + pub fn new() -> Self { + Self { + map: InvalidationMap::new(), + ts_state_to_selector: TSStateDependencyMap::default(), + type_to_selector: LocalNameDependencyMap::default(), + any_to_selector: SmallVec::default(), + used: false, + needs_ancestors_traversal: false, + } + } + + /// Returns the number of dependencies stored in the invalidation map. + pub fn len(&self) -> usize { + self.map.len() + } + + /// Clears this map, leaving it empty. + pub fn clear(&mut self) { + self.map.clear(); + self.ts_state_to_selector.clear(); + self.type_to_selector.clear(); + self.any_to_selector.clear(); + } + + /// Shrink the capacity of hash maps if needed. + pub fn shrink_if_needed(&mut self) { + self.map.shrink_if_needed(); + self.ts_state_to_selector.shrink_if_needed(); + self.type_to_selector.shrink_if_needed(); + } +} + +impl InvalidationMap { + /// Creates an empty `InvalidationMap`. + pub fn new() -> Self { + Self { + class_to_selector: IdOrClassDependencyMap::new(), + id_to_selector: IdOrClassDependencyMap::new(), + state_affecting_selectors: StateDependencyMap::new(), + document_state_selectors: Vec::new(), + other_attribute_affecting_selectors: LocalNameDependencyMap::default(), + } + } + + /// Returns the number of dependencies stored in the invalidation map. + pub fn len(&self) -> usize { + self.state_affecting_selectors.len() + + self.document_state_selectors.len() + + self.other_attribute_affecting_selectors + .iter() + .fold(0, |accum, (_, ref v)| accum + v.len()) + + self.id_to_selector + .iter() + .fold(0, |accum, (_, ref v)| accum + v.len()) + + self.class_to_selector + .iter() + .fold(0, |accum, (_, ref v)| accum + v.len()) + } + + /// Clears this map, leaving it empty. + pub fn clear(&mut self) { + self.class_to_selector.clear(); + self.id_to_selector.clear(); + self.state_affecting_selectors.clear(); + self.document_state_selectors.clear(); + self.other_attribute_affecting_selectors.clear(); + } + + /// Shrink the capacity of hash maps if needed. + pub fn shrink_if_needed(&mut self) { + self.class_to_selector.shrink_if_needed(); + self.id_to_selector.shrink_if_needed(); + self.state_affecting_selectors.shrink_if_needed(); + self.other_attribute_affecting_selectors.shrink_if_needed(); + } +} + +/// Adds a selector to the given `InvalidationMap`. Returns Err(..) to signify OOM. +pub fn note_selector_for_invalidation( + selector: &Selector<SelectorImpl>, + quirks_mode: QuirksMode, + map: &mut InvalidationMap, + relative_selector_invalidation_map: &mut RelativeSelectorInvalidationMap, +) -> Result<(), AllocErr> { + debug!("note_selector_for_invalidation({:?})", selector); + + let mut document_state = DocumentState::empty(); + { + let mut parent_stack = ParentSelectors::new(); + let mut alloc_error = None; + let mut collector = SelectorDependencyCollector { + map, + relative_selector_invalidation_map, + document_state: &mut document_state, + selector, + parent_selectors: &mut parent_stack, + quirks_mode, + compound_state: PerCompoundState::new(0), + alloc_error: &mut alloc_error, + }; + + let visit_result = collector.visit_whole_selector(); + debug_assert_eq!(!visit_result, alloc_error.is_some()); + if let Some(alloc_error) = alloc_error { + return Err(alloc_error); + } + } + + if !document_state.is_empty() { + let dep = DocumentStateDependency { + state: document_state, + dependency: Dependency::for_full_selector_invalidation(selector.clone()), + }; + map.document_state_selectors.try_reserve(1)?; + map.document_state_selectors.push(dep); + } + Ok(()) +} +struct PerCompoundState { + /// The offset at which our compound starts. + offset: usize, + + /// The state this compound selector is affected by. + element_state: ElementState, +} + +impl PerCompoundState { + fn new(offset: usize) -> Self { + Self { + offset, + element_state: ElementState::empty(), + } + } +} + +struct ParentDependencyEntry { + selector: Selector<SelectorImpl>, + offset: usize, + cached_dependency: Option<Arc<Dependency>>, +} + +trait Collector { + fn dependency(&mut self) -> Dependency; + fn id_map(&mut self) -> &mut IdOrClassDependencyMap; + fn class_map(&mut self) -> &mut IdOrClassDependencyMap; + fn state_map(&mut self) -> &mut StateDependencyMap; + fn attribute_map(&mut self) -> &mut LocalNameDependencyMap; + fn update_states(&mut self, element_state: ElementState, document_state: DocumentState); + + // In normal invalidations, type-based dependencies don't need to be explicitly tracked; + // elements don't change their types, and mutations cause invalidations to go descendant + // (Where they are about to be styled anyway), and/or later-sibling direction (Where they + // siblings after inserted/removed elements get restyled anyway). + // However, for relative selectors, a DOM mutation can affect and arbitrary ancestor and/or + // earlier siblings, so we need to keep track of them. + fn type_map(&mut self) -> &mut LocalNameDependencyMap { + unreachable!(); + } + + // Tree-structural pseudo-selectors generally invalidates in a well-defined way, which are + // handled by RestyleManager. However, for relative selectors, as with type invalidations, + // the direction of invalidation becomes arbitrary, so we need to keep track of them. + fn ts_state_map(&mut self) -> &mut TSStateDependencyMap { + unreachable!(); + } + + // Same story as type invalidation maps. + fn any_vec(&mut self) -> &mut AnyDependencyMap { + unreachable!(); + } +} + +fn on_attribute<C: Collector>( + local_name: &LocalName, + local_name_lower: &LocalName, + collector: &mut C, +) -> Result<(), AllocErr> { + add_attr_dependency(local_name.clone(), collector)?; + + if local_name != local_name_lower { + add_attr_dependency(local_name_lower.clone(), collector)?; + } + Ok(()) +} + +fn on_id_or_class<C: Collector>( + s: &Component<SelectorImpl>, + quirks_mode: QuirksMode, + collector: &mut C, +) -> Result<(), AllocErr> { + let dependency = collector.dependency(); + let (atom, map) = match *s { + Component::ID(ref atom) => (atom, collector.id_map()), + Component::Class(ref atom) => (atom, collector.class_map()), + _ => unreachable!(), + }; + let entry = map.try_entry(atom.0.clone(), quirks_mode)?; + let vec = entry.or_insert_with(SmallVec::new); + vec.try_reserve(1)?; + vec.push(dependency); + Ok(()) +} + +fn add_attr_dependency<C: Collector>(name: LocalName, collector: &mut C) -> Result<(), AllocErr> { + let dependency = collector.dependency(); + let map = collector.attribute_map(); + add_local_name(name, dependency, map) +} + +fn add_local_name( + name: LocalName, + dependency: Dependency, + map: &mut LocalNameDependencyMap, +) -> Result<(), AllocErr> { + map.try_reserve(1)?; + let vec = map.entry(name).or_default(); + vec.try_reserve(1)?; + vec.push(dependency); + Ok(()) +} + +fn on_pseudo_class<C: Collector>(pc: &NonTSPseudoClass, collector: &mut C) -> Result<(), AllocErr> { + collector.update_states(pc.state_flag(), pc.document_state_flag()); + + let attr_name = match *pc { + #[cfg(feature = "gecko")] + NonTSPseudoClass::MozTableBorderNonzero => local_name!("border"), + #[cfg(feature = "gecko")] + NonTSPseudoClass::MozSelectListBox => { + // This depends on two attributes. + add_attr_dependency(local_name!("multiple"), collector)?; + return add_attr_dependency(local_name!("size"), collector); + }, + NonTSPseudoClass::Lang(..) => local_name!("lang"), + _ => return Ok(()), + }; + + add_attr_dependency(attr_name, collector) +} + +fn add_pseudo_class_dependency<C: Collector>( + element_state: ElementState, + quirks_mode: QuirksMode, + collector: &mut C, +) -> Result<(), AllocErr> { + if element_state.is_empty() { + return Ok(()); + } + let dependency = collector.dependency(); + collector.state_map().insert( + StateDependency { + dep: dependency, + state: element_state, + }, + quirks_mode, + ) +} + +type ParentSelectors = SmallVec<[ParentDependencyEntry; 5]>; + +/// A struct that collects invalidations for a given compound selector. +struct SelectorDependencyCollector<'a> { + map: &'a mut InvalidationMap, + relative_selector_invalidation_map: &'a mut RelativeSelectorInvalidationMap, + + /// The document this _complex_ selector is affected by. + /// + /// We don't need to track state per compound selector, since it's global + /// state and it changes for everything. + document_state: &'a mut DocumentState, + + /// The current selector and offset we're iterating. + selector: &'a Selector<SelectorImpl>, + + /// The stack of parent selectors that we have, and at which offset of the + /// sequence. + /// + /// This starts empty. It grows when we find nested :is and :where selector + /// lists. The dependency field is cached and reference counted. + parent_selectors: &'a mut ParentSelectors, + + /// The quirks mode of the document where we're inserting dependencies. + quirks_mode: QuirksMode, + + /// State relevant to a given compound selector. + compound_state: PerCompoundState, + + /// The allocation error, if we OOM. + alloc_error: &'a mut Option<AllocErr>, +} + +fn parent_dependency( + parent_selectors: &mut ParentSelectors, + outer_parent: Option<&Arc<Dependency>>, +) -> Option<Arc<Dependency>> { + if parent_selectors.is_empty() { + return outer_parent.cloned(); + } + + fn dependencies_from( + entries: &mut [ParentDependencyEntry], + outer_parent: &Option<&Arc<Dependency>>, + ) -> Option<Arc<Dependency>> { + if entries.is_empty() { + return None; + } + + let last_index = entries.len() - 1; + let (previous, last) = entries.split_at_mut(last_index); + let last = &mut last[0]; + let selector = &last.selector; + let selector_offset = last.offset; + Some( + last.cached_dependency + .get_or_insert_with(|| { + Arc::new(Dependency { + selector: selector.clone(), + selector_offset, + parent: dependencies_from(previous, outer_parent), + relative_kind: None, + }) + }) + .clone(), + ) + } + + dependencies_from(parent_selectors, &outer_parent) +} + +impl<'a> Collector for SelectorDependencyCollector<'a> { + fn dependency(&mut self) -> Dependency { + let parent = parent_dependency(self.parent_selectors, None); + Dependency { + selector: self.selector.clone(), + selector_offset: self.compound_state.offset, + parent, + relative_kind: None, + } + } + + fn id_map(&mut self) -> &mut IdOrClassDependencyMap { + &mut self.map.id_to_selector + } + + fn class_map(&mut self) -> &mut IdOrClassDependencyMap { + &mut self.map.class_to_selector + } + + fn state_map(&mut self) -> &mut StateDependencyMap { + &mut self.map.state_affecting_selectors + } + + fn attribute_map(&mut self) -> &mut LocalNameDependencyMap { + &mut self.map.other_attribute_affecting_selectors + } + + fn update_states(&mut self, element_state: ElementState, document_state: DocumentState) { + self.compound_state.element_state |= element_state; + *self.document_state |= document_state; + } +} + +impl<'a> SelectorDependencyCollector<'a> { + fn visit_whole_selector(&mut self) -> bool { + let iter = self.selector.iter(); + self.visit_whole_selector_from(iter, 0) + } + + fn visit_whole_selector_from( + &mut self, + mut iter: SelectorIter<SelectorImpl>, + mut index: usize, + ) -> bool { + loop { + // Reset the compound state. + self.compound_state = PerCompoundState::new(index); + + // Visit all the simple selectors in this sequence. + for ss in &mut iter { + if !ss.visit(self) { + return false; + } + index += 1; // Account for the simple selector. + } + + if let Err(err) = add_pseudo_class_dependency( + self.compound_state.element_state, + self.quirks_mode, + self, + ) { + *self.alloc_error = Some(err); + return false; + } + + let combinator = iter.next_sequence(); + if combinator.is_none() { + return true; + } + index += 1; // account for the combinator + } + } +} + +impl<'a> SelectorVisitor for SelectorDependencyCollector<'a> { + type Impl = SelectorImpl; + + fn visit_selector_list( + &mut self, + _list_kind: SelectorListKind, + list: &[Selector<SelectorImpl>], + ) -> bool { + for selector in list { + // Here we cheat a bit: We can visit the rightmost compound with + // the "outer" visitor, and it'd be fine. This reduces the amount of + // state and attribute invalidations, and we need to check the outer + // selector to the left anyway to avoid over-invalidation, so it + // avoids matching it twice uselessly. + let mut iter = selector.iter(); + let mut index = 0; + + for ss in &mut iter { + if !ss.visit(self) { + return false; + } + index += 1; + } + + let combinator = iter.next_sequence(); + if combinator.is_none() { + continue; + } + + index += 1; // account for the combinator. + + self.parent_selectors.push(ParentDependencyEntry { + selector: self.selector.clone(), + offset: self.compound_state.offset, + cached_dependency: None, + }); + let mut nested = SelectorDependencyCollector { + map: &mut *self.map, + relative_selector_invalidation_map: &mut *self.relative_selector_invalidation_map, + document_state: &mut *self.document_state, + selector, + parent_selectors: &mut *self.parent_selectors, + quirks_mode: self.quirks_mode, + compound_state: PerCompoundState::new(index), + alloc_error: &mut *self.alloc_error, + }; + if !nested.visit_whole_selector_from(iter, index) { + return false; + } + self.parent_selectors.pop(); + } + true + } + + fn visit_relative_selector_list( + &mut self, + list: &[selectors::parser::RelativeSelector<Self::Impl>], + ) -> bool { + self.relative_selector_invalidation_map.used = true; + for relative_selector in list { + // We can't cheat here like we do with other selector lists - the rightmost + // compound of a relative selector is not the subject of the invalidation. + self.parent_selectors.push(ParentDependencyEntry { + selector: self.selector.clone(), + offset: self.compound_state.offset, + cached_dependency: None, + }); + let mut nested = RelativeSelectorDependencyCollector { + map: &mut *self.relative_selector_invalidation_map, + document_state: &mut *self.document_state, + selector: &relative_selector, + combinator_count: RelativeSelectorCombinatorCount::new(relative_selector), + parent_selectors: &mut *self.parent_selectors, + quirks_mode: self.quirks_mode, + compound_state: RelativeSelectorPerCompoundState::new(0), + alloc_error: &mut *self.alloc_error, + }; + if !nested.visit_whole_selector() { + return false; + } + self.parent_selectors.pop(); + } + true + } + + fn visit_simple_selector(&mut self, s: &Component<SelectorImpl>) -> bool { + match *s { + Component::ID(..) | Component::Class(..) => { + if let Err(err) = on_id_or_class(s, self.quirks_mode, self) { + *self.alloc_error = Some(err.into()); + return false; + } + true + }, + Component::NonTSPseudoClass(ref pc) => { + if let Err(err) = on_pseudo_class(pc, self) { + *self.alloc_error = Some(err.into()); + return false; + } + true + }, + _ => true, + } + } + + fn visit_attribute_selector( + &mut self, + _: &NamespaceConstraint<&Namespace>, + local_name: &LocalName, + local_name_lower: &LocalName, + ) -> bool { + if let Err(err) = on_attribute(local_name, local_name_lower, self) { + *self.alloc_error = Some(err); + return false; + } + true + } +} + +struct RelativeSelectorPerCompoundState { + state: PerCompoundState, + ts_state: TSStateForInvalidation, + added_entry: bool, +} + +impl RelativeSelectorPerCompoundState { + fn new(offset: usize) -> Self { + Self { + state: PerCompoundState::new(offset), + ts_state: TSStateForInvalidation::empty(), + added_entry: false, + } + } +} + +/// A struct that collects invalidations for a given compound selector. +struct RelativeSelectorDependencyCollector<'a> { + map: &'a mut RelativeSelectorInvalidationMap, + + /// The document this _complex_ selector is affected by. + /// + /// We don't need to track state per compound selector, since it's global + /// state and it changes for everything. + document_state: &'a mut DocumentState, + + /// The current inner relative selector and offset we're iterating. + selector: &'a RelativeSelector<SelectorImpl>, + /// Running combinator for this inner relative selector. + combinator_count: RelativeSelectorCombinatorCount, + + /// The stack of parent selectors that we have, and at which offset of the + /// sequence. + /// + /// This starts empty. It grows when we find nested :is and :where selector + /// lists. The dependency field is cached and reference counted. + parent_selectors: &'a mut ParentSelectors, + + /// The quirks mode of the document where we're inserting dependencies. + quirks_mode: QuirksMode, + + /// State relevant to a given compound selector. + compound_state: RelativeSelectorPerCompoundState, + + /// The allocation error, if we OOM. + alloc_error: &'a mut Option<AllocErr>, +} + +fn add_non_unique_info<C: Collector>( + selector: &Selector<SelectorImpl>, + offset: usize, + collector: &mut C, +) -> Result<(), AllocErr> { + // Go through this compound again. + for ss in selector.iter_from(offset) { + match ss { + Component::LocalName(ref name) => { + let dependency = collector.dependency(); + add_local_name(name.name.clone(), dependency, &mut collector.type_map())?; + if name.name != name.lower_name { + let dependency = collector.dependency(); + add_local_name( + name.lower_name.clone(), + dependency, + &mut collector.type_map(), + )?; + } + return Ok(()); + }, + _ => (), + }; + } + // Ouch. Add one for *. + collector.any_vec().try_reserve(1)?; + let dependency = collector.dependency(); + collector.any_vec().push(dependency); + Ok(()) +} + +fn add_ts_pseudo_class_dependency<C: Collector>( + state: TSStateForInvalidation, + quirks_mode: QuirksMode, + collector: &mut C, +) -> Result<(), AllocErr> { + if state.is_empty() { + return Ok(()); + } + let dependency = collector.dependency(); + collector.ts_state_map().insert( + TSStateDependency { + dep: dependency, + state, + }, + quirks_mode, + ) +} + +impl<'a> RelativeSelectorDependencyCollector<'a> { + fn visit_whole_selector(&mut self) -> bool { + let mut iter = self.selector.selector.iter_skip_relative_selector_anchor(); + let mut index = 0; + + self.map.needs_ancestors_traversal |= match self.selector.match_hint { + RelativeSelectorMatchHint::InNextSiblingSubtree | + RelativeSelectorMatchHint::InSiblingSubtree | + RelativeSelectorMatchHint::InSubtree => true, + _ => false, + }; + loop { + // Reset the compound state. + self.compound_state = RelativeSelectorPerCompoundState::new(index); + + // Visit all the simple selectors in this sequence. + for ss in &mut iter { + if !ss.visit(self) { + return false; + } + index += 1; // Account for the simple selector. + } + + if let Err(err) = add_pseudo_class_dependency( + self.compound_state.state.element_state, + self.quirks_mode, + self, + ) { + *self.alloc_error = Some(err); + return false; + } + if let Err(err) = + add_ts_pseudo_class_dependency(self.compound_state.ts_state, self.quirks_mode, self) + { + *self.alloc_error = Some(err); + return false; + } + + if !self.compound_state.added_entry { + // Not great - we didn't add any uniquely identifiable information. + if let Err(err) = add_non_unique_info( + &self.selector.selector, + self.compound_state.state.offset, + self, + ) { + *self.alloc_error = Some(err); + return false; + } + } + + let combinator = iter.next_sequence(); + if let Some(c) = combinator { + match c { + Combinator::Child | Combinator::Descendant => { + self.combinator_count.child_or_descendants -= 1 + }, + Combinator::NextSibling | Combinator::LaterSibling => { + self.combinator_count.adjacent_or_next_siblings -= 1 + }, + Combinator::Part | Combinator::PseudoElement | Combinator::SlotAssignment => (), + } + } else { + return true; + } + index += 1; // account for the combinator + } + } +} + +impl<'a> Collector for RelativeSelectorDependencyCollector<'a> { + fn dependency(&mut self) -> Dependency { + let parent = parent_dependency(self.parent_selectors, None); + Dependency { + selector: self.selector.selector.clone(), + selector_offset: self.compound_state.state.offset, + relative_kind: Some(match self.combinator_count.get_match_hint() { + RelativeSelectorMatchHint::InChild => RelativeDependencyInvalidationKind::Parent, + RelativeSelectorMatchHint::InSubtree => { + RelativeDependencyInvalidationKind::Ancestors + }, + RelativeSelectorMatchHint::InNextSibling => { + RelativeDependencyInvalidationKind::PrevSibling + }, + RelativeSelectorMatchHint::InSibling => { + RelativeDependencyInvalidationKind::EarlierSibling + }, + RelativeSelectorMatchHint::InNextSiblingSubtree => { + RelativeDependencyInvalidationKind::AncestorPrevSibling + }, + RelativeSelectorMatchHint::InSiblingSubtree => { + RelativeDependencyInvalidationKind::AncestorEarlierSibling + }, + }), + parent, + } + } + + fn id_map(&mut self) -> &mut IdOrClassDependencyMap { + &mut self.map.map.id_to_selector + } + + fn class_map(&mut self) -> &mut IdOrClassDependencyMap { + &mut self.map.map.class_to_selector + } + + fn state_map(&mut self) -> &mut StateDependencyMap { + &mut self.map.map.state_affecting_selectors + } + + fn attribute_map(&mut self) -> &mut LocalNameDependencyMap { + &mut self.map.map.other_attribute_affecting_selectors + } + + fn update_states(&mut self, element_state: ElementState, document_state: DocumentState) { + self.compound_state.state.element_state |= element_state; + *self.document_state |= document_state; + } + + fn type_map(&mut self) -> &mut LocalNameDependencyMap { + &mut self.map.type_to_selector + } + + fn ts_state_map(&mut self) -> &mut TSStateDependencyMap { + &mut self.map.ts_state_to_selector + } + + fn any_vec(&mut self) -> &mut AnyDependencyMap { + &mut self.map.any_to_selector + } +} + +impl<'a> SelectorVisitor for RelativeSelectorDependencyCollector<'a> { + type Impl = SelectorImpl; + + fn visit_selector_list( + &mut self, + _list_kind: SelectorListKind, + list: &[Selector<SelectorImpl>], + ) -> bool { + let mut parent_stack = ParentSelectors::new(); + let parent_dependency = Arc::new(self.dependency()); + for selector in list { + // Subjects inside relative selectors aren't really subjects. + // This simplifies compound state tracking as well (Additional + // states we track for relative selector's inner selectors should + // not leak out of the relevant selector). + let mut nested = RelativeSelectorInnerDependencyCollector { + map: &mut *self.map, + parent_dependency: &parent_dependency, + document_state: &mut *self.document_state, + selector, + parent_selectors: &mut parent_stack, + quirks_mode: self.quirks_mode, + compound_state: RelativeSelectorPerCompoundState::new(0), + alloc_error: &mut *self.alloc_error, + }; + if !nested.visit_whole_selector() { + return false; + } + } + true + } + + fn visit_relative_selector_list( + &mut self, + _list: &[selectors::parser::RelativeSelector<Self::Impl>], + ) -> bool { + // Ignore nested relative selectors. These can happen as a result of nesting. + true + } + + fn visit_simple_selector(&mut self, s: &Component<SelectorImpl>) -> bool { + match *s { + Component::ID(..) | Component::Class(..) => { + self.compound_state.added_entry = true; + if let Err(err) = on_id_or_class(s, self.quirks_mode, self) { + *self.alloc_error = Some(err.into()); + return false; + } + true + }, + Component::NonTSPseudoClass(ref pc) => { + if !pc + .state_flag() + .intersects(ElementState::VISITED_OR_UNVISITED) + { + // Visited/Unvisited styling doesn't take the usual state invalidation path. + self.compound_state.added_entry = true; + } + if let Err(err) = on_pseudo_class(pc, self) { + *self.alloc_error = Some(err.into()); + return false; + } + true + }, + Component::Empty => { + self.compound_state + .ts_state + .insert(TSStateForInvalidation::EMPTY); + true + }, + Component::Nth(data) => { + let kind = if data.is_simple_edge() { + TSStateForInvalidation::NTH_EDGE + } else { + TSStateForInvalidation::NTH + }; + self.compound_state + .ts_state + .insert(kind); + true + }, + Component::RelativeSelectorAnchor => unreachable!("Should not visit this far"), + _ => true, + } + } + + fn visit_attribute_selector( + &mut self, + _: &NamespaceConstraint<&Namespace>, + local_name: &LocalName, + local_name_lower: &LocalName, + ) -> bool { + self.compound_state.added_entry = true; + if let Err(err) = on_attribute(local_name, local_name_lower, self) { + *self.alloc_error = Some(err); + return false; + } + true + } +} + +/// A struct that collects invalidations from a complex selector inside a relative selector. +/// TODO(dshin): All of this duplication is not great Perhaps should be merged to the normal +/// one, if possible? See bug 1855690. +struct RelativeSelectorInnerDependencyCollector<'a, 'b> { + map: &'a mut RelativeSelectorInvalidationMap, + + /// The document this _complex_ selector is affected by. + /// + /// We don't need to track state per compound selector, since it's global + /// state and it changes for everything. + document_state: &'a mut DocumentState, + + /// Parent relative selector dependency. + parent_dependency: &'b Arc<Dependency>, + + /// The current inner relative selector and offset we're iterating. + selector: &'a Selector<SelectorImpl>, + + /// The stack of parent selectors that we have, and at which offset of the + /// sequence. + /// + /// This starts empty. It grows when we find nested :is and :where selector + /// lists. The dependency field is cached and reference counted. + parent_selectors: &'a mut ParentSelectors, + + /// The quirks mode of the document where we're inserting dependencies. + quirks_mode: QuirksMode, + + /// State relevant to a given compound selector. + compound_state: RelativeSelectorPerCompoundState, + + /// The allocation error, if we OOM. + alloc_error: &'a mut Option<AllocErr>, +} + +impl<'a, 'b> Collector for RelativeSelectorInnerDependencyCollector<'a, 'b> { + fn dependency(&mut self) -> Dependency { + let parent = parent_dependency(self.parent_selectors, Some(self.parent_dependency)); + Dependency { + selector: self.selector.clone(), + selector_offset: self.compound_state.state.offset, + parent, + relative_kind: None, + } + } + + fn id_map(&mut self) -> &mut IdOrClassDependencyMap { + &mut self.map.map.id_to_selector + } + + fn class_map(&mut self) -> &mut IdOrClassDependencyMap { + &mut self.map.map.class_to_selector + } + + fn state_map(&mut self) -> &mut StateDependencyMap { + &mut self.map.map.state_affecting_selectors + } + + fn attribute_map(&mut self) -> &mut LocalNameDependencyMap { + &mut self.map.map.other_attribute_affecting_selectors + } + + fn update_states(&mut self, element_state: ElementState, document_state: DocumentState) { + self.compound_state.state.element_state |= element_state; + *self.document_state |= document_state; + } + + fn type_map(&mut self) -> &mut LocalNameDependencyMap { + &mut self.map.type_to_selector + } + + fn ts_state_map(&mut self) -> &mut TSStateDependencyMap { + &mut self.map.ts_state_to_selector + } + + fn any_vec(&mut self) -> &mut AnyDependencyMap { + &mut self.map.any_to_selector + } +} + +impl<'a, 'b> RelativeSelectorInnerDependencyCollector<'a, 'b> { + fn visit_whole_selector(&mut self) -> bool { + let mut iter = self.selector.iter(); + let mut index = 0; + loop { + // Reset the compound state. + self.compound_state = RelativeSelectorPerCompoundState::new(index); + + // Visit all the simple selectors in this sequence. + for ss in &mut iter { + if !ss.visit(self) { + return false; + } + index += 1; // Account for the simple selector. + } + + if let Err(err) = add_pseudo_class_dependency( + self.compound_state.state.element_state, + self.quirks_mode, + self, + ) { + *self.alloc_error = Some(err); + return false; + } + + if let Err(err) = + add_ts_pseudo_class_dependency(self.compound_state.ts_state, self.quirks_mode, self) + { + *self.alloc_error = Some(err); + return false; + } + + if !self.compound_state.added_entry { + // Not great - we didn't add any uniquely identifiable information. + if let Err(err) = + add_non_unique_info(&self.selector, self.compound_state.state.offset, self) + { + *self.alloc_error = Some(err); + return false; + } + } + + let combinator = iter.next_sequence(); + if combinator.is_none() { + return true; + } + index += 1; // account for the combinator + } + } +} + +impl<'a, 'b> SelectorVisitor for RelativeSelectorInnerDependencyCollector<'a, 'b> { + type Impl = SelectorImpl; + + fn visit_selector_list( + &mut self, + _list_kind: SelectorListKind, + list: &[Selector<SelectorImpl>], + ) -> bool { + let parent_dependency = Arc::new(self.dependency()); + for selector in list { + // Subjects inside relative selectors aren't really subjects. + // This simplifies compound state tracking as well (Additional + // states we track for relative selector's inner selectors should + // not leak out of the relevant selector). + let mut nested = RelativeSelectorInnerDependencyCollector { + map: &mut *self.map, + parent_dependency: &parent_dependency, + document_state: &mut *self.document_state, + selector, + parent_selectors: &mut *self.parent_selectors, + quirks_mode: self.quirks_mode, + compound_state: RelativeSelectorPerCompoundState::new(0), + alloc_error: &mut *self.alloc_error, + }; + if !nested.visit_whole_selector() { + return false; + } + } + true + } + + fn visit_relative_selector_list( + &mut self, + _list: &[selectors::parser::RelativeSelector<Self::Impl>], + ) -> bool { + // Ignore nested relative selectors. These can happen as a result of nesting. + true + } + + fn visit_simple_selector(&mut self, s: &Component<SelectorImpl>) -> bool { + match *s { + Component::ID(..) | Component::Class(..) => { + self.compound_state.added_entry = true; + if let Err(err) = on_id_or_class(s, self.quirks_mode, self) { + *self.alloc_error = Some(err.into()); + return false; + } + true + }, + Component::NonTSPseudoClass(ref pc) => { + if !pc + .state_flag() + .intersects(ElementState::VISITED_OR_UNVISITED) + { + // Visited/Unvisited styling doesn't take the usual state invalidation path. + self.compound_state.added_entry = true; + } + if let Err(err) = on_pseudo_class(pc, self) { + *self.alloc_error = Some(err.into()); + return false; + } + true + }, + Component::Empty => { + self.compound_state + .ts_state + .insert(TSStateForInvalidation::EMPTY); + true + }, + Component::Nth(data) => { + let kind = if data.is_simple_edge() { + TSStateForInvalidation::NTH_EDGE + } else { + TSStateForInvalidation::NTH + }; + self.compound_state + .ts_state + .insert(kind); + true + }, + Component::RelativeSelectorAnchor => unreachable!("Should not visit this far"), + _ => true, + } + } + + fn visit_attribute_selector( + &mut self, + _: &NamespaceConstraint<&Namespace>, + local_name: &LocalName, + local_name_lower: &LocalName, + ) -> bool { + self.compound_state.added_entry = true; + if let Err(err) = on_attribute(local_name, local_name_lower, self) { + *self.alloc_error = Some(err); + return false; + } + true + } +} diff --git a/servo/components/style/invalidation/element/invalidator.rs b/servo/components/style/invalidation/element/invalidator.rs new file mode 100644 index 0000000000..5dee1f5dcf --- /dev/null +++ b/servo/components/style/invalidation/element/invalidator.rs @@ -0,0 +1,1130 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! The struct that takes care of encapsulating all the logic on where and how +//! element styles need to be invalidated. + +use crate::context::StackLimitChecker; +use crate::dom::{TElement, TNode, TShadowRoot}; +use crate::invalidation::element::invalidation_map::{ + Dependency, DependencyInvalidationKind, NormalDependencyInvalidationKind, + RelativeDependencyInvalidationKind, +}; +use selectors::matching::matches_compound_selector_from; +use selectors::matching::{CompoundSelectorMatchingResult, MatchingContext}; +use selectors::parser::{Combinator, Component}; +use selectors::OpaqueElement; +use smallvec::SmallVec; +use std::fmt; +use std::fmt::Write; + +struct SiblingInfo<E> +where + E: TElement, +{ + affected: E, + prev_sibling: Option<E>, + next_sibling: Option<E>, +} + +/// Traversal mapping for elements under consideration. It acts like a snapshot map, +/// though this only "maps" one element at most. +/// For general invalidations, this has no effect, especially since when +/// DOM mutates, the mutation's effect should not escape the subtree being mutated. +/// This is not the case for relative selectors, unfortunately, so we may end up +/// traversing a portion of the DOM tree that mutated. In case the mutation is removal, +/// its sibling relation is severed by the time the invalidation happens. This structure +/// recovers that relation. Note - it assumes that there is only one element under this +/// effect. +pub struct SiblingTraversalMap<E> +where + E: TElement, +{ + info: Option<SiblingInfo<E>>, +} + +impl<E> Default for SiblingTraversalMap<E> +where + E: TElement, +{ + fn default() -> Self { + Self { info: None } + } +} + +impl<E> SiblingTraversalMap<E> +where + E: TElement, +{ + /// Create a new traversal map with the affected element. + pub fn new(affected: E, prev_sibling: Option<E>, next_sibling: Option<E>) -> Self { + Self { + info: Some(SiblingInfo { + affected, + prev_sibling, + next_sibling, + }), + } + } + + /// Get the element's previous sibling element. + pub fn next_sibling_for(&self, element: &E) -> Option<E> { + if let Some(ref info) = self.info { + if *element == info.affected { + return info.next_sibling; + } + } + element.next_sibling_element() + } + + /// Get the element's previous sibling element. + pub fn prev_sibling_for(&self, element: &E) -> Option<E> { + if let Some(ref info) = self.info { + if *element == info.affected { + return info.prev_sibling; + } + } + element.prev_sibling_element() + } +} + +/// A trait to abstract the collection of invalidations for a given pass. +pub trait InvalidationProcessor<'a, 'b, E> +where + E: TElement, +{ + /// Whether an invalidation that contains only a pseudo-element selector + /// like ::before or ::after triggers invalidation of the element that would + /// originate it. + fn invalidates_on_pseudo_element(&self) -> bool { + false + } + + /// Whether the invalidation processor only cares about light-tree + /// descendants of a given element, that is, doesn't invalidate + /// pseudo-elements, NAC, shadow dom... + fn light_tree_only(&self) -> bool { + false + } + + /// When a dependency from a :where or :is selector matches, it may still be + /// the case that we don't need to invalidate the full style. Consider the + /// case of: + /// + /// div .foo:where(.bar *, .baz) .qux + /// + /// We can get to the `*` part after a .bar class change, but you only need + /// to restyle the element if it also matches .foo. + /// + /// Similarly, you only need to restyle .baz if the whole result of matching + /// the selector changes. + /// + /// This function is called to check the result of matching the "outer" + /// dependency that we generate for the parent of the `:where` selector, + /// that is, in the case above it should match + /// `div .foo:where(.bar *, .baz)`. + /// + /// Returning true unconditionally here is over-optimistic and may + /// over-invalidate. + fn check_outer_dependency(&mut self, dependency: &Dependency, element: E) -> bool; + + /// The matching context that should be used to process invalidations. + fn matching_context(&mut self) -> &mut MatchingContext<'b, E::Impl>; + + /// The traversal map that should be used to process invalidations. + fn sibling_traversal_map(&self) -> &SiblingTraversalMap<E>; + + /// Collect invalidations for a given element's descendants and siblings. + /// + /// Returns whether the element itself was invalidated. + fn collect_invalidations( + &mut self, + element: E, + self_invalidations: &mut InvalidationVector<'a>, + descendant_invalidations: &mut DescendantInvalidationLists<'a>, + sibling_invalidations: &mut InvalidationVector<'a>, + ) -> bool; + + /// Returns whether the invalidation process should process the descendants + /// of the given element. + fn should_process_descendants(&mut self, element: E) -> bool; + + /// Executes an arbitrary action when the recursion limit is exceded (if + /// any). + fn recursion_limit_exceeded(&mut self, element: E); + + /// Executes an action when `Self` is invalidated. + fn invalidated_self(&mut self, element: E); + + /// Executes an action when `sibling` is invalidated as a sibling of + /// `of`. + fn invalidated_sibling(&mut self, sibling: E, of: E); + + /// Executes an action when any descendant of `Self` is invalidated. + fn invalidated_descendants(&mut self, element: E, child: E); + + /// Executes an action when an element in a relative selector is reached. + /// Lets the dependency to be borrowed for further processing out of the + /// invalidation traversal. + fn found_relative_selector_invalidation( + &mut self, + _element: E, + _kind: RelativeDependencyInvalidationKind, + _relative_dependency: &'a Dependency, + ) { + debug_assert!(false, "Reached relative selector dependency"); + } +} + +/// Different invalidation lists for descendants. +#[derive(Debug, Default)] +pub struct DescendantInvalidationLists<'a> { + /// Invalidations for normal DOM children and pseudo-elements. + /// + /// TODO(emilio): Having a list of invalidations just for pseudo-elements + /// may save some work here and there. + pub dom_descendants: InvalidationVector<'a>, + /// Invalidations for slotted children of an element. + pub slotted_descendants: InvalidationVector<'a>, + /// Invalidations for ::part()s of an element. + pub parts: InvalidationVector<'a>, +} + +impl<'a> DescendantInvalidationLists<'a> { + fn is_empty(&self) -> bool { + self.dom_descendants.is_empty() && + self.slotted_descendants.is_empty() && + self.parts.is_empty() + } +} + +/// The struct that takes care of encapsulating all the logic on where and how +/// element styles need to be invalidated. +pub struct TreeStyleInvalidator<'a, 'b, 'c, E, P: 'a> +where + 'b: 'a, + E: TElement, + P: InvalidationProcessor<'b, 'c, E>, +{ + element: E, + stack_limit_checker: Option<&'a StackLimitChecker>, + processor: &'a mut P, + _marker: std::marker::PhantomData<(&'b (), &'c ())>, +} + +/// A vector of invalidations, optimized for small invalidation sets. +pub type InvalidationVector<'a> = SmallVec<[Invalidation<'a>; 10]>; + +/// The kind of descendant invalidation we're processing. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +enum DescendantInvalidationKind { + /// A DOM descendant invalidation. + Dom, + /// A ::slotted() descendant invalidation. + Slotted, + /// A ::part() descendant invalidation. + Part, +} + +/// The kind of invalidation we're processing. +/// +/// We can use this to avoid pushing invalidations of the same kind to our +/// descendants or siblings. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +enum InvalidationKind { + Descendant(DescendantInvalidationKind), + Sibling, +} + +/// An `Invalidation` is a complex selector that describes which elements, +/// relative to a current element we are processing, must be restyled. +#[derive(Clone)] +pub struct Invalidation<'a> { + /// The dependency that generated this invalidation. + /// + /// Note that the offset inside the dependency is not really useful after + /// construction. + dependency: &'a Dependency, + /// The right shadow host from where the rule came from, if any. + /// + /// This is needed to ensure that we match the selector with the right + /// state, as whether some selectors like :host and ::part() match depends + /// on it. + scope: Option<OpaqueElement>, + /// The offset of the selector pointing to a compound selector. + /// + /// This order is a "parse order" offset, that is, zero is the leftmost part + /// of the selector written as parsed / serialized. + /// + /// It is initialized from the offset from `dependency`. + offset: usize, + /// Whether the invalidation was already matched by any previous sibling or + /// ancestor. + /// + /// If this is the case, we can avoid pushing invalidations generated by + /// this one if the generated invalidation is effective for all the siblings + /// or descendants after us. + matched_by_any_previous: bool, +} + +impl<'a> Invalidation<'a> { + /// Create a new invalidation for matching a dependency. + pub fn new(dependency: &'a Dependency, scope: Option<OpaqueElement>) -> Self { + debug_assert!( + dependency.selector_offset == dependency.selector.len() + 1 || + dependency.normal_invalidation_kind() != + NormalDependencyInvalidationKind::Element, + "No point to this, if the dependency matched the element we should just invalidate it" + ); + Self { + dependency, + scope, + // + 1 to go past the combinator. + offset: dependency.selector.len() + 1 - dependency.selector_offset, + matched_by_any_previous: false, + } + } + + /// Whether this invalidation is effective for the next sibling or + /// descendant after us. + fn effective_for_next(&self) -> bool { + if self.offset == 0 { + return true; + } + + // TODO(emilio): For pseudo-elements this should be mostly false, except + // for the weird pseudos in <input type="number">. + // + // We should be able to do better here! + match self + .dependency + .selector + .combinator_at_parse_order(self.offset - 1) + { + Combinator::Descendant | Combinator::LaterSibling | Combinator::PseudoElement => true, + Combinator::Part | + Combinator::SlotAssignment | + Combinator::NextSibling | + Combinator::Child => false, + } + } + + fn kind(&self) -> InvalidationKind { + if self.offset == 0 { + return InvalidationKind::Descendant(DescendantInvalidationKind::Dom); + } + + match self + .dependency + .selector + .combinator_at_parse_order(self.offset - 1) + { + Combinator::Child | Combinator::Descendant | Combinator::PseudoElement => { + InvalidationKind::Descendant(DescendantInvalidationKind::Dom) + }, + Combinator::Part => InvalidationKind::Descendant(DescendantInvalidationKind::Part), + Combinator::SlotAssignment => { + InvalidationKind::Descendant(DescendantInvalidationKind::Slotted) + }, + Combinator::NextSibling | Combinator::LaterSibling => InvalidationKind::Sibling, + } + } +} + +impl<'a> fmt::Debug for Invalidation<'a> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + use cssparser::ToCss; + + f.write_str("Invalidation(")?; + for component in self + .dependency + .selector + .iter_raw_parse_order_from(self.offset) + { + if matches!(*component, Component::Combinator(..)) { + break; + } + component.to_css(f)?; + } + f.write_char(')') + } +} + +/// The result of processing a single invalidation for a given element. +struct SingleInvalidationResult { + /// Whether the element itself was invalidated. + invalidated_self: bool, + /// Whether the invalidation matched, either invalidating the element or + /// generating another invalidation. + matched: bool, +} + +/// The result of a whole invalidation process for a given element. +pub struct InvalidationResult { + /// Whether the element itself was invalidated. + invalidated_self: bool, + /// Whether the element's descendants were invalidated. + invalidated_descendants: bool, + /// Whether the element's siblings were invalidated. + invalidated_siblings: bool, +} + +impl InvalidationResult { + /// Create an emtpy result. + pub fn empty() -> Self { + Self { + invalidated_self: false, + invalidated_descendants: false, + invalidated_siblings: false, + } + } + + /// Whether the invalidation has invalidate the element itself. + pub fn has_invalidated_self(&self) -> bool { + self.invalidated_self + } + + /// Whether the invalidation has invalidate desendants. + pub fn has_invalidated_descendants(&self) -> bool { + self.invalidated_descendants + } + + /// Whether the invalidation has invalidate siblings. + pub fn has_invalidated_siblings(&self) -> bool { + self.invalidated_siblings + } +} + +impl<'a, 'b, 'c, E, P: 'a> TreeStyleInvalidator<'a, 'b, 'c, E, P> +where + 'b: 'a, + E: TElement, + P: InvalidationProcessor<'b, 'c, E>, +{ + /// Trivially constructs a new `TreeStyleInvalidator`. + pub fn new( + element: E, + stack_limit_checker: Option<&'a StackLimitChecker>, + processor: &'a mut P, + ) -> Self { + Self { + element, + stack_limit_checker, + processor, + _marker: std::marker::PhantomData, + } + } + + /// Perform the invalidation pass. + pub fn invalidate(mut self) -> InvalidationResult { + debug!("StyleTreeInvalidator::invalidate({:?})", self.element); + + let mut self_invalidations = InvalidationVector::new(); + let mut descendant_invalidations = DescendantInvalidationLists::default(); + let mut sibling_invalidations = InvalidationVector::new(); + + let mut invalidated_self = self.processor.collect_invalidations( + self.element, + &mut self_invalidations, + &mut descendant_invalidations, + &mut sibling_invalidations, + ); + + debug!("Collected invalidations (self: {}): ", invalidated_self); + debug!( + " > self: {}, {:?}", + self_invalidations.len(), + self_invalidations + ); + debug!(" > descendants: {:?}", descendant_invalidations); + debug!( + " > siblings: {}, {:?}", + sibling_invalidations.len(), + sibling_invalidations + ); + + let invalidated_self_from_collection = invalidated_self; + + invalidated_self |= self.process_descendant_invalidations( + &self_invalidations, + &mut descendant_invalidations, + &mut sibling_invalidations, + DescendantInvalidationKind::Dom, + ); + + if invalidated_self && !invalidated_self_from_collection { + self.processor.invalidated_self(self.element); + } + + let invalidated_descendants = self.invalidate_descendants(&descendant_invalidations); + let invalidated_siblings = self.invalidate_siblings(&mut sibling_invalidations); + + InvalidationResult { + invalidated_self, + invalidated_descendants, + invalidated_siblings, + } + } + + /// Go through later DOM siblings, invalidating style as needed using the + /// `sibling_invalidations` list. + /// + /// Returns whether any sibling's style or any sibling descendant's style + /// was invalidated. + fn invalidate_siblings(&mut self, sibling_invalidations: &mut InvalidationVector<'b>) -> bool { + if sibling_invalidations.is_empty() { + return false; + } + + let mut current = self + .processor + .sibling_traversal_map() + .next_sibling_for(&self.element); + let mut any_invalidated = false; + + while let Some(sibling) = current { + let mut sibling_invalidator = + TreeStyleInvalidator::new(sibling, self.stack_limit_checker, self.processor); + + let mut invalidations_for_descendants = DescendantInvalidationLists::default(); + let invalidated_sibling = sibling_invalidator.process_sibling_invalidations( + &mut invalidations_for_descendants, + sibling_invalidations, + ); + + if invalidated_sibling { + sibling_invalidator + .processor + .invalidated_sibling(sibling, self.element); + } + + any_invalidated |= invalidated_sibling; + + any_invalidated |= + sibling_invalidator.invalidate_descendants(&invalidations_for_descendants); + + if sibling_invalidations.is_empty() { + break; + } + + current = self + .processor + .sibling_traversal_map() + .next_sibling_for(&sibling); + } + + any_invalidated + } + + fn invalidate_pseudo_element_or_nac( + &mut self, + child: E, + invalidations: &[Invalidation<'b>], + ) -> bool { + let mut sibling_invalidations = InvalidationVector::new(); + + let result = self.invalidate_child( + child, + invalidations, + &mut sibling_invalidations, + DescendantInvalidationKind::Dom, + ); + + // Roots of NAC subtrees can indeed generate sibling invalidations, but + // they can be just ignored, since they have no siblings. + // + // Note that we can end up testing selectors that wouldn't end up + // matching due to this being NAC, like those coming from document + // rules, but we overinvalidate instead of checking this. + + result + } + + /// Invalidate a child and recurse down invalidating its descendants if + /// needed. + fn invalidate_child( + &mut self, + child: E, + invalidations: &[Invalidation<'b>], + sibling_invalidations: &mut InvalidationVector<'b>, + descendant_invalidation_kind: DescendantInvalidationKind, + ) -> bool { + let mut invalidations_for_descendants = DescendantInvalidationLists::default(); + + let mut invalidated_child = false; + let invalidated_descendants = { + let mut child_invalidator = + TreeStyleInvalidator::new(child, self.stack_limit_checker, self.processor); + + invalidated_child |= child_invalidator.process_sibling_invalidations( + &mut invalidations_for_descendants, + sibling_invalidations, + ); + + invalidated_child |= child_invalidator.process_descendant_invalidations( + invalidations, + &mut invalidations_for_descendants, + sibling_invalidations, + descendant_invalidation_kind, + ); + + if invalidated_child { + child_invalidator.processor.invalidated_self(child); + } + + child_invalidator.invalidate_descendants(&invalidations_for_descendants) + }; + + // The child may not be a flattened tree child of the current element, + // but may be arbitrarily deep. + // + // Since we keep the traversal flags in terms of the flattened tree, + // we need to propagate it as appropriate. + if invalidated_child || invalidated_descendants { + self.processor.invalidated_descendants(self.element, child); + } + + invalidated_child || invalidated_descendants + } + + fn invalidate_nac(&mut self, invalidations: &[Invalidation<'b>]) -> bool { + let mut any_nac_root = false; + + let element = self.element; + element.each_anonymous_content_child(|nac| { + any_nac_root |= self.invalidate_pseudo_element_or_nac(nac, invalidations); + }); + + any_nac_root + } + + // NB: It's important that this operates on DOM children, which is what + // selector-matching operates on. + fn invalidate_dom_descendants_of( + &mut self, + parent: E::ConcreteNode, + invalidations: &[Invalidation<'b>], + ) -> bool { + let mut any_descendant = false; + + let mut sibling_invalidations = InvalidationVector::new(); + for child in parent.dom_children() { + let child = match child.as_element() { + Some(e) => e, + None => continue, + }; + + any_descendant |= self.invalidate_child( + child, + invalidations, + &mut sibling_invalidations, + DescendantInvalidationKind::Dom, + ); + } + + any_descendant + } + + fn invalidate_parts_in_shadow_tree( + &mut self, + shadow: <E::ConcreteNode as TNode>::ConcreteShadowRoot, + invalidations: &[Invalidation<'b>], + ) -> bool { + debug_assert!(!invalidations.is_empty()); + + let mut any = false; + let mut sibling_invalidations = InvalidationVector::new(); + + for node in shadow.as_node().dom_descendants() { + let element = match node.as_element() { + Some(e) => e, + None => continue, + }; + + if element.has_part_attr() { + any |= self.invalidate_child( + element, + invalidations, + &mut sibling_invalidations, + DescendantInvalidationKind::Part, + ); + debug_assert!( + sibling_invalidations.is_empty(), + "::part() shouldn't have sibling combinators to the right, \ + this makes no sense! {:?}", + sibling_invalidations + ); + } + + if let Some(shadow) = element.shadow_root() { + if element.exports_any_part() { + any |= self.invalidate_parts_in_shadow_tree(shadow, invalidations) + } + } + } + + any + } + + fn invalidate_parts(&mut self, invalidations: &[Invalidation<'b>]) -> bool { + if invalidations.is_empty() { + return false; + } + + let shadow = match self.element.shadow_root() { + Some(s) => s, + None => return false, + }; + + self.invalidate_parts_in_shadow_tree(shadow, invalidations) + } + + fn invalidate_slotted_elements(&mut self, invalidations: &[Invalidation<'b>]) -> bool { + if invalidations.is_empty() { + return false; + } + + let slot = self.element; + self.invalidate_slotted_elements_in_slot(slot, invalidations) + } + + fn invalidate_slotted_elements_in_slot( + &mut self, + slot: E, + invalidations: &[Invalidation<'b>], + ) -> bool { + let mut any = false; + + let mut sibling_invalidations = InvalidationVector::new(); + for node in slot.slotted_nodes() { + let element = match node.as_element() { + Some(e) => e, + None => continue, + }; + + if element.is_html_slot_element() { + any |= self.invalidate_slotted_elements_in_slot(element, invalidations); + } else { + any |= self.invalidate_child( + element, + invalidations, + &mut sibling_invalidations, + DescendantInvalidationKind::Slotted, + ); + } + + debug_assert!( + sibling_invalidations.is_empty(), + "::slotted() shouldn't have sibling combinators to the right, \ + this makes no sense! {:?}", + sibling_invalidations + ); + } + + any + } + + fn invalidate_non_slotted_descendants(&mut self, invalidations: &[Invalidation<'b>]) -> bool { + if invalidations.is_empty() { + return false; + } + + if self.processor.light_tree_only() { + let node = self.element.as_node(); + return self.invalidate_dom_descendants_of(node, invalidations); + } + + let mut any_descendant = false; + + // NOTE(emilio): This is only needed for Shadow DOM to invalidate + // correctly on :host(..) changes. Instead of doing this, we could add + // a third kind of invalidation list that walks shadow root children, + // but it's not clear it's worth it. + // + // Also, it's needed as of right now for document state invalidation, + // where we rely on iterating every element that ends up in the composed + // doc, but we could fix that invalidating per subtree. + if let Some(root) = self.element.shadow_root() { + any_descendant |= self.invalidate_dom_descendants_of(root.as_node(), invalidations); + } + + if let Some(marker) = self.element.marker_pseudo_element() { + any_descendant |= self.invalidate_pseudo_element_or_nac(marker, invalidations); + } + + if let Some(before) = self.element.before_pseudo_element() { + any_descendant |= self.invalidate_pseudo_element_or_nac(before, invalidations); + } + + let node = self.element.as_node(); + any_descendant |= self.invalidate_dom_descendants_of(node, invalidations); + + if let Some(after) = self.element.after_pseudo_element() { + any_descendant |= self.invalidate_pseudo_element_or_nac(after, invalidations); + } + + any_descendant |= self.invalidate_nac(invalidations); + + any_descendant + } + + /// Given the descendant invalidation lists, go through the current + /// element's descendants, and invalidate style on them. + fn invalidate_descendants(&mut self, invalidations: &DescendantInvalidationLists<'b>) -> bool { + if invalidations.is_empty() { + return false; + } + + debug!( + "StyleTreeInvalidator::invalidate_descendants({:?})", + self.element + ); + debug!(" > {:?}", invalidations); + + let should_process = self.processor.should_process_descendants(self.element); + + if !should_process { + return false; + } + + if let Some(checker) = self.stack_limit_checker { + if checker.limit_exceeded() { + self.processor.recursion_limit_exceeded(self.element); + return true; + } + } + + let mut any_descendant = false; + + any_descendant |= self.invalidate_non_slotted_descendants(&invalidations.dom_descendants); + any_descendant |= self.invalidate_slotted_elements(&invalidations.slotted_descendants); + any_descendant |= self.invalidate_parts(&invalidations.parts); + + any_descendant + } + + /// Process the given sibling invalidations coming from our previous + /// sibling. + /// + /// The sibling invalidations are somewhat special because they can be + /// modified on the fly. New invalidations may be added and removed. + /// + /// In particular, all descendants get the same set of invalidations from + /// the parent, but the invalidations from a given sibling depend on the + /// ones we got from the previous one. + /// + /// Returns whether invalidated the current element's style. + fn process_sibling_invalidations( + &mut self, + descendant_invalidations: &mut DescendantInvalidationLists<'b>, + sibling_invalidations: &mut InvalidationVector<'b>, + ) -> bool { + let mut i = 0; + let mut new_sibling_invalidations = InvalidationVector::new(); + let mut invalidated_self = false; + + while i < sibling_invalidations.len() { + let result = self.process_invalidation( + &sibling_invalidations[i], + descendant_invalidations, + &mut new_sibling_invalidations, + InvalidationKind::Sibling, + ); + + invalidated_self |= result.invalidated_self; + sibling_invalidations[i].matched_by_any_previous |= result.matched; + if sibling_invalidations[i].effective_for_next() { + i += 1; + } else { + sibling_invalidations.remove(i); + } + } + + sibling_invalidations.extend(new_sibling_invalidations.drain(..)); + invalidated_self + } + + /// Process a given invalidation list coming from our parent, + /// adding to `descendant_invalidations` and `sibling_invalidations` as + /// needed. + /// + /// Returns whether our style was invalidated as a result. + fn process_descendant_invalidations( + &mut self, + invalidations: &[Invalidation<'b>], + descendant_invalidations: &mut DescendantInvalidationLists<'b>, + sibling_invalidations: &mut InvalidationVector<'b>, + descendant_invalidation_kind: DescendantInvalidationKind, + ) -> bool { + let mut invalidated = false; + + for invalidation in invalidations { + let result = self.process_invalidation( + invalidation, + descendant_invalidations, + sibling_invalidations, + InvalidationKind::Descendant(descendant_invalidation_kind), + ); + + invalidated |= result.invalidated_self; + if invalidation.effective_for_next() { + let mut invalidation = invalidation.clone(); + invalidation.matched_by_any_previous |= result.matched; + debug_assert_eq!( + descendant_invalidation_kind, + DescendantInvalidationKind::Dom, + "Slotted or part invalidations don't propagate." + ); + descendant_invalidations.dom_descendants.push(invalidation); + } + } + + invalidated + } + + /// Processes a given invalidation, potentially invalidating the style of + /// the current element. + /// + /// Returns whether invalidated the style of the element, and whether the + /// invalidation should be effective to subsequent siblings or descendants + /// down in the tree. + fn process_invalidation( + &mut self, + invalidation: &Invalidation<'b>, + descendant_invalidations: &mut DescendantInvalidationLists<'b>, + sibling_invalidations: &mut InvalidationVector<'b>, + invalidation_kind: InvalidationKind, + ) -> SingleInvalidationResult { + debug!( + "TreeStyleInvalidator::process_invalidation({:?}, {:?}, {:?})", + self.element, invalidation, invalidation_kind + ); + + let matching_result = { + let context = self.processor.matching_context(); + context.current_host = invalidation.scope; + + matches_compound_selector_from( + &invalidation.dependency.selector, + invalidation.offset, + context, + &self.element, + ) + }; + + let next_invalidation = match matching_result { + CompoundSelectorMatchingResult::NotMatched => { + return SingleInvalidationResult { + invalidated_self: false, + matched: false, + } + }, + CompoundSelectorMatchingResult::FullyMatched => { + debug!(" > Invalidation matched completely"); + // We matched completely. If we're an inner selector now we need + // to go outside our selector and carry on invalidating. + let mut cur_dependency = invalidation.dependency; + loop { + cur_dependency = match cur_dependency.parent { + None => { + return SingleInvalidationResult { + invalidated_self: true, + matched: true, + } + }, + Some(ref p) => { + let invalidation_kind = p.invalidation_kind(); + match invalidation_kind { + DependencyInvalidationKind::Normal(_) => &**p, + DependencyInvalidationKind::Relative(kind) => { + self.processor.found_relative_selector_invalidation( + self.element, + kind, + &**p, + ); + return SingleInvalidationResult { + invalidated_self: false, + matched: true, + }; + }, + } + }, + }; + + debug!(" > Checking outer dependency {:?}", cur_dependency); + + // The inner selector changed, now check if the full + // previous part of the selector did, before keeping + // checking for descendants. + if !self + .processor + .check_outer_dependency(cur_dependency, self.element) + { + return SingleInvalidationResult { + invalidated_self: false, + matched: false, + }; + } + + if cur_dependency.normal_invalidation_kind() == + NormalDependencyInvalidationKind::Element + { + continue; + } + + debug!(" > Generating invalidation"); + break Invalidation::new(cur_dependency, invalidation.scope); + } + }, + CompoundSelectorMatchingResult::Matched { + next_combinator_offset, + } => Invalidation { + dependency: invalidation.dependency, + scope: invalidation.scope, + offset: next_combinator_offset + 1, + matched_by_any_previous: false, + }, + }; + + debug_assert_ne!( + next_invalidation.offset, 0, + "Rightmost selectors shouldn't generate more invalidations", + ); + + let mut invalidated_self = false; + let next_combinator = next_invalidation + .dependency + .selector + .combinator_at_parse_order(next_invalidation.offset - 1); + + if matches!(next_combinator, Combinator::PseudoElement) && + self.processor.invalidates_on_pseudo_element() + { + // We need to invalidate the element whenever pseudos change, for + // two reasons: + // + // * Eager pseudo styles are stored as part of the originating + // element's computed style. + // + // * Lazy pseudo-styles might be cached on the originating + // element's pseudo-style cache. + // + // This could be more fine-grained (perhaps with a RESTYLE_PSEUDOS + // hint?). + // + // Note that we'll also restyle the pseudo-element because it would + // match this invalidation. + // + // FIXME: For non-element-backed pseudos this is still not quite + // correct. For example for ::selection even though we invalidate + // the style properly there's nothing that triggers a repaint + // necessarily. Though this matches old Gecko behavior, and the + // ::selection implementation needs to change significantly anyway + // to implement https://github.com/w3c/csswg-drafts/issues/2474 for + // example. + invalidated_self = true; + } + + debug!( + " > Invalidation matched, next: {:?}, ({:?})", + next_invalidation, next_combinator + ); + + let next_invalidation_kind = next_invalidation.kind(); + + // We can skip pushing under some circumstances, and we should + // because otherwise the invalidation list could grow + // exponentially. + // + // * First of all, both invalidations need to be of the same + // kind. This is because of how we propagate them going to + // the right of the tree for sibling invalidations and going + // down the tree for children invalidations. A sibling + // invalidation that ends up generating a children + // invalidation ends up (correctly) in five different lists, + // not in the same list five different times. + // + // * Then, the invalidation needs to be matched by a previous + // ancestor/sibling, in order to know that this invalidation + // has been generated already. + // + // * Finally, the new invalidation needs to be + // `effective_for_next()`, in order for us to know that it is + // still in the list, since we remove the dependencies that + // aren't from the lists for our children / siblings. + // + // To go through an example, let's imagine we are processing a + // dom subtree like: + // + // <div><address><div><div/></div></address></div> + // + // And an invalidation list with a single invalidation like: + // + // [div div div] + // + // When we process the invalidation list for the outer div, we + // match it, and generate a `div div` invalidation, so for the + // <address> child we have: + // + // [div div div, div div] + // + // With the first of them marked as `matched`. + // + // When we process the <address> child, we don't match any of + // them, so both invalidations go untouched to our children. + // + // When we process the second <div>, we match _both_ + // invalidations. + // + // However, when matching the first, we can tell it's been + // matched, and not push the corresponding `div div` + // invalidation, since we know it's necessarily already on the + // list. + // + // Thus, without skipping the push, we'll arrive to the + // innermost <div> with: + // + // [div div div, div div, div div, div] + // + // While skipping it, we won't arrive here with duplicating + // dependencies: + // + // [div div div, div div, div] + // + let can_skip_pushing = next_invalidation_kind == invalidation_kind && + invalidation.matched_by_any_previous && + next_invalidation.effective_for_next(); + + if can_skip_pushing { + debug!( + " > Can avoid push, since the invalidation had \ + already been matched before" + ); + } else { + match next_invalidation_kind { + InvalidationKind::Descendant(DescendantInvalidationKind::Dom) => { + descendant_invalidations + .dom_descendants + .push(next_invalidation); + }, + InvalidationKind::Descendant(DescendantInvalidationKind::Part) => { + descendant_invalidations.parts.push(next_invalidation); + }, + InvalidationKind::Descendant(DescendantInvalidationKind::Slotted) => { + descendant_invalidations + .slotted_descendants + .push(next_invalidation); + }, + InvalidationKind::Sibling => { + sibling_invalidations.push(next_invalidation); + }, + } + } + + SingleInvalidationResult { + invalidated_self, + matched: true, + } + } +} diff --git a/servo/components/style/invalidation/element/mod.rs b/servo/components/style/invalidation/element/mod.rs new file mode 100644 index 0000000000..0ddb9f1863 --- /dev/null +++ b/servo/components/style/invalidation/element/mod.rs @@ -0,0 +1,13 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Invalidation of element styles due to attribute or style changes. + +pub mod document_state; +pub mod element_wrapper; +pub mod invalidation_map; +pub mod invalidator; +pub mod relative_selector; +pub mod restyle_hints; +pub mod state_and_attributes; diff --git a/servo/components/style/invalidation/element/relative_selector.rs b/servo/components/style/invalidation/element/relative_selector.rs new file mode 100644 index 0000000000..ccb48e349f --- /dev/null +++ b/servo/components/style/invalidation/element/relative_selector.rs @@ -0,0 +1,1164 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Invalidation of element styles relative selectors. + +use crate::data::ElementData; +use crate::dom::{TElement, TNode}; +use crate::gecko_bindings::structs::ServoElementSnapshotTable; +use crate::invalidation::element::element_wrapper::ElementWrapper; +use crate::invalidation::element::invalidation_map::{ + Dependency, DependencyInvalidationKind, NormalDependencyInvalidationKind, + RelativeDependencyInvalidationKind, RelativeSelectorInvalidationMap, +}; +use crate::invalidation::element::invalidator::{ + DescendantInvalidationLists, Invalidation, InvalidationProcessor, InvalidationResult, + InvalidationVector, SiblingTraversalMap, TreeStyleInvalidator, +}; +use crate::invalidation::element::restyle_hints::RestyleHint; +use crate::invalidation::element::state_and_attributes::{ + check_dependency, dependency_may_be_relevant, invalidated_descendants, invalidated_self, + invalidated_sibling, push_invalidation, should_process_descendants, +}; +use crate::stylist::{CascadeData, Stylist}; +use dom::ElementState; +use fxhash::FxHashMap; +use selectors::matching::{ + matches_compound_selector_from, matches_selector, CompoundSelectorMatchingResult, + ElementSelectorFlags, MatchingContext, MatchingForInvalidation, MatchingMode, + NeedsSelectorFlags, QuirksMode, SelectorCaches, VisitedHandlingMode, +}; +use selectors::parser::{Combinator, SelectorKey}; +use selectors::OpaqueElement; +use smallvec::SmallVec; +use std::ops::DerefMut; + +/// Kind of DOM mutation this relative selector invalidation is being carried out in. +#[derive(Clone, Copy)] +pub enum DomMutationOperation { + /// Insertion operation, can cause side effect, but presumed already happened. + Insert, + /// Append operation, cannot cause side effect. + Append, + /// Removal operation, can cause side effect, but presumed already happened. Sibling relationships are destroyed. + Remove, + /// Invalidating for side effect of a DOM operation, for the previous sibling. + SideEffectPrevSibling, + /// Invalidating for side effect of a DOM operation, for the next sibling. + SideEffectNextSibling, +} + +impl DomMutationOperation { + fn accept<E: TElement>(&self, d: &Dependency, e: E) -> bool { + match self { + Self::Insert | Self::Append | Self::Remove => { + e.relative_selector_search_direction().is_some() + }, + // `:has(+ .a + .b)` with `.anchor + .a + .remove + .b` - `.a` would be present + // in the search path. + Self::SideEffectPrevSibling => { + e.relative_selector_search_direction().is_some() && + d.right_combinator_is_next_sibling() + }, + // If an element is being removed and would cause next-sibling match to happen, + // e.g. `:has(+ .a)` with `.anchor + .remove + .a`, `.a` isn't yet searched + // for relative selector matching. + Self::SideEffectNextSibling => d.dependency_is_relative_with_single_next_sibling(), + } + } + + fn is_side_effect(&self) -> bool { + match self { + Self::Insert | Self::Append | Self::Remove => false, + Self::SideEffectPrevSibling | Self::SideEffectNextSibling => true, + } + } +} + +/// Context required to try and optimize away relative dependencies. +struct OptimizationContext<'a, E: TElement> { + sibling_traversal_map: &'a SiblingTraversalMap<E>, + quirks_mode: QuirksMode, + operation: DomMutationOperation, +} + +impl<'a, E: TElement> OptimizationContext<'a, E> { + fn can_be_ignored( + &self, + is_subtree: bool, + element: E, + host: Option<OpaqueElement>, + dependency: &Dependency, + ) -> bool { + if is_subtree { + // Subtree elements don't have unaffected sibling to look at. + return false; + } + debug_assert!( + matches!( + dependency.invalidation_kind(), + DependencyInvalidationKind::Relative(..) + ), + "Non-relative selector being evaluated for optimization" + ); + // This optimization predecates on the fact that there may be a sibling that can readily + // "take over" this element. + let sibling = match self.sibling_traversal_map.prev_sibling_for(&element) { + None => { + if matches!(self.operation, DomMutationOperation::Append) { + return false; + } + match self.sibling_traversal_map.next_sibling_for(&element) { + Some(s) => s, + None => return false, + } + }, + Some(s) => s, + }; + { + // Run through the affected compund. + let mut iter = dependency.selector.iter_from(dependency.selector_offset); + while let Some(c) = iter.next() { + if c.has_indexed_selector_in_subject() { + // We do not calculate indices during invalidation as they're wasteful - as a side effect, + // such selectors always return true, breaking this optimization. Note that we only check + // this compound only because the check to skip compares against this element's sibling. + // i.e. Given `:has(:nth-child(2) .foo)`, we'd try to find `.foo`'s sibling, which + // shares `:nth-child` up the selector. + return false; + } + } + } + let is_rightmost = dependency.selector_offset == 0; + if !is_rightmost { + let combinator = dependency + .selector + .combinator_at_match_order(dependency.selector_offset - 1); + if combinator.is_ancestor() { + // We can safely ignore these, since we're about to traverse the + // rest of the affected tree anyway to find the rightmost invalidated element. + return true; + } + if combinator.is_sibling() && matches!(self.operation, DomMutationOperation::Append) { + // If we're in the subtree, same argument applies as ancestor combinator case. + // If we're at the top of the DOM tree being mutated, we can ignore it if the + // operation is append - we know we'll cover all the later siblings and their descendants. + return true; + } + } + let mut caches = SelectorCaches::default(); + let mut matching_context = MatchingContext::new( + MatchingMode::Normal, + None, + &mut caches, + self.quirks_mode, + NeedsSelectorFlags::No, + MatchingForInvalidation::Yes, + ); + matching_context.current_host = host; + let sibling_matches = matches_selector( + &dependency.selector, + dependency.selector_offset, + None, + &sibling, + &mut matching_context, + ); + if sibling_matches { + // Remember that at this point, we know that the combinator to the right of this + // compound is a sibling combinator. Effectively, we've found a standin for the + // element we're mutating. + // e.g. Given `:has(... .a ~ .b ...)`, we're the mutating element matching `... .a`, + // if we find a sibling that matches the `... .a`, it can stand in for us. + debug_assert!(dependency.parent.is_some(), "No relative selector outer dependency?"); + return dependency.parent.as_ref().map_or(false, |par| { + // ... However, if the standin sibling can be the anchor, we can't skip it, since + // that sibling should be invlidated to become the anchor. + !matches_selector( + &par.selector, + par.selector_offset, + None, + &sibling, + &mut matching_context + ) + }); + } + // Ok, there's no standin element - but would this element have matched the upstream + // selector anyway? If we don't, either the match exists somewhere far from us + // (In which case our mutation doesn't really matter), or it doesn't exist at all, + // so we can just skip the invalidation. + let (combinator, prev_offset) = { + let mut iter = dependency.selector.iter_from(dependency.selector_offset); + let mut o = dependency.selector_offset; + while iter.next().is_some() { + o += 1; + } + let combinator = iter.next_sequence(); + o += 1; + debug_assert!( + combinator.is_some(), + "Should at least see a relative combinator" + ); + (combinator.unwrap(), o) + }; + if combinator.is_sibling() { + if prev_offset >= dependency.selector.len() - 1 { + // Hit the relative combinator - we don't have enough information to + // see if there's going to be a downstream match. + return false; + } + if matches!(self.operation, DomMutationOperation::Remove) { + // This is sad :( The sibling relation of a removed element is lost, and we don't + // propagate sibling traversal map to selector matching context, so we need to do + // manual matching here. TODO(dshin): Worth changing selector matching for this? + + // Try matching this compound, then... + // Note: We'll not hit the leftmost sequence (Since we would have returned early + // if we'd hit the relative selector anchor). + if matches!( + matches_compound_selector_from( + &dependency.selector, + dependency.selector.len() - prev_offset + 1, + &mut matching_context, + &element + ), + CompoundSelectorMatchingResult::NotMatched + ) { + return true; + } + + // ... Match the rest of the selector, manually traversing. + let mut prev_sibling = self.sibling_traversal_map.prev_sibling_for(&element); + while let Some(sib) = prev_sibling { + if matches_selector( + &dependency.selector, + prev_offset, + None, + &sib, + &mut matching_context, + ) { + return false; + } + if matches!(combinator, Combinator::NextSibling) { + break; + } + prev_sibling = self.sibling_traversal_map.prev_sibling_for(&sib); + } + return true; + } + } + !matches_selector( + &dependency.selector, + dependency.selector_offset, + None, + &element, + &mut matching_context, + ) + } +} + +/// Overall invalidator for handling relative selector invalidations. +pub struct RelativeSelectorInvalidator<'a, 'b, E> +where + E: TElement + 'a, +{ + /// Element triggering the invalidation. + pub element: E, + /// Quirks mode of the current invalidation. + pub quirks_mode: QuirksMode, + /// Snapshot containing changes to invalidate against. + /// Can be None if it's a DOM mutation. + pub snapshot_table: Option<&'b ServoElementSnapshotTable>, + /// Callback to trigger when the subject element is invalidated. + pub invalidated: fn(E, &InvalidationResult), + /// The traversal map that should be used to process invalidations. + pub sibling_traversal_map: SiblingTraversalMap<E>, + /// Marker for 'a lifetime. + pub _marker: ::std::marker::PhantomData<&'a ()>, +} + +struct RelativeSelectorInvalidation<'a> { + host: Option<OpaqueElement>, + kind: RelativeDependencyInvalidationKind, + dependency: &'a Dependency, +} + +type ElementDependencies<'a> = SmallVec<[(Option<OpaqueElement>, &'a Dependency); 1]>; +type Dependencies<'a, E> = SmallVec<[(E, ElementDependencies<'a>); 1]>; +type AlreadyInvalidated<'a, E> = SmallVec<[(E, Option<OpaqueElement>, &'a Dependency); 2]>; + +/// Interface for collecting relative selector dependencies. +pub struct RelativeSelectorDependencyCollector<'a, E> +where + E: TElement, +{ + /// Dependencies that need to run through the normal invalidation that may generate + /// a relative selector invalidation. + dependencies: FxHashMap<E, ElementDependencies<'a>>, + /// Dependencies that created an invalidation right away. + invalidations: AlreadyInvalidated<'a, E>, + /// The top element in the subtree being invalidated. + top: E, + /// Optional context that will be used to try and skip invalidations + /// by running selector matches. + optimization_context: Option<OptimizationContext<'a, E>>, +} + +type Invalidations<'a> = SmallVec<[RelativeSelectorInvalidation<'a>; 1]>; +type InnerInvalidations<'a, E> = SmallVec<[(E, RelativeSelectorInvalidation<'a>); 1]>; + +struct ToInvalidate<'a, E: TElement + 'a> { + /// Dependencies to run through normal invalidator. + dependencies: Dependencies<'a, E>, + /// Dependencies already invalidated. + invalidations: Invalidations<'a>, +} + +impl<'a, E: TElement + 'a> Default for ToInvalidate<'a, E> { + fn default() -> Self { + Self { + dependencies: Dependencies::default(), + invalidations: Invalidations::default(), + } + } +} + +fn dependency_selectors_match(a: &Dependency, b: &Dependency) -> bool { + if a.invalidation_kind() != b.invalidation_kind() { + return false; + } + if SelectorKey::new(&a.selector) != SelectorKey::new(&b.selector) { + return false; + } + let mut a_parent = a.parent.as_ref(); + let mut b_parent = b.parent.as_ref(); + while let (Some(a_p), Some(b_p)) = (a_parent, b_parent) { + if SelectorKey::new(&a_p.selector) != SelectorKey::new(&b_p.selector) { + return false; + } + a_parent = a_p.parent.as_ref(); + b_parent = b_p.parent.as_ref(); + } + a_parent.is_none() && b_parent.is_none() +} + +impl<'a, E> RelativeSelectorDependencyCollector<'a, E> +where + E: TElement, +{ + fn new(top: E, optimization_context: Option<OptimizationContext<'a, E>>) -> Self { + Self { + dependencies: FxHashMap::default(), + invalidations: AlreadyInvalidated::default(), + top, + optimization_context, + } + } + + fn insert_invalidation( + &mut self, + element: E, + dependency: &'a Dependency, + host: Option<OpaqueElement>, + ) { + match self + .invalidations + .iter_mut() + .find(|(_, _, d)| dependency_selectors_match(dependency, d)) + { + Some((e, h, d)) => { + // Just keep one. + if d.selector_offset > dependency.selector_offset { + (*e, *h, *d) = (element, host, dependency); + } + }, + None => { + self.invalidations.push((element, host, dependency)); + }, + } + } + + /// Add this dependency, if it is unique (i.e. Different outer dependency or same outer dependency + /// but requires a different invalidation traversal). + pub fn add_dependency( + &mut self, + dependency: &'a Dependency, + element: E, + host: Option<OpaqueElement>, + ) { + match dependency.invalidation_kind() { + DependencyInvalidationKind::Normal(..) => { + self.dependencies + .entry(element) + .and_modify(|v| v.push((host, dependency))) + .or_default() + .push((host, dependency)); + }, + DependencyInvalidationKind::Relative(kind) => { + debug_assert!( + dependency.parent.is_some(), + "Orphaned inner relative selector?" + ); + if element != self.top && + matches!( + kind, + RelativeDependencyInvalidationKind::Parent | + RelativeDependencyInvalidationKind::PrevSibling | + RelativeDependencyInvalidationKind::EarlierSibling + ) + { + return; + } + self.insert_invalidation(element, dependency, host); + }, + }; + } + + /// Get the dependencies in a list format. + fn get(self) -> ToInvalidate<'a, E> { + let mut result = ToInvalidate::default(); + for (element, host, dependency) in self.invalidations { + match dependency.invalidation_kind() { + DependencyInvalidationKind::Normal(_) => { + unreachable!("Inner selector in invalidation?") + }, + DependencyInvalidationKind::Relative(kind) => { + if let Some(context) = self.optimization_context.as_ref() { + if context.can_be_ignored(element != self.top, element, host, dependency) { + continue; + } + } + let dependency = dependency.parent.as_ref().unwrap(); + result.invalidations.push(RelativeSelectorInvalidation { + kind, + host, + dependency, + }); + // We move the invalidation up to the top of the subtree to avoid unnecessary traveral, but + // this means that we need to take ancestor-earlier sibling invalidations into account, as + // they'd look into earlier siblings of the top of the subtree as well. + if element != self.top && + matches!( + kind, + RelativeDependencyInvalidationKind::AncestorEarlierSibling | + RelativeDependencyInvalidationKind::AncestorPrevSibling + ) + { + result.invalidations.push(RelativeSelectorInvalidation { + kind: if matches!( + kind, + RelativeDependencyInvalidationKind::AncestorPrevSibling + ) { + RelativeDependencyInvalidationKind::PrevSibling + } else { + RelativeDependencyInvalidationKind::EarlierSibling + }, + host, + dependency, + }); + } + }, + }; + } + for (key, element_dependencies) in self.dependencies { + // At least for now, we don't try to optimize away dependencies emitted from nested selectors. + result.dependencies.push((key, element_dependencies)); + } + result + } + + fn collect_all_dependencies_for_element( + &mut self, + element: E, + scope: Option<OpaqueElement>, + quirks_mode: QuirksMode, + map: &'a RelativeSelectorInvalidationMap, + operation: DomMutationOperation, + ) { + element + .id() + .map(|v| match map.map.id_to_selector.get(v, quirks_mode) { + Some(v) => { + for dependency in v { + if !operation.accept(dependency, element) { + continue; + } + self.add_dependency(dependency, element, scope); + } + }, + None => (), + }); + element.each_class(|v| match map.map.class_to_selector.get(v, quirks_mode) { + Some(v) => { + for dependency in v { + if !operation.accept(dependency, element) { + continue; + } + self.add_dependency(dependency, element, scope); + } + }, + None => (), + }); + element.each_attr_name( + |v| match map.map.other_attribute_affecting_selectors.get(v) { + Some(v) => { + for dependency in v { + if !operation.accept(dependency, element) { + continue; + } + self.add_dependency(dependency, element, scope); + } + }, + None => (), + }, + ); + let state = element.state(); + map.map.state_affecting_selectors.lookup_with_additional( + element, + quirks_mode, + None, + &[], + ElementState::empty(), + |dependency| { + if !dependency.state.intersects(state) { + return true; + } + if !operation.accept(&dependency.dep, element) { + return true; + } + self.add_dependency(&dependency.dep, element, scope); + true + }, + ); + + if let Some(v) = map.type_to_selector.get(element.local_name()) { + for dependency in v { + if !operation.accept(dependency, element) { + continue; + } + self.add_dependency(dependency, element, scope); + } + } + + for dependency in &map.any_to_selector { + if !operation.accept(dependency, element) { + continue; + } + self.add_dependency(dependency, element, scope); + } + } + + fn is_empty(&self) -> bool { + self.invalidations.is_empty() && self.dependencies.is_empty() + } +} + +impl<'a, 'b, E> RelativeSelectorInvalidator<'a, 'b, E> +where + E: TElement + 'a, +{ + /// Gather relative selector dependencies for the given element, and invalidate as necessary. + #[inline(never)] + pub fn invalidate_relative_selectors_for_this<F>( + self, + stylist: &'a Stylist, + mut gather_dependencies: F, + ) where + F: FnMut( + &E, + Option<OpaqueElement>, + &'a CascadeData, + QuirksMode, + &mut RelativeSelectorDependencyCollector<'a, E>, + ), + { + let mut collector = RelativeSelectorDependencyCollector::new(self.element, None); + stylist.for_each_cascade_data_with_scope(self.element, |data, scope| { + let map = data.relative_selector_invalidation_map(); + if !map.used { + return; + } + gather_dependencies( + &self.element, + scope.map(|e| e.opaque()), + data, + self.quirks_mode, + &mut collector, + ); + }); + if collector.is_empty() { + return; + } + self.invalidate_from_dependencies(collector.get()); + } + + /// Gather relative selector dependencies for the given element (And its subtree) that mutated, and invalidate as necessary. + #[inline(never)] + pub fn invalidate_relative_selectors_for_dom_mutation( + self, + subtree: bool, + stylist: &'a Stylist, + inherited_search_path: ElementSelectorFlags, + operation: DomMutationOperation, + ) { + let mut collector = RelativeSelectorDependencyCollector::new( + self.element, + if operation.is_side_effect() { + None + } else { + Some(OptimizationContext { + sibling_traversal_map: &self.sibling_traversal_map, + quirks_mode: self.quirks_mode, + operation, + }) + }, + ); + let mut traverse_subtree = false; + self.element.apply_selector_flags(inherited_search_path); + stylist.for_each_cascade_data_with_scope(self.element, |data, scope| { + let map = data.relative_selector_invalidation_map(); + if !map.used { + return; + } + traverse_subtree |= map.needs_ancestors_traversal; + collector.collect_all_dependencies_for_element( + self.element, + scope.map(|e| e.opaque()), + self.quirks_mode, + map, + operation, + ); + }); + + if subtree && traverse_subtree { + for node in self.element.as_node().dom_descendants() { + let descendant = match node.as_element() { + Some(e) => e, + None => continue, + }; + descendant.apply_selector_flags(inherited_search_path); + stylist.for_each_cascade_data_with_scope(descendant, |data, scope| { + let map = data.relative_selector_invalidation_map(); + if !map.used { + return; + } + collector.collect_all_dependencies_for_element( + descendant, + scope.map(|e| e.opaque()), + self.quirks_mode, + map, + operation, + ); + }); + } + } + if collector.is_empty() { + return; + } + self.invalidate_from_dependencies(collector.get()); + } + + /// Carry out complete invalidation triggered by a relative selector invalidation. + fn invalidate_from_dependencies(&self, to_invalidate: ToInvalidate<'a, E>) { + for (element, dependencies) in to_invalidate.dependencies { + let mut selector_caches = SelectorCaches::default(); + let mut processor = RelativeSelectorInnerInvalidationProcessor::new( + self.quirks_mode, + self.snapshot_table, + &dependencies, + &mut selector_caches, + &self.sibling_traversal_map, + ); + TreeStyleInvalidator::new(element, None, &mut processor).invalidate(); + for (element, invalidation) in processor.take_invalidations() { + self.invalidate_upwards(element, &invalidation); + } + } + for invalidation in to_invalidate.invalidations { + self.invalidate_upwards(self.element, &invalidation); + } + } + + fn invalidate_upwards(&self, element: E, invalidation: &RelativeSelectorInvalidation<'a>) { + // This contains the main reason for why relative selector invalidation is handled + // separately - It travels ancestor and/or earlier sibling direction. + match invalidation.kind { + RelativeDependencyInvalidationKind::Parent => { + element.parent_element().map(|e| { + if !Self::in_search_direction( + &e, + ElementSelectorFlags::RELATIVE_SELECTOR_SEARCH_DIRECTION_ANCESTOR, + ) { + return; + } + self.handle_anchor(e, invalidation.dependency, invalidation.host); + }); + }, + RelativeDependencyInvalidationKind::Ancestors => { + let mut parent = element.parent_element(); + while let Some(par) = parent { + if !Self::in_search_direction( + &par, + ElementSelectorFlags::RELATIVE_SELECTOR_SEARCH_DIRECTION_ANCESTOR, + ) { + return; + } + self.handle_anchor(par, invalidation.dependency, invalidation.host); + parent = par.parent_element(); + } + }, + RelativeDependencyInvalidationKind::PrevSibling => { + self.sibling_traversal_map + .prev_sibling_for(&element) + .map(|e| { + if !Self::in_search_direction( + &e, + ElementSelectorFlags::RELATIVE_SELECTOR_SEARCH_DIRECTION_SIBLING, + ) { + return; + } + self.handle_anchor(e, invalidation.dependency, invalidation.host); + }); + }, + RelativeDependencyInvalidationKind::AncestorPrevSibling => { + let mut parent = element.parent_element(); + while let Some(par) = parent { + if !Self::in_search_direction( + &par, + ElementSelectorFlags::RELATIVE_SELECTOR_SEARCH_DIRECTION_ANCESTOR, + ) { + return; + } + par.prev_sibling_element().map(|e| { + if !Self::in_search_direction( + &e, + ElementSelectorFlags::RELATIVE_SELECTOR_SEARCH_DIRECTION_SIBLING, + ) { + return; + } + self.handle_anchor(e, invalidation.dependency, invalidation.host); + }); + parent = par.parent_element(); + } + }, + RelativeDependencyInvalidationKind::EarlierSibling => { + let mut sibling = self.sibling_traversal_map.prev_sibling_for(&element); + while let Some(sib) = sibling { + if !Self::in_search_direction( + &sib, + ElementSelectorFlags::RELATIVE_SELECTOR_SEARCH_DIRECTION_SIBLING, + ) { + return; + } + self.handle_anchor(sib, invalidation.dependency, invalidation.host); + sibling = sib.prev_sibling_element(); + } + }, + RelativeDependencyInvalidationKind::AncestorEarlierSibling => { + let mut parent = element.parent_element(); + while let Some(par) = parent { + if !Self::in_search_direction( + &par, + ElementSelectorFlags::RELATIVE_SELECTOR_SEARCH_DIRECTION_ANCESTOR, + ) { + return; + } + let mut sibling = par.prev_sibling_element(); + while let Some(sib) = sibling { + if !Self::in_search_direction( + &sib, + ElementSelectorFlags::RELATIVE_SELECTOR_SEARCH_DIRECTION_SIBLING, + ) { + return; + } + self.handle_anchor(sib, invalidation.dependency, invalidation.host); + sibling = sib.prev_sibling_element(); + } + parent = par.parent_element(); + } + }, + } + } + + /// Is this element in the direction of the given relative selector search path? + fn in_search_direction(element: &E, desired: ElementSelectorFlags) -> bool { + if let Some(direction) = element.relative_selector_search_direction() { + direction.intersects(desired) + } else { + false + } + } + + /// Handle a potential relative selector anchor. + fn handle_anchor( + &self, + element: E, + outer_dependency: &Dependency, + host: Option<OpaqueElement>, + ) { + let is_rightmost = Self::is_subject(outer_dependency); + if (is_rightmost && + !element.has_selector_flags(ElementSelectorFlags::ANCHORS_RELATIVE_SELECTOR)) || + (!is_rightmost && + !element.has_selector_flags( + ElementSelectorFlags::ANCHORS_RELATIVE_SELECTOR_NON_SUBJECT, + )) + { + // If it was never a relative selector anchor, don't bother. + return; + } + let mut selector_caches = SelectorCaches::default(); + let matching_context = MatchingContext::<'_, E::Impl>::new_for_visited( + MatchingMode::Normal, + None, + &mut selector_caches, + VisitedHandlingMode::AllLinksVisitedAndUnvisited, + self.quirks_mode, + NeedsSelectorFlags::No, + MatchingForInvalidation::Yes, + ); + let mut data = match element.mutate_data() { + Some(data) => data, + None => return, + }; + let mut processor = RelativeSelectorOuterInvalidationProcessor { + element, + host, + data: data.deref_mut(), + dependency: &*outer_dependency, + matching_context, + traversal_map: &self.sibling_traversal_map, + }; + let result = TreeStyleInvalidator::new(element, None, &mut processor).invalidate(); + (self.invalidated)(element, &result); + } + + /// Does this relative selector dependency have its relative selector in the subject position? + fn is_subject(outer_dependency: &Dependency) -> bool { + debug_assert!( + matches!( + outer_dependency.invalidation_kind(), + DependencyInvalidationKind::Normal(_) + ), + "Outer selector of relative selector is relative?" + ); + + if let Some(p) = outer_dependency.parent.as_ref() { + if !Self::is_subject(p.as_ref()) { + // Not subject in outer selector. + return false; + } + } + outer_dependency.selector.is_rightmost(outer_dependency.selector_offset) + } +} + +/// Blindly invalidate everything outside of a relative selector. +/// Consider `:is(.a :has(.b) .c ~ .d) ~ .e .f`, where .b gets deleted. +/// Since the tree mutated, we cannot rely on snapshots. +pub struct RelativeSelectorOuterInvalidationProcessor<'a, 'b, E: TElement> { + /// Element being invalidated. + pub element: E, + /// The current shadow host, if any. + pub host: Option<OpaqueElement>, + /// Data for the element being invalidated. + pub data: &'a mut ElementData, + /// Dependency to be processed. + pub dependency: &'b Dependency, + /// Matching context to use for invalidation. + pub matching_context: MatchingContext<'a, E::Impl>, + /// Traversal map for this invalidation. + pub traversal_map: &'a SiblingTraversalMap<E>, +} + +impl<'a, 'b: 'a, E: 'a> InvalidationProcessor<'b, 'a, E> + for RelativeSelectorOuterInvalidationProcessor<'a, 'b, E> +where + E: TElement, +{ + fn invalidates_on_pseudo_element(&self) -> bool { + true + } + + fn check_outer_dependency(&mut self, _dependency: &Dependency, _element: E) -> bool { + // At this point, we know a relative selector invalidated, and are ignoring them. + true + } + + fn matching_context(&mut self) -> &mut MatchingContext<'a, E::Impl> { + &mut self.matching_context + } + + fn sibling_traversal_map(&self) -> &SiblingTraversalMap<E> { + self.traversal_map + } + + fn collect_invalidations( + &mut self, + element: E, + _self_invalidations: &mut InvalidationVector<'b>, + descendant_invalidations: &mut DescendantInvalidationLists<'b>, + sibling_invalidations: &mut InvalidationVector<'b>, + ) -> bool { + debug_assert_eq!(element, self.element); + debug_assert!( + self.matching_context.matching_for_invalidation(), + "Not matching for invalidation?" + ); + + // Ok, this element can potentially an anchor to the given dependency. + // Before we do the potentially-costly ancestor/earlier sibling traversal, + // See if it can actuall be an anchor by trying to match the "rest" of the selector + // outside and to the left of `:has` in question. + // e.g. Element under consideration can only be the anchor to `:has` in + // `.foo .bar ~ .baz:has()`, iff it matches `.foo .bar ~ .baz`. + let invalidated_self = { + let mut d = self.dependency; + loop { + debug_assert!( + matches!( + d.invalidation_kind(), + DependencyInvalidationKind::Normal(_) + ), + "Unexpected outer relative dependency" + ); + if !dependency_may_be_relevant(d, &element, false) { + break false; + } + if !matches_selector( + &d.selector, + d.selector_offset, + None, + &element, + self.matching_context(), + ) { + break false; + } + let invalidation_kind = d.normal_invalidation_kind(); + if matches!(invalidation_kind, NormalDependencyInvalidationKind::Element) { + if let Some(ref parent) = d.parent { + d = parent; + continue; + } + break true; + } + debug_assert_ne!(d.selector_offset, 0); + debug_assert_ne!(d.selector_offset, d.selector.len()); + let invalidation = Invalidation::new(&d, self.host); + break push_invalidation( + invalidation, + invalidation_kind, + descendant_invalidations, + sibling_invalidations + ); + } + }; + + if invalidated_self { + self.data.hint.insert(RestyleHint::RESTYLE_SELF); + } + invalidated_self + } + + fn should_process_descendants(&mut self, element: E) -> bool { + if element == self.element { + return should_process_descendants(&self.data); + } + + match element.borrow_data() { + Some(d) => should_process_descendants(&d), + None => return false, + } + } + + fn recursion_limit_exceeded(&mut self, _element: E) { + unreachable!("Unexpected recursion limit"); + } + + fn invalidated_descendants(&mut self, element: E, child: E) { + invalidated_descendants(element, child) + } + + fn invalidated_self(&mut self, element: E) { + debug_assert_ne!(element, self.element); + invalidated_self(element); + } + + fn invalidated_sibling(&mut self, element: E, of: E) { + debug_assert_ne!(element, self.element); + invalidated_sibling(element, of); + } +} + +/// Invalidation for the selector(s) inside a relative selector. +pub struct RelativeSelectorInnerInvalidationProcessor<'a, 'b, 'c, E> +where + E: TElement + 'a, +{ + /// Matching context to be used. + matching_context: MatchingContext<'b, E::Impl>, + /// Table of snapshots. + snapshot_table: Option<&'c ServoElementSnapshotTable>, + /// Incoming dependencies to be processed. + dependencies: &'c ElementDependencies<'a>, + /// Generated invalidations. + invalidations: InnerInvalidations<'a, E>, + /// Traversal map for this invalidation. + traversal_map: &'b SiblingTraversalMap<E>, +} + +impl<'a, 'b, 'c, E> RelativeSelectorInnerInvalidationProcessor<'a, 'b, 'c, E> +where + E: TElement + 'a, +{ + fn new( + quirks_mode: QuirksMode, + snapshot_table: Option<&'c ServoElementSnapshotTable>, + dependencies: &'c ElementDependencies<'a>, + selector_caches: &'b mut SelectorCaches, + traversal_map: &'b SiblingTraversalMap<E>, + ) -> Self { + let matching_context = MatchingContext::new_for_visited( + MatchingMode::Normal, + None, + selector_caches, + VisitedHandlingMode::AllLinksVisitedAndUnvisited, + quirks_mode, + NeedsSelectorFlags::No, + MatchingForInvalidation::Yes, + ); + Self { + matching_context, + snapshot_table, + dependencies, + invalidations: InnerInvalidations::default(), + traversal_map, + } + } + + fn note_dependency( + &mut self, + element: E, + scope: Option<OpaqueElement>, + dependency: &'a Dependency, + descendant_invalidations: &mut DescendantInvalidationLists<'a>, + sibling_invalidations: &mut InvalidationVector<'a>, + ) { + match dependency.invalidation_kind() { + DependencyInvalidationKind::Normal(_) => (), + DependencyInvalidationKind::Relative(kind) => { + self.found_relative_selector_invalidation(element, kind, dependency); + return; + }, + } + if matches!( + dependency.normal_invalidation_kind(), + NormalDependencyInvalidationKind::Element + ) { + // Ok, keep heading outwards. + debug_assert!( + dependency.parent.is_some(), + "Orphaned inner selector dependency?" + ); + if let Some(parent) = dependency.parent.as_ref() { + self.note_dependency( + element, + scope, + parent, + descendant_invalidations, + sibling_invalidations, + ); + } + return; + } + let invalidation = Invalidation::new(&dependency, scope); + match dependency.normal_invalidation_kind() { + NormalDependencyInvalidationKind::Descendants => { + // Descendant invalidations are simplified due to pseudo-elements not being available within the relative selector. + descendant_invalidations.dom_descendants.push(invalidation) + }, + NormalDependencyInvalidationKind::Siblings => sibling_invalidations.push(invalidation), + _ => unreachable!(), + } + } + + /// Take the generated invalidations. + fn take_invalidations(self) -> InnerInvalidations<'a, E> { + self.invalidations + } +} + +impl<'a, 'b, 'c, E> InvalidationProcessor<'a, 'b, E> + for RelativeSelectorInnerInvalidationProcessor<'a, 'b, 'c, E> +where + E: TElement + 'a, +{ + fn check_outer_dependency(&mut self, dependency: &Dependency, element: E) -> bool { + if let Some(snapshot_table) = self.snapshot_table { + let wrapper = ElementWrapper::new(element, snapshot_table); + return check_dependency(dependency, &element, &wrapper, &mut self.matching_context); + } + // Just invalidate if we don't have a snapshot. + true + } + + fn matching_context(&mut self) -> &mut MatchingContext<'b, E::Impl> { + return &mut self.matching_context; + } + + fn collect_invalidations( + &mut self, + element: E, + _self_invalidations: &mut InvalidationVector<'a>, + descendant_invalidations: &mut DescendantInvalidationLists<'a>, + sibling_invalidations: &mut InvalidationVector<'a>, + ) -> bool { + for (scope, dependency) in self.dependencies { + self.note_dependency( + element, + *scope, + dependency, + descendant_invalidations, + sibling_invalidations, + ) + } + false + } + + fn should_process_descendants(&mut self, _element: E) -> bool { + true + } + + fn recursion_limit_exceeded(&mut self, _element: E) { + unreachable!("Unexpected recursion limit"); + } + + // Don't do anything for normal invalidations. + fn invalidated_self(&mut self, _element: E) {} + fn invalidated_sibling(&mut self, _sibling: E, _of: E) {} + fn invalidated_descendants(&mut self, _element: E, _child: E) {} + + fn found_relative_selector_invalidation( + &mut self, + element: E, + kind: RelativeDependencyInvalidationKind, + dep: &'a Dependency, + ) { + debug_assert!(dep.parent.is_some(), "Orphaned inners selector?"); + if element.relative_selector_search_direction().is_none() { + return; + } + self.invalidations.push(( + element, + RelativeSelectorInvalidation { + host: self.matching_context.current_host, + kind, + dependency: dep.parent.as_ref().unwrap(), + }, + )); + } + + fn sibling_traversal_map(&self) -> &SiblingTraversalMap<E> { + &self.traversal_map + } +} diff --git a/servo/components/style/invalidation/element/restyle_hints.rs b/servo/components/style/invalidation/element/restyle_hints.rs new file mode 100644 index 0000000000..fe89636e88 --- /dev/null +++ b/servo/components/style/invalidation/element/restyle_hints.rs @@ -0,0 +1,191 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Restyle hints: an optimization to avoid unnecessarily matching selectors. + +use crate::traversal_flags::TraversalFlags; + +bitflags! { + /// The kind of restyle we need to do for a given element. + #[repr(C)] + #[derive(Clone, Copy, Debug)] + pub struct RestyleHint: u16 { + /// Do a selector match of the element. + const RESTYLE_SELF = 1 << 0; + + /// Do a selector match of the element's pseudo-elements. Always to be combined with + /// RESTYLE_SELF. + const RESTYLE_PSEUDOS = 1 << 1; + + /// Do a selector match if the element is a pseudo-element. + const RESTYLE_SELF_IF_PSEUDO = 1 << 2; + + /// Do a selector match of the element's descendants. + const RESTYLE_DESCENDANTS = 1 << 3; + + /// Recascade the current element. + const RECASCADE_SELF = 1 << 4; + + /// Recascade the current element if it inherits any reset style. + const RECASCADE_SELF_IF_INHERIT_RESET_STYLE = 1 << 5; + + /// Recascade all descendant elements. + const RECASCADE_DESCENDANTS = 1 << 6; + + /// Replace the style data coming from CSS transitions without updating + /// any other style data. This hint is only processed in animation-only + /// traversal which is prior to normal traversal. + const RESTYLE_CSS_TRANSITIONS = 1 << 7; + + /// Replace the style data coming from CSS animations without updating + /// any other style data. This hint is only processed in animation-only + /// traversal which is prior to normal traversal. + const RESTYLE_CSS_ANIMATIONS = 1 << 8; + + /// Don't re-run selector-matching on the element, only the style + /// attribute has changed, and this change didn't have any other + /// dependencies. + const RESTYLE_STYLE_ATTRIBUTE = 1 << 9; + + /// Replace the style data coming from SMIL animations without updating + /// any other style data. This hint is only processed in animation-only + /// traversal which is prior to normal traversal. + const RESTYLE_SMIL = 1 << 10; + } +} + +impl RestyleHint { + /// Creates a new `RestyleHint` indicating that the current element and all + /// its descendants must be fully restyled. + pub fn restyle_subtree() -> Self { + RestyleHint::RESTYLE_SELF | RestyleHint::RESTYLE_DESCENDANTS + } + + /// Creates a new `RestyleHint` indicating that the current element and all + /// its descendants must be recascaded. + pub fn recascade_subtree() -> Self { + RestyleHint::RECASCADE_SELF | RestyleHint::RECASCADE_DESCENDANTS + } + + /// Returns whether this hint invalidates the element and all its + /// descendants. + pub fn contains_subtree(&self) -> bool { + self.contains(Self::restyle_subtree()) + } + + /// Returns whether we'll recascade all of the descendants. + pub fn will_recascade_subtree(&self) -> bool { + self.contains_subtree() || self.contains(Self::recascade_subtree()) + } + + /// Returns whether we need to restyle this element. + pub fn has_non_animation_invalidations(&self) -> bool { + !(*self & !Self::for_animations()).is_empty() + } + + /// Propagates this restyle hint to a child element. + pub fn propagate(&mut self, traversal_flags: &TraversalFlags) -> Self { + use std::mem; + + // In the middle of an animation only restyle, we don't need to + // propagate any restyle hints, and we need to remove ourselves. + if traversal_flags.for_animation_only() { + self.remove_animation_hints(); + return Self::empty(); + } + + debug_assert!( + !self.has_animation_hint(), + "There should not be any animation restyle hints \ + during normal traversal" + ); + + // Else we should clear ourselves, and return the propagated hint. + mem::replace(self, Self::empty()).propagate_for_non_animation_restyle() + } + + /// Returns a new `RestyleHint` appropriate for children of the current element. + fn propagate_for_non_animation_restyle(&self) -> Self { + if self.contains(RestyleHint::RESTYLE_DESCENDANTS) { + return Self::restyle_subtree(); + } + let mut result = Self::empty(); + if self.contains(RestyleHint::RESTYLE_PSEUDOS) { + result |= Self::RESTYLE_SELF_IF_PSEUDO; + } + if self.contains(RestyleHint::RECASCADE_DESCENDANTS) { + result |= Self::recascade_subtree(); + } + result + } + + /// Returns a hint that contains all the replacement hints. + pub fn replacements() -> Self { + RestyleHint::RESTYLE_STYLE_ATTRIBUTE | Self::for_animations() + } + + /// The replacements for the animation cascade levels. + #[inline] + pub fn for_animations() -> Self { + RestyleHint::RESTYLE_SMIL | + RestyleHint::RESTYLE_CSS_ANIMATIONS | + RestyleHint::RESTYLE_CSS_TRANSITIONS + } + + /// Returns whether the hint specifies that an animation cascade level must + /// be replaced. + #[inline] + pub fn has_animation_hint(&self) -> bool { + self.intersects(Self::for_animations()) + } + + /// Returns whether the hint specifies that an animation cascade level must + /// be replaced. + #[inline] + pub fn has_animation_hint_or_recascade(&self) -> bool { + self.intersects( + Self::for_animations() | + Self::RECASCADE_SELF | + Self::RECASCADE_SELF_IF_INHERIT_RESET_STYLE, + ) + } + + /// Returns whether the hint specifies some restyle work other than an + /// animation cascade level replacement. + #[inline] + pub fn has_non_animation_hint(&self) -> bool { + !(*self & !Self::for_animations()).is_empty() + } + + /// Returns whether the hint specifies that some cascade levels must be + /// replaced. + #[inline] + pub fn has_replacements(&self) -> bool { + self.intersects(Self::replacements()) + } + + /// Removes all of the animation-related hints. + #[inline] + pub fn remove_animation_hints(&mut self) { + self.remove(Self::for_animations()); + + // While RECASCADE_SELF is not animation-specific, we only ever add and process it during + // traversal. If we are here, removing animation hints, then we are in an animation-only + // traversal, and we know that any RECASCADE_SELF flag must have been set due to changes in + // inherited values after restyling for animations, and thus we want to remove it so that + // we don't later try to restyle the element during a normal restyle. + // (We could have separate RECASCADE_SELF_NORMAL and RECASCADE_SELF_ANIMATIONS flags to + // make it clear, but this isn't currently necessary.) + self.remove(Self::RECASCADE_SELF | Self::RECASCADE_SELF_IF_INHERIT_RESET_STYLE); + } +} + +impl Default for RestyleHint { + fn default() -> Self { + Self::empty() + } +} + +#[cfg(feature = "servo")] +malloc_size_of_is_0!(RestyleHint); diff --git a/servo/components/style/invalidation/element/state_and_attributes.rs b/servo/components/style/invalidation/element/state_and_attributes.rs new file mode 100644 index 0000000000..1c58cddf1e --- /dev/null +++ b/servo/components/style/invalidation/element/state_and_attributes.rs @@ -0,0 +1,601 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! An invalidation processor for style changes due to state and attribute +//! changes. + +use crate::context::SharedStyleContext; +use crate::data::ElementData; +use crate::dom::{TElement, TNode}; +use crate::invalidation::element::element_wrapper::{ElementSnapshot, ElementWrapper}; +use crate::invalidation::element::invalidation_map::*; +use crate::invalidation::element::invalidator::{ + DescendantInvalidationLists, InvalidationVector, SiblingTraversalMap, +}; +use crate::invalidation::element::invalidator::{Invalidation, InvalidationProcessor}; +use crate::invalidation::element::restyle_hints::RestyleHint; +use crate::selector_map::SelectorMap; +use crate::selector_parser::Snapshot; +use crate::stylesheets::origin::OriginSet; +use crate::{Atom, WeakAtom}; +use dom::ElementState; +use selectors::attr::CaseSensitivity; +use selectors::matching::{ + matches_selector, MatchingContext, MatchingForInvalidation, MatchingMode, NeedsSelectorFlags, + SelectorCaches, VisitedHandlingMode, +}; +use smallvec::SmallVec; + +/// The collector implementation. +struct Collector<'a, 'b: 'a, 'selectors: 'a, E> +where + E: TElement, +{ + element: E, + wrapper: ElementWrapper<'b, E>, + snapshot: &'a Snapshot, + matching_context: &'a mut MatchingContext<'b, E::Impl>, + lookup_element: E, + removed_id: Option<&'a WeakAtom>, + added_id: Option<&'a WeakAtom>, + classes_removed: &'a SmallVec<[Atom; 8]>, + classes_added: &'a SmallVec<[Atom; 8]>, + state_changes: ElementState, + descendant_invalidations: &'a mut DescendantInvalidationLists<'selectors>, + sibling_invalidations: &'a mut InvalidationVector<'selectors>, + invalidates_self: bool, +} + +/// An invalidation processor for style changes due to state and attribute +/// changes. +pub struct StateAndAttrInvalidationProcessor<'a, 'b: 'a, E: TElement> { + shared_context: &'a SharedStyleContext<'b>, + element: E, + data: &'a mut ElementData, + matching_context: MatchingContext<'a, E::Impl>, + traversal_map: SiblingTraversalMap<E>, +} + +impl<'a, 'b: 'a, E: TElement + 'b> StateAndAttrInvalidationProcessor<'a, 'b, E> { + /// Creates a new StateAndAttrInvalidationProcessor. + pub fn new( + shared_context: &'a SharedStyleContext<'b>, + element: E, + data: &'a mut ElementData, + selector_caches: &'a mut SelectorCaches, + ) -> Self { + let matching_context = MatchingContext::new_for_visited( + MatchingMode::Normal, + None, + selector_caches, + VisitedHandlingMode::AllLinksVisitedAndUnvisited, + shared_context.quirks_mode(), + NeedsSelectorFlags::No, + MatchingForInvalidation::Yes, + ); + + Self { + shared_context, + element, + data, + matching_context, + traversal_map: SiblingTraversalMap::default(), + } + } +} + +/// Checks a dependency against a given element and wrapper, to see if something +/// changed. +pub fn check_dependency<E, W>( + dependency: &Dependency, + element: &E, + wrapper: &W, + context: &mut MatchingContext<'_, E::Impl>, +) -> bool +where + E: TElement, + W: selectors::Element<Impl = E::Impl>, +{ + let matches_now = matches_selector( + &dependency.selector, + dependency.selector_offset, + None, + element, + context, + ); + + let matched_then = matches_selector( + &dependency.selector, + dependency.selector_offset, + None, + wrapper, + context, + ); + + matched_then != matches_now +} + +/// Whether we should process the descendants of a given element for style +/// invalidation. +pub fn should_process_descendants(data: &ElementData) -> bool { + !data.styles.is_display_none() && !data.hint.contains(RestyleHint::RESTYLE_DESCENDANTS) +} + +/// Propagates the bits after invalidating a descendant child. +pub fn propagate_dirty_bit_up_to<E>(ancestor: E, child: E) +where + E: TElement, +{ + // The child may not be a flattened tree child of the current element, + // but may be arbitrarily deep. + // + // Since we keep the traversal flags in terms of the flattened tree, + // we need to propagate it as appropriate. + let mut current = child.traversal_parent(); + while let Some(parent) = current.take() { + unsafe { parent.set_dirty_descendants() }; + current = parent.traversal_parent(); + + if parent == ancestor { + return; + } + } + debug_assert!( + false, + "Should've found {:?} as an ancestor of {:?}", + ancestor, child + ); +} + +/// Propagates the bits after invalidating a descendant child, if needed. +pub fn invalidated_descendants<E>(element: E, child: E) +where + E: TElement, +{ + if !child.has_data() { + return; + } + propagate_dirty_bit_up_to(element, child) +} + +/// Sets the appropriate restyle hint after invalidating the style of a given +/// element. +pub fn invalidated_self<E>(element: E) -> bool +where + E: TElement, +{ + let mut data = match element.mutate_data() { + Some(data) => data, + None => return false, + }; + data.hint.insert(RestyleHint::RESTYLE_SELF); + true +} + +/// Sets the appropriate hint after invalidating the style of a sibling. +pub fn invalidated_sibling<E>(element: E, of: E) +where + E: TElement, +{ + debug_assert_eq!( + element.as_node().parent_node(), + of.as_node().parent_node(), + "Should be siblings" + ); + if !invalidated_self(element) { + return; + } + if element.traversal_parent() != of.traversal_parent() { + let parent = element.as_node().parent_element_or_host(); + debug_assert!( + parent.is_some(), + "How can we have siblings without parent nodes?" + ); + if let Some(e) = parent { + propagate_dirty_bit_up_to(e, element) + } + } +} + +impl<'a, 'b: 'a, E: 'a> InvalidationProcessor<'a, 'a, E> + for StateAndAttrInvalidationProcessor<'a, 'b, E> +where + E: TElement, +{ + /// We need to invalidate style on pseudo-elements, in order to process + /// changes that could otherwise end up in ::before or ::after content being + /// generated, and invalidate lazy pseudo caches. + fn invalidates_on_pseudo_element(&self) -> bool { + true + } + + fn check_outer_dependency(&mut self, dependency: &Dependency, element: E) -> bool { + // We cannot assert about `element` having a snapshot here (in fact it + // most likely won't), because it may be an arbitrary descendant or + // later-sibling of the element we started invalidating with. + let wrapper = ElementWrapper::new(element, &*self.shared_context.snapshot_map); + check_dependency(dependency, &element, &wrapper, &mut self.matching_context) + } + + fn matching_context(&mut self) -> &mut MatchingContext<'a, E::Impl> { + &mut self.matching_context + } + + fn sibling_traversal_map(&self) -> &SiblingTraversalMap<E> { + &self.traversal_map + } + + fn collect_invalidations( + &mut self, + element: E, + _self_invalidations: &mut InvalidationVector<'a>, + descendant_invalidations: &mut DescendantInvalidationLists<'a>, + sibling_invalidations: &mut InvalidationVector<'a>, + ) -> bool { + debug_assert_eq!(element, self.element); + debug_assert!(element.has_snapshot(), "Why bothering?"); + + let wrapper = ElementWrapper::new(element, &*self.shared_context.snapshot_map); + + let state_changes = wrapper.state_changes(); + let Some(snapshot) = wrapper.snapshot() else { + return false; + }; + + if !snapshot.has_attrs() && state_changes.is_empty() { + return false; + } + + let mut classes_removed = SmallVec::<[Atom; 8]>::new(); + let mut classes_added = SmallVec::<[Atom; 8]>::new(); + if snapshot.class_changed() { + // TODO(emilio): Do this more efficiently! + snapshot.each_class(|c| { + if !element.has_class(c, CaseSensitivity::CaseSensitive) { + classes_removed.push(c.0.clone()) + } + }); + + element.each_class(|c| { + if !snapshot.has_class(c, CaseSensitivity::CaseSensitive) { + classes_added.push(c.0.clone()) + } + }) + } + + let mut id_removed = None; + let mut id_added = None; + if snapshot.id_changed() { + let old_id = snapshot.id_attr(); + let current_id = element.id(); + + if old_id != current_id { + id_removed = old_id; + id_added = current_id; + } + } + + if log_enabled!(::log::Level::Debug) { + debug!("Collecting changes for: {:?}", element); + if !state_changes.is_empty() { + debug!(" > state: {:?}", state_changes); + } + if snapshot.id_changed() { + debug!(" > id changed: +{:?} -{:?}", id_added, id_removed); + } + if snapshot.class_changed() { + debug!( + " > class changed: +{:?} -{:?}", + classes_added, classes_removed + ); + } + let mut attributes_changed = false; + snapshot.each_attr_changed(|_| { + attributes_changed = true; + }); + if attributes_changed { + debug!( + " > attributes changed, old: {}", + snapshot.debug_list_attributes() + ) + } + } + + let lookup_element = if element.implemented_pseudo_element().is_some() { + element.pseudo_element_originating_element().unwrap() + } else { + element + }; + + let mut shadow_rule_datas = SmallVec::<[_; 3]>::new(); + let matches_document_author_rules = + element.each_applicable_non_document_style_rule_data(|data, host| { + shadow_rule_datas.push((data, host.opaque())) + }); + + let invalidated_self = { + let mut collector = Collector { + wrapper, + lookup_element, + state_changes, + element, + snapshot: &snapshot, + matching_context: &mut self.matching_context, + removed_id: id_removed, + added_id: id_added, + classes_removed: &classes_removed, + classes_added: &classes_added, + descendant_invalidations, + sibling_invalidations, + invalidates_self: false, + }; + + let document_origins = if !matches_document_author_rules { + OriginSet::ORIGIN_USER_AGENT | OriginSet::ORIGIN_USER + } else { + OriginSet::all() + }; + + for (cascade_data, origin) in self.shared_context.stylist.iter_origins() { + if document_origins.contains(origin.into()) { + collector + .collect_dependencies_in_invalidation_map(cascade_data.invalidation_map()); + } + } + + for &(ref data, ref host) in &shadow_rule_datas { + collector.matching_context.current_host = Some(host.clone()); + collector.collect_dependencies_in_invalidation_map(data.invalidation_map()); + } + + collector.invalidates_self + }; + + // If we generated a ton of descendant invalidations, it's probably not + // worth to go ahead and try to process them. + // + // Just restyle the descendants directly. + // + // This number is completely made-up, but the page that made us add this + // code generated 1960+ invalidations (bug 1420741). + // + // We don't look at slotted_descendants because those don't propagate + // down more than one level anyway. + if descendant_invalidations.dom_descendants.len() > 150 { + self.data.hint.insert(RestyleHint::RESTYLE_DESCENDANTS); + } + + if invalidated_self { + self.data.hint.insert(RestyleHint::RESTYLE_SELF); + } + + invalidated_self + } + + fn should_process_descendants(&mut self, element: E) -> bool { + if element == self.element { + return should_process_descendants(&self.data); + } + + match element.borrow_data() { + Some(d) => should_process_descendants(&d), + None => return false, + } + } + + fn recursion_limit_exceeded(&mut self, element: E) { + if element == self.element { + self.data.hint.insert(RestyleHint::RESTYLE_DESCENDANTS); + return; + } + + if let Some(mut data) = element.mutate_data() { + data.hint.insert(RestyleHint::RESTYLE_DESCENDANTS); + } + } + + fn invalidated_descendants(&mut self, element: E, child: E) { + invalidated_descendants(element, child) + } + + fn invalidated_self(&mut self, element: E) { + debug_assert_ne!(element, self.element); + invalidated_self(element); + } + + fn invalidated_sibling(&mut self, element: E, of: E) { + debug_assert_ne!(element, self.element); + invalidated_sibling(element, of); + } +} + +impl<'a, 'b, 'selectors, E> Collector<'a, 'b, 'selectors, E> +where + E: TElement, + 'selectors: 'a, +{ + fn collect_dependencies_in_invalidation_map(&mut self, map: &'selectors InvalidationMap) { + let quirks_mode = self.matching_context.quirks_mode(); + let removed_id = self.removed_id; + if let Some(ref id) = removed_id { + if let Some(deps) = map.id_to_selector.get(id, quirks_mode) { + for dep in deps { + self.scan_dependency(dep); + } + } + } + + let added_id = self.added_id; + if let Some(ref id) = added_id { + if let Some(deps) = map.id_to_selector.get(id, quirks_mode) { + for dep in deps { + self.scan_dependency(dep); + } + } + } + + for class in self.classes_added.iter().chain(self.classes_removed.iter()) { + if let Some(deps) = map.class_to_selector.get(class, quirks_mode) { + for dep in deps { + self.scan_dependency(dep); + } + } + } + + self.snapshot.each_attr_changed(|attribute| { + if let Some(deps) = map.other_attribute_affecting_selectors.get(attribute) { + for dep in deps { + self.scan_dependency(dep); + } + } + }); + + self.collect_state_dependencies(&map.state_affecting_selectors) + } + + fn collect_state_dependencies(&mut self, map: &'selectors SelectorMap<StateDependency>) { + if self.state_changes.is_empty() { + return; + } + map.lookup_with_additional( + self.lookup_element, + self.matching_context.quirks_mode(), + self.removed_id, + self.classes_removed, + self.state_changes, + |dependency| { + if !dependency.state.intersects(self.state_changes) { + return true; + } + self.scan_dependency(&dependency.dep); + true + }, + ); + } + + /// Check whether a dependency should be taken into account. + #[inline] + fn check_dependency(&mut self, dependency: &Dependency) -> bool { + check_dependency( + dependency, + &self.element, + &self.wrapper, + &mut self.matching_context, + ) + } + + fn scan_dependency(&mut self, dependency: &'selectors Dependency) { + debug_assert!( + matches!( + dependency.invalidation_kind(), + DependencyInvalidationKind::Normal(_) + ), + "Found relative selector dependency" + ); + debug!( + "TreeStyleInvalidator::scan_dependency({:?}, {:?})", + self.element, dependency + ); + + if !self.dependency_may_be_relevant(dependency) { + return; + } + + if self.check_dependency(dependency) { + return self.note_dependency(dependency); + } + } + + fn note_dependency(&mut self, dependency: &'selectors Dependency) { + debug_assert!(self.dependency_may_be_relevant(dependency)); + + let invalidation_kind = dependency.normal_invalidation_kind(); + if matches!(invalidation_kind, NormalDependencyInvalidationKind::Element) { + if let Some(ref parent) = dependency.parent { + // We know something changed in the inner selector, go outwards + // now. + self.scan_dependency(parent); + } else { + self.invalidates_self = true; + } + return; + } + + debug_assert_ne!(dependency.selector_offset, 0); + debug_assert_ne!(dependency.selector_offset, dependency.selector.len()); + + let invalidation = + Invalidation::new(&dependency, self.matching_context.current_host.clone()); + + self.invalidates_self |= push_invalidation( + invalidation, + invalidation_kind, + self.descendant_invalidations, + self.sibling_invalidations, + ); + } + + /// Returns whether `dependency` may cause us to invalidate the style of + /// more elements than what we've already invalidated. + fn dependency_may_be_relevant(&self, dependency: &Dependency) -> bool { + match dependency.normal_invalidation_kind() { + NormalDependencyInvalidationKind::Element => !self.invalidates_self, + NormalDependencyInvalidationKind::SlottedElements => { + self.element.is_html_slot_element() + }, + NormalDependencyInvalidationKind::Parts => self.element.shadow_root().is_some(), + NormalDependencyInvalidationKind::ElementAndDescendants | + NormalDependencyInvalidationKind::Siblings | + NormalDependencyInvalidationKind::Descendants => true, + } + } +} + +pub(crate) fn push_invalidation<'a>( + invalidation: Invalidation<'a>, + invalidation_kind: NormalDependencyInvalidationKind, + descendant_invalidations: &mut DescendantInvalidationLists<'a>, + sibling_invalidations: &mut InvalidationVector<'a>, +) -> bool { + match invalidation_kind { + NormalDependencyInvalidationKind::Element => unreachable!(), + NormalDependencyInvalidationKind::ElementAndDescendants => { + descendant_invalidations.dom_descendants.push(invalidation); + true + }, + NormalDependencyInvalidationKind::Descendants => { + descendant_invalidations.dom_descendants.push(invalidation); + false + }, + NormalDependencyInvalidationKind::Siblings => { + sibling_invalidations.push(invalidation); + false + }, + NormalDependencyInvalidationKind::Parts => { + descendant_invalidations.parts.push(invalidation); + false + }, + NormalDependencyInvalidationKind::SlottedElements => { + descendant_invalidations + .slotted_descendants + .push(invalidation); + false + }, + } +} + +pub(crate) fn dependency_may_be_relevant<E: TElement>( + dependency: &Dependency, + element: &E, + already_invalidated_self: bool, +) -> bool { + match dependency.normal_invalidation_kind() { + NormalDependencyInvalidationKind::Element => !already_invalidated_self, + NormalDependencyInvalidationKind::SlottedElements => element.is_html_slot_element(), + NormalDependencyInvalidationKind::Parts => element.shadow_root().is_some(), + NormalDependencyInvalidationKind::ElementAndDescendants | + NormalDependencyInvalidationKind::Siblings | + NormalDependencyInvalidationKind::Descendants => true, + } +} diff --git a/servo/components/style/invalidation/media_queries.rs b/servo/components/style/invalidation/media_queries.rs new file mode 100644 index 0000000000..6928b29d3d --- /dev/null +++ b/servo/components/style/invalidation/media_queries.rs @@ -0,0 +1,130 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Code related to the invalidation of media-query-affected rules. + +use crate::context::QuirksMode; +use crate::media_queries::Device; +use crate::shared_lock::SharedRwLockReadGuard; +use crate::stylesheets::{DocumentRule, ImportRule, MediaRule}; +use crate::stylesheets::{NestedRuleIterationCondition, StylesheetContents, SupportsRule}; +use fxhash::FxHashSet; + +/// A key for a given media query result. +/// +/// NOTE: It happens to be the case that all the media lists we care about +/// happen to have a stable address, so we can just use an opaque pointer to +/// represent them. +/// +/// Also, note that right now when a rule or stylesheet is removed, we do a full +/// style flush, so there's no need to worry about other item created with the +/// same pointer address. +/// +/// If this changes, though, we may need to remove the item from the cache if +/// present before it goes away. +#[derive(Clone, Copy, Debug, Eq, Hash, MallocSizeOf, PartialEq)] +pub struct MediaListKey(usize); + +impl MediaListKey { + /// Create a MediaListKey from a raw usize. + pub fn from_raw(k: usize) -> Self { + MediaListKey(k) + } +} + +/// A trait to get a given `MediaListKey` for a given item that can hold a +/// `MediaList`. +pub trait ToMediaListKey: Sized { + /// Get a `MediaListKey` for this item. This key needs to uniquely identify + /// the item. + fn to_media_list_key(&self) -> MediaListKey { + MediaListKey(self as *const Self as usize) + } +} + +impl ToMediaListKey for StylesheetContents {} +impl ToMediaListKey for ImportRule {} +impl ToMediaListKey for MediaRule {} + +/// A struct that holds the result of a media query evaluation pass for the +/// media queries that evaluated successfully. +#[derive(Clone, Debug, MallocSizeOf, PartialEq)] +pub struct EffectiveMediaQueryResults { + /// The set of media lists that matched last time. + set: FxHashSet<MediaListKey>, +} + +impl EffectiveMediaQueryResults { + /// Trivially constructs an empty `EffectiveMediaQueryResults`. + pub fn new() -> Self { + Self { + set: FxHashSet::default(), + } + } + + /// Resets the results, using an empty key. + pub fn clear(&mut self) { + self.set.clear() + } + + /// Returns whether a given item was known to be effective when the results + /// were cached. + pub fn was_effective<T>(&self, item: &T) -> bool + where + T: ToMediaListKey, + { + self.set.contains(&item.to_media_list_key()) + } + + /// Notices that an effective item has been seen, and caches it as matching. + pub fn saw_effective<T>(&mut self, item: &T) + where + T: ToMediaListKey, + { + // NOTE(emilio): We can't assert that we don't cache the same item twice + // because of stylesheet reusing... shrug. + self.set.insert(item.to_media_list_key()); + } +} + +/// A filter that filters over effective rules, but allowing all potentially +/// effective `@media` rules. +pub struct PotentiallyEffectiveMediaRules; + +impl NestedRuleIterationCondition for PotentiallyEffectiveMediaRules { + fn process_import( + _: &SharedRwLockReadGuard, + _: &Device, + _: QuirksMode, + _: &ImportRule, + ) -> bool { + true + } + + fn process_media(_: &SharedRwLockReadGuard, _: &Device, _: QuirksMode, _: &MediaRule) -> bool { + true + } + + /// Whether we should process the nested rules in a given `@-moz-document` rule. + fn process_document( + guard: &SharedRwLockReadGuard, + device: &Device, + quirks_mode: QuirksMode, + rule: &DocumentRule, + ) -> bool { + use crate::stylesheets::EffectiveRules; + EffectiveRules::process_document(guard, device, quirks_mode, rule) + } + + /// Whether we should process the nested rules in a given `@supports` rule. + fn process_supports( + guard: &SharedRwLockReadGuard, + device: &Device, + quirks_mode: QuirksMode, + rule: &SupportsRule, + ) -> bool { + use crate::stylesheets::EffectiveRules; + EffectiveRules::process_supports(guard, device, quirks_mode, rule) + } +} diff --git a/servo/components/style/invalidation/mod.rs b/servo/components/style/invalidation/mod.rs new file mode 100644 index 0000000000..12b0d06853 --- /dev/null +++ b/servo/components/style/invalidation/mod.rs @@ -0,0 +1,10 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Different bits of code related to invalidating style. + +pub mod element; +pub mod media_queries; +pub mod stylesheets; +pub mod viewport_units; diff --git a/servo/components/style/invalidation/stylesheets.rs b/servo/components/style/invalidation/stylesheets.rs new file mode 100644 index 0000000000..d845897aa4 --- /dev/null +++ b/servo/components/style/invalidation/stylesheets.rs @@ -0,0 +1,651 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! A collection of invalidations due to changes in which stylesheets affect a +//! document. + +#![deny(unsafe_code)] + +use crate::context::QuirksMode; +use crate::dom::{TDocument, TElement, TNode}; +use crate::invalidation::element::element_wrapper::{ElementSnapshot, ElementWrapper}; +use crate::invalidation::element::restyle_hints::RestyleHint; +use crate::media_queries::Device; +use crate::selector_map::{MaybeCaseInsensitiveHashMap, PrecomputedHashMap}; +use crate::selector_parser::{SelectorImpl, Snapshot, SnapshotMap}; +use crate::shared_lock::SharedRwLockReadGuard; +use crate::stylesheets::{CssRule, StylesheetInDocument}; +use crate::stylesheets::{EffectiveRules, EffectiveRulesIterator}; +use crate::values::AtomIdent; +use crate::LocalName as SelectorLocalName; +use crate::{Atom, ShrinkIfNeeded}; +use selectors::parser::{Component, LocalName, Selector}; + +/// The kind of change that happened for a given rule. +#[repr(u32)] +#[derive(Clone, Copy, Debug, Eq, Hash, MallocSizeOf, PartialEq)] +pub enum RuleChangeKind { + /// The rule was inserted. + Insertion, + /// The rule was removed. + Removal, + /// Some change in the rule which we don't know about, and could have made + /// the rule change in any way. + Generic, + /// A change in the declarations of a style rule. + StyleRuleDeclarations, +} + +/// A style sheet invalidation represents a kind of element or subtree that may +/// need to be restyled. Whether it represents a whole subtree or just a single +/// element is determined by the given InvalidationKind in +/// StylesheetInvalidationSet's maps. +#[derive(Debug, Eq, Hash, MallocSizeOf, PartialEq)] +enum Invalidation { + /// An element with a given id. + ID(AtomIdent), + /// An element with a given class name. + Class(AtomIdent), + /// An element with a given local name. + LocalName { + name: SelectorLocalName, + lower_name: SelectorLocalName, + }, +} + +impl Invalidation { + fn is_id(&self) -> bool { + matches!(*self, Invalidation::ID(..)) + } + + fn is_id_or_class(&self) -> bool { + matches!(*self, Invalidation::ID(..) | Invalidation::Class(..)) + } +} + +/// Whether we should invalidate just the element, or the whole subtree within +/// it. +#[derive(Clone, Copy, Debug, Eq, MallocSizeOf, Ord, PartialEq, PartialOrd)] +enum InvalidationKind { + None = 0, + Element, + Scope, +} + +impl std::ops::BitOrAssign for InvalidationKind { + #[inline] + fn bitor_assign(&mut self, other: Self) { + *self = std::cmp::max(*self, other); + } +} + +impl InvalidationKind { + #[inline] + fn is_scope(self) -> bool { + matches!(self, Self::Scope) + } + + #[inline] + fn add(&mut self, other: Option<&InvalidationKind>) { + if let Some(other) = other { + *self |= *other; + } + } +} + +/// A set of invalidations due to stylesheet additions. +/// +/// TODO(emilio): We might be able to do the same analysis for media query +/// changes too (or even selector changes?). +#[derive(Debug, Default, MallocSizeOf)] +pub struct StylesheetInvalidationSet { + classes: MaybeCaseInsensitiveHashMap<Atom, InvalidationKind>, + ids: MaybeCaseInsensitiveHashMap<Atom, InvalidationKind>, + local_names: PrecomputedHashMap<SelectorLocalName, InvalidationKind>, + fully_invalid: bool, +} + +impl StylesheetInvalidationSet { + /// Create an empty `StylesheetInvalidationSet`. + pub fn new() -> Self { + Default::default() + } + + /// Mark the DOM tree styles' as fully invalid. + pub fn invalidate_fully(&mut self) { + debug!("StylesheetInvalidationSet::invalidate_fully"); + self.clear(); + self.fully_invalid = true; + } + + fn shrink_if_needed(&mut self) { + if self.fully_invalid { + return; + } + self.classes.shrink_if_needed(); + self.ids.shrink_if_needed(); + self.local_names.shrink_if_needed(); + } + + /// Analyze the given stylesheet, and collect invalidations from their + /// rules, in order to avoid doing a full restyle when we style the document + /// next time. + pub fn collect_invalidations_for<S>( + &mut self, + device: &Device, + stylesheet: &S, + guard: &SharedRwLockReadGuard, + ) where + S: StylesheetInDocument, + { + debug!("StylesheetInvalidationSet::collect_invalidations_for"); + if self.fully_invalid { + debug!(" > Fully invalid already"); + return; + } + + if !stylesheet.enabled() || !stylesheet.is_effective_for_device(device, guard) { + debug!(" > Stylesheet was not effective"); + return; // Nothing to do here. + } + + let quirks_mode = device.quirks_mode(); + for rule in stylesheet.effective_rules(device, guard) { + self.collect_invalidations_for_rule( + rule, + guard, + device, + quirks_mode, + /* is_generic_change = */ false, + ); + if self.fully_invalid { + break; + } + } + + self.shrink_if_needed(); + + debug!(" > resulting class invalidations: {:?}", self.classes); + debug!(" > resulting id invalidations: {:?}", self.ids); + debug!( + " > resulting local name invalidations: {:?}", + self.local_names + ); + debug!(" > fully_invalid: {}", self.fully_invalid); + } + + /// Clears the invalidation set, invalidating elements as needed if + /// `document_element` is provided. + /// + /// Returns true if any invalidations ocurred. + pub fn flush<E>(&mut self, document_element: Option<E>, snapshots: Option<&SnapshotMap>) -> bool + where + E: TElement, + { + debug!( + "Stylist::flush({:?}, snapshots: {})", + document_element, + snapshots.is_some() + ); + let have_invalidations = match document_element { + Some(e) => self.process_invalidations(e, snapshots), + None => false, + }; + self.clear(); + have_invalidations + } + + /// Returns whether there's no invalidation to process. + pub fn is_empty(&self) -> bool { + !self.fully_invalid && + self.classes.is_empty() && + self.ids.is_empty() && + self.local_names.is_empty() + } + + fn invalidation_kind_for<E>( + &self, + element: E, + snapshot: Option<&Snapshot>, + quirks_mode: QuirksMode, + ) -> InvalidationKind + where + E: TElement, + { + debug_assert!(!self.fully_invalid); + + let mut kind = InvalidationKind::None; + + if !self.classes.is_empty() { + element.each_class(|c| { + kind.add(self.classes.get(c, quirks_mode)); + }); + + if kind.is_scope() { + return kind; + } + + if let Some(snapshot) = snapshot { + snapshot.each_class(|c| { + kind.add(self.classes.get(c, quirks_mode)); + }); + + if kind.is_scope() { + return kind; + } + } + } + + if !self.ids.is_empty() { + if let Some(ref id) = element.id() { + kind.add(self.ids.get(id, quirks_mode)); + if kind.is_scope() { + return kind; + } + } + + if let Some(ref old_id) = snapshot.and_then(|s| s.id_attr()) { + kind.add(self.ids.get(old_id, quirks_mode)); + if kind.is_scope() { + return kind; + } + } + } + + if !self.local_names.is_empty() { + kind.add(self.local_names.get(element.local_name())); + } + + kind + } + + /// Clears the invalidation set without processing. + pub fn clear(&mut self) { + self.classes.clear(); + self.ids.clear(); + self.local_names.clear(); + self.fully_invalid = false; + debug_assert!(self.is_empty()); + } + + fn process_invalidations<E>(&self, element: E, snapshots: Option<&SnapshotMap>) -> bool + where + E: TElement, + { + debug!("Stylist::process_invalidations({:?}, {:?})", element, self); + + { + let mut data = match element.mutate_data() { + Some(data) => data, + None => return false, + }; + + if self.fully_invalid { + debug!("process_invalidations: fully_invalid({:?})", element); + data.hint.insert(RestyleHint::restyle_subtree()); + return true; + } + } + + if self.is_empty() { + debug!("process_invalidations: empty invalidation set"); + return false; + } + + let quirks_mode = element.as_node().owner_doc().quirks_mode(); + self.process_invalidations_in_subtree(element, snapshots, quirks_mode) + } + + /// Process style invalidations in a given subtree. This traverses the + /// subtree looking for elements that match the invalidations in our hash + /// map members. + /// + /// Returns whether it invalidated at least one element's style. + #[allow(unsafe_code)] + fn process_invalidations_in_subtree<E>( + &self, + element: E, + snapshots: Option<&SnapshotMap>, + quirks_mode: QuirksMode, + ) -> bool + where + E: TElement, + { + debug!("process_invalidations_in_subtree({:?})", element); + let mut data = match element.mutate_data() { + Some(data) => data, + None => return false, + }; + + if !data.has_styles() { + return false; + } + + if data.hint.contains_subtree() { + debug!( + "process_invalidations_in_subtree: {:?} was already invalid", + element + ); + return false; + } + + let element_wrapper = snapshots.map(|s| ElementWrapper::new(element, s)); + let snapshot = element_wrapper.as_ref().and_then(|e| e.snapshot()); + + match self.invalidation_kind_for(element, snapshot, quirks_mode) { + InvalidationKind::None => {}, + InvalidationKind::Element => { + debug!( + "process_invalidations_in_subtree: {:?} matched self", + element + ); + data.hint.insert(RestyleHint::RESTYLE_SELF); + }, + InvalidationKind::Scope => { + debug!( + "process_invalidations_in_subtree: {:?} matched subtree", + element + ); + data.hint.insert(RestyleHint::restyle_subtree()); + return true; + }, + } + + let mut any_children_invalid = false; + + for child in element.traversal_children() { + let child = match child.as_element() { + Some(e) => e, + None => continue, + }; + + any_children_invalid |= + self.process_invalidations_in_subtree(child, snapshots, quirks_mode); + } + + if any_children_invalid { + debug!( + "Children of {:?} changed, setting dirty descendants", + element + ); + unsafe { element.set_dirty_descendants() } + } + + data.hint.contains(RestyleHint::RESTYLE_SELF) || any_children_invalid + } + + /// TODO(emilio): Reuse the bucket stuff from selectormap? That handles + /// :is() / :where() etc. + fn scan_component( + component: &Component<SelectorImpl>, + invalidation: &mut Option<Invalidation>, + ) { + match *component { + Component::LocalName(LocalName { + ref name, + ref lower_name, + }) => { + if invalidation.is_none() { + *invalidation = Some(Invalidation::LocalName { + name: name.clone(), + lower_name: lower_name.clone(), + }); + } + }, + Component::Class(ref class) => { + if invalidation.as_ref().map_or(true, |s| !s.is_id_or_class()) { + *invalidation = Some(Invalidation::Class(class.clone())); + } + }, + Component::ID(ref id) => { + if invalidation.as_ref().map_or(true, |s| !s.is_id()) { + *invalidation = Some(Invalidation::ID(id.clone())); + } + }, + _ => { + // Ignore everything else, at least for now. + }, + } + } + + /// Collect invalidations for a given selector. + /// + /// We look at the outermost local name, class, or ID selector to the left + /// of an ancestor combinator, in order to restyle only a given subtree. + /// + /// If the selector has no ancestor combinator, then we do the same for + /// the only sequence it has, but record it as an element invalidation + /// instead of a subtree invalidation. + /// + /// We prefer IDs to classs, and classes to local names, on the basis + /// that the former should be more specific than the latter. We also + /// prefer to generate subtree invalidations for the outermost part + /// of the selector, to reduce the amount of traversal we need to do + /// when flushing invalidations. + fn collect_invalidations( + &mut self, + selector: &Selector<SelectorImpl>, + quirks_mode: QuirksMode, + ) { + debug!( + "StylesheetInvalidationSet::collect_invalidations({:?})", + selector + ); + + let mut element_invalidation: Option<Invalidation> = None; + let mut subtree_invalidation: Option<Invalidation> = None; + + let mut scan_for_element_invalidation = true; + let mut scan_for_subtree_invalidation = false; + + let mut iter = selector.iter(); + + loop { + for component in &mut iter { + if scan_for_element_invalidation { + Self::scan_component(component, &mut element_invalidation); + } else if scan_for_subtree_invalidation { + Self::scan_component(component, &mut subtree_invalidation); + } + } + match iter.next_sequence() { + None => break, + Some(combinator) => { + scan_for_subtree_invalidation = combinator.is_ancestor(); + }, + } + scan_for_element_invalidation = false; + } + + if let Some(s) = subtree_invalidation { + debug!(" > Found subtree invalidation: {:?}", s); + if self.insert_invalidation(s, InvalidationKind::Scope, quirks_mode) { + return; + } + } + + if let Some(s) = element_invalidation { + debug!(" > Found element invalidation: {:?}", s); + if self.insert_invalidation(s, InvalidationKind::Element, quirks_mode) { + return; + } + } + + // The selector was of a form that we can't handle. Any element could + // match it, so let's just bail out. + debug!(" > Can't handle selector or OOMd, marking fully invalid"); + self.invalidate_fully() + } + + fn insert_invalidation( + &mut self, + invalidation: Invalidation, + kind: InvalidationKind, + quirks_mode: QuirksMode, + ) -> bool { + match invalidation { + Invalidation::Class(c) => { + let entry = match self.classes.try_entry(c.0, quirks_mode) { + Ok(e) => e, + Err(..) => return false, + }; + *entry.or_insert(InvalidationKind::None) |= kind; + }, + Invalidation::ID(i) => { + let entry = match self.ids.try_entry(i.0, quirks_mode) { + Ok(e) => e, + Err(..) => return false, + }; + *entry.or_insert(InvalidationKind::None) |= kind; + }, + Invalidation::LocalName { name, lower_name } => { + let insert_lower = name != lower_name; + if self.local_names.try_reserve(1).is_err() { + return false; + } + let entry = self.local_names.entry(name); + *entry.or_insert(InvalidationKind::None) |= kind; + if insert_lower { + if self.local_names.try_reserve(1).is_err() { + return false; + } + let entry = self.local_names.entry(lower_name); + *entry.or_insert(InvalidationKind::None) |= kind; + } + }, + } + + true + } + + /// Collects invalidations for a given CSS rule, if not fully invalid + /// already. + /// + /// TODO(emilio): we can't check whether the rule is inside a non-effective + /// subtree, we potentially could do that. + pub fn rule_changed<S>( + &mut self, + stylesheet: &S, + rule: &CssRule, + guard: &SharedRwLockReadGuard, + device: &Device, + quirks_mode: QuirksMode, + change_kind: RuleChangeKind, + ) where + S: StylesheetInDocument, + { + debug!("StylesheetInvalidationSet::rule_changed"); + if self.fully_invalid { + return; + } + + if !stylesheet.enabled() || !stylesheet.is_effective_for_device(device, guard) { + debug!(" > Stylesheet was not effective"); + return; // Nothing to do here. + } + + // If the change is generic, we don't have the old rule information to know e.g., the old + // media condition, or the old selector text, so we might need to invalidate more + // aggressively. That only applies to the changed rules, for other rules we can just + // collect invalidations as normal. + let is_generic_change = change_kind == RuleChangeKind::Generic; + self.collect_invalidations_for_rule(rule, guard, device, quirks_mode, is_generic_change); + if self.fully_invalid { + return; + } + + if !is_generic_change && !EffectiveRules::is_effective(guard, device, quirks_mode, rule) { + return; + } + + let rules = EffectiveRulesIterator::effective_children(device, quirks_mode, guard, rule); + for rule in rules { + self.collect_invalidations_for_rule( + rule, + guard, + device, + quirks_mode, + /* is_generic_change = */ false, + ); + if self.fully_invalid { + break; + } + } + } + + /// Collects invalidations for a given CSS rule. + fn collect_invalidations_for_rule( + &mut self, + rule: &CssRule, + guard: &SharedRwLockReadGuard, + device: &Device, + quirks_mode: QuirksMode, + is_generic_change: bool, + ) { + use crate::stylesheets::CssRule::*; + debug!("StylesheetInvalidationSet::collect_invalidations_for_rule"); + debug_assert!(!self.fully_invalid, "Not worth being here!"); + + match *rule { + Style(ref lock) => { + if is_generic_change { + // TODO(emilio): We need to do this for selector / keyframe + // name / font-face changes, because we don't have the old + // selector / name. If we distinguish those changes + // specially, then we can at least use this invalidation for + // style declaration changes. + return self.invalidate_fully(); + } + + let style_rule = lock.read_with(guard); + for selector in style_rule.selectors.slice() { + self.collect_invalidations(selector, quirks_mode); + if self.fully_invalid { + return; + } + } + }, + Namespace(..) => { + // It's not clear what handling changes for this correctly would + // look like. + }, + LayerStatement(..) => { + // Layer statement insertions might alter styling order, so we need to always + // invalidate fully. + return self.invalidate_fully(); + }, + Document(..) | Import(..) | Media(..) | Supports(..) | Container(..) | + LayerBlock(..) => { + // Do nothing, relevant nested rules are visited as part of rule iteration. + }, + FontFace(..) => { + // Do nothing, @font-face doesn't affect computed style information on it's own. + // We'll restyle when the font face loads, if needed. + }, + Page(..) | Margin(..) => { + // Do nothing, we don't support OM mutations on print documents, and page rules + // can't affect anything else. + }, + Keyframes(ref lock) => { + if is_generic_change { + return self.invalidate_fully(); + } + let keyframes_rule = lock.read_with(guard); + if device.animation_name_may_be_referenced(&keyframes_rule.name) { + debug!( + " > Found @keyframes rule potentially referenced \ + from the page, marking the whole tree invalid." + ); + self.invalidate_fully(); + } else { + // Do nothing, this animation can't affect the style of existing elements. + } + }, + CounterStyle(..) | Property(..) | FontFeatureValues(..) | FontPaletteValues(..) => { + debug!(" > Found unsupported rule, marking the whole subtree invalid."); + self.invalidate_fully(); + }, + } + } +} diff --git a/servo/components/style/invalidation/viewport_units.rs b/servo/components/style/invalidation/viewport_units.rs new file mode 100644 index 0000000000..06faeb14c4 --- /dev/null +++ b/servo/components/style/invalidation/viewport_units.rs @@ -0,0 +1,71 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Invalidates style of all elements that depend on viewport units. + +use crate::data::ViewportUnitUsage; +use crate::dom::{TElement, TNode}; +use crate::invalidation::element::restyle_hints::RestyleHint; + +/// Invalidates style of all elements that depend on viewport units. +/// +/// Returns whether any element was invalidated. +pub fn invalidate<E>(root: E) -> bool +where + E: TElement, +{ + debug!("invalidation::viewport_units::invalidate({:?})", root); + invalidate_recursively(root) +} + +fn invalidate_recursively<E>(element: E) -> bool +where + E: TElement, +{ + let mut data = match element.mutate_data() { + Some(data) => data, + None => return false, + }; + + if data.hint.will_recascade_subtree() { + debug!("invalidate_recursively: {:?} was already invalid", element); + return false; + } + + let usage = data.styles.viewport_unit_usage(); + let uses_viewport_units = usage != ViewportUnitUsage::None; + if uses_viewport_units { + debug!( + "invalidate_recursively: {:?} uses viewport units {:?}", + element, usage + ); + } + + match usage { + ViewportUnitUsage::None => {}, + ViewportUnitUsage::FromQuery => { + data.hint.insert(RestyleHint::RESTYLE_SELF); + }, + ViewportUnitUsage::FromDeclaration => { + data.hint.insert(RestyleHint::RECASCADE_SELF); + }, + } + + let mut any_children_invalid = false; + for child in element.traversal_children() { + if let Some(child) = child.as_element() { + any_children_invalid |= invalidate_recursively(child); + } + } + + if any_children_invalid { + debug!( + "invalidate_recursively: Children of {:?} changed, setting dirty descendants", + element + ); + unsafe { element.set_dirty_descendants() } + } + + uses_viewport_units || any_children_invalid +} |