/* 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/. */ //! A wrapper over an element and a snapshot, that allows us to selector-match //! against a past state of the element. use crate::dom::TElement; use crate::selector_parser::{AttrValue, NonTSPseudoClass, PseudoElement, SelectorImpl}; use crate::selector_parser::{Snapshot, SnapshotMap}; use crate::values::AtomIdent; use crate::{CaseSensitivityExt, LocalName, Namespace, WeakAtom}; use dom::ElementState; use selectors::attr::{AttrSelectorOperation, CaseSensitivity, NamespaceConstraint}; use selectors::matching::{ElementSelectorFlags, MatchingContext}; use selectors::{Element, OpaqueElement}; use std::cell::Cell; use std::fmt; /// In order to compute restyle hints, we perform a selector match against a /// list of partial selectors whose rightmost simple selector may be sensitive /// to the thing being changed. We do this matching twice, once for the element /// as it exists now and once for the element as it existed at the time of the /// last restyle. If the results of the selector match differ, that means that /// the given partial selector is sensitive to the change, and we compute a /// restyle hint based on its combinator. /// /// In order to run selector matching against the old element state, we generate /// a wrapper for the element which claims to have the old state. This is the /// ElementWrapper logic below. /// /// Gecko does this differently for element states, and passes a mask called /// mStateMask, which indicates the states that need to be ignored during /// selector matching. This saves an ElementWrapper allocation and an additional /// selector match call at the expense of additional complexity inside the /// selector matching logic. This only works for boolean states though, so we /// still need to take the ElementWrapper approach for attribute-dependent /// style. So we do it the same both ways for now to reduce complexity, but it's /// worth measuring the performance impact (if any) of the mStateMask approach. pub trait ElementSnapshot: Sized { /// The state of the snapshot, if any. fn state(&self) -> Option; /// If this snapshot contains attribute information. fn has_attrs(&self) -> bool; /// Gets the attribute information of the snapshot as a string. /// /// Only for debugging purposes. fn debug_list_attributes(&self) -> String { String::new() } /// The ID attribute per this snapshot. Should only be called if /// `has_attrs()` returns true. fn id_attr(&self) -> Option<&WeakAtom>; /// Whether this snapshot contains the class `name`. Should only be called /// if `has_attrs()` returns true. fn has_class(&self, name: &AtomIdent, case_sensitivity: CaseSensitivity) -> bool; /// Whether this snapshot represents the part named `name`. Should only be /// called if `has_attrs()` returns true. fn is_part(&self, name: &AtomIdent) -> bool; /// See Element::imported_part. fn imported_part(&self, name: &AtomIdent) -> Option; /// A callback that should be called for each class of the snapshot. Should /// only be called if `has_attrs()` returns true. fn each_class(&self, _: F) where F: FnMut(&AtomIdent); /// The `xml:lang=""` or `lang=""` attribute value per this snapshot. fn lang_attr(&self) -> Option; } /// A simple wrapper over an element and a snapshot, that allows us to /// selector-match against a past state of the element. #[derive(Clone)] pub struct ElementWrapper<'a, E> where E: TElement, { element: E, cached_snapshot: Cell>, snapshot_map: &'a SnapshotMap, } impl<'a, E> ElementWrapper<'a, E> where E: TElement, { /// Trivially constructs an `ElementWrapper`. pub 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. pub 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 } /// Returns the states that have changed since the element was snapshotted. pub 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.state(), None => ElementState::empty(), } } /// Returns the value of the `xml:lang=""` (or, if appropriate, `lang=""`) /// attribute from this element's snapshot or the closest ancestor /// element snapshot with the attribute specified. fn get_lang(&self) -> Option { let mut current = self.clone(); loop { let lang = match self.snapshot() { Some(snapshot) if snapshot.has_attrs() => snapshot.lang_attr(), _ => current.element.lang_attr(), }; if lang.is_some() { return lang; } current = current.parent_element()?; } } } impl<'a, E> fmt::Debug for ElementWrapper<'a, E> where E: TElement, { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { // Ignore other fields for now, can change later if needed. self.element.fmt(f) } } impl<'a, E> Element for ElementWrapper<'a, E> where E: TElement, { type Impl = SelectorImpl; fn match_non_ts_pseudo_class( &self, pseudo_class: &NonTSPseudoClass, context: &mut MatchingContext, ) -> bool { // Some pseudo-classes need special handling to evaluate them against // the snapshot. match *pseudo_class { // For :link and :visited, we don't actually want to test the // element state directly. // // Instead, we use the `visited_handling` to determine if they // match. NonTSPseudoClass::Link => { return self.is_link() && context.visited_handling().matches_unvisited(); }, NonTSPseudoClass::Visited => { return self.is_link() && context.visited_handling().matches_visited(); }, #[cfg(feature = "gecko")] NonTSPseudoClass::MozTableBorderNonzero => { if let Some(snapshot) = self.snapshot() { if snapshot.has_other_pseudo_class_state() { return snapshot.mIsTableBorderNonzero(); } } }, #[cfg(feature = "gecko")] NonTSPseudoClass::MozBrowserFrame => { if let Some(snapshot) = self.snapshot() { if snapshot.has_other_pseudo_class_state() { return snapshot.mIsMozBrowserFrame(); } } }, #[cfg(feature = "gecko")] NonTSPseudoClass::MozSelectListBox => { if let Some(snapshot) = self.snapshot() { if snapshot.has_other_pseudo_class_state() { return snapshot.mIsSelectListBox(); } } }, // :lang() needs to match using the closest ancestor xml:lang="" or // lang="" attribtue from snapshots. NonTSPseudoClass::Lang(ref lang_arg) => { return self .element .match_element_lang(Some(self.get_lang()), lang_arg); }, _ => {}, } let flag = pseudo_class.state_flag(); if flag.is_empty() { return self .element .match_non_ts_pseudo_class(pseudo_class, context); } 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), } } fn set_selector_flags(&self, _flags: ElementSelectorFlags) { debug_assert!(false, "Shouldn't need selector flags for invalidation"); } fn match_pseudo_element( &self, pseudo_element: &PseudoElement, context: &mut MatchingContext, ) -> bool { self.element.match_pseudo_element(pseudo_element, context) } fn is_link(&self) -> bool { match self.snapshot().and_then(|s| s.state()) { Some(state) => state.intersects(ElementState::VISITED_OR_UNVISITED), None => self.element.is_link(), } } fn opaque(&self) -> OpaqueElement { self.element.opaque() } fn parent_element(&self) -> Option { let parent = self.element.parent_element()?; Some(Self::new(parent, self.snapshot_map)) } fn parent_node_is_shadow_root(&self) -> bool { self.element.parent_node_is_shadow_root() } fn containing_shadow_host(&self) -> Option { let host = self.element.containing_shadow_host()?; Some(Self::new(host, self.snapshot_map)) } fn prev_sibling_element(&self) -> Option { let sibling = self.element.prev_sibling_element()?; Some(Self::new(sibling, self.snapshot_map)) } fn next_sibling_element(&self) -> Option { let sibling = self.element.next_sibling_element()?; Some(Self::new(sibling, self.snapshot_map)) } fn first_element_child(&self) -> Option { let child = self.element.first_element_child()?; Some(Self::new(child, self.snapshot_map)) } #[inline] fn is_html_element_in_html_document(&self) -> bool { self.element.is_html_element_in_html_document() } #[inline] fn is_html_slot_element(&self) -> bool { self.element.is_html_slot_element() } #[inline] fn has_local_name( &self, local_name: &::BorrowedLocalName, ) -> bool { self.element.has_local_name(local_name) } #[inline] fn has_namespace( &self, ns: &::BorrowedNamespaceUrl, ) -> bool { self.element.has_namespace(ns) } #[inline] fn is_same_type(&self, other: &Self) -> bool { self.element.is_same_type(&other.element) } 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 has_id(&self, id: &AtomIdent, case_sensitivity: CaseSensitivity) -> bool { match self.snapshot() { Some(snapshot) if snapshot.has_attrs() => snapshot .id_attr() .map_or(false, |atom| case_sensitivity.eq_atom(&atom, id)), _ => self.element.has_id(id, case_sensitivity), } } fn is_part(&self, name: &AtomIdent) -> bool { match self.snapshot() { Some(snapshot) if snapshot.has_attrs() => snapshot.is_part(name), _ => self.element.is_part(name), } } fn imported_part(&self, name: &AtomIdent) -> Option { match self.snapshot() { Some(snapshot) if snapshot.has_attrs() => snapshot.imported_part(name), _ => self.element.imported_part(name), } } fn has_class(&self, name: &AtomIdent, case_sensitivity: CaseSensitivity) -> bool { match self.snapshot() { Some(snapshot) if snapshot.has_attrs() => snapshot.has_class(name, case_sensitivity), _ => self.element.has_class(name, case_sensitivity), } } fn is_empty(&self) -> bool { self.element.is_empty() } fn is_root(&self) -> bool { self.element.is_root() } fn is_pseudo_element(&self) -> bool { self.element.is_pseudo_element() } fn pseudo_element_originating_element(&self) -> Option { self.element .pseudo_element_originating_element() .map(|e| ElementWrapper::new(e, self.snapshot_map)) } fn assigned_slot(&self) -> Option { self.element .assigned_slot() .map(|e| ElementWrapper::new(e, self.snapshot_map)) } }