mirror of
https://github.com/servo/servo.git
synced 2025-06-23 00:24:35 +01:00
989 lines
37 KiB
Rust
989 lines
37 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/. */
|
|
|
|
//! 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, IN_VISITED_OR_UNVISITED_STATE};
|
|
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, Snapshot};
|
|
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,
|
|
// TODO(emilio): It's tempting enough to just avoid running invalidation for
|
|
// elements without data.
|
|
//
|
|
// But that's be wrong for sibling invalidations when a new element has been
|
|
// inserted in the tree and still has no data (though I _think_ the slow
|
|
// selector bits save us, it'd be nice not to depend on them).
|
|
//
|
|
// Seems like we could at least avoid running invalidation for the
|
|
// descendants if an element has no data, though.
|
|
data: Option<&'a mut ElementData>,
|
|
shared_context: &'a SharedStyleContext<'b>,
|
|
}
|
|
|
|
type InvalidationVector = SmallVec<[Invalidation; 10]>;
|
|
|
|
/// The kind of invalidation we're processing.
|
|
///
|
|
/// We can use this to avoid pushing invalidations of the same kind to our
|
|
/// descendants or siblings.
|
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
enum InvalidationKind {
|
|
Descendant,
|
|
Sibling,
|
|
}
|
|
|
|
/// 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,
|
|
/// Whether the invalidation was already matched by any previous sibling or
|
|
/// ancestor.
|
|
///
|
|
/// If this is the case, we can avoid pushing invalidations generated by
|
|
/// this one if the generated invalidation is effective for all the siblings
|
|
/// or descendants after us.
|
|
matched_by_any_previous: bool,
|
|
}
|
|
|
|
impl Invalidation {
|
|
/// Whether this invalidation is effective for the next sibling or
|
|
/// descendant after us.
|
|
fn effective_for_next(&self) -> bool {
|
|
// TODO(emilio): For pseudo-elements this should be mostly false, except
|
|
// for the weird pseudos in <input type="number">.
|
|
//
|
|
// We should be able to do better here!
|
|
match self.selector.combinator_at(self.offset) {
|
|
Combinator::NextSibling |
|
|
Combinator::Child => false,
|
|
_ => true,
|
|
}
|
|
}
|
|
|
|
fn kind(&self) -> InvalidationKind {
|
|
if self.selector.combinator_at(self.offset).is_ancestor() {
|
|
InvalidationKind::Descendant
|
|
} else {
|
|
InvalidationKind::Sibling
|
|
}
|
|
}
|
|
}
|
|
|
|
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_parse_order_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 matched, either invalidating the element or
|
|
/// generating another invalidation.
|
|
matched: 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;
|
|
}
|
|
|
|
// 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.
|
|
let data = self.data.as_mut().unwrap();
|
|
data.restyle.hint.insert(RestyleHint::restyle_subtree());
|
|
}
|
|
|
|
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,
|
|
snapshot: &snapshot,
|
|
shared_context: self.shared_context,
|
|
lookup_element: lookup_element,
|
|
removed_id: id_removed.as_ref(),
|
|
added_id: id_added.as_ref(),
|
|
classes_removed: &classes_removed,
|
|
classes_added: &classes_added,
|
|
state_changes: state_changes,
|
|
descendant_invalidations: &mut descendant_invalidations,
|
|
sibling_invalidations: &mut sibling_invalidations,
|
|
invalidates_self: false,
|
|
};
|
|
|
|
shared_context.stylist.each_invalidation_map(|invalidation_map| {
|
|
collector.collect_dependencies_in_invalidation_map(invalidation_map);
|
|
});
|
|
|
|
// TODO(emilio): Consider storing dependencies from the UA sheet in
|
|
// a different map. If we do that, we can skip the stuff on the
|
|
// shared stylist iff cut_off_inheritance is true, and we can look
|
|
// just at that map.
|
|
let _cut_off_inheritance =
|
|
self.element.each_xbl_stylist(|stylist| {
|
|
stylist.each_invalidation_map(|invalidation_map| {
|
|
collector.collect_dependencies_in_invalidation_map(invalidation_map);
|
|
});
|
|
});
|
|
|
|
collector.invalidates_self
|
|
};
|
|
|
|
if invalidated_self {
|
|
if let Some(ref mut data) = self.data {
|
|
data.restyle.hint.insert(RESTYLE_SELF);
|
|
}
|
|
}
|
|
|
|
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.mutate_data();
|
|
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
|
|
}
|
|
|
|
fn invalidate_pseudo_element_or_nac(
|
|
&mut self,
|
|
child: E,
|
|
invalidations: &InvalidationVector
|
|
) -> bool {
|
|
let mut sibling_invalidations = InvalidationVector::new();
|
|
|
|
let result = self.invalidate_child(
|
|
child,
|
|
invalidations,
|
|
&mut sibling_invalidations
|
|
);
|
|
|
|
// Roots of NAC subtrees can indeed generate sibling invalidations, but
|
|
// they can be just ignored, since they have no siblings.
|
|
//
|
|
// Note that we can end up testing selectors that wouldn't end up
|
|
// matching due to this being NAC, like those coming from document
|
|
// rules, but we overinvalidate instead of checking this.
|
|
|
|
result
|
|
}
|
|
|
|
/// Invalidate a child and recurse down invalidating its descendants if
|
|
/// needed.
|
|
fn invalidate_child(
|
|
&mut self,
|
|
child: E,
|
|
invalidations: &InvalidationVector,
|
|
sibling_invalidations: &mut InvalidationVector,
|
|
) -> bool {
|
|
let mut child_data = child.mutate_data();
|
|
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();
|
|
let mut invalidated_child = false;
|
|
|
|
invalidated_child |=
|
|
child_invalidator.process_sibling_invalidations(
|
|
&mut invalidations_for_descendants,
|
|
sibling_invalidations,
|
|
);
|
|
|
|
invalidated_child |=
|
|
child_invalidator.process_descendant_invalidations(
|
|
invalidations,
|
|
&mut invalidations_for_descendants,
|
|
sibling_invalidations,
|
|
);
|
|
|
|
// The child may not be a flattened tree child of the current element,
|
|
// but may be arbitrarily deep.
|
|
//
|
|
// Since we keep the traversal flags in terms of the flattened tree,
|
|
// we need to propagate it as appropriate.
|
|
if invalidated_child && child.get_data().is_some() {
|
|
let mut current = child.traversal_parent();
|
|
while let Some(parent) = current.take() {
|
|
if parent == self.element {
|
|
break;
|
|
}
|
|
|
|
unsafe { parent.set_dirty_descendants() };
|
|
current = parent.traversal_parent();
|
|
}
|
|
}
|
|
|
|
let invalidated_descendants = child_invalidator.invalidate_descendants(
|
|
&invalidations_for_descendants
|
|
);
|
|
|
|
invalidated_child || invalidated_descendants
|
|
}
|
|
|
|
fn invalidate_nac(
|
|
&mut self,
|
|
invalidations: &InvalidationVector,
|
|
) -> bool {
|
|
let mut any_nac_root = false;
|
|
|
|
let element = self.element;
|
|
element.each_anonymous_content_child(|nac| {
|
|
any_nac_root |=
|
|
self.invalidate_pseudo_element_or_nac(nac, invalidations);
|
|
});
|
|
|
|
any_nac_root
|
|
}
|
|
|
|
// NB: It's important that this operates on DOM children, which is what
|
|
// selector-matching operates on.
|
|
fn invalidate_dom_descendants_of(
|
|
&mut self,
|
|
parent: E::ConcreteNode,
|
|
invalidations: &InvalidationVector,
|
|
) -> bool {
|
|
let mut any_descendant = false;
|
|
|
|
let mut sibling_invalidations = InvalidationVector::new();
|
|
for child in parent.children() {
|
|
// TODO(emilio): We handle <xbl:children> fine, because they appear
|
|
// in selector-matching (note bug 1374247, though).
|
|
//
|
|
// This probably needs a shadow root check on `child` here, and
|
|
// recursing if that's the case.
|
|
//
|
|
// Also, what's the deal with HTML <content>? We don't need to
|
|
// support that for now, though we probably need to recurse into the
|
|
// distributed children too.
|
|
let child = match child.as_element() {
|
|
Some(e) => e,
|
|
None => continue,
|
|
};
|
|
|
|
any_descendant |= self.invalidate_child(
|
|
child,
|
|
invalidations,
|
|
&mut sibling_invalidations,
|
|
);
|
|
}
|
|
|
|
any_descendant
|
|
}
|
|
|
|
/// 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 data.restyle.hint.contains_subtree() {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
let mut any_descendant = false;
|
|
|
|
if let Some(anon_content) = self.element.xbl_binding_anonymous_content() {
|
|
any_descendant |=
|
|
self.invalidate_dom_descendants_of(anon_content, invalidations);
|
|
}
|
|
|
|
// TODO(emilio): Having a list of invalidations just for pseudo-elements
|
|
// may save some work here and there.
|
|
if let Some(before) = self.element.before_pseudo_element() {
|
|
any_descendant |=
|
|
self.invalidate_pseudo_element_or_nac(before, invalidations);
|
|
}
|
|
|
|
let node = self.element.as_node();
|
|
any_descendant |=
|
|
self.invalidate_dom_descendants_of(node, invalidations);
|
|
|
|
if let Some(after) = self.element.after_pseudo_element() {
|
|
any_descendant |=
|
|
self.invalidate_pseudo_element_or_nac(after, invalidations);
|
|
}
|
|
|
|
any_descendant |= self.invalidate_nac(invalidations);
|
|
|
|
if any_descendant && self.data.as_ref().map_or(false, |d| !d.styles.is_display_none()) {
|
|
unsafe { self.element.set_dirty_descendants() };
|
|
}
|
|
|
|
any_descendant
|
|
}
|
|
|
|
/// Process the given sibling invalidations coming from our previous
|
|
/// sibling.
|
|
///
|
|
/// The sibling invalidations are somewhat special because they can be
|
|
/// modified on the fly. New invalidations may be added and removed.
|
|
///
|
|
/// In particular, all descendants get the same set of invalidations from
|
|
/// the parent, but the invalidations from a given sibling depend on the
|
|
/// ones we got from the previous one.
|
|
///
|
|
/// Returns whether invalidated the current element's style.
|
|
fn process_sibling_invalidations(
|
|
&mut self,
|
|
descendant_invalidations: &mut 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,
|
|
InvalidationKind::Sibling,
|
|
);
|
|
|
|
invalidated_self |= result.invalidated_self;
|
|
sibling_invalidations[i].matched_by_any_previous |= result.matched;
|
|
if sibling_invalidations[i].effective_for_next() {
|
|
i += 1;
|
|
} else {
|
|
sibling_invalidations.remove(i);
|
|
}
|
|
}
|
|
|
|
sibling_invalidations.extend(new_sibling_invalidations.drain());
|
|
invalidated_self
|
|
}
|
|
|
|
/// Process a given invalidation list coming from our parent,
|
|
/// adding to `descendant_invalidations` and `sibling_invalidations` as
|
|
/// needed.
|
|
///
|
|
/// Returns whether our style was invalidated as a result.
|
|
fn process_descendant_invalidations(
|
|
&mut self,
|
|
invalidations: &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,
|
|
InvalidationKind::Descendant,
|
|
);
|
|
|
|
invalidated |= result.invalidated_self;
|
|
if invalidation.effective_for_next() {
|
|
let mut invalidation = invalidation.clone();
|
|
invalidation.matched_by_any_previous |= result.matched;
|
|
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,
|
|
invalidation_kind: InvalidationKind,
|
|
) -> InvalidationResult {
|
|
debug!("TreeStyleInvalidator::process_invalidation({:?}, {:?}, {:?})",
|
|
self.element, invalidation, invalidation_kind);
|
|
|
|
let mut context =
|
|
MatchingContext::new_for_visited(
|
|
MatchingMode::Normal,
|
|
None,
|
|
VisitedHandlingMode::AllLinksVisitedAndUnvisited,
|
|
self.shared_context.quirks_mode,
|
|
);
|
|
|
|
let matching_result = matches_compound_selector(
|
|
&invalidation.selector,
|
|
invalidation.offset,
|
|
&mut context,
|
|
&self.element
|
|
);
|
|
|
|
let mut invalidated_self = false;
|
|
let mut matched = false;
|
|
match matching_result {
|
|
CompoundSelectorMatchingResult::Matched { next_combinator_offset: 0 } => {
|
|
debug!(" > Invalidation matched completely");
|
|
matched = true;
|
|
invalidated_self = true;
|
|
}
|
|
CompoundSelectorMatchingResult::Matched { next_combinator_offset } => {
|
|
let next_combinator =
|
|
invalidation.selector.combinator_at(next_combinator_offset);
|
|
matched = true;
|
|
|
|
if matches!(next_combinator, Combinator::PseudoElement) {
|
|
// This will usually be the very next component, except for
|
|
// the fact that we store compound selectors the other way
|
|
// around, so there could also be state pseudo-classes.
|
|
let pseudo_selector =
|
|
invalidation.selector
|
|
.iter_raw_parse_order_from(next_combinator_offset - 1)
|
|
.skip_while(|c| matches!(**c, Component::NonTSPseudoClass(..)))
|
|
.next()
|
|
.unwrap();
|
|
|
|
let pseudo = match *pseudo_selector {
|
|
Component::PseudoElement(ref pseudo) => pseudo,
|
|
_ => {
|
|
unreachable!(
|
|
"Someone seriously messed up selector parsing: \
|
|
{:?} at offset {:?}: {:?}",
|
|
invalidation.selector,
|
|
next_combinator_offset,
|
|
pseudo_selector,
|
|
)
|
|
}
|
|
};
|
|
|
|
// FIXME(emilio): This is not ideal, and could not be
|
|
// accurate if we ever have stateful element-backed eager
|
|
// pseudos.
|
|
//
|
|
// Ideally, we'd just remove element-backed eager pseudos
|
|
// altogether, given they work fine without it. Only gotcha
|
|
// is that we wouldn't style them in parallel, which may or
|
|
// may not be an issue.
|
|
//
|
|
// Also, this could be more fine grained now (perhaps a
|
|
// RESTYLE_PSEUDOS hint?).
|
|
//
|
|
// Note that we'll also restyle the pseudo-element because
|
|
// it would match this invalidation.
|
|
if pseudo.is_eager() {
|
|
invalidated_self = true;
|
|
}
|
|
}
|
|
|
|
|
|
let next_invalidation = Invalidation {
|
|
selector: invalidation.selector.clone(),
|
|
offset: next_combinator_offset,
|
|
matched_by_any_previous: false,
|
|
};
|
|
|
|
debug!(" > Invalidation matched, next: {:?}, ({:?})",
|
|
next_invalidation, next_combinator);
|
|
|
|
let next_invalidation_kind = next_invalidation.kind();
|
|
|
|
// We can skip pushing under some circumstances, and we should
|
|
// because otherwise the invalidation list could grow
|
|
// exponentially.
|
|
//
|
|
// * First of all, both invalidations need to be of the same
|
|
// kind. This is because of how we propagate them going to
|
|
// the right of the tree for sibling invalidations and going
|
|
// down the tree for children invalidations. A sibling
|
|
// invalidation that ends up generating a children
|
|
// invalidation ends up (correctly) in five different lists,
|
|
// not in the same list five different times.
|
|
//
|
|
// * Then, the invalidation needs to be matched by a previous
|
|
// ancestor/sibling, in order to know that this invalidation
|
|
// has been generated already.
|
|
//
|
|
// * Finally, the new invalidation needs to be
|
|
// `effective_for_next()`, in order for us to know that it is
|
|
// still in the list, since we remove the dependencies that
|
|
// aren't from the lists for our children / siblings.
|
|
//
|
|
// To go through an example, let's imagine we are processing a
|
|
// dom subtree like:
|
|
//
|
|
// <div><address><div><div/></div></address></div>
|
|
//
|
|
// And an invalidation list with a single invalidation like:
|
|
//
|
|
// [div div div]
|
|
//
|
|
// When we process the invalidation list for the outer div, we
|
|
// match it, and generate a `div div` invalidation, so for the
|
|
// <address> child we have:
|
|
//
|
|
// [div div div, div div]
|
|
//
|
|
// With the first of them marked as `matched`.
|
|
//
|
|
// When we process the <address> child, we don't match any of
|
|
// them, so both invalidations go untouched to our children.
|
|
//
|
|
// When we process the second <div>, we match _both_
|
|
// invalidations.
|
|
//
|
|
// However, when matching the first, we can tell it's been
|
|
// matched, and not push the corresponding `div div`
|
|
// invalidation, since we know it's necessarily already on the
|
|
// list.
|
|
//
|
|
// Thus, without skipping the push, we'll arrive to the
|
|
// innermost <div> with:
|
|
//
|
|
// [div div div, div div, div div, div]
|
|
//
|
|
// While skipping it, we won't arrive here with duplicating
|
|
// dependencies:
|
|
//
|
|
// [div div div, div div, div]
|
|
//
|
|
let can_skip_pushing =
|
|
next_invalidation_kind == invalidation_kind &&
|
|
invalidation.matched_by_any_previous &&
|
|
next_invalidation.effective_for_next();
|
|
|
|
if can_skip_pushing {
|
|
debug!(" > Can avoid push, since the invalidation had \
|
|
already been matched before");
|
|
} else {
|
|
match next_invalidation_kind {
|
|
InvalidationKind::Descendant => {
|
|
descendant_invalidations.push(next_invalidation);
|
|
}
|
|
InvalidationKind::Sibling => {
|
|
sibling_invalidations.push(next_invalidation);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
CompoundSelectorMatchingResult::NotMatched => {}
|
|
}
|
|
|
|
if invalidated_self {
|
|
if let Some(ref mut data) = self.data {
|
|
data.restyle.hint.insert(RESTYLE_SELF);
|
|
}
|
|
}
|
|
|
|
InvalidationResult { invalidated_self, matched, }
|
|
}
|
|
}
|
|
|
|
struct InvalidationCollector<'a, 'b: 'a, E>
|
|
where E: TElement,
|
|
{
|
|
element: E,
|
|
wrapper: ElementWrapper<'b, E>,
|
|
snapshot: &'a Snapshot,
|
|
shared_context: &'a SharedStyleContext<'b>,
|
|
lookup_element: E,
|
|
removed_id: Option<&'a Atom>,
|
|
added_id: Option<&'a Atom>,
|
|
classes_removed: &'a SmallVec<[Atom; 8]>,
|
|
classes_added: &'a SmallVec<[Atom; 8]>,
|
|
state_changes: ElementState,
|
|
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_invalidation_map(
|
|
&mut self,
|
|
map: &InvalidationMap,
|
|
) {
|
|
let quirks_mode = self.shared_context.quirks_mode;
|
|
let removed_id = self.removed_id;
|
|
if let Some(ref id) = removed_id {
|
|
if let Some(deps) = map.id_to_selector.get(id, quirks_mode) {
|
|
self.collect_dependencies_in_map(deps)
|
|
}
|
|
}
|
|
|
|
let added_id = self.added_id;
|
|
if let Some(ref id) = added_id {
|
|
if let Some(deps) = map.id_to_selector.get(id, quirks_mode) {
|
|
self.collect_dependencies_in_map(deps)
|
|
}
|
|
}
|
|
|
|
for class in self.classes_added.iter().chain(self.classes_removed.iter()) {
|
|
if let Some(deps) = map.class_to_selector.get(class, quirks_mode) {
|
|
self.collect_dependencies_in_map(deps)
|
|
}
|
|
}
|
|
|
|
let should_examine_attribute_selector_map =
|
|
self.snapshot.other_attr_changed() ||
|
|
(self.snapshot.class_changed() && map.has_class_attribute_selectors) ||
|
|
(self.snapshot.id_changed() && map.has_id_attribute_selectors);
|
|
|
|
if should_examine_attribute_selector_map {
|
|
self.collect_dependencies_in_map(
|
|
&map.other_attribute_affecting_selectors
|
|
)
|
|
}
|
|
|
|
let state_changes = self.state_changes;
|
|
if !state_changes.is_empty() {
|
|
self.collect_state_dependencies(
|
|
&map.state_affecting_selectors,
|
|
state_changes,
|
|
)
|
|
}
|
|
}
|
|
|
|
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,
|
|
/* is_visited_dependent = */ false
|
|
);
|
|
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,
|
|
dependency.state.intersects(IN_VISITED_OR_UNVISITED_STATE)
|
|
);
|
|
true
|
|
},
|
|
);
|
|
}
|
|
|
|
fn scan_dependency(
|
|
&mut self,
|
|
dependency: &Dependency,
|
|
is_visited_dependent: bool
|
|
) {
|
|
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,
|
|
None,
|
|
&self.wrapper,
|
|
&mut then_context,
|
|
&mut |_, _| {});
|
|
let matches_now =
|
|
matches_selector(&dependency.selector,
|
|
dependency.selector_offset,
|
|
None,
|
|
&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 {
|
|
return self.note_dependency(dependency);
|
|
}
|
|
|
|
// 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 state or attribute change of
|
|
// this element (for example, for things like [foo]:visited).
|
|
//
|
|
// NOTE: This thing is actually untested because testing it is flaky,
|
|
// see the tests that were added and then backed out in bug 1328509.
|
|
if is_visited_dependent && now_context.relevant_link_found {
|
|
then_context.visited_handling = VisitedHandlingMode::RelevantLinkVisited;
|
|
let matched_then =
|
|
matches_selector(&dependency.selector,
|
|
dependency.selector_offset,
|
|
None,
|
|
&self.wrapper,
|
|
&mut then_context,
|
|
&mut |_, _| {});
|
|
|
|
now_context.visited_handling = VisitedHandlingMode::RelevantLinkVisited;
|
|
let matches_now =
|
|
matches_selector(&dependency.selector,
|
|
dependency.selector_offset,
|
|
None,
|
|
&self.element,
|
|
&mut now_context,
|
|
&mut |_, _| {});
|
|
if matched_then != matches_now {
|
|
return 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,
|
|
matched_by_any_previous: false,
|
|
});
|
|
} 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,
|
|
matched_by_any_previous: false,
|
|
});
|
|
}
|
|
}
|
|
|
|
/// 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
|
|
}
|
|
}
|