diff options
Diffstat (limited to 'servo/components/style/invalidation/element/state_and_attributes.rs')
-rw-r--r-- | servo/components/style/invalidation/element/state_and_attributes.rs | 555 |
1 files changed, 555 insertions, 0 deletions
diff --git a/servo/components/style/invalidation/element/state_and_attributes.rs b/servo/components/style/invalidation/element/state_and_attributes.rs new file mode 100644 index 0000000000..f88ebffe06 --- /dev/null +++ b/servo/components/style/invalidation/element/state_and_attributes.rs @@ -0,0 +1,555 @@ +/* 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 https://mozilla.org/MPL/2.0/. */ + +//! An invalidation processor for style changes due to state and attribute +//! changes. + +use crate::context::SharedStyleContext; +use crate::data::ElementData; +use crate::dom::{TElement, TNode}; +use crate::invalidation::element::element_wrapper::{ElementSnapshot, ElementWrapper}; +use crate::invalidation::element::invalidation_map::*; +use crate::invalidation::element::invalidator::{DescendantInvalidationLists, InvalidationVector}; +use crate::invalidation::element::invalidator::{Invalidation, InvalidationProcessor}; +use crate::invalidation::element::restyle_hints::RestyleHint; +use crate::selector_map::SelectorMap; +use crate::selector_parser::Snapshot; +use crate::stylesheets::origin::OriginSet; +use crate::{Atom, WeakAtom}; +use dom::ElementState; +use selectors::attr::CaseSensitivity; +use selectors::matching::{ + matches_selector, MatchingContext, MatchingMode, NeedsSelectorFlags, VisitedHandlingMode, +}; +use selectors::NthIndexCache; +use smallvec::SmallVec; + +/// 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(), + NeedsSelectorFlags::No, + ); + + Self { + shared_context, + element, + data, + matching_context, + } + } +} + +/// Checks a dependency against a given element and wrapper, to see if something +/// changed. +pub fn check_dependency<E, W>( + dependency: &Dependency, + element: &E, + wrapper: &W, + context: &mut MatchingContext<'_, E::Impl>, +) -> bool +where + E: TElement, + W: selectors::Element<Impl = E::Impl>, +{ + let matches_now = matches_selector( + &dependency.selector, + dependency.selector_offset, + None, + element, + context, + ); + + let matched_then = matches_selector( + &dependency.selector, + dependency.selector_offset, + None, + wrapper, + context, + ); + + matched_then != matches_now +} + +/// 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 propagate_dirty_bit_up_to<E>(ancestor: E, child: E) +where + E: TElement, +{ + // 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 == ancestor { + return; + } + } + debug_assert!(false, "Should've found {:?} as an ancestor of {:?}", ancestor, child); +} + +/// Propagates the bits after invalidating a descendant child, if needed. +pub fn invalidated_descendants<E>(element: E, child: E) +where + E: TElement, +{ + if !child.has_data() { + return; + } + propagate_dirty_bit_up_to(element, child) +} + +/// Sets the appropriate restyle hint after invalidating the style of a given +/// element. +pub fn invalidated_self<E>(element: E) -> bool +where + E: TElement, +{ + let mut data = match element.mutate_data() { + Some(data) => data, + None => return false, + }; + data.hint.insert(RestyleHint::RESTYLE_SELF); + true +} + +/// Sets the appropriate hint after invalidating the style of a sibling. +pub fn invalidated_sibling<E>(element: E, of: E) +where + E: TElement, +{ + debug_assert_eq!(element.as_node().parent_node(), of.as_node().parent_node(), "Should be siblings"); + if !invalidated_self(element) { + return; + } + if element.traversal_parent() != of.traversal_parent() { + let parent = element.as_node().parent_element_or_host(); + debug_assert!(parent.is_some(), "How can we have siblings without parent nodes?"); + if let Some(e) = parent { + propagate_dirty_bit_up_to(e, element) + } + } +} + +impl<'a, 'b: 'a, E: 'a> InvalidationProcessor<'a, E> + for StateAndAttrInvalidationProcessor<'a, 'b, E> +where + E: TElement, +{ + /// We need to invalidate style on pseudo-elements, in order to process + /// changes that could otherwise end up in ::before or ::after content being + /// generated, and invalidate lazy pseudo caches. + fn invalidates_on_pseudo_element(&self) -> bool { + true + } + + fn check_outer_dependency(&mut self, dependency: &Dependency, element: E) -> bool { + // We cannot assert about `element` having a snapshot here (in fact it + // most likely won't), because it may be an arbitrary descendant or + // later-sibling of the element we started invalidating with. + let wrapper = ElementWrapper::new(element, &*self.shared_context.snapshot_map); + check_dependency(dependency, &element, &wrapper, &mut self.matching_context) + } + + 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 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. + // + // TODO(emilio): This piece of code should be removed when + // layout.css.always-repaint-on-unvisited is true, since we cannot get + // into this situation in that case. + if state_changes.contains(ElementState::VISITED_OR_UNVISITED) { + 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 for siblings. + 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.0.clone()) + } + }); + + element.each_class(|c| { + if !snapshot.has_class(c, CaseSensitivity::CaseSensitive) { + classes_added.push(c.0.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 + ); + } + let mut attributes_changed = false; + snapshot.each_attr_changed(|_| { + attributes_changed = true; + }); + if attributes_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, host| { + shadow_rule_datas.push((data, host.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 { + OriginSet::ORIGIN_USER_AGENT | OriginSet::ORIGIN_USER + } 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, ref host) in &shadow_rule_datas { + collector.matching_context.current_host = Some(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); + } + + fn invalidated_sibling(&mut self, element: E, of: E) { + debug_assert_ne!(element, self.element); + invalidated_sibling(element, of); + } +} + +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); + } + } + } + + 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); + } + } + } + + 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); + } + } + } + + self.snapshot.each_attr_changed(|attribute| { + if let Some(deps) = map.other_attribute_affecting_selectors.get(attribute) { + for dep in deps { + self.scan_dependency(dep); + } + } + }); + + self.collect_state_dependencies(&map.state_affecting_selectors) + } + + fn collect_state_dependencies(&mut self, map: &'selectors SelectorMap<StateDependency>) { + if self.state_changes.is_empty() { + return; + } + map.lookup_with_additional( + self.lookup_element, + self.matching_context.quirks_mode(), + self.removed_id, + self.classes_removed, + self.state_changes, + |dependency| { + if !dependency.state.intersects(self.state_changes) { + return true; + } + self.scan_dependency(&dependency.dep); + true + }, + ); + } + + /// Check whether a dependency should be taken into account. + #[inline] + fn check_dependency(&mut self, dependency: &Dependency) -> bool { + check_dependency( + dependency, + &self.element, + &self.wrapper, + &mut self.matching_context, + ) + } + + fn scan_dependency(&mut self, dependency: &'selectors Dependency) { + debug!( + "TreeStyleInvalidator::scan_dependency({:?}, {:?})", + self.element, dependency + ); + + if !self.dependency_may_be_relevant(dependency) { + return; + } + + if self.check_dependency(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) { + if let Some(ref parent) = dependency.parent { + // We know something changed in the inner selector, go outwards + // now. + self.scan_dependency(parent); + } else { + 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, self.matching_context.current_host.clone()); + + 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::Parts => { + self.descendant_invalidations.parts.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::Parts => self.element.shadow_root().is_some(), + DependencyInvalidationKind::ElementAndDescendants | + DependencyInvalidationKind::Siblings | + DependencyInvalidationKind::Descendants => true, + } + } +} |