/* 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. #![deny(missing_docs)] use Atom; use LocalName; use Namespace; use context::{SharedStyleContext, ThreadLocalStyleContext}; use dom::TElement; use element_state::*; #[cfg(feature = "gecko")] use gecko_bindings::structs::nsRestyleHint; #[cfg(feature = "servo")] use heapsize::HeapSizeOf; use selector_map::{SelectorMap, SelectorMapEntry}; use selector_parser::{NonTSPseudoClass, PseudoElement, SelectorImpl, Snapshot, SnapshotMap, AttrValue}; use selectors::Element; use selectors::attr::{AttrSelectorOperation, NamespaceConstraint}; use selectors::matching::{ElementSelectorFlags, MatchingContext, MatchingMode}; use selectors::matching::{RelevantLinkStatus, VisitedHandlingMode, matches_selector}; use selectors::parser::{Combinator, Component, Selector, SelectorInner, SelectorMethods}; use selectors::visitor::SelectorVisitor; use smallvec::SmallVec; use std::cell::Cell; use std::clone::Clone; use std::cmp; /// When the ElementState of an element (like IN_HOVER_STATE) changes, /// certain pseudo-classes (like :hover) may require us to restyle that /// element, its siblings, and/or its descendants. Similarly, when various /// attributes of an element change, we may also need to restyle things with /// id, class, and attribute selectors. Doing this conservatively is /// expensive, and so we use RestyleHints to short-circuit work we know is /// unnecessary. #[derive(Debug, Clone, PartialEq)] pub struct RestyleHint { /// Depths at which selector matching must be re-run. match_under_self: RestyleDepths, /// Rerun selector matching on all later siblings of the element and all /// of their descendants. match_later_siblings: bool, /// Whether the current element and/or all descendants must be recascade. recascade: CascadeHint, /// Levels of the cascade whose rule nodes should be recomputed and /// replaced. pub replacements: RestyleReplacements, } bitflags! { /// Cascade levels that can be updated for an element by simply replacing /// their rule node. /// /// Note that the bit values here must be kept in sync with the Gecko /// nsRestyleHint values. If you add more bits with matching values, /// please add assertions to assert_restyle_hints_match below. pub flags RestyleReplacements: u8 { /// 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 = 0x10, /// 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 = 0x20, /// 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 = 0x40, /// 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 = 0x80, } } /// Eight bit wide bitfield representing depths of a DOM subtree's descendants, /// used to represent which elements must have selector matching re-run on them. /// /// The least significant bit indicates that selector matching must be re-run /// for the element itself, the second least significant bit for the element's /// children, the third its grandchildren, and so on. If the most significant /// bit it set, it indicates that that selector matching must be re-run for /// elements at that depth and all of their descendants. #[derive(Debug, Clone, Copy, PartialEq)] struct RestyleDepths(u8); impl RestyleDepths { /// Returns a `RestyleDepths` representing no element depths. fn empty() -> Self { RestyleDepths(0) } /// Returns a `RestyleDepths` representing the current element depth. fn for_self() -> Self { RestyleDepths(0x01) } /// Returns a `RestyleDepths` representing the depths of all descendants of /// the current element. fn for_descendants() -> Self { RestyleDepths(0xfe) } /// Returns a `RestyleDepths` representing the current element depth and the /// depths of all the current element's descendants. fn for_self_and_descendants() -> Self { RestyleDepths(0xff) } /// Returns a `RestyleDepths` representing the specified depth, where zero /// is the current element depth, one is its children's depths, etc. fn for_depth(depth: u32) -> Self { RestyleDepths(1u8 << cmp::min(depth, 7)) } /// Returns whether this `RestyleDepths` represents the current element /// depth and the depths of all the current element's descendants. fn is_self_and_descendants(&self) -> bool { self.0 == 0xff } /// Returns whether this `RestyleDepths` includes any element depth. fn is_any(&self) -> bool { self.0 != 0 } /// Returns whether this `RestyleDepths` includes the current element depth. fn has_self(&self) -> bool { (self.0 & 0x01) != 0 } /// Returns a new `RestyleDepths` with all depth values represented by this /// `RestyleDepths` reduced by one. fn propagate(&self) -> Self { RestyleDepths((self.0 >> 1) | (self.0 & 0x80)) } /// Returns a new `RestyleDepths` that represents the union of the depths /// from `self` and `other`. fn insert(&mut self, other: RestyleDepths) { self.0 |= other.0; } /// Returns whether this `RestyleDepths` includes all depths represented /// by `other`. fn contains(&self, other: RestyleDepths) -> bool { (self.0 & other.0) == other.0 } } bitflags! { /// Flags representing whether the current element or its descendants /// must be recascaded. /// /// FIXME(bholley): This should eventually become more fine-grained. pub flags CascadeHint: u8 { /// Recascade the current element. const RECASCADE_SELF = 0x01, /// Recascade all descendant elements. const RECASCADE_DESCENDANTS = 0x02, } } impl CascadeHint { /// Creates a new `CascadeHint` indicating that the current element and all /// its descendants must be recascaded. fn subtree() -> CascadeHint { RECASCADE_SELF | RECASCADE_DESCENDANTS } /// Returns a new `CascadeHint` appropriate for children of the current /// element. fn propagate(&self) -> Self { if self.contains(RECASCADE_DESCENDANTS) { CascadeHint::subtree() } else { CascadeHint::empty() } } } /// Asserts that all RestyleReplacements 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 = RestyleReplacements::all(); $( assert_eq!(structs::$a.0 as usize, $b.bits() as usize, stringify!($b)); replacements.remove($b); )* assert_eq!(replacements, RestyleReplacements::empty(), "all RestyleReplacements 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, } } impl RestyleHint { /// Creates a new, empty `RestyleHint`, which represents no restyling work /// needs to be done. #[inline] pub fn empty() -> Self { RestyleHint { match_under_self: RestyleDepths::empty(), match_later_siblings: false, recascade: CascadeHint::empty(), replacements: RestyleReplacements::empty(), } } /// Creates a new `RestyleHint` that indicates selector matching must be /// re-run on the element. #[inline] pub fn for_self() -> Self { RestyleHint { match_under_self: RestyleDepths::for_self(), match_later_siblings: false, recascade: CascadeHint::empty(), replacements: RestyleReplacements::empty(), } } /// Creates a new `RestyleHint` that indicates selector matching must be /// re-run on all of the element's descendants. #[inline] pub fn descendants() -> Self { RestyleHint { match_under_self: RestyleDepths::for_descendants(), match_later_siblings: false, recascade: CascadeHint::empty(), replacements: RestyleReplacements::empty(), } } /// Creates a new `RestyleHint` that indicates selector matching must be /// re-run on the descendants of element at the specified depth, where 0 /// indicates the element itself, 1 is its children, 2 its grandchildren, /// etc. #[inline] pub fn descendants_at_depth(depth: u32) -> Self { RestyleHint { match_under_self: RestyleDepths::for_depth(depth), match_later_siblings: false, recascade: CascadeHint::empty(), replacements: RestyleReplacements::empty(), } } /// Creates a new `RestyleHint` that indicates selector matching must be /// re-run on all of the element's later siblings and their descendants. #[inline] pub fn later_siblings() -> Self { RestyleHint { match_under_self: RestyleDepths::empty(), match_later_siblings: true, recascade: CascadeHint::empty(), replacements: RestyleReplacements::empty(), } } /// Creates a new `RestyleHint` that indicates selector matching must be /// re-run on the element and all of its descendants. #[inline] pub fn subtree() -> Self { RestyleHint { match_under_self: RestyleDepths::for_self_and_descendants(), match_later_siblings: false, recascade: CascadeHint::empty(), replacements: RestyleReplacements::empty(), } } /// Creates a new `RestyleHint` that indicates selector matching must be /// re-run on the element, its descendants, its later siblings, and /// their descendants. #[inline] pub fn subtree_and_later_siblings() -> Self { RestyleHint { match_under_self: RestyleDepths::for_self_and_descendants(), match_later_siblings: true, recascade: CascadeHint::empty(), replacements: RestyleReplacements::empty(), } } /// Creates a new `RestyleHint` that indicates the specified rule node /// replacements must be performed on the element. #[inline] pub fn for_replacements(replacements: RestyleReplacements) -> Self { RestyleHint { match_under_self: RestyleDepths::empty(), match_later_siblings: false, recascade: CascadeHint::empty(), replacements: replacements, } } /// Creates a new `RestyleHint` that indicates the element must be /// recascaded. pub fn recascade_self() -> Self { RestyleHint { match_under_self: RestyleDepths::empty(), match_later_siblings: false, recascade: RECASCADE_SELF, replacements: RestyleReplacements::empty(), } } /// Returns whether this `RestyleHint` represents no needed restyle work. #[inline] pub fn is_empty(&self) -> bool { *self == RestyleHint::empty() } /// Returns whether this `RestyleHint` represents the maximum possible /// restyle work, and thus any `insert()` calls will have no effect. #[inline] pub fn is_maximum(&self) -> bool { self.match_under_self.is_self_and_descendants() && self.match_later_siblings && self.recascade.is_all() && self.replacements.is_all() } /// Returns whether the hint specifies that some work must be performed on /// the current element. #[inline] pub fn affects_self(&self) -> bool { self.match_self() || self.has_recascade_self() || !self.replacements.is_empty() } /// Returns whether the hint specifies that the currently element must be /// recascaded. pub fn has_recascade_self(&self) -> bool { self.recascade.contains(RECASCADE_SELF) } /// Returns whether the hint specifies that later siblings must be restyled. #[inline] pub fn affects_later_siblings(&self) -> bool { self.match_later_siblings } /// Returns whether the hint specifies that an animation cascade level must /// be replaced. #[inline] pub fn has_animation_hint(&self) -> bool { self.replacements.intersects(RestyleReplacements::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.match_under_self.is_any() || self.match_later_siblings || !self.recascade.is_empty() || self.replacements.contains(RESTYLE_STYLE_ATTRIBUTE) } /// Returns whether the hint specifies that selector matching must be re-run /// for the element. #[inline] pub fn match_self(&self) -> bool { self.match_under_self.has_self() } /// Returns whether the hint specifies that some cascade levels must be /// replaced. #[inline] pub fn has_replacements(&self) -> bool { !self.replacements.is_empty() } /// Returns a new `RestyleHint` appropriate for children of the current /// element. #[inline] pub fn propagate_for_non_animation_restyle(&self) -> Self { RestyleHint { match_under_self: self.match_under_self.propagate(), match_later_siblings: false, recascade: self.recascade.propagate(), replacements: RestyleReplacements::empty(), } } /// Removes all of the animation-related hints. #[inline] pub fn remove_animation_hints(&mut self) { self.replacements.remove(RestyleReplacements::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.recascade.remove(RECASCADE_SELF); } /// Removes the later siblings hint, and returns whether it was present. pub fn remove_later_siblings_hint(&mut self) -> bool { let later_siblings = self.match_later_siblings; self.match_later_siblings = false; later_siblings } /// Unions the specified `RestyleHint` into this one. #[inline] pub fn insert_from(&mut self, other: &Self) { self.match_under_self.insert(other.match_under_self); self.match_later_siblings |= other.match_later_siblings; self.recascade.insert(other.recascade); self.replacements.insert(other.replacements); } /// Unions the specified `RestyleHint` into this one. #[inline] pub fn insert(&mut self, other: Self) { // A later patch should make it worthwhile to have an insert() function // that consumes its argument. self.insert_from(&other) } /// Returns whether this `RestyleHint` represents at least as much restyle /// work as the specified one. #[inline] pub fn contains(&self, other: &Self) -> bool { self.match_under_self.contains(other.match_under_self) && (self.match_later_siblings & other.match_later_siblings) == other.match_later_siblings && self.recascade.contains(other.recascade) && self.replacements.contains(other.replacements) } } impl RestyleReplacements { /// The replacements for the animation cascade levels. #[inline] pub fn for_animations() -> Self { RESTYLE_SMIL | RESTYLE_CSS_ANIMATIONS | RESTYLE_CSS_TRANSITIONS } } #[cfg(feature = "gecko")] impl From for RestyleReplacements { fn from(raw: nsRestyleHint) -> Self { Self::from_bits_truncate(raw.0 as u8) } } #[cfg(feature = "gecko")] impl From 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 match_under_self = RestyleDepths::empty(); if (raw.0 & (eRestyle_Self.0 | eRestyle_Subtree.0)) != 0 { match_under_self.insert(RestyleDepths::for_self()); } if (raw.0 & (eRestyle_Subtree.0 | eRestyle_SomeDescendants.0)) != 0 { match_under_self.insert(RestyleDepths::for_descendants()); } let mut recascade = CascadeHint::empty(); if (raw.0 & eRestyle_ForceDescendants.0) != 0 { recascade.insert(CascadeHint::subtree()) } RestyleHint { match_under_self: match_under_self, match_later_siblings: (raw.0 & eRestyle_LaterSiblings.0) != 0, recascade: recascade, replacements: raw.into(), } } } #[cfg(feature = "servo")] impl HeapSizeOf for RestyleHint { fn heap_size_of_children(&self) -> usize { 0 } } /// 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; /// 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; /// Whether this snapshot contains the class `name`. Should only be called /// if `has_attrs()` returns true. fn has_class(&self, name: &Atom) -> 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(&self, F) where F: FnMut(&Atom); } struct ElementWrapper<'a, E> where E: TElement, { element: E, cached_snapshot: Cell>, snapshot_map: &'a SnapshotMap, } impl<'a, E> ElementWrapper<'a, E> where E: TElement, { /// Trivially constructs an `ElementWrapper`. 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. 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 } 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(), } } } #[cfg(feature = "gecko")] fn dir_selector_to_state(s: &[u16]) -> ElementState { // 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() } } 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, relevant_link: &RelevantLinkStatus, _setter: &mut F) -> bool where F: FnMut(&Self, ElementSelectorFlags), { // :moz-any is quite special, because we need to keep matching as a // snapshot. #[cfg(feature = "gecko")] { use selectors::matching::matches_complex_selector; if let NonTSPseudoClass::MozAny(ref selectors) = *pseudo_class { return selectors.iter().any(|s| { matches_complex_selector(s, self, context, _setter) }) } } // :dir needs special handling. It's 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")] { if let NonTSPseudoClass::Dir(ref s) = *pseudo_class { 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. if *pseudo_class == NonTSPseudoClass::Link { return relevant_link.is_unvisited(self, context) } if *pseudo_class == NonTSPseudoClass::Visited { return relevant_link.is_visited(self, context) } 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 { let mut context = MatchingContext::new(MatchingMode::Normal, None); self.match_non_ts_pseudo_class(&NonTSPseudoClass::AnyLink, &mut context, &RelevantLinkStatus::default(), &mut |_, _| {}) } fn parent_element(&self) -> Option { self.element.parent_element() .map(|e| ElementWrapper::new(e, self.snapshot_map)) } fn first_child_element(&self) -> Option { self.element.first_child_element() .map(|e| ElementWrapper::new(e, self.snapshot_map)) } fn last_child_element(&self) -> Option { self.element.last_child_element() .map(|e| ElementWrapper::new(e, self.snapshot_map)) } fn prev_sibling_element(&self) -> Option { self.element.prev_sibling_element() .map(|e| ElementWrapper::new(e, self.snapshot_map)) } fn next_sibling_element(&self) -> Option { 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) -> &::BorrowedLocalName { self.element.get_local_name() } fn get_namespace(&self) -> &::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 get_id(&self) -> Option { match self.snapshot() { Some(snapshot) if snapshot.has_attrs() => snapshot.id_attr(), _ => self.element.get_id() } } fn has_class(&self, name: &Atom) -> bool { match self.snapshot() { Some(snapshot) if snapshot.has_attrs() => snapshot.has_class(name), _ => self.element.has_class(name) } } 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.element.closest_non_native_anonymous_ancestor() .map(|e| ElementWrapper::new(e, self.snapshot_map)) } } fn selector_to_state(sel: &Component) -> ElementState { match *sel { // 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")] Component::NonTSPseudoClass(NonTSPseudoClass::Dir(ref s)) => dir_selector_to_state(s), Component::NonTSPseudoClass(ref pc) => pc.state_flag(), _ => ElementState::empty(), } } fn is_attr_selector(sel: &Component) -> bool { match *sel { Component::ID(_) | Component::Class(_) | Component::AttributeInNoNamespaceExists { .. } | Component::AttributeInNoNamespace { .. } | Component::AttributeOther(_) => true, _ => false, } } #[derive(Clone, Debug)] #[cfg_attr(feature = "servo", derive(HeapSizeOf))] /// The aspects of an selector which are sensitive. pub struct Sensitivities { /// The states which are sensitive. pub states: ElementState, /// Whether attributes are sensitive. pub attrs: bool, } impl Sensitivities { fn is_empty(&self) -> bool { self.states.is_empty() && !self.attrs } fn new() -> Sensitivities { Sensitivities { states: ElementState::empty(), attrs: false, } } fn sensitive_to(&self, attrs: bool, states: ElementState) -> bool { (attrs && self.attrs) || self.states.intersects(states) } } /// 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 { #[cfg_attr(feature = "servo", ignore_heap_size_of = "Arc")] selector: SelectorInner, /// The hint associated with this dependency. pub hint: RestyleHint, /// The sensitivities associated with this dependency. pub sensitivities: Sensitivities, } impl SelectorMapEntry for Dependency { fn selector(&self) -> &SelectorInner { &self.selector } } /// The following visitor visits all the simple selectors for a given complex /// selector, taking care of :not and :any combinators, collecting whether any /// of them is sensitive to attribute or state changes. struct SensitivitiesVisitor { sensitivities: Sensitivities, } impl SelectorVisitor for SensitivitiesVisitor { type Impl = SelectorImpl; fn visit_simple_selector(&mut self, s: &Component) -> bool { self.sensitivities.states.insert(selector_to_state(s)); self.sensitivities.attrs |= is_attr_selector(s); true } } /// A set of dependencies for a given stylist. /// /// Note that we can have many dependencies, often more than the total number /// of selectors given that we can get multiple partial selectors for a given /// selector. As such, we want all the usual optimizations, including the /// SelectorMap and the bloom filter. #[derive(Debug)] #[cfg_attr(feature = "servo", derive(HeapSizeOf))] pub struct DependencySet { /// This is for all other normal element's selectors/selector parts. dependencies: SelectorMap, } /// The data that we need to compute a given restyle hint. pub enum HintComputationContext<'a, E: 'a> where E: TElement, { /// The data we need to compute a restyle hint for the root of the /// traversal. /// /// We don't bother with the bloom filter there for multiple reasons: /// /// * The root of the traversal uses to be the root of the document, so we /// don't gain much using a bloom filter. /// /// * The chances that a non-root-element root of the traversal has a /// snapshot is quite low. Root, /// The data we need to compute a restyle hint for a child. /// /// This needs a full-blown style context in order to get the selector /// filters up-to-date, and the dom depth in order to insert into the filter /// properly if needed. Child { /// The thread-local context, that holds the bloom filter alive. local_context: &'a mut ThreadLocalStyleContext, /// The dom depth of this element. dom_depth: usize, } } impl DependencySet { /// Adds a selector to this `DependencySet`. pub fn note_selector(&mut self, selector: &Selector) { let mut combinator = None; let mut iter = selector.inner.complex.iter(); let mut index = 0; let mut child_combinators_seen = 0; let mut saw_descendant_combinator = false; loop { let sequence_start = index; let mut visitor = SensitivitiesVisitor { sensitivities: Sensitivities::new() }; // 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 complex selectors as well. for ss in &mut iter { ss.visit(&mut visitor); index += 1; // Account for the simple selector. } // Keep track of how many child combinators we've encountered, // and whether we've encountered a descendant combinator at all. match combinator { Some(Combinator::Child) => child_combinators_seen += 1, Some(Combinator::Descendant) => saw_descendant_combinator = true, _ => {} } // If we found a sensitivity, add an entry in the dependency set. if !visitor.sensitivities.is_empty() { // Compute a RestyleHint given the current combinator and the // tracked number of child combinators and presence of a // descendant combinator. let hint = match combinator { // NB: RestyleHint::subtree() and not // RestyleHint::descendants() is needed to handle properly // eager pseudos, otherwise we may leave a stale style on // the parent. Some(Combinator::PseudoElement) => RestyleHint::subtree(), Some(Combinator::Child) if !saw_descendant_combinator => { RestyleHint::descendants_at_depth(child_combinators_seen) } Some(Combinator::Child) | Some(Combinator::Descendant) => RestyleHint::descendants(), Some(Combinator::NextSibling) | Some(Combinator::LaterSibling) => RestyleHint::later_siblings(), None => RestyleHint::for_self(), }; let dep_selector = if sequence_start == 0 { // Reuse the bloom hashes if this is the base selector. selector.inner.clone() } else { SelectorInner::new(selector.inner.complex.slice_from(sequence_start)) }; self.dependencies.insert(Dependency { sensitivities: visitor.sensitivities, hint: hint, selector: dep_selector, }); } combinator = iter.next_sequence(); if combinator.is_none() { break; } index += 1; // Account for the combinator. } } /// Create an empty `DependencySet`. pub fn new() -> Self { DependencySet { dependencies: SelectorMap::new(), } } /// Return the total number of dependencies that this set contains. pub fn len(&self) -> usize { self.dependencies.len() } /// Clear this dependency set. pub fn clear(&mut self) { self.dependencies = SelectorMap::new(); } /// Compute a restyle hint given an element and a snapshot, per the rules /// explained in the rest of the documentation. pub fn compute_hint<'a, E>( &self, el: &E, shared_context: &SharedStyleContext, hint_context: HintComputationContext<'a, E>) -> RestyleHint where E: TElement, { debug_assert!(el.has_snapshot(), "Shouldn't be here!"); let snapshot_el = ElementWrapper::new(*el, shared_context.snapshot_map); let snapshot = snapshot_el.snapshot().expect("has_snapshot lied so badly"); let state_changes = snapshot_el.state_changes(); let attrs_changed = snapshot.has_attrs(); if state_changes.is_empty() && !attrs_changed { return RestyleHint::empty(); } let mut hint = RestyleHint::empty(); // If we are sensitive to visitedness and the visited state changed, we // force a restyle here. Matching doesn't depend on the actual visited // state at all, so we can't look at matching results to decide what to // do for this case. if state_changes.intersects(IN_VISITED_OR_UNVISITED_STATE) { trace!(" > visitedness change, force subtree restyle"); // We can't just return here because there may also be attribute // changes as well that imply additional hints. hint = RestyleHint::subtree(); } // Compute whether the snapshot has any different id or class attributes // from the element. If it does, we need to pass those to the lookup, so // that we get all the possible applicable selectors from the rulehash. let mut additional_id = None; let mut additional_classes = SmallVec::<[Atom; 8]>::new(); if attrs_changed { let id = snapshot.id_attr(); if id.is_some() && id != el.get_id() { additional_id = id; } snapshot.each_class(|c| { if !el.has_class(c) { additional_classes.push(c.clone()) } }); } let bloom_filter = match hint_context { HintComputationContext::Root => None, HintComputationContext::Child { mut local_context, dom_depth } => { local_context .bloom_filter .insert_parents_recovering(*el, dom_depth); local_context.bloom_filter.assert_complete(*el); Some(local_context.bloom_filter.filter()) } }; let lookup_element = if el.implemented_pseudo_element().is_some() { el.closest_non_native_anonymous_ancestor().unwrap() } else { *el }; self.dependencies .lookup_with_additional(lookup_element, additional_id, &additional_classes, &mut |dep| { trace!("scanning dependency: {:?}", dep); if !dep.sensitivities.sensitive_to(attrs_changed, state_changes) { trace!(" > non-sensitive"); return true; } if hint.contains(&dep.hint) { trace!(" > hint was already there"); return true; } // NOTE(emilio): 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 we _need_ it right now. let mut then_context = MatchingContext::new_for_visited(MatchingMode::Normal, None, VisitedHandlingMode::AllLinksUnvisited); let matched_then = matches_selector(&dep.selector, &snapshot_el, &mut then_context, &mut |_, _| {}); let mut now_context = MatchingContext::new_for_visited(MatchingMode::Normal, bloom_filter, VisitedHandlingMode::AllLinksUnvisited); let matches_now = matches_selector(&dep.selector, el, &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 { hint.insert_from(&dep.hint); return !hint.is_maximum() } // If there is a relevant link, then we also matched in visited // mode. Match again in this mode to ensure this also matches. // Note that we never actually match directly against the element's // true visited state at all, since that would expose us to timing // attacks. The matching process only considers the relevant link // state and visited handling mode when deciding if visited // matches. Instead, we are rematching here in case there is some // :visited selector whose matching result changed for some _other_ // element state or attribute. if now_context.relevant_link_found && dep.sensitivities.states.intersects(IN_VISITED_OR_UNVISITED_STATE) { then_context.visited_handling = VisitedHandlingMode::RelevantLinkVisited; let matched_then = matches_selector(&dep.selector, &snapshot_el, &mut then_context, &mut |_, _| {}); now_context.visited_handling = VisitedHandlingMode::RelevantLinkVisited; let matches_now = matches_selector(&dep.selector, el, &mut now_context, &mut |_, _| {}); if matched_then != matches_now { hint.insert_from(&dep.hint); return !hint.is_maximum() } } !hint.is_maximum() }); debug!("Calculated restyle hint: {:?} for {:?}. (State={:?}, {} Deps)", hint, el, el.get_state(), self.len()); hint } }