diff options
Diffstat (limited to 'components/style/invalidation/mod.rs')
-rw-r--r-- | components/style/invalidation/mod.rs | 296 |
1 files changed, 296 insertions, 0 deletions
diff --git a/components/style/invalidation/mod.rs b/components/style/invalidation/mod.rs new file mode 100644 index 00000000000..93f0d8783ad --- /dev/null +++ b/components/style/invalidation/mod.rs @@ -0,0 +1,296 @@ +/* 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 collection of invalidations due to changes in which stylesheets affect a +//! document. + +#![deny(unsafe_code)] + +use Atom; +use data::StoredRestyleHint; +use dom::{TElement, TNode}; +use fnv::FnvHashSet; +use selector_parser::SelectorImpl; +use selectors::parser::{Component, Selector}; +use shared_lock::SharedRwLockReadGuard; +use stylesheets::{CssRule, Stylesheet}; +use stylist::Stylist; + +/// An invalidation scope represents a kind of subtree that may need to be +/// restyled. +#[derive(Debug, Hash, Eq, PartialEq)] +enum InvalidationScope { + /// All the descendants of an element with a given id. + ID(Atom), + /// All the descendants of an element with a given class name. + Class(Atom), +} + +impl InvalidationScope { + fn is_id(&self) -> bool { + matches!(*self, InvalidationScope::ID(..)) + } + + fn matches<E>(&self, element: E) -> bool + where E: TElement, + { + match *self { + InvalidationScope::Class(ref class) => { + element.has_class(class) + } + InvalidationScope::ID(ref id) => { + match element.get_id() { + Some(element_id) => element_id == *id, + None => false, + } + } + } + } +} + +/// A set of invalidations due to stylesheet additions. +/// +/// TODO(emilio): We might be able to do the same analysis for removals and +/// media query changes too? +pub struct StylesheetInvalidationSet { + /// The style scopes we know we have to restyle so far. + invalid_scopes: FnvHashSet<InvalidationScope>, + /// Whether the whole document should be invalid. + fully_invalid: bool, +} + +impl StylesheetInvalidationSet { + /// Create an empty `StylesheetInvalidationSet`. + pub fn new() -> Self { + Self { + invalid_scopes: FnvHashSet::default(), + fully_invalid: false, + } + } + + /// Mark the DOM tree styles' as fully invalid. + pub fn invalidate_fully(&mut self) { + debug!("StylesheetInvalidationSet::invalidate_fully"); + self.invalid_scopes.clear(); + self.fully_invalid = true; + } + + /// 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( + &mut self, + stylist: &Stylist, + stylesheet: &Stylesheet, + guard: &SharedRwLockReadGuard) + { + debug!("StylesheetInvalidationSet::collect_invalidations_for"); + if self.fully_invalid { + debug!(" > Fully invalid already"); + return; + } + + if stylesheet.disabled() || + !stylesheet.is_effective_for_device(stylist.device(), guard) { + debug!(" > Stylesheet was not effective"); + return; // Nothing to do here. + } + + for rule in stylesheet.effective_rules(stylist.device(), guard) { + self.collect_invalidations_for_rule(rule, guard); + if self.fully_invalid { + self.invalid_scopes.clear(); + break; + } + } + + debug!(" > resulting invalidations: {:?}", self.invalid_scopes); + debug!(" > fully_invalid: {}", self.fully_invalid); + } + + /// Clears the invalidation set, invalidating elements as needed if + /// `document_element` is provided. + pub fn flush<E>(&mut self, document_element: Option<E>) + where E: TElement, + { + if let Some(e) = document_element { + self.process_invalidations_in_subtree(e); + } + self.invalid_scopes.clear(); + self.fully_invalid = false; + } + + /// Process style invalidations in a given subtree, that is, look for all + /// the relevant scopes in the subtree, and mark as dirty only the relevant + /// ones. + /// + /// Returns whether it invalidated at least one element's style. + #[allow(unsafe_code)] + fn process_invalidations_in_subtree<E>(&self, element: E) -> bool + where E: TElement, + { + let mut data = match element.mutate_data() { + Some(data) => data, + None => return false, + }; + + if !data.has_styles() { + return false; + } + + if let Some(ref r) = data.get_restyle() { + if r.hint.contains_subtree() { + debug!("process_invalidations_in_subtree: {:?} was already invalid", + element); + return false; + } + } + + if self.fully_invalid { + debug!("process_invalidations_in_subtree: fully_invalid({:?})", + element); + data.ensure_restyle().hint.insert(StoredRestyleHint::subtree()); + return true; + } + + for scope in &self.invalid_scopes { + if scope.matches(element) { + debug!("process_invalidations_in_subtree: {:?} matched {:?}", + element, scope); + data.ensure_restyle().hint.insert(StoredRestyleHint::subtree()); + return true; + } + } + + + let mut any_children_invalid = false; + + for child in element.as_node().children() { + let child = match child.as_element() { + Some(e) => e, + None => continue, + }; + + any_children_invalid |= self.process_invalidations_in_subtree(child); + } + + if any_children_invalid { + debug!("Children of {:?} changed, setting dirty descendants", + element); + unsafe { element.set_dirty_descendants() } + } + + return any_children_invalid + } + + fn scan_component( + component: &Component<SelectorImpl>, + scope: &mut Option<InvalidationScope>) + { + match *component { + Component::Class(ref class) => { + if scope.as_ref().map_or(true, |s| !s.is_id()) { + *scope = Some(InvalidationScope::Class(class.clone())); + } + } + Component::ID(ref id) => { + if scope.is_none() { + *scope = Some(InvalidationScope::ID(id.clone())); + } + } + _ => { + // Ignore everything else, at least for now. + } + } + } + + /// Collect a style scopes for a given selector. + /// + /// We look at the outermost class or id selector to the left of an ancestor + /// combinator, in order to restyle only a given subtree. + /// + /// We prefer id scopes to class scopes, and outermost scopes to innermost + /// scopes (to reduce the amount of traversal we need to do). + fn collect_scopes(&mut self, selector: &Selector<SelectorImpl>) { + debug!("StylesheetInvalidationSet::collect_scopes({:?})", selector); + + let mut scope: Option<InvalidationScope> = None; + + let mut scan = true; + let mut iter = selector.inner.complex.iter(); + + loop { + for component in &mut iter { + if scan { + Self::scan_component(component, &mut scope); + } + } + match iter.next_sequence() { + None => break, + Some(combinator) => { + scan = combinator.is_ancestor(); + } + } + } + + match scope { + Some(s) => { + debug!(" > Found scope: {:?}", s); + self.invalid_scopes.insert(s); + } + None => { + debug!(" > Scope not found"); + + // If we didn't find a scope, any element could match this, so + // let's just bail out. + self.fully_invalid = true; + } + } + } + + /// Collects invalidations for a given CSS rule. + fn collect_invalidations_for_rule( + &mut self, + rule: &CssRule, + guard: &SharedRwLockReadGuard) + { + use stylesheets::CssRule::*; + debug!("StylesheetInvalidationSet::collect_invalidations_for_rule"); + debug_assert!(!self.fully_invalid, "Not worth to be here!"); + + match *rule { + Style(ref lock) => { + let style_rule = lock.read_with(guard); + for selector in &style_rule.selectors.0 { + self.collect_scopes(selector); + if self.fully_invalid { + return; + } + } + } + Document(..) | + Namespace(..) | + Import(..) | + Media(..) | + Supports(..) => { + // Do nothing, relevant nested rules are visited as part of the + // iteration. + } + FontFace(..) | + CounterStyle(..) | + Keyframes(..) | + Page(..) | + Viewport(..) => { + debug!(" > Found unsupported rule, marking the whole subtree \ + invalid."); + + // TODO(emilio): Can we do better here? + // + // At least in `@page`, we could check the relevant media, I + // guess. + self.fully_invalid = true; + } + } + } +} |