mirror of
https://github.com/servo/servo.git
synced 2025-07-20 05:43:41 +01:00
It used to be this way because of lifetime issues (plus the shadow datas were in RwLocks at some point IIRC). Now we guarantee that as long as the element is away the cascade data is as well, so we don't need to thread it around. Differential Revision: https://phabricator.services.mozilla.com/D2768
559 lines
19 KiB
Rust
559 lines
19 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/. */
|
|
|
|
//! An invalidation processor for style changes due to state and attribute
|
|
//! changes.
|
|
|
|
use {Atom, WeakAtom};
|
|
use context::SharedStyleContext;
|
|
use data::ElementData;
|
|
use dom::TElement;
|
|
use element_state::ElementState;
|
|
use invalidation::element::element_wrapper::{ElementSnapshot, ElementWrapper};
|
|
use invalidation::element::invalidation_map::*;
|
|
use invalidation::element::invalidator::{DescendantInvalidationLists, InvalidationVector};
|
|
use invalidation::element::invalidator::{Invalidation, InvalidationProcessor};
|
|
use invalidation::element::restyle_hints::RestyleHint;
|
|
use selector_map::SelectorMap;
|
|
use selector_parser::Snapshot;
|
|
use selectors::NthIndexCache;
|
|
use selectors::attr::CaseSensitivity;
|
|
use selectors::matching::{MatchingContext, MatchingMode, VisitedHandlingMode};
|
|
use selectors::matching::matches_selector;
|
|
use smallvec::SmallVec;
|
|
use stylesheets::origin::{Origin, OriginSet};
|
|
|
|
#[derive(Debug, PartialEq)]
|
|
enum VisitedDependent {
|
|
Yes,
|
|
No,
|
|
}
|
|
|
|
/// The collector implementation.
|
|
struct Collector<'a, 'b: 'a, 'selectors: 'a, E>
|
|
where
|
|
E: TElement,
|
|
{
|
|
element: E,
|
|
wrapper: ElementWrapper<'b, E>,
|
|
snapshot: &'a Snapshot,
|
|
matching_context: &'a mut MatchingContext<'b, E::Impl>,
|
|
lookup_element: E,
|
|
removed_id: Option<&'a WeakAtom>,
|
|
added_id: Option<&'a WeakAtom>,
|
|
classes_removed: &'a SmallVec<[Atom; 8]>,
|
|
classes_added: &'a SmallVec<[Atom; 8]>,
|
|
state_changes: ElementState,
|
|
descendant_invalidations: &'a mut DescendantInvalidationLists<'selectors>,
|
|
sibling_invalidations: &'a mut InvalidationVector<'selectors>,
|
|
invalidates_self: bool,
|
|
}
|
|
|
|
/// An invalidation processor for style changes due to state and attribute
|
|
/// changes.
|
|
pub struct StateAndAttrInvalidationProcessor<'a, 'b: 'a, E: TElement> {
|
|
shared_context: &'a SharedStyleContext<'b>,
|
|
element: E,
|
|
data: &'a mut ElementData,
|
|
matching_context: MatchingContext<'a, E::Impl>,
|
|
}
|
|
|
|
impl<'a, 'b: 'a, E: TElement + 'b> StateAndAttrInvalidationProcessor<'a, 'b, E> {
|
|
/// Creates a new StateAndAttrInvalidationProcessor.
|
|
pub fn new(
|
|
shared_context: &'a SharedStyleContext<'b>,
|
|
element: E,
|
|
data: &'a mut ElementData,
|
|
nth_index_cache: &'a mut NthIndexCache,
|
|
) -> Self {
|
|
let matching_context = MatchingContext::new_for_visited(
|
|
MatchingMode::Normal,
|
|
None,
|
|
Some(nth_index_cache),
|
|
VisitedHandlingMode::AllLinksVisitedAndUnvisited,
|
|
shared_context.quirks_mode(),
|
|
);
|
|
|
|
Self {
|
|
shared_context,
|
|
element,
|
|
data,
|
|
matching_context,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Whether we should process the descendants of a given element for style
|
|
/// invalidation.
|
|
pub fn should_process_descendants(data: &ElementData) -> bool {
|
|
!data.styles.is_display_none() && !data.hint.contains(RestyleHint::RESTYLE_DESCENDANTS)
|
|
}
|
|
|
|
/// Propagates the bits after invalidating a descendant child.
|
|
pub fn invalidated_descendants<E>(element: E, child: E)
|
|
where
|
|
E: TElement,
|
|
{
|
|
if child.get_data().is_none() {
|
|
return;
|
|
}
|
|
|
|
// 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.
|
|
let mut current = child.traversal_parent();
|
|
while let Some(parent) = current.take() {
|
|
unsafe { parent.set_dirty_descendants() };
|
|
current = parent.traversal_parent();
|
|
|
|
if parent == element {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Sets the appropriate restyle hint after invalidating the style of a given
|
|
/// element.
|
|
pub fn invalidated_self<E>(element: E)
|
|
where
|
|
E: TElement,
|
|
{
|
|
if let Some(mut data) = element.mutate_data() {
|
|
data.hint.insert(RestyleHint::RESTYLE_SELF);
|
|
}
|
|
}
|
|
|
|
impl<'a, 'b: 'a, E: 'a> InvalidationProcessor<'a, E>
|
|
for StateAndAttrInvalidationProcessor<'a, 'b, E>
|
|
where
|
|
E: TElement,
|
|
{
|
|
/// We need to invalidate style on an eager pseudo-element, in order to
|
|
/// process changes that could otherwise end up in ::before or ::after
|
|
/// content being generated.
|
|
fn invalidates_on_eager_pseudo_element(&self) -> bool {
|
|
true
|
|
}
|
|
|
|
fn matching_context(&mut self) -> &mut MatchingContext<'a, E::Impl> {
|
|
&mut self.matching_context
|
|
}
|
|
|
|
fn collect_invalidations(
|
|
&mut self,
|
|
element: E,
|
|
_self_invalidations: &mut InvalidationVector<'a>,
|
|
descendant_invalidations: &mut DescendantInvalidationLists<'a>,
|
|
sibling_invalidations: &mut InvalidationVector<'a>,
|
|
) -> bool {
|
|
debug_assert_eq!(element, self.element);
|
|
debug_assert!(element.has_snapshot(), "Why bothering?");
|
|
|
|
let wrapper = ElementWrapper::new(element, &*self.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 false;
|
|
}
|
|
|
|
// 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(ElementState::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.
|
|
self.data.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 !element.has_class(c, CaseSensitivity::CaseSensitive) {
|
|
classes_removed.push(c.clone())
|
|
}
|
|
});
|
|
|
|
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 = element.id();
|
|
|
|
if old_id != current_id {
|
|
id_removed = old_id;
|
|
id_added = current_id;
|
|
}
|
|
}
|
|
|
|
if log_enabled!(::log::Level::Debug) {
|
|
debug!("Collecting changes for: {:?}", element);
|
|
if !state_changes.is_empty() {
|
|
debug!(" > state: {:?}", state_changes);
|
|
}
|
|
if snapshot.id_changed() {
|
|
debug!(
|
|
" > id changed: +{:?} -{:?}",
|
|
id_added,
|
|
id_removed
|
|
);
|
|
}
|
|
if snapshot.class_changed() {
|
|
debug!(
|
|
" > class changed: +{:?} -{:?}",
|
|
classes_added,
|
|
classes_removed
|
|
);
|
|
}
|
|
if snapshot.other_attr_changed() {
|
|
debug!(
|
|
" > attributes changed, old: {}",
|
|
snapshot.debug_list_attributes()
|
|
)
|
|
}
|
|
}
|
|
|
|
let lookup_element = if element.implemented_pseudo_element().is_some() {
|
|
element.pseudo_element_originating_element().unwrap()
|
|
} else {
|
|
element
|
|
};
|
|
|
|
let mut shadow_rule_datas = SmallVec::<[_; 3]>::new();
|
|
let matches_document_author_rules =
|
|
element.each_applicable_non_document_style_rule_data(|data, quirks_mode, host| {
|
|
shadow_rule_datas.push((data, quirks_mode, host.map(|h| h.opaque())))
|
|
});
|
|
|
|
|
|
let invalidated_self = {
|
|
let mut collector = Collector {
|
|
wrapper,
|
|
lookup_element,
|
|
state_changes,
|
|
element,
|
|
snapshot: &snapshot,
|
|
matching_context: &mut self.matching_context,
|
|
removed_id: id_removed,
|
|
added_id: id_added,
|
|
classes_removed: &classes_removed,
|
|
classes_added: &classes_added,
|
|
descendant_invalidations,
|
|
sibling_invalidations,
|
|
invalidates_self: false,
|
|
};
|
|
|
|
let document_origins = if !matches_document_author_rules {
|
|
Origin::UserAgent.into()
|
|
} else {
|
|
OriginSet::all()
|
|
};
|
|
|
|
for (cascade_data, origin) in self.shared_context.stylist.iter_origins() {
|
|
if document_origins.contains(origin.into()) {
|
|
collector
|
|
.collect_dependencies_in_invalidation_map(cascade_data.invalidation_map());
|
|
}
|
|
}
|
|
|
|
for &(ref data, quirks_mode, ref host) in &shadow_rule_datas {
|
|
// FIXME(emilio): Replace with assert / remove when we figure
|
|
// out what to do with the quirks mode mismatches
|
|
// (that is, when bug 1406875 is properly fixed).
|
|
collector.matching_context.set_quirks_mode(quirks_mode);
|
|
collector.matching_context.current_host = host.clone();
|
|
collector.collect_dependencies_in_invalidation_map(data.invalidation_map());
|
|
}
|
|
|
|
collector.invalidates_self
|
|
};
|
|
|
|
// If we generated a ton of descendant invalidations, it's probably not
|
|
// worth to go ahead and try to process them.
|
|
//
|
|
// Just restyle the descendants directly.
|
|
//
|
|
// This number is completely made-up, but the page that made us add this
|
|
// code generated 1960+ invalidations (bug 1420741).
|
|
//
|
|
// We don't look at slotted_descendants because those don't propagate
|
|
// down more than one level anyway.
|
|
if descendant_invalidations.dom_descendants.len() > 150 {
|
|
self.data.hint.insert(RestyleHint::RESTYLE_DESCENDANTS);
|
|
}
|
|
|
|
if invalidated_self {
|
|
self.data.hint.insert(RestyleHint::RESTYLE_SELF);
|
|
}
|
|
|
|
invalidated_self
|
|
}
|
|
|
|
fn should_process_descendants(&mut self, element: E) -> bool {
|
|
if element == self.element {
|
|
return should_process_descendants(&self.data);
|
|
}
|
|
|
|
match element.borrow_data() {
|
|
Some(d) => should_process_descendants(&d),
|
|
None => return false,
|
|
}
|
|
}
|
|
|
|
fn recursion_limit_exceeded(&mut self, element: E) {
|
|
if element == self.element {
|
|
self.data.hint.insert(RestyleHint::RESTYLE_DESCENDANTS);
|
|
return;
|
|
}
|
|
|
|
if let Some(mut data) = element.mutate_data() {
|
|
data.hint.insert(RestyleHint::RESTYLE_DESCENDANTS);
|
|
}
|
|
}
|
|
|
|
fn invalidated_descendants(&mut self, element: E, child: E) {
|
|
invalidated_descendants(element, child)
|
|
}
|
|
|
|
fn invalidated_self(&mut self, element: E) {
|
|
debug_assert_ne!(element, self.element);
|
|
invalidated_self(element);
|
|
}
|
|
}
|
|
|
|
impl<'a, 'b, 'selectors, E> Collector<'a, 'b, 'selectors, E>
|
|
where
|
|
E: TElement,
|
|
'selectors: 'a,
|
|
{
|
|
fn collect_dependencies_in_invalidation_map(&mut self, map: &'selectors InvalidationMap) {
|
|
let quirks_mode = self.matching_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) {
|
|
for dep in deps {
|
|
self.scan_dependency(dep, VisitedDependent::No);
|
|
}
|
|
}
|
|
}
|
|
|
|
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) {
|
|
for dep in deps {
|
|
self.scan_dependency(dep, VisitedDependent::No);
|
|
}
|
|
}
|
|
}
|
|
|
|
for class in self.classes_added.iter().chain(self.classes_removed.iter()) {
|
|
if let Some(deps) = map.class_to_selector.get(class, quirks_mode) {
|
|
for dep in deps {
|
|
self.scan_dependency(dep, VisitedDependent::No);
|
|
}
|
|
}
|
|
}
|
|
|
|
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: &'selectors SelectorMap<Dependency>) {
|
|
map.lookup_with_additional(
|
|
self.lookup_element,
|
|
self.matching_context.quirks_mode(),
|
|
self.removed_id,
|
|
self.classes_removed,
|
|
|dependency| {
|
|
self.scan_dependency(dependency, VisitedDependent::No);
|
|
true
|
|
},
|
|
);
|
|
}
|
|
|
|
fn collect_state_dependencies(
|
|
&mut self,
|
|
map: &'selectors SelectorMap<StateDependency>,
|
|
state_changes: ElementState,
|
|
) {
|
|
map.lookup_with_additional(
|
|
self.lookup_element,
|
|
self.matching_context.quirks_mode(),
|
|
self.removed_id,
|
|
self.classes_removed,
|
|
|dependency| {
|
|
if !dependency.state.intersects(state_changes) {
|
|
return true;
|
|
}
|
|
let visited_dependent = if dependency
|
|
.state
|
|
.intersects(ElementState::IN_VISITED_OR_UNVISITED_STATE)
|
|
{
|
|
VisitedDependent::Yes
|
|
} else {
|
|
VisitedDependent::No
|
|
};
|
|
self.scan_dependency(&dependency.dep, visited_dependent);
|
|
true
|
|
},
|
|
);
|
|
}
|
|
|
|
/// Check whether a dependency should be taken into account, using a given
|
|
/// visited handling mode.
|
|
fn check_dependency(
|
|
&mut self,
|
|
visited_handling_mode: VisitedHandlingMode,
|
|
dependency: &Dependency,
|
|
) -> bool {
|
|
let element = &self.element;
|
|
let wrapper = &self.wrapper;
|
|
self.matching_context
|
|
.with_visited_handling_mode(visited_handling_mode, |mut context| {
|
|
let matches_now = matches_selector(
|
|
&dependency.selector,
|
|
dependency.selector_offset,
|
|
None,
|
|
element,
|
|
&mut context,
|
|
&mut |_, _| {},
|
|
);
|
|
|
|
let matched_then = matches_selector(
|
|
&dependency.selector,
|
|
dependency.selector_offset,
|
|
None,
|
|
wrapper,
|
|
&mut context,
|
|
&mut |_, _| {},
|
|
);
|
|
|
|
matched_then != matches_now
|
|
})
|
|
}
|
|
|
|
fn scan_dependency(
|
|
&mut self,
|
|
dependency: &'selectors Dependency,
|
|
is_visited_dependent: VisitedDependent,
|
|
) {
|
|
debug!(
|
|
"TreeStyleInvalidator::scan_dependency({:?}, {:?}, {:?})",
|
|
self.element, dependency, is_visited_dependent,
|
|
);
|
|
|
|
if !self.dependency_may_be_relevant(dependency) {
|
|
return;
|
|
}
|
|
|
|
let should_account_for_dependency =
|
|
self.check_dependency(VisitedHandlingMode::AllLinksVisitedAndUnvisited, dependency);
|
|
|
|
if should_account_for_dependency {
|
|
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 == VisitedDependent::Yes && self.element.is_link() {
|
|
let should_account_for_dependency =
|
|
self.check_dependency(VisitedHandlingMode::RelevantLinkVisited, dependency);
|
|
|
|
if should_account_for_dependency {
|
|
return self.note_dependency(dependency);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn note_dependency(&mut self, dependency: &'selectors Dependency) {
|
|
debug_assert!(self.dependency_may_be_relevant(dependency));
|
|
|
|
let invalidation_kind = dependency.invalidation_kind();
|
|
if matches!(invalidation_kind, DependencyInvalidationKind::Element) {
|
|
self.invalidates_self = true;
|
|
return;
|
|
}
|
|
|
|
debug_assert_ne!(dependency.selector_offset, 0);
|
|
debug_assert_ne!(dependency.selector_offset, dependency.selector.len());
|
|
|
|
let invalidation = Invalidation::new(
|
|
&dependency.selector,
|
|
dependency.selector.len() - dependency.selector_offset + 1,
|
|
);
|
|
|
|
match invalidation_kind {
|
|
DependencyInvalidationKind::Element => unreachable!(),
|
|
DependencyInvalidationKind::ElementAndDescendants => {
|
|
self.invalidates_self = true;
|
|
self.descendant_invalidations
|
|
.dom_descendants
|
|
.push(invalidation);
|
|
},
|
|
DependencyInvalidationKind::Descendants => {
|
|
self.descendant_invalidations
|
|
.dom_descendants
|
|
.push(invalidation);
|
|
},
|
|
DependencyInvalidationKind::Siblings => {
|
|
self.sibling_invalidations.push(invalidation);
|
|
},
|
|
DependencyInvalidationKind::SlottedElements => {
|
|
self.descendant_invalidations
|
|
.slotted_descendants
|
|
.push(invalidation);
|
|
},
|
|
}
|
|
}
|
|
|
|
/// 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 {
|
|
match dependency.invalidation_kind() {
|
|
DependencyInvalidationKind::Element => !self.invalidates_self,
|
|
DependencyInvalidationKind::SlottedElements => self.element.is_html_slot_element(),
|
|
DependencyInvalidationKind::ElementAndDescendants |
|
|
DependencyInvalidationKind::Siblings |
|
|
DependencyInvalidationKind::Descendants => true,
|
|
}
|
|
}
|
|
}
|