mirror of
https://github.com/servo/servo.git
synced 2025-08-06 06:00:15 +01:00
style: Implement a more fine-grained invalidation method.
This commit also removes the old restyle_hints module and splits it into multiple modules under components/style/invalidation/element/. The basic approach is to walk down the tree using compound selectors as needed, in order to do as little selector-matching as possible. Bug: 1368240 MozReview-Commit-ID: 2YO8fKFygZI
This commit is contained in:
parent
fd10729941
commit
cb06375fe2
21 changed files with 1673 additions and 1486 deletions
341
components/style/invalidation/element/element_wrapper.rs
Normal file
341
components/style/invalidation/element/element_wrapper.rs
Normal file
|
@ -0,0 +1,341 @@
|
|||
/* 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 http://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 {Atom, CaseSensitivityExt, LocalName, Namespace};
|
||||
use dom::TElement;
|
||||
use element_state::ElementState;
|
||||
use selector_parser::{NonTSPseudoClass, PseudoElement, SelectorImpl, Snapshot, SnapshotMap, AttrValue};
|
||||
use selectors::Element;
|
||||
use selectors::attr::{AttrSelectorOperation, CaseSensitivity, NamespaceConstraint};
|
||||
use selectors::matching::{ElementSelectorFlags, LocalMatchingContext, MatchingContext};
|
||||
use selectors::matching::RelevantLinkStatus;
|
||||
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;
|
||||
|
||||
/// The ID attribute per this snapshot. Should only be called if
|
||||
/// `has_attrs()` returns true.
|
||||
fn id_attr(&self) -> Option<Atom>;
|
||||
|
||||
/// Whether this snapshot contains the class `name`. Should only be called
|
||||
/// if `has_attrs()` returns true.
|
||||
fn has_class(&self, name: &Atom, case_sensitivity: CaseSensitivity) -> bool;
|
||||
|
||||
/// 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(&Atom);
|
||||
|
||||
/// 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.get_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;
|
||||
}
|
||||
match current.parent_element() {
|
||||
Some(parent) => current = parent,
|
||||
None => return None,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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<F>(&self,
|
||||
pseudo_class: &NonTSPseudoClass,
|
||||
context: &mut LocalMatchingContext<Self::Impl>,
|
||||
relevant_link: &RelevantLinkStatus,
|
||||
_setter: &mut F)
|
||||
-> bool
|
||||
where F: FnMut(&Self, ElementSelectorFlags),
|
||||
{
|
||||
// Some pseudo-classes need special handling to evaluate them against
|
||||
// the snapshot.
|
||||
match *pseudo_class {
|
||||
#[cfg(feature = "gecko")]
|
||||
NonTSPseudoClass::MozAny(ref selectors) => {
|
||||
use selectors::matching::matches_complex_selector;
|
||||
return selectors.iter().any(|s| {
|
||||
matches_complex_selector(s.iter(), self, context, _setter)
|
||||
})
|
||||
}
|
||||
|
||||
// :dir is implemented in terms of state flags, but which state flag
|
||||
// it maps to depends on the argument to :dir. That means we can't
|
||||
// just add its state flags to the NonTSPseudoClass, because if we
|
||||
// added all of them there, and tested via intersects() here, we'd
|
||||
// get incorrect behavior for :not(:dir()) cases.
|
||||
//
|
||||
// FIXME(bz): How can I set this up so once Servo adds :dir()
|
||||
// support we don't forget to update this code?
|
||||
#[cfg(feature = "gecko")]
|
||||
NonTSPseudoClass::Dir(ref s) => {
|
||||
use invalidation::element::invalidation_map::dir_selector_to_state;
|
||||
let selector_flag = dir_selector_to_state(s);
|
||||
if selector_flag.is_empty() {
|
||||
// :dir() with some random argument; does not match.
|
||||
return false;
|
||||
}
|
||||
let state = match self.snapshot().and_then(|s| s.state()) {
|
||||
Some(snapshot_state) => snapshot_state,
|
||||
None => self.element.get_state(),
|
||||
};
|
||||
return state.contains(selector_flag);
|
||||
}
|
||||
|
||||
// For :link and :visited, we don't actually want to test the element
|
||||
// state directly. Instead, we use the `relevant_link` to determine if
|
||||
// they match.
|
||||
NonTSPseudoClass::Link => {
|
||||
return relevant_link.is_unvisited(self, context.shared);
|
||||
}
|
||||
NonTSPseudoClass::Visited => {
|
||||
return relevant_link.is_visited(self, context.shared);
|
||||
}
|
||||
|
||||
#[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::MozBrowserFrame => {
|
||||
if let Some(snapshot) = self.snapshot() {
|
||||
if snapshot.has_other_pseudo_class_state() {
|
||||
return snapshot.mIsMozBrowserFrame();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// :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,
|
||||
relevant_link,
|
||||
&mut |_, _| {})
|
||||
}
|
||||
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,
|
||||
relevant_link,
|
||||
&mut |_, _| {})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn match_pseudo_element(&self,
|
||||
pseudo_element: &PseudoElement,
|
||||
context: &mut MatchingContext)
|
||||
-> bool
|
||||
{
|
||||
self.element.match_pseudo_element(pseudo_element, context)
|
||||
}
|
||||
|
||||
fn is_link(&self) -> bool {
|
||||
self.element.is_link()
|
||||
}
|
||||
|
||||
fn parent_element(&self) -> Option<Self> {
|
||||
self.element.parent_element()
|
||||
.map(|e| ElementWrapper::new(e, self.snapshot_map))
|
||||
}
|
||||
|
||||
fn first_child_element(&self) -> Option<Self> {
|
||||
self.element.first_child_element()
|
||||
.map(|e| ElementWrapper::new(e, self.snapshot_map))
|
||||
}
|
||||
|
||||
fn last_child_element(&self) -> Option<Self> {
|
||||
self.element.last_child_element()
|
||||
.map(|e| ElementWrapper::new(e, self.snapshot_map))
|
||||
}
|
||||
|
||||
fn prev_sibling_element(&self) -> Option<Self> {
|
||||
self.element.prev_sibling_element()
|
||||
.map(|e| ElementWrapper::new(e, self.snapshot_map))
|
||||
}
|
||||
|
||||
fn next_sibling_element(&self) -> Option<Self> {
|
||||
self.element.next_sibling_element()
|
||||
.map(|e| ElementWrapper::new(e, self.snapshot_map))
|
||||
}
|
||||
|
||||
fn is_html_element_in_html_document(&self) -> bool {
|
||||
self.element.is_html_element_in_html_document()
|
||||
}
|
||||
|
||||
fn get_local_name(&self) -> &<Self::Impl as ::selectors::SelectorImpl>::BorrowedLocalName {
|
||||
self.element.get_local_name()
|
||||
}
|
||||
|
||||
fn get_namespace(&self) -> &<Self::Impl as ::selectors::SelectorImpl>::BorrowedNamespaceUrl {
|
||||
self.element.get_namespace()
|
||||
}
|
||||
|
||||
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: &Atom, 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 has_class(&self, name: &Atom, 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 pseudo_element_originating_element(&self) -> Option<Self> {
|
||||
self.element.closest_non_native_anonymous_ancestor()
|
||||
.map(|e| ElementWrapper::new(e, self.snapshot_map))
|
||||
}
|
||||
}
|
402
components/style/invalidation/element/invalidation_map.rs
Normal file
402
components/style/invalidation/element/invalidation_map.rs
Normal file
|
@ -0,0 +1,402 @@
|
|||
/* 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 http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
//! Code for invalidations due to state or attribute changes.
|
||||
|
||||
use {Atom, LocalName, Namespace};
|
||||
use context::QuirksMode;
|
||||
use element_state::ElementState;
|
||||
use selector_map::{MaybeCaseInsensitiveHashMap, SelectorMap, SelectorMapEntry};
|
||||
use selector_parser::SelectorImpl;
|
||||
use selectors::attr::NamespaceConstraint;
|
||||
use selectors::parser::{AncestorHashes, Combinator, Component};
|
||||
use selectors::parser::{Selector, SelectorAndHashes, SelectorIter, SelectorMethods};
|
||||
use selectors::visitor::SelectorVisitor;
|
||||
use smallvec::SmallVec;
|
||||
|
||||
#[cfg(feature = "gecko")]
|
||||
/// Gets the element state relevant to the given `:dir` pseudo-class selector.
|
||||
pub fn dir_selector_to_state(s: &[u16]) -> ElementState {
|
||||
use element_state::{IN_LTR_STATE, IN_RTL_STATE};
|
||||
|
||||
// Jump through some hoops to deal with our Box<[u16]> thing.
|
||||
const LTR: [u16; 4] = [b'l' as u16, b't' as u16, b'r' as u16, 0];
|
||||
const RTL: [u16; 4] = [b'r' as u16, b't' as u16, b'l' as u16, 0];
|
||||
|
||||
if LTR == *s {
|
||||
IN_LTR_STATE
|
||||
} else if RTL == *s {
|
||||
IN_RTL_STATE
|
||||
} else {
|
||||
// :dir(something-random) is a valid selector, but shouldn't
|
||||
// match anything.
|
||||
ElementState::empty()
|
||||
}
|
||||
}
|
||||
|
||||
/// 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)]
|
||||
#[cfg_attr(feature = "servo", derive(HeapSizeOf))]
|
||||
pub struct Dependency {
|
||||
/// The dependency selector.
|
||||
#[cfg_attr(feature = "servo", ignore_heap_size_of = "Arc")]
|
||||
pub selector: Selector<SelectorImpl>,
|
||||
/// The ancestor hashes associated with the above selector at the given
|
||||
/// offset.
|
||||
#[cfg_attr(feature = "servo", ignore_heap_size_of = "No heap data")]
|
||||
pub hashes: AncestorHashes,
|
||||
/// The offset into the selector that we should match on.
|
||||
pub selector_offset: usize,
|
||||
}
|
||||
|
||||
impl Dependency {
|
||||
/// Returns the combinator to the right of the partial selector this
|
||||
/// dependency represents.
|
||||
///
|
||||
/// TODO(emilio): Consider storing inline if it helps cache locality?
|
||||
pub fn combinator(&self) -> Option<Combinator> {
|
||||
if self.selector_offset == 0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(self.selector.combinator_at(self.selector_offset))
|
||||
}
|
||||
|
||||
/// Whether this dependency affects the style of the element.
|
||||
///
|
||||
/// NOTE(emilio): pseudo-elements need to be here to account for eager
|
||||
/// pseudos, since they just grab the style from the originating element.
|
||||
///
|
||||
/// TODO(emilio): We could look at the selector itself to see if it's an
|
||||
/// eager pseudo, and return false here if not.
|
||||
pub fn affects_self(&self) -> bool {
|
||||
matches!(self.combinator(), None | Some(Combinator::PseudoElement))
|
||||
}
|
||||
|
||||
/// Whether this dependency may affect style of any of our descendants.
|
||||
pub fn affects_descendants(&self) -> bool {
|
||||
matches!(self.combinator(), Some(Combinator::PseudoElement) |
|
||||
Some(Combinator::Child) |
|
||||
Some(Combinator::Descendant))
|
||||
}
|
||||
|
||||
/// Whether this dependency may affect style of any of our later siblings.
|
||||
pub fn affects_later_siblings(&self) -> bool {
|
||||
matches!(self.combinator(), Some(Combinator::NextSibling) |
|
||||
Some(Combinator::LaterSibling))
|
||||
}
|
||||
}
|
||||
|
||||
impl SelectorMapEntry for Dependency {
|
||||
fn selector(&self) -> SelectorIter<SelectorImpl> {
|
||||
self.selector.iter_from(self.selector_offset)
|
||||
}
|
||||
|
||||
fn hashes(&self) -> &AncestorHashes {
|
||||
&self.hashes
|
||||
}
|
||||
}
|
||||
|
||||
/// The same, but for state selectors, which can track more exactly what state
|
||||
/// do they track.
|
||||
#[derive(Clone)]
|
||||
#[cfg_attr(feature = "servo", derive(HeapSizeOf))]
|
||||
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.iter_from(self.dep.selector_offset)
|
||||
}
|
||||
|
||||
fn hashes(&self) -> &AncestorHashes {
|
||||
&self.dep.hashes
|
||||
}
|
||||
}
|
||||
|
||||
/// 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.
|
||||
#[cfg_attr(feature = "servo", derive(HeapSizeOf))]
|
||||
pub struct InvalidationMap {
|
||||
/// A map from a given class name to all the selectors with that class
|
||||
/// selector.
|
||||
pub class_to_selector: MaybeCaseInsensitiveHashMap<Atom, SelectorMap<Dependency>>,
|
||||
/// 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: MaybeCaseInsensitiveHashMap<Atom, SelectorMap<Dependency>>,
|
||||
/// A map of all the state dependencies.
|
||||
pub state_affecting_selectors: SelectorMap<StateDependency>,
|
||||
/// A map of other attribute affecting selectors.
|
||||
pub other_attribute_affecting_selectors: SelectorMap<Dependency>,
|
||||
/// Whether there are attribute rules of the form `[class~="foo"]` that may
|
||||
/// match. In that case, we need to look at
|
||||
/// `other_attribute_affecting_selectors` too even if only the `class` has
|
||||
/// changed.
|
||||
pub has_class_attribute_selectors: bool,
|
||||
/// Whether there are attribute rules of the form `[id|="foo"]` that may
|
||||
/// match. In that case, we need to look at
|
||||
/// `other_attribute_affecting_selectors` too even if only the `id` has
|
||||
/// changed.
|
||||
pub has_id_attribute_selectors: bool,
|
||||
}
|
||||
|
||||
impl InvalidationMap {
|
||||
/// Creates an empty `InvalidationMap`.
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
class_to_selector: MaybeCaseInsensitiveHashMap::new(),
|
||||
id_to_selector: MaybeCaseInsensitiveHashMap::new(),
|
||||
state_affecting_selectors: SelectorMap::new(),
|
||||
other_attribute_affecting_selectors: SelectorMap::new(),
|
||||
has_class_attribute_selectors: false,
|
||||
has_id_attribute_selectors: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the number of dependencies stored in the invalidation map.
|
||||
pub fn len(&self) -> usize {
|
||||
self.state_affecting_selectors.len() +
|
||||
self.other_attribute_affecting_selectors.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()
|
||||
})
|
||||
}
|
||||
|
||||
/// Adds a selector to this `InvalidationMap`.
|
||||
pub fn note_selector(
|
||||
&mut self,
|
||||
selector_and_hashes: &SelectorAndHashes<SelectorImpl>,
|
||||
quirks_mode: QuirksMode)
|
||||
{
|
||||
self.collect_invalidations_for(selector_and_hashes, quirks_mode)
|
||||
}
|
||||
|
||||
/// 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 = SelectorMap::new();
|
||||
self.other_attribute_affecting_selectors = SelectorMap::new();
|
||||
self.has_id_attribute_selectors = false;
|
||||
self.has_class_attribute_selectors = false;
|
||||
}
|
||||
|
||||
fn collect_invalidations_for(
|
||||
&mut self,
|
||||
selector_and_hashes: &SelectorAndHashes<SelectorImpl>,
|
||||
quirks_mode: QuirksMode)
|
||||
{
|
||||
debug!("InvalidationMap::collect_invalidations_for({:?})",
|
||||
selector_and_hashes.selector);
|
||||
|
||||
let mut iter = selector_and_hashes.selector.iter();
|
||||
let mut combinator;
|
||||
let mut index = 0;
|
||||
|
||||
loop {
|
||||
let sequence_start = index;
|
||||
|
||||
let mut compound_visitor = CompoundSelectorDependencyCollector {
|
||||
classes: SmallVec::new(),
|
||||
ids: SmallVec::new(),
|
||||
state: ElementState::empty(),
|
||||
other_attributes: false,
|
||||
has_id_attribute_selectors: false,
|
||||
has_class_attribute_selectors: false,
|
||||
};
|
||||
|
||||
// Visit all the simple selectors in this sequence.
|
||||
//
|
||||
// Note that this works because we can't have combinators nested
|
||||
// inside simple selectors (i.e. in :not() or :-moz-any()).
|
||||
//
|
||||
// If we ever support that we'll need to visit nested complex
|
||||
// selectors as well, in order to mark them as affecting descendants
|
||||
// at least.
|
||||
for ss in &mut iter {
|
||||
ss.visit(&mut compound_visitor);
|
||||
index += 1; // Account for the simple selector.
|
||||
}
|
||||
|
||||
// Reuse the bloom hashes if this is the base selector. Otherwise,
|
||||
// rebuild them.
|
||||
let mut hashes = None;
|
||||
|
||||
let mut get_hashes = || -> AncestorHashes {
|
||||
if hashes.is_none() {
|
||||
hashes = Some(if sequence_start == 0 {
|
||||
selector_and_hashes.hashes.clone()
|
||||
} else {
|
||||
let seq_iter = selector_and_hashes.selector.iter_from(sequence_start);
|
||||
AncestorHashes::from_iter(seq_iter)
|
||||
});
|
||||
}
|
||||
hashes.clone().unwrap()
|
||||
};
|
||||
|
||||
self.has_id_attribute_selectors |= compound_visitor.has_id_attribute_selectors;
|
||||
self.has_class_attribute_selectors |= compound_visitor.has_class_attribute_selectors;
|
||||
|
||||
for class in compound_visitor.classes {
|
||||
self.class_to_selector
|
||||
.entry(class, quirks_mode)
|
||||
.or_insert_with(SelectorMap::new)
|
||||
.insert(Dependency {
|
||||
selector: selector_and_hashes.selector.clone(),
|
||||
selector_offset: sequence_start,
|
||||
hashes: get_hashes(),
|
||||
}, quirks_mode);
|
||||
}
|
||||
|
||||
for id in compound_visitor.ids {
|
||||
self.id_to_selector
|
||||
.entry(id, quirks_mode)
|
||||
.or_insert_with(SelectorMap::new)
|
||||
.insert(Dependency {
|
||||
selector: selector_and_hashes.selector.clone(),
|
||||
selector_offset: sequence_start,
|
||||
hashes: get_hashes(),
|
||||
}, quirks_mode);
|
||||
}
|
||||
|
||||
if !compound_visitor.state.is_empty() {
|
||||
self.state_affecting_selectors
|
||||
.insert(StateDependency {
|
||||
dep: Dependency {
|
||||
selector: selector_and_hashes.selector.clone(),
|
||||
selector_offset: sequence_start,
|
||||
hashes: get_hashes(),
|
||||
},
|
||||
state: compound_visitor.state,
|
||||
}, quirks_mode);
|
||||
}
|
||||
|
||||
if compound_visitor.other_attributes {
|
||||
self.other_attribute_affecting_selectors
|
||||
.insert(Dependency {
|
||||
selector: selector_and_hashes.selector.clone(),
|
||||
selector_offset: sequence_start,
|
||||
hashes: get_hashes(),
|
||||
}, quirks_mode);
|
||||
}
|
||||
|
||||
combinator = iter.next_sequence();
|
||||
if combinator.is_none() {
|
||||
break;
|
||||
}
|
||||
|
||||
index += 1; // Account for the combinator.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A struct that collects invalidations for a given compound selector.
|
||||
struct CompoundSelectorDependencyCollector {
|
||||
/// The state this compound selector is affected by.
|
||||
state: ElementState,
|
||||
|
||||
/// The classes this compound selector is affected by.
|
||||
///
|
||||
/// NB: This will be often a single class, but could be multiple in
|
||||
/// presence of :not, :-moz-any, .foo.bar.baz, etc.
|
||||
classes: SmallVec<[Atom; 5]>,
|
||||
|
||||
/// The IDs this compound selector is affected by.
|
||||
///
|
||||
/// NB: This will be almost always a single id, but could be multiple in
|
||||
/// presence of :not, :-moz-any, #foo#bar, etc.
|
||||
ids: SmallVec<[Atom; 5]>,
|
||||
|
||||
/// Whether it affects other attribute-dependent selectors that aren't ID or
|
||||
/// class selectors (NB: We still set this to true in presence of [class] or
|
||||
/// [id] attribute selectors).
|
||||
other_attributes: bool,
|
||||
|
||||
/// Whether there were attribute selectors with the id attribute.
|
||||
has_id_attribute_selectors: bool,
|
||||
|
||||
/// Whether there were attribute selectors with the class attribute.
|
||||
has_class_attribute_selectors: bool,
|
||||
}
|
||||
|
||||
impl SelectorVisitor for CompoundSelectorDependencyCollector {
|
||||
type Impl = SelectorImpl;
|
||||
|
||||
fn visit_simple_selector(&mut self, s: &Component<SelectorImpl>) -> bool {
|
||||
#[cfg(feature = "gecko")]
|
||||
use selector_parser::NonTSPseudoClass;
|
||||
|
||||
match *s {
|
||||
Component::ID(ref id) => {
|
||||
self.ids.push(id.clone());
|
||||
}
|
||||
Component::Class(ref class) => {
|
||||
self.classes.push(class.clone());
|
||||
}
|
||||
Component::NonTSPseudoClass(ref pc) => {
|
||||
self.other_attributes |= pc.is_attr_based();
|
||||
self.state |= match *pc {
|
||||
#[cfg(feature = "gecko")]
|
||||
NonTSPseudoClass::Dir(ref s) => {
|
||||
dir_selector_to_state(s)
|
||||
}
|
||||
_ => pc.state_flag(),
|
||||
};
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
fn visit_attribute_selector(
|
||||
&mut self,
|
||||
constraint: &NamespaceConstraint<&Namespace>,
|
||||
_local_name: &LocalName,
|
||||
local_name_lower: &LocalName,
|
||||
) -> bool {
|
||||
self.other_attributes = true;
|
||||
let may_match_in_no_namespace = match *constraint {
|
||||
NamespaceConstraint::Any => true,
|
||||
NamespaceConstraint::Specific(ref ns) => ns.is_empty(),
|
||||
};
|
||||
|
||||
if may_match_in_no_namespace {
|
||||
self.has_id_attribute_selectors |= *local_name_lower == local_name!("id");
|
||||
self.has_class_attribute_selectors |= *local_name_lower == local_name!("class");
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
}
|
601
components/style/invalidation/element/invalidator.rs
Normal file
601
components/style/invalidation/element/invalidator.rs
Normal file
|
@ -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 http://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 Atom;
|
||||
use context::SharedStyleContext;
|
||||
use data::ElementData;
|
||||
use dom::{TElement, TNode};
|
||||
use element_state::ElementState;
|
||||
use invalidation::element::element_wrapper::{ElementSnapshot, ElementWrapper};
|
||||
use invalidation::element::invalidation_map::*;
|
||||
use invalidation::element::restyle_hints::*;
|
||||
use selector_map::SelectorMap;
|
||||
use selector_parser::SelectorImpl;
|
||||
use selectors::attr::CaseSensitivity;
|
||||
use selectors::matching::{MatchingContext, MatchingMode, VisitedHandlingMode};
|
||||
use selectors::matching::{matches_selector, matches_compound_selector};
|
||||
use selectors::matching::CompoundSelectorMatchingResult;
|
||||
use selectors::parser::{Combinator, Component, Selector};
|
||||
use smallvec::SmallVec;
|
||||
use std::fmt;
|
||||
|
||||
/// 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: 'a, E>
|
||||
where E: TElement,
|
||||
{
|
||||
element: E,
|
||||
data: Option<&'a mut ElementData>,
|
||||
shared_context: &'a SharedStyleContext<'b>,
|
||||
}
|
||||
|
||||
type InvalidationVector = SmallVec<[Invalidation; 10]>;
|
||||
|
||||
/// An `Invalidation` is a complex selector that describes which elements,
|
||||
/// relative to a current element we are processing, must be restyled.
|
||||
///
|
||||
/// When `offset` points to the right-most compound selector in `selector`,
|
||||
/// then the Invalidation `represents` the fact that the current element
|
||||
/// must be restyled if the compound selector matches. Otherwise, if
|
||||
/// describes which descendants (or later siblings) must be restyled.
|
||||
#[derive(Clone)]
|
||||
struct Invalidation {
|
||||
selector: Selector<SelectorImpl>,
|
||||
offset: usize,
|
||||
}
|
||||
|
||||
impl fmt::Debug for Invalidation {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
use cssparser::ToCss;
|
||||
|
||||
f.write_str("Invalidation(")?;
|
||||
for component in self.selector.iter_raw_rev_from(self.offset - 1) {
|
||||
if matches!(*component, Component::Combinator(..)) {
|
||||
break;
|
||||
}
|
||||
component.to_css(f)?;
|
||||
}
|
||||
f.write_str(")")
|
||||
}
|
||||
}
|
||||
|
||||
/// The result of processing a single invalidation for a given element.
|
||||
struct InvalidationResult {
|
||||
/// Whether the element itself was invalidated.
|
||||
invalidated_self: bool,
|
||||
/// Whether the invalidation we've processed is effective for the next
|
||||
/// sibling or descendant after us.
|
||||
effective_for_next: bool,
|
||||
}
|
||||
|
||||
impl<'a, 'b: 'a, E> TreeStyleInvalidator<'a, 'b, E>
|
||||
where E: TElement,
|
||||
{
|
||||
/// Trivially constructs a new `TreeStyleInvalidator`.
|
||||
pub fn new(
|
||||
element: E,
|
||||
data: Option<&'a mut ElementData>,
|
||||
shared_context: &'a SharedStyleContext<'b>,
|
||||
) -> Self {
|
||||
Self {
|
||||
element: element,
|
||||
data: data,
|
||||
shared_context: shared_context,
|
||||
}
|
||||
}
|
||||
|
||||
/// Perform the invalidation pass.
|
||||
pub fn invalidate(mut self) {
|
||||
debug!("StyleTreeInvalidator::invalidate({:?})", self.element);
|
||||
debug_assert!(self.element.has_snapshot(), "Why bothering?");
|
||||
debug_assert!(self.data.is_some(), "How exactly?");
|
||||
|
||||
let shared_context = self.shared_context;
|
||||
|
||||
let wrapper =
|
||||
ElementWrapper::new(self.element, shared_context.snapshot_map);
|
||||
let state_changes = wrapper.state_changes();
|
||||
let snapshot = wrapper.snapshot().expect("has_snapshot lied");
|
||||
|
||||
if !snapshot.has_attrs() && state_changes.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
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 !self.element.has_class(c, CaseSensitivity::CaseSensitive) {
|
||||
classes_removed.push(c.clone())
|
||||
}
|
||||
});
|
||||
|
||||
self.element.each_class(|c| {
|
||||
if !snapshot.has_class(c, CaseSensitivity::CaseSensitive) {
|
||||
classes_added.push(c.clone())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
let mut id_removed = None;
|
||||
let mut id_added = None;
|
||||
if snapshot.id_changed() {
|
||||
let old_id = snapshot.id_attr();
|
||||
let current_id = self.element.get_id();
|
||||
|
||||
if old_id != current_id {
|
||||
id_removed = old_id;
|
||||
id_added = current_id;
|
||||
}
|
||||
}
|
||||
|
||||
let lookup_element =
|
||||
if self.element.implemented_pseudo_element().is_some() {
|
||||
self.element.pseudo_element_originating_element().unwrap()
|
||||
} else {
|
||||
self.element
|
||||
};
|
||||
|
||||
let mut descendant_invalidations = InvalidationVector::new();
|
||||
let mut sibling_invalidations = InvalidationVector::new();
|
||||
let invalidated_self = {
|
||||
let mut collector = InvalidationCollector {
|
||||
wrapper: wrapper,
|
||||
element: self.element,
|
||||
shared_context: self.shared_context,
|
||||
lookup_element: lookup_element,
|
||||
removed_id: id_removed.as_ref(),
|
||||
classes_removed: &classes_removed,
|
||||
descendant_invalidations: &mut descendant_invalidations,
|
||||
sibling_invalidations: &mut sibling_invalidations,
|
||||
invalidates_self: false,
|
||||
};
|
||||
|
||||
let map = shared_context.stylist.invalidation_map();
|
||||
|
||||
if let Some(ref id) = id_removed {
|
||||
if let Some(deps) = map.id_to_selector.get(id, shared_context.quirks_mode) {
|
||||
collector.collect_dependencies_in_map(deps)
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(ref id) = id_added {
|
||||
if let Some(deps) = map.id_to_selector.get(id, shared_context.quirks_mode) {
|
||||
collector.collect_dependencies_in_map(deps)
|
||||
}
|
||||
}
|
||||
|
||||
for class in classes_added.iter().chain(classes_removed.iter()) {
|
||||
if let Some(deps) = map.class_to_selector.get(class, shared_context.quirks_mode) {
|
||||
collector.collect_dependencies_in_map(deps)
|
||||
}
|
||||
}
|
||||
|
||||
let should_examine_attribute_selector_map =
|
||||
snapshot.other_attr_changed() ||
|
||||
(snapshot.class_changed() && map.has_class_attribute_selectors) ||
|
||||
(snapshot.id_changed() && map.has_id_attribute_selectors);
|
||||
|
||||
if should_examine_attribute_selector_map {
|
||||
collector.collect_dependencies_in_map(
|
||||
&map.other_attribute_affecting_selectors
|
||||
)
|
||||
}
|
||||
|
||||
if !state_changes.is_empty() {
|
||||
collector.collect_state_dependencies(
|
||||
&map.state_affecting_selectors,
|
||||
state_changes,
|
||||
)
|
||||
}
|
||||
|
||||
collector.invalidates_self
|
||||
};
|
||||
|
||||
if invalidated_self {
|
||||
if let Some(ref mut data) = self.data {
|
||||
data.ensure_restyle().hint.insert(RESTYLE_SELF.into());
|
||||
}
|
||||
}
|
||||
|
||||
debug!("Collected invalidations (self: {}): ", invalidated_self);
|
||||
debug!(" > descendants: {:?}", descendant_invalidations);
|
||||
debug!(" > siblings: {:?}", sibling_invalidations);
|
||||
self.invalidate_descendants(&descendant_invalidations);
|
||||
self.invalidate_siblings(&mut sibling_invalidations);
|
||||
}
|
||||
|
||||
/// 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,
|
||||
) -> bool {
|
||||
if sibling_invalidations.is_empty() {
|
||||
return false;
|
||||
}
|
||||
|
||||
let mut current = self.element.next_sibling_element();
|
||||
let mut any_invalidated = false;
|
||||
|
||||
while let Some(sibling) = current {
|
||||
let mut sibling_data = sibling.get_data().map(|d| d.borrow_mut());
|
||||
let sibling_data = sibling_data.as_mut().map(|d| &mut **d);
|
||||
|
||||
let mut sibling_invalidator = TreeStyleInvalidator::new(
|
||||
sibling,
|
||||
sibling_data,
|
||||
self.shared_context
|
||||
);
|
||||
|
||||
let mut invalidations_for_descendants = InvalidationVector::new();
|
||||
any_invalidated |=
|
||||
sibling_invalidator.process_sibling_invalidations(
|
||||
&mut invalidations_for_descendants,
|
||||
sibling_invalidations,
|
||||
);
|
||||
|
||||
any_invalidated |=
|
||||
sibling_invalidator.invalidate_descendants(
|
||||
&invalidations_for_descendants
|
||||
);
|
||||
|
||||
if sibling_invalidations.is_empty() {
|
||||
break;
|
||||
}
|
||||
|
||||
current = sibling.next_sibling_element();
|
||||
}
|
||||
|
||||
any_invalidated
|
||||
}
|
||||
|
||||
/// Given a descendant invalidation list, go through the current element's
|
||||
/// descendants, and invalidate style on them.
|
||||
fn invalidate_descendants(
|
||||
&mut self,
|
||||
invalidations: &InvalidationVector,
|
||||
) -> bool {
|
||||
if invalidations.is_empty() {
|
||||
return false;
|
||||
}
|
||||
|
||||
debug!("StyleTreeInvalidator::invalidate_descendants({:?})",
|
||||
self.element);
|
||||
debug!(" > {:?}", invalidations);
|
||||
|
||||
match self.data {
|
||||
None => return false,
|
||||
Some(ref data) => {
|
||||
if let Some(restyle) = data.get_restyle() {
|
||||
if restyle.hint.contains_subtree() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut sibling_invalidations = InvalidationVector::new();
|
||||
|
||||
let mut any_children = false;
|
||||
for child in self.element.as_node().traversal_children() {
|
||||
let child = match child.as_element() {
|
||||
Some(e) => e,
|
||||
None => continue,
|
||||
};
|
||||
|
||||
let mut child_data = child.get_data().map(|d| d.borrow_mut());
|
||||
let child_data = child_data.as_mut().map(|d| &mut **d);
|
||||
|
||||
let mut child_invalidator = TreeStyleInvalidator::new(
|
||||
child,
|
||||
child_data,
|
||||
self.shared_context
|
||||
);
|
||||
|
||||
let mut invalidations_for_descendants = InvalidationVector::new();
|
||||
any_children |= child_invalidator.process_sibling_invalidations(
|
||||
&mut invalidations_for_descendants,
|
||||
&mut sibling_invalidations,
|
||||
);
|
||||
|
||||
any_children |= child_invalidator.process_descendant_invalidations(
|
||||
invalidations,
|
||||
&mut invalidations_for_descendants,
|
||||
&mut sibling_invalidations,
|
||||
);
|
||||
|
||||
any_children |= child_invalidator.invalidate_descendants(
|
||||
&invalidations_for_descendants
|
||||
);
|
||||
}
|
||||
|
||||
if any_children {
|
||||
unsafe { self.element.set_dirty_descendants() };
|
||||
}
|
||||
|
||||
any_children
|
||||
}
|
||||
|
||||
/// 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 InvalidationVector,
|
||||
sibling_invalidations: &mut InvalidationVector,
|
||||
) -> 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
|
||||
);
|
||||
|
||||
invalidated_self |= result.invalidated_self;
|
||||
if !result.effective_for_next {
|
||||
sibling_invalidations.remove(i);
|
||||
} else {
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
|
||||
sibling_invalidations.extend(new_sibling_invalidations.into_iter());
|
||||
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: &InvalidationVector,
|
||||
descendant_invalidations: &mut InvalidationVector,
|
||||
sibling_invalidations: &mut InvalidationVector,
|
||||
) -> bool {
|
||||
let mut invalidated = false;
|
||||
|
||||
for invalidation in invalidations {
|
||||
let result = self.process_invalidation(
|
||||
invalidation,
|
||||
descendant_invalidations,
|
||||
sibling_invalidations,
|
||||
);
|
||||
|
||||
invalidated |= result.invalidated_self;
|
||||
if result.effective_for_next {
|
||||
descendant_invalidations.push(invalidation.clone());
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
descendant_invalidations: &mut InvalidationVector,
|
||||
sibling_invalidations: &mut InvalidationVector
|
||||
) -> InvalidationResult {
|
||||
debug!("TreeStyleInvalidator::process_invalidation({:?}, {:?})",
|
||||
self.element, invalidation);
|
||||
|
||||
let mut context = MatchingContext::new(MatchingMode::Normal, None,
|
||||
self.shared_context.quirks_mode);
|
||||
let matching_result = matches_compound_selector(
|
||||
&invalidation.selector,
|
||||
invalidation.offset,
|
||||
&mut context,
|
||||
&self.element
|
||||
);
|
||||
|
||||
let mut invalidated_self = false;
|
||||
match matching_result {
|
||||
CompoundSelectorMatchingResult::Matched { next_combinator_offset: 0 } => {
|
||||
if let Some(ref mut data) = self.data {
|
||||
data.ensure_restyle().hint.insert(RESTYLE_SELF.into());
|
||||
invalidated_self = true;
|
||||
}
|
||||
}
|
||||
CompoundSelectorMatchingResult::Matched { next_combinator_offset } => {
|
||||
let next_combinator =
|
||||
invalidation.selector.combinator_at(next_combinator_offset);
|
||||
|
||||
if next_combinator.is_ancestor() {
|
||||
descendant_invalidations.push(Invalidation {
|
||||
selector: invalidation.selector.clone(),
|
||||
offset: next_combinator_offset,
|
||||
})
|
||||
} else {
|
||||
sibling_invalidations.push(Invalidation {
|
||||
selector: invalidation.selector.clone(),
|
||||
offset: next_combinator_offset,
|
||||
})
|
||||
}
|
||||
}
|
||||
CompoundSelectorMatchingResult::NotMatched => {}
|
||||
}
|
||||
|
||||
// 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!
|
||||
let effective_for_next =
|
||||
match invalidation.selector.combinator_at(invalidation.offset) {
|
||||
Combinator::NextSibling |
|
||||
Combinator::Child => false,
|
||||
_ => true,
|
||||
};
|
||||
|
||||
InvalidationResult {
|
||||
invalidated_self: invalidated_self,
|
||||
effective_for_next: effective_for_next,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct InvalidationCollector<'a, 'b: 'a, E>
|
||||
where E: TElement,
|
||||
{
|
||||
element: E,
|
||||
wrapper: ElementWrapper<'b, E>,
|
||||
shared_context: &'a SharedStyleContext<'b>,
|
||||
lookup_element: E,
|
||||
removed_id: Option<&'a Atom>,
|
||||
classes_removed: &'a SmallVec<[Atom; 8]>,
|
||||
descendant_invalidations: &'a mut InvalidationVector,
|
||||
sibling_invalidations: &'a mut InvalidationVector,
|
||||
invalidates_self: bool,
|
||||
}
|
||||
|
||||
impl<'a, 'b: 'a, E> InvalidationCollector<'a, 'b, E>
|
||||
where E: TElement,
|
||||
{
|
||||
fn collect_dependencies_in_map(
|
||||
&mut self,
|
||||
map: &SelectorMap<Dependency>,
|
||||
) {
|
||||
map.lookup_with_additional(
|
||||
self.lookup_element,
|
||||
self.shared_context.quirks_mode,
|
||||
self.removed_id,
|
||||
self.classes_removed,
|
||||
&mut |dependency| {
|
||||
self.scan_dependency(dependency);
|
||||
true
|
||||
},
|
||||
);
|
||||
}
|
||||
fn collect_state_dependencies(
|
||||
&mut self,
|
||||
map: &SelectorMap<StateDependency>,
|
||||
state_changes: ElementState,
|
||||
) {
|
||||
map.lookup_with_additional(
|
||||
self.lookup_element,
|
||||
self.shared_context.quirks_mode,
|
||||
self.removed_id,
|
||||
self.classes_removed,
|
||||
&mut |dependency| {
|
||||
if !dependency.state.intersects(state_changes) {
|
||||
return true;
|
||||
}
|
||||
self.scan_dependency(&dependency.dep);
|
||||
true
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
fn scan_dependency(&mut self, dependency: &Dependency) {
|
||||
debug!("TreeStyleInvalidator::scan_dependency({:?}, {:?}, {})",
|
||||
self.element,
|
||||
dependency,
|
||||
is_visited_dependent);
|
||||
|
||||
if !self.dependency_may_be_relevant(dependency) {
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO(emilio): Add a bloom filter here?
|
||||
//
|
||||
// If we decide to do so, we can't use the bloom filter for snapshots,
|
||||
// given that arbitrary elements in the parent chain may have mutated
|
||||
// their id's/classes, which means that they won't be in the filter, and
|
||||
// as such we may fast-reject selectors incorrectly.
|
||||
//
|
||||
// We may be able to improve this if we record as we go down the tree
|
||||
// whether any parent had a snapshot, and whether those snapshots were
|
||||
// taken due to an element class/id change, but it's not clear it'd be
|
||||
// worth it.
|
||||
let mut now_context =
|
||||
MatchingContext::new_for_visited(MatchingMode::Normal, None,
|
||||
VisitedHandlingMode::AllLinksUnvisited,
|
||||
self.shared_context.quirks_mode);
|
||||
let mut then_context =
|
||||
MatchingContext::new_for_visited(MatchingMode::Normal, None,
|
||||
VisitedHandlingMode::AllLinksUnvisited,
|
||||
self.shared_context.quirks_mode);
|
||||
|
||||
let matched_then =
|
||||
matches_selector(&dependency.selector,
|
||||
dependency.selector_offset,
|
||||
&dependency.hashes,
|
||||
&self.wrapper,
|
||||
&mut then_context,
|
||||
&mut |_, _| {});
|
||||
let matches_now =
|
||||
matches_selector(&dependency.selector,
|
||||
dependency.selector_offset,
|
||||
&dependency.hashes,
|
||||
&self.element,
|
||||
&mut now_context,
|
||||
&mut |_, _| {});
|
||||
|
||||
// Check for mismatches in both the match result and also the status
|
||||
// of whether a relevant link was found.
|
||||
if matched_then != matches_now ||
|
||||
then_context.relevant_link_found != now_context.relevant_link_found {
|
||||
self.note_dependency(dependency);
|
||||
}
|
||||
}
|
||||
|
||||
fn note_dependency(&mut self, dependency: &Dependency) {
|
||||
if dependency.affects_self() {
|
||||
self.invalidates_self = true;
|
||||
}
|
||||
|
||||
if dependency.affects_descendants() {
|
||||
debug_assert_ne!(dependency.selector_offset, 0);
|
||||
debug_assert!(!dependency.affects_later_siblings());
|
||||
self.descendant_invalidations.push(Invalidation {
|
||||
selector: dependency.selector.clone(),
|
||||
offset: dependency.selector_offset,
|
||||
});
|
||||
} else if dependency.affects_later_siblings() {
|
||||
debug_assert_ne!(dependency.selector_offset, 0);
|
||||
self.sibling_invalidations.push(Invalidation {
|
||||
selector: dependency.selector.clone(),
|
||||
offset: dependency.selector_offset,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
if dependency.affects_descendants() || dependency.affects_later_siblings() {
|
||||
return true;
|
||||
}
|
||||
|
||||
debug_assert!(dependency.affects_self());
|
||||
!self.invalidates_self
|
||||
}
|
||||
}
|
10
components/style/invalidation/element/mod.rs
Normal file
10
components/style/invalidation/element/mod.rs
Normal file
|
@ -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 http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
//! Invalidation of element styles due to attribute or style changes.
|
||||
|
||||
pub mod element_wrapper;
|
||||
pub mod invalidation_map;
|
||||
pub mod invalidator;
|
||||
pub mod restyle_hints;
|
212
components/style/invalidation/element/restyle_hints.rs
Normal file
212
components/style/invalidation/element/restyle_hints.rs
Normal file
|
@ -0,0 +1,212 @@
|
|||
/* 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 http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
//! Restyle hints: an optimization to avoid unnecessarily matching selectors.
|
||||
|
||||
#[cfg(feature = "gecko")]
|
||||
use gecko_bindings::structs::nsRestyleHint;
|
||||
|
||||
bitflags! {
|
||||
/// The kind of restyle we need to do for a given element.
|
||||
pub flags RestyleHint: u8 {
|
||||
/// Do a selector match of the element.
|
||||
const RESTYLE_SELF = 1 << 0,
|
||||
|
||||
/// Do a selector match of the element's descendants.
|
||||
const RESTYLE_DESCENDANTS = 1 << 1,
|
||||
|
||||
/// Recascade the current element.
|
||||
const RECASCADE_SELF = 1 << 2,
|
||||
|
||||
/// Recascade all descendant elements.
|
||||
const RECASCADE_DESCENDANTS = 1 << 3,
|
||||
|
||||
/// 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 << 4,
|
||||
|
||||
/// 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 << 5,
|
||||
|
||||
/// 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 << 6,
|
||||
|
||||
/// 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 << 7,
|
||||
}
|
||||
}
|
||||
|
||||
impl RestyleHint {
|
||||
/// Creates a new `RestyleHint` indicating that the current element and all
|
||||
/// its descendants must be fully restyled.
|
||||
pub fn restyle_subtree() -> Self {
|
||||
RESTYLE_SELF | RESTYLE_DESCENDANTS
|
||||
}
|
||||
|
||||
/// Creates a new `RestyleHint` indicating that the current element and all
|
||||
/// its descendants must be recascaded.
|
||||
pub fn recascade_subtree() -> Self {
|
||||
RECASCADE_SELF | RECASCADE_DESCENDANTS
|
||||
}
|
||||
|
||||
/// Returns a new `CascadeHint` appropriate for children of the current
|
||||
/// element.
|
||||
pub fn propagate_for_non_animation_restyle(&self) -> Self {
|
||||
if self.contains(RESTYLE_DESCENDANTS) {
|
||||
return Self::restyle_subtree()
|
||||
}
|
||||
if self.contains(RECASCADE_DESCENDANTS) {
|
||||
return Self::recascade_subtree()
|
||||
}
|
||||
Self::empty()
|
||||
}
|
||||
|
||||
/// Creates a new `RestyleHint` that indicates the element must be
|
||||
/// recascaded.
|
||||
pub fn recascade_self() -> Self {
|
||||
RECASCADE_SELF
|
||||
}
|
||||
|
||||
/// Returns a hint that contains all the replacement hints.
|
||||
pub fn replacements() -> Self {
|
||||
RESTYLE_STYLE_ATTRIBUTE | Self::for_animations()
|
||||
}
|
||||
|
||||
/// The replacements for the animation cascade levels.
|
||||
#[inline]
|
||||
pub fn for_animations() -> Self {
|
||||
RESTYLE_SMIL | RESTYLE_CSS_ANIMATIONS | RESTYLE_CSS_TRANSITIONS
|
||||
}
|
||||
|
||||
/// Returns whether the hint specifies that some work must be performed on
|
||||
/// the current element.
|
||||
#[inline]
|
||||
pub fn affects_self(&self) -> bool {
|
||||
self.intersects(RESTYLE_SELF | RECASCADE_SELF | Self::replacements())
|
||||
}
|
||||
|
||||
/// Returns whether the hint specifies that the currently element must be
|
||||
/// recascaded.
|
||||
pub fn has_recascade_self(&self) -> bool {
|
||||
self.contains(RECASCADE_SELF)
|
||||
}
|
||||
|
||||
/// 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 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 selector matching must be re-run
|
||||
/// for the element.
|
||||
#[inline]
|
||||
pub fn match_self(&self) -> bool {
|
||||
self.intersects(RESTYLE_SELF)
|
||||
}
|
||||
|
||||
/// 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(RECASCADE_SELF);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "gecko")]
|
||||
impl From<nsRestyleHint> for RestyleHint {
|
||||
fn from(raw: nsRestyleHint) -> Self {
|
||||
use gecko_bindings::structs::nsRestyleHint_eRestyle_ForceDescendants as eRestyle_ForceDescendants;
|
||||
use gecko_bindings::structs::nsRestyleHint_eRestyle_LaterSiblings as eRestyle_LaterSiblings;
|
||||
use gecko_bindings::structs::nsRestyleHint_eRestyle_Self as eRestyle_Self;
|
||||
use gecko_bindings::structs::nsRestyleHint_eRestyle_SomeDescendants as eRestyle_SomeDescendants;
|
||||
use gecko_bindings::structs::nsRestyleHint_eRestyle_Subtree as eRestyle_Subtree;
|
||||
|
||||
let mut hint = RestyleHint::empty();
|
||||
|
||||
debug_assert!(raw.0 & eRestyle_LaterSiblings.0 == 0,
|
||||
"Handle later siblings manually if necessary plz.");
|
||||
|
||||
if (raw.0 & (eRestyle_Self.0 | eRestyle_Subtree.0)) != 0 {
|
||||
hint.insert(RESTYLE_SELF);
|
||||
}
|
||||
|
||||
if (raw.0 & (eRestyle_Subtree.0 | eRestyle_SomeDescendants.0)) != 0 {
|
||||
hint.insert(RESTYLE_DESCENDANTS);
|
||||
}
|
||||
|
||||
if (raw.0 & eRestyle_ForceDescendants.0) != 0 {
|
||||
hint.insert(RECASCADE_DESCENDANTS);
|
||||
}
|
||||
|
||||
hint.insert(RestyleHint::from_bits_truncate(raw.0 as u8));
|
||||
|
||||
hint
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "servo")]
|
||||
impl ::heapsize::HeapSizeOf for RestyleHint {
|
||||
fn heap_size_of_children(&self) -> usize { 0 }
|
||||
}
|
||||
|
||||
/// Asserts that all replacement hints have a matching nsRestyleHint value.
|
||||
#[cfg(feature = "gecko")]
|
||||
#[inline]
|
||||
pub fn assert_restyle_hints_match() {
|
||||
use gecko_bindings::structs;
|
||||
|
||||
macro_rules! check_restyle_hints {
|
||||
( $( $a:ident => $b:ident ),*, ) => {
|
||||
if cfg!(debug_assertions) {
|
||||
let mut replacements = RestyleHint::replacements();
|
||||
$(
|
||||
assert_eq!(structs::$a.0 as usize, $b.bits() as usize, stringify!($b));
|
||||
replacements.remove($b);
|
||||
)*
|
||||
assert_eq!(replacements, RestyleHint::empty(),
|
||||
"all RestyleHint replacement bits should have an \
|
||||
assertion");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
check_restyle_hints! {
|
||||
nsRestyleHint_eRestyle_CSSTransitions => RESTYLE_CSS_TRANSITIONS,
|
||||
nsRestyleHint_eRestyle_CSSAnimations => RESTYLE_CSS_ANIMATIONS,
|
||||
nsRestyleHint_eRestyle_StyleAttribute => RESTYLE_STYLE_ATTRIBUTE,
|
||||
nsRestyleHint_eRestyle_StyleAttribute_Animations => RESTYLE_SMIL,
|
||||
}
|
||||
}
|
|
@ -4,5 +4,6 @@
|
|||
|
||||
//! Different bits of code related to invalidating style.
|
||||
|
||||
pub mod element;
|
||||
pub mod media_queries;
|
||||
pub mod stylesheets;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue