servo/components/style/restyle_hints.rs
2017-05-21 00:12:16 +02:00

1063 lines
38 KiB
Rust

/* 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_parser::{NonTSPseudoClass, PseudoElement, SelectorImpl, Snapshot, SnapshotMap, AttrValue};
use selectors::Element;
use selectors::attr::{AttrSelectorOperation, NamespaceConstraint};
use selectors::matching::{ElementSelectorFlags, MatchingContext, MatchingMode};
use selectors::matching::matches_selector;
use selectors::parser::{Combinator, Component, Selector, SelectorInner, SelectorMethods};
use selectors::visitor::SelectorVisitor;
use smallvec::SmallVec;
use std::borrow::Borrow;
use std::cell::Cell;
use std::clone::Clone;
use std::cmp;
use stylist::SelectorMap;
/// 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,
/// 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
}
}
/// 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,
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,
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,
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,
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,
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,
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,
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,
replacements: replacements,
}
}
/// 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.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.replacements.is_empty()
}
/// 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.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 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,
replacements: RestyleReplacements::empty(),
}
}
/// Removes all of the animation-related hints.
#[inline]
pub fn remove_animation_hints(&mut self) {
self.replacements.remove(RestyleReplacements::for_animations());
}
/// 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.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(&mut 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.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<nsRestyleHint> for RestyleReplacements {
fn from(raw: nsRestyleHint) -> Self {
Self::from_bits_truncate(raw.0 as u8)
}
}
#[cfg(feature = "gecko")]
impl From<nsRestyleHint> for RestyleHint {
fn from(raw: nsRestyleHint) -> Self {
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());
}
RestyleHint {
match_under_self: match_under_self,
match_later_siblings: (raw.0 & eRestyle_LaterSiblings.0) != 0,
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<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) -> 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);
}
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`.
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<F>(&self,
pseudo_class: &NonTSPseudoClass,
context: &mut MatchingContext,
_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);
}
}
let flag = pseudo_class.state_flag();
if flag.is_empty() {
return self.element.match_non_ts_pseudo_class(pseudo_class,
context,
&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,
&mut |_, _| {})
}
}
}
fn match_pseudo_element(&self,
pseudo_element: &PseudoElement,
context: &mut MatchingContext)
-> bool
{
self.element.match_pseudo_element(pseudo_element, context)
}
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 get_id(&self) -> Option<Atom> {
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> {
self.element.closest_non_native_anonymous_ancestor()
.map(|e| ElementWrapper::new(e, self.snapshot_map))
}
}
fn selector_to_state(sel: &Component<SelectorImpl>) -> 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<SelectorImpl>) -> 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<SelectorImpl>,
/// The hint associated with this dependency.
pub hint: RestyleHint,
/// The sensitivities associated with this dependency.
pub sensitivities: Sensitivities,
}
impl Borrow<SelectorInner<SelectorImpl>> for Dependency {
fn borrow(&self) -> &SelectorInner<SelectorImpl> {
&self.selector
}
}
/// A similar version of the above, but for pseudo-elements, which only care
/// about the full selector, and need it in order to properly track
/// pseudo-element selector state.
///
/// NOTE(emilio): We could add a `hint` and `sensitivities` field to the
/// `PseudoElementDependency` and stop posting `RESTYLE_DESCENDANTS`s hints if
/// we visited all the pseudo-elements of an element unconditionally as part of
/// the traversal.
///
/// That would allow us to stop posting `RESTYLE_DESCENDANTS` hints for dumb
/// selectors, and storing pseudo dependencies in the element dependency map.
///
/// That would allow us to avoid restyling the element itself when a selector
/// has only changed a pseudo-element's style, too.
///
/// There's no good way to do that right now though, and I think for the
/// foreseeable future we may just want to optimize that `RESTYLE_DESCENDANTS`
/// to become a `RESTYLE_PSEUDO_ELEMENTS` or something like that, in order to at
/// least not restyle the whole subtree.
#[derive(Clone, Debug)]
#[cfg_attr(feature = "servo", derive(HeapSizeOf))]
struct PseudoElementDependency {
#[cfg_attr(feature = "servo", ignore_heap_size_of = "defined in selectors")]
selector: Selector<SelectorImpl>,
}
impl Borrow<SelectorInner<SelectorImpl>> for PseudoElementDependency {
fn borrow(&self) -> &SelectorInner<SelectorImpl> {
&self.selector.inner
}
}
/// 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<SelectorImpl>) -> 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<Dependency>,
}
/// 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<E>,
/// 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<SelectorImpl>) {
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();
// 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 mut element_matching_context =
MatchingContext::new(MatchingMode::Normal, bloom_filter);
// 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 snapshot_matching_context =
MatchingContext::new(MatchingMode::Normal, None);
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;
}
// We can ignore the selector flags, since they would have already
// been set during original matching for any element that might
// change its matching behavior here.
let matched_then =
matches_selector(&dep.selector, &snapshot_el,
&mut snapshot_matching_context,
&mut |_, _| {});
let matches_now =
matches_selector(&dep.selector, el,
&mut element_matching_context,
&mut |_, _| {});
if matched_then != matches_now {
hint.insert_from(&dep.hint);
}
!hint.is_maximum()
});
debug!("Calculated restyle hint: {:?} for {:?}. (State={:?}, {} Deps)",
hint, el, el.get_state(), self.len());
hint
}
}