Skip to main content

style/invalidation/element/
element_wrapper.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5//! A wrapper over an element and a snapshot, that allows us to selector-match
6//! against a past state of the element.
7
8use crate::dom::TElement;
9use crate::selector_parser::{
10    AttrValue, NonTSPseudoClass, PseudoElement, SelectorImpl, Snapshot, SnapshotMap,
11};
12use crate::values::AtomIdent;
13use crate::{CaseSensitivityExt, LocalName, Namespace, WeakAtom};
14use dom::ElementState;
15use selectors::attr::{AttrSelectorOperation, CaseSensitivity, NamespaceConstraint};
16use selectors::bloom::BloomFilter;
17use selectors::matching::{ElementSelectorFlags, MatchingContext};
18use selectors::{Element, OpaqueElement};
19use std::cell::Cell;
20use std::fmt;
21
22/// In order to compute restyle hints, we perform a selector match against a
23/// list of partial selectors whose rightmost simple selector may be sensitive
24/// to the thing being changed. We do this matching twice, once for the element
25/// as it exists now and once for the element as it existed at the time of the
26/// last restyle. If the results of the selector match differ, that means that
27/// the given partial selector is sensitive to the change, and we compute a
28/// restyle hint based on its combinator.
29///
30/// In order to run selector matching against the old element state, we generate
31/// a wrapper for the element which claims to have the old state. This is the
32/// ElementWrapper logic below.
33///
34/// Gecko does this differently for element states, and passes a mask called
35/// mStateMask, which indicates the states that need to be ignored during
36/// selector matching. This saves an ElementWrapper allocation and an additional
37/// selector match call at the expense of additional complexity inside the
38/// selector matching logic. This only works for boolean states though, so we
39/// still need to take the ElementWrapper approach for attribute-dependent
40/// style. So we do it the same both ways for now to reduce complexity, but it's
41/// worth measuring the performance impact (if any) of the mStateMask approach.
42pub trait ElementSnapshot: Sized {
43    /// The state of the snapshot, if any.
44    fn state(&self) -> Option<ElementState>;
45
46    /// If this snapshot contains attribute information.
47    fn has_attrs(&self) -> bool;
48
49    /// Gets the attribute information of the snapshot as a string.
50    ///
51    /// Only for debugging purposes.
52    fn debug_list_attributes(&self) -> String {
53        String::new()
54    }
55
56    /// The ID attribute per this snapshot. Should only be called if
57    /// `has_attrs()` returns true.
58    fn id_attr(&self) -> Option<&WeakAtom>;
59
60    /// Whether this snapshot contains the class `name`. Should only be called
61    /// if `has_attrs()` returns true.
62    fn has_class(&self, name: &AtomIdent, case_sensitivity: CaseSensitivity) -> bool;
63
64    /// Whether this snapshot represents the part named `name`. Should only be
65    /// called if `has_attrs()` returns true.
66    fn is_part(&self, name: &AtomIdent) -> bool;
67
68    /// See Element::imported_part.
69    fn imported_part(&self, name: &AtomIdent) -> Option<AtomIdent>;
70
71    /// A callback that should be called for each class of the snapshot. Should
72    /// only be called if `has_attrs()` returns true.
73    fn each_class<F>(&self, _: F)
74    where
75        F: FnMut(&AtomIdent);
76
77    /// If this snapshot contains CustomStateSet information.
78    fn has_custom_states(&self) -> bool;
79
80    /// A callback that should be called for each CustomState of the snapshot.
81    fn has_custom_state(&self, state: &AtomIdent) -> bool;
82
83    /// A callback that should be called for each CustomState of the snapshot.
84    fn each_custom_state<F>(&self, callback: F)
85    where
86        F: FnMut(&AtomIdent);
87
88    /// The `xml:lang=""` or `lang=""` attribute value per this snapshot.
89    fn lang_attr(&self) -> Option<AttrValue>;
90}
91
92/// A snapshot map plus a small lookup cache, shared by all the `ElementWrapper`s created during
93/// a given invalidation.
94pub struct Snapshots<'a> {
95    map: &'a SnapshotMap,
96    // A simple cache of the last looked-up snapshot, since it's not uncommon when e.g. matching a
97    // compound, to do subsequent lookups on the same element.
98    last_snapshot: Cell<Option<(OpaqueElement, &'a Snapshot)>>,
99}
100
101impl<'a> Snapshots<'a> {
102    /// Creates a new `Snapshots` with an empty cache.
103    pub fn new(map: &'a SnapshotMap) -> Self {
104        Self {
105            map,
106            last_snapshot: Cell::new(None),
107        }
108    }
109}
110
111/// A simple wrapper over an element and a snapshot, that allows us to
112/// selector-match against a past state of the element.
113#[derive(Clone, Copy)]
114pub struct ElementWrapper<'a, 'b, E>
115where
116    E: TElement,
117{
118    element: E,
119    snapshots: &'a Snapshots<'b>,
120}
121
122impl<'a, 'b, E> ElementWrapper<'a, 'b, E>
123where
124    E: TElement,
125{
126    /// Trivially constructs an `ElementWrapper`.
127    pub fn new(el: E, snapshots: &'a Snapshots<'b>) -> Self {
128        Self {
129            element: el,
130            snapshots,
131        }
132    }
133
134    /// Gets the snapshot associated with this element, if any.
135    pub fn snapshot(&self) -> Option<&'b Snapshot> {
136        if !self.element.has_snapshot() {
137            return None;
138        }
139
140        let opaque = self.element.opaque();
141        if let Some((e, s)) = self.snapshots.last_snapshot.get()
142            && e == opaque
143        {
144            return Some(s);
145        }
146
147        let snapshot = self.snapshots.map.get(&self.element);
148        debug_assert!(snapshot.is_some(), "has_snapshot lied!");
149        let snapshot = snapshot?;
150        self.snapshots.last_snapshot.set(Some((opaque, snapshot)));
151        Some(snapshot)
152    }
153
154    /// Returns the states that have changed since the element was snapshotted.
155    pub fn state_changes(&self) -> ElementState {
156        let snapshot = match self.snapshot() {
157            Some(s) => s,
158            None => return ElementState::empty(),
159        };
160
161        match snapshot.state() {
162            Some(state) => state ^ self.element.state(),
163            None => ElementState::empty(),
164        }
165    }
166
167    /// Returns the value of the `xml:lang=""` (or, if appropriate, `lang=""`)
168    /// attribute from this element's snapshot or the closest ancestor
169    /// element snapshot with the attribute specified.
170    fn get_lang(&self) -> Option<AttrValue> {
171        let mut current = *self;
172        loop {
173            let lang = match self.snapshot() {
174                Some(snapshot) if snapshot.has_attrs() => snapshot.lang_attr(),
175                _ => current.element.lang_attr(),
176            };
177            if lang.is_some() {
178                return lang;
179            }
180            current = current.parent_element()?;
181        }
182    }
183}
184
185impl<E> fmt::Debug for ElementWrapper<'_, '_, E>
186where
187    E: TElement,
188{
189    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
190        // Ignore other fields for now, can change later if needed.
191        self.element.fmt(f)
192    }
193}
194
195impl<E> Element for ElementWrapper<'_, '_, E>
196where
197    E: TElement,
198{
199    type Impl = SelectorImpl;
200
201    fn match_non_ts_pseudo_class(
202        &self,
203        pseudo_class: &NonTSPseudoClass,
204        context: &mut MatchingContext<Self::Impl>,
205    ) -> bool {
206        // Some pseudo-classes need special handling to evaluate them against
207        // the snapshot.
208        match *pseudo_class {
209            // For :link and :visited, we don't actually want to test the
210            // element state directly.
211            //
212            // Instead, we use the `visited_handling` to determine if they
213            // match.
214            NonTSPseudoClass::Link => {
215                return self.is_link() && context.visited_handling().matches_unvisited();
216            },
217            NonTSPseudoClass::Visited => {
218                return self.is_link() && context.visited_handling().matches_visited();
219            },
220
221            #[cfg(feature = "gecko")]
222            NonTSPseudoClass::MozTableBorderNonzero => {
223                if let Some(snapshot) = self.snapshot()
224                    && snapshot.has_other_pseudo_class_state()
225                {
226                    return snapshot.mIsTableBorderNonzero();
227                }
228            },
229
230            #[cfg(feature = "gecko")]
231            NonTSPseudoClass::MozSelectListBox => {
232                if let Some(snapshot) = self.snapshot()
233                    && snapshot.has_other_pseudo_class_state()
234                {
235                    return snapshot.mIsSelectListBox();
236                }
237            },
238
239            // :lang() needs to match using the closest ancestor xml:lang="" or
240            // lang="" attribtue from snapshots.
241            NonTSPseudoClass::Lang(ref lang_arg) => {
242                return self
243                    .element
244                    .match_element_lang(Some(self.get_lang()), lang_arg);
245            },
246
247            // :heading should match against snapshot before element
248            #[cfg(feature = "gecko")]
249            NonTSPseudoClass::Heading(ref levels) => {
250                return levels.matches_state(
251                    self.snapshot()
252                        .and_then(|s| s.state())
253                        .unwrap_or_else(|| self.element.state()),
254                );
255            },
256
257            // CustomStateSet should match against the snapshot before element
258            NonTSPseudoClass::CustomState(ref state) => return self.has_custom_state(&state.0),
259
260            // :paused and :playing require special handling because :playing negates the PAUSED
261            // state flag, which the generic snapshot_state.intersects(flag) path cannot express.
262            #[cfg(feature = "gecko")]
263            NonTSPseudoClass::Paused | NonTSPseudoClass::Playing => {
264                let state = self
265                    .snapshot()
266                    .and_then(|s| s.state())
267                    .unwrap_or_else(|| self.element.state());
268                return self.element.is_html_media_element()
269                    && (*pseudo_class == NonTSPseudoClass::Paused)
270                        == state.intersects(ElementState::PAUSED);
271            },
272
273            _ => {},
274        }
275
276        let flag = pseudo_class.state_flag();
277        if flag.is_empty() {
278            return self
279                .element
280                .match_non_ts_pseudo_class(pseudo_class, context);
281        }
282        match self.snapshot().and_then(|s| s.state()) {
283            Some(snapshot_state) => snapshot_state.intersects(flag),
284            None => self
285                .element
286                .match_non_ts_pseudo_class(pseudo_class, context),
287        }
288    }
289
290    fn apply_selector_flags(&self, _flags: ElementSelectorFlags) {
291        debug_assert!(false, "Shouldn't need selector flags for invalidation");
292    }
293
294    fn match_pseudo_element(
295        &self,
296        pseudo_element: &PseudoElement,
297        context: &mut MatchingContext<Self::Impl>,
298    ) -> bool {
299        self.element.match_pseudo_element(pseudo_element, context)
300    }
301
302    fn is_link(&self) -> bool {
303        match self.snapshot().and_then(|s| s.state()) {
304            Some(state) => state.intersects(ElementState::VISITED_OR_UNVISITED),
305            None => self.element.is_link(),
306        }
307    }
308
309    fn opaque(&self) -> OpaqueElement {
310        self.element.opaque()
311    }
312
313    fn parent_element(&self) -> Option<Self> {
314        let parent = self.element.parent_element()?;
315        Some(Self::new(parent, self.snapshots))
316    }
317
318    fn parent_node_is_shadow_root(&self) -> bool {
319        self.element.parent_node_is_shadow_root()
320    }
321
322    fn containing_shadow_host(&self) -> Option<Self> {
323        let host = self.element.containing_shadow_host()?;
324        Some(Self::new(host, self.snapshots))
325    }
326
327    fn prev_sibling_element(&self) -> Option<Self> {
328        let sibling = self.element.prev_sibling_element()?;
329        Some(Self::new(sibling, self.snapshots))
330    }
331
332    fn next_sibling_element(&self) -> Option<Self> {
333        let sibling = self.element.next_sibling_element()?;
334        Some(Self::new(sibling, self.snapshots))
335    }
336
337    fn first_element_child(&self) -> Option<Self> {
338        let child = self.element.first_element_child()?;
339        Some(Self::new(child, self.snapshots))
340    }
341
342    #[inline]
343    fn is_html_element_in_html_document(&self) -> bool {
344        self.element.is_html_element_in_html_document()
345    }
346
347    #[inline]
348    fn is_html_slot_element(&self) -> bool {
349        self.element.is_html_slot_element()
350    }
351
352    #[inline]
353    fn has_local_name(
354        &self,
355        local_name: &<Self::Impl as ::selectors::SelectorImpl>::BorrowedLocalName,
356    ) -> bool {
357        self.element.has_local_name(local_name)
358    }
359
360    #[inline]
361    fn has_namespace(
362        &self,
363        ns: &<Self::Impl as ::selectors::SelectorImpl>::BorrowedNamespaceUrl,
364    ) -> bool {
365        self.element.has_namespace(ns)
366    }
367
368    #[inline]
369    fn is_same_type(&self, other: &Self) -> bool {
370        self.element.is_same_type(&other.element)
371    }
372
373    fn attr_matches(
374        &self,
375        ns: &NamespaceConstraint<&Namespace>,
376        local_name: &LocalName,
377        operation: &AttrSelectorOperation<&AttrValue>,
378    ) -> bool {
379        match self.snapshot() {
380            Some(snapshot) if snapshot.has_attrs() => {
381                snapshot.attr_matches(ns, local_name, operation)
382            },
383            _ => self.element.attr_matches(ns, local_name, operation),
384        }
385    }
386
387    fn has_id(&self, id: &AtomIdent, case_sensitivity: CaseSensitivity) -> bool {
388        match self.snapshot() {
389            Some(snapshot) if snapshot.has_attrs() => snapshot
390                .id_attr()
391                .is_some_and(|atom| case_sensitivity.eq_atom(atom, id)),
392            _ => self.element.has_id(id, case_sensitivity),
393        }
394    }
395
396    fn is_part(&self, name: &AtomIdent) -> bool {
397        match self.snapshot() {
398            Some(snapshot) if snapshot.has_attrs() => snapshot.is_part(name),
399            _ => self.element.is_part(name),
400        }
401    }
402
403    fn imported_part(&self, name: &AtomIdent) -> Option<AtomIdent> {
404        match self.snapshot() {
405            Some(snapshot) if snapshot.has_attrs() => snapshot.imported_part(name),
406            _ => self.element.imported_part(name),
407        }
408    }
409
410    fn has_class(&self, name: &AtomIdent, case_sensitivity: CaseSensitivity) -> bool {
411        match self.snapshot() {
412            Some(snapshot) if snapshot.has_attrs() => snapshot.has_class(name, case_sensitivity),
413            _ => self.element.has_class(name, case_sensitivity),
414        }
415    }
416
417    fn has_custom_state(&self, state: &AtomIdent) -> bool {
418        match self.snapshot() {
419            Some(snapshot) if snapshot.has_custom_states() => snapshot.has_custom_state(state),
420            _ => self.element.has_custom_state(state),
421        }
422    }
423
424    fn is_empty(&self) -> bool {
425        self.element.is_empty()
426    }
427
428    fn is_root(&self) -> bool {
429        self.element.is_root()
430    }
431
432    fn is_pseudo_element(&self) -> bool {
433        self.element.is_pseudo_element()
434    }
435
436    fn pseudo_element_originating_element(&self) -> Option<Self> {
437        self.element
438            .pseudo_element_originating_element()
439            .map(|e| ElementWrapper::new(e, self.snapshots))
440    }
441
442    fn assigned_slot(&self) -> Option<Self> {
443        self.element
444            .assigned_slot()
445            .map(|e| ElementWrapper::new(e, self.snapshots))
446    }
447
448    fn add_element_unique_hashes(&self, _filter: &mut BloomFilter) -> bool {
449        // Should not be relevant in the context of checking past elements in invalidation.
450        false
451    }
452}