style/invalidation/
stylesheets.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 collection of invalidations due to changes in which stylesheets affect a
6//! document.
7
8#![deny(unsafe_code)]
9
10use crate::context::QuirksMode;
11use crate::dom::{TDocument, TElement, TNode};
12use crate::invalidation::element::element_wrapper::{ElementSnapshot, ElementWrapper};
13use crate::invalidation::element::restyle_hints::RestyleHint;
14use crate::media_queries::Device;
15use crate::selector_parser::{SelectorImpl, Snapshot, SnapshotMap};
16use crate::shared_lock::SharedRwLockReadGuard;
17use crate::simple_buckets_map::SimpleBucketsMap;
18use crate::stylesheets::{CssRule, StylesheetInDocument};
19use crate::stylesheets::{EffectiveRules, EffectiveRulesIterator};
20use crate::values::AtomIdent;
21use crate::LocalName as SelectorLocalName;
22use selectors::parser::{Component, LocalName, Selector};
23
24/// The kind of change that happened for a given rule.
25#[repr(u32)]
26#[derive(Clone, Copy, Debug, Eq, Hash, MallocSizeOf, PartialEq)]
27pub enum RuleChangeKind {
28    /// Some change in the rule which we don't know about, and could have made
29    /// the rule change in any way.
30    Generic = 0,
31    /// The rule was inserted.
32    Insertion,
33    /// The rule was removed.
34    Removal,
35    /// A change in the declarations of a style rule.
36    StyleRuleDeclarations,
37}
38
39/// A style sheet invalidation represents a kind of element or subtree that may
40/// need to be restyled. Whether it represents a whole subtree or just a single
41/// element is determined by the given InvalidationKind in
42/// StylesheetInvalidationSet's maps.
43#[derive(Debug, Eq, Hash, MallocSizeOf, PartialEq)]
44enum Invalidation {
45    /// An element with a given id.
46    ID(AtomIdent),
47    /// An element with a given class name.
48    Class(AtomIdent),
49    /// An element with a given local name.
50    LocalName {
51        name: SelectorLocalName,
52        lower_name: SelectorLocalName,
53    },
54}
55
56impl Invalidation {
57    fn is_id(&self) -> bool {
58        matches!(*self, Invalidation::ID(..))
59    }
60
61    fn is_id_or_class(&self) -> bool {
62        matches!(*self, Invalidation::ID(..) | Invalidation::Class(..))
63    }
64}
65
66/// Whether we should invalidate just the element, or the whole subtree within
67/// it.
68#[derive(Clone, Copy, Debug, Eq, MallocSizeOf, Ord, PartialEq, PartialOrd)]
69enum InvalidationKind {
70    None = 0,
71    Element,
72    Scope,
73}
74
75impl std::ops::BitOrAssign for InvalidationKind {
76    #[inline]
77    fn bitor_assign(&mut self, other: Self) {
78        *self = std::cmp::max(*self, other);
79    }
80}
81
82impl InvalidationKind {
83    #[inline]
84    fn is_scope(self) -> bool {
85        matches!(self, Self::Scope)
86    }
87
88    #[inline]
89    fn add(&mut self, other: Option<&InvalidationKind>) {
90        if let Some(other) = other {
91            *self |= *other;
92        }
93    }
94}
95
96/// A set of invalidations due to stylesheet additions.
97///
98/// TODO(emilio): We might be able to do the same analysis for media query
99/// changes too (or even selector changes?).
100#[derive(Debug, Default, MallocSizeOf)]
101pub struct StylesheetInvalidationSet {
102    buckets: SimpleBucketsMap<InvalidationKind>,
103    fully_invalid: bool,
104}
105
106impl StylesheetInvalidationSet {
107    /// Create an empty `StylesheetInvalidationSet`.
108    pub fn new() -> Self {
109        Default::default()
110    }
111
112    /// Mark the DOM tree styles' as fully invalid.
113    pub fn invalidate_fully(&mut self) {
114        debug!("StylesheetInvalidationSet::invalidate_fully");
115        self.clear();
116        self.fully_invalid = true;
117    }
118
119    fn shrink_if_needed(&mut self) {
120        if self.fully_invalid {
121            return;
122        }
123        self.buckets.shrink_if_needed();
124    }
125
126    /// Analyze the given stylesheet, and collect invalidations from their
127    /// rules, in order to avoid doing a full restyle when we style the document
128    /// next time.
129    pub fn collect_invalidations_for<S>(
130        &mut self,
131        device: &Device,
132        stylesheet: &S,
133        guard: &SharedRwLockReadGuard,
134    ) where
135        S: StylesheetInDocument,
136    {
137        debug!("StylesheetInvalidationSet::collect_invalidations_for");
138        if self.fully_invalid {
139            debug!(" > Fully invalid already");
140            return;
141        }
142
143        if !stylesheet.enabled() || !stylesheet.is_effective_for_device(device, guard) {
144            debug!(" > Stylesheet was not effective");
145            return; // Nothing to do here.
146        }
147
148        let quirks_mode = device.quirks_mode();
149        for rule in stylesheet.effective_rules(device, guard) {
150            self.collect_invalidations_for_rule(
151                rule,
152                guard,
153                device,
154                quirks_mode,
155                /* is_generic_change = */ false,
156            );
157            if self.fully_invalid {
158                break;
159            }
160        }
161
162        self.shrink_if_needed();
163
164        debug!(
165            " > resulting class invalidations: {:?}",
166            self.buckets.classes
167        );
168        debug!(" > resulting id invalidations: {:?}", self.buckets.ids);
169        debug!(
170            " > resulting local name invalidations: {:?}",
171            self.buckets.local_names
172        );
173        debug!(" > fully_invalid: {}", self.fully_invalid);
174    }
175
176    /// Clears the invalidation set, invalidating elements as needed if
177    /// `document_element` is provided.
178    ///
179    /// Returns true if any invalidations ocurred.
180    pub fn flush<E>(&mut self, document_element: Option<E>, snapshots: Option<&SnapshotMap>) -> bool
181    where
182        E: TElement,
183    {
184        debug!(
185            "Stylist::flush({:?}, snapshots: {})",
186            document_element,
187            snapshots.is_some()
188        );
189        let have_invalidations = match document_element {
190            Some(e) => self.process_invalidations(e, snapshots),
191            None => false,
192        };
193        self.clear();
194        have_invalidations
195    }
196
197    /// Returns whether there's no invalidation to process.
198    pub fn is_empty(&self) -> bool {
199        !self.fully_invalid && self.buckets.is_empty()
200    }
201
202    fn invalidation_kind_for<E>(
203        &self,
204        element: E,
205        snapshot: Option<&Snapshot>,
206        quirks_mode: QuirksMode,
207    ) -> InvalidationKind
208    where
209        E: TElement,
210    {
211        debug_assert!(!self.fully_invalid);
212
213        let mut kind = InvalidationKind::None;
214
215        if !self.buckets.classes.is_empty() {
216            element.each_class(|c| {
217                kind.add(self.buckets.classes.get(c, quirks_mode));
218            });
219
220            if kind.is_scope() {
221                return kind;
222            }
223
224            if let Some(snapshot) = snapshot {
225                snapshot.each_class(|c| {
226                    kind.add(self.buckets.classes.get(c, quirks_mode));
227                });
228
229                if kind.is_scope() {
230                    return kind;
231                }
232            }
233        }
234
235        if !self.buckets.ids.is_empty() {
236            if let Some(ref id) = element.id() {
237                kind.add(self.buckets.ids.get(id, quirks_mode));
238                if kind.is_scope() {
239                    return kind;
240                }
241            }
242
243            if let Some(ref old_id) = snapshot.and_then(|s| s.id_attr()) {
244                kind.add(self.buckets.ids.get(old_id, quirks_mode));
245                if kind.is_scope() {
246                    return kind;
247                }
248            }
249        }
250
251        if !self.buckets.local_names.is_empty() {
252            kind.add(self.buckets.local_names.get(element.local_name()));
253        }
254
255        kind
256    }
257
258    /// Clears the invalidation set without processing.
259    pub fn clear(&mut self) {
260        self.buckets.clear();
261        self.fully_invalid = false;
262        debug_assert!(self.is_empty());
263    }
264
265    fn process_invalidations<E>(&self, element: E, snapshots: Option<&SnapshotMap>) -> bool
266    where
267        E: TElement,
268    {
269        debug!("Stylist::process_invalidations({:?}, {:?})", element, self);
270
271        {
272            let mut data = match element.mutate_data() {
273                Some(data) => data,
274                None => return false,
275            };
276
277            if self.fully_invalid {
278                debug!("process_invalidations: fully_invalid({:?})", element);
279                data.hint.insert(RestyleHint::restyle_subtree());
280                return true;
281            }
282        }
283
284        if self.is_empty() {
285            debug!("process_invalidations: empty invalidation set");
286            return false;
287        }
288
289        let quirks_mode = element.as_node().owner_doc().quirks_mode();
290        self.process_invalidations_in_subtree(element, snapshots, quirks_mode)
291    }
292
293    /// Process style invalidations in a given subtree. This traverses the
294    /// subtree looking for elements that match the invalidations in our hash
295    /// map members.
296    ///
297    /// Returns whether it invalidated at least one element's style.
298    #[allow(unsafe_code)]
299    fn process_invalidations_in_subtree<E>(
300        &self,
301        element: E,
302        snapshots: Option<&SnapshotMap>,
303        quirks_mode: QuirksMode,
304    ) -> bool
305    where
306        E: TElement,
307    {
308        debug!("process_invalidations_in_subtree({:?})", element);
309        let mut data = match element.mutate_data() {
310            Some(data) => data,
311            None => return false,
312        };
313
314        if !data.has_styles() {
315            return false;
316        }
317
318        if data.hint.contains_subtree() {
319            debug!(
320                "process_invalidations_in_subtree: {:?} was already invalid",
321                element
322            );
323            return false;
324        }
325
326        let element_wrapper = snapshots.map(|s| ElementWrapper::new(element, s));
327        let snapshot = element_wrapper.as_ref().and_then(|e| e.snapshot());
328
329        match self.invalidation_kind_for(element, snapshot, quirks_mode) {
330            InvalidationKind::None => {},
331            InvalidationKind::Element => {
332                debug!(
333                    "process_invalidations_in_subtree: {:?} matched self",
334                    element
335                );
336                data.hint.insert(RestyleHint::RESTYLE_SELF);
337            },
338            InvalidationKind::Scope => {
339                debug!(
340                    "process_invalidations_in_subtree: {:?} matched subtree",
341                    element
342                );
343                data.hint.insert(RestyleHint::restyle_subtree());
344                return true;
345            },
346        }
347
348        let mut any_children_invalid = false;
349
350        for child in element.traversal_children() {
351            let child = match child.as_element() {
352                Some(e) => e,
353                None => continue,
354            };
355
356            any_children_invalid |=
357                self.process_invalidations_in_subtree(child, snapshots, quirks_mode);
358        }
359
360        if any_children_invalid {
361            debug!(
362                "Children of {:?} changed, setting dirty descendants",
363                element
364            );
365            unsafe { element.set_dirty_descendants() }
366        }
367
368        data.hint.contains(RestyleHint::RESTYLE_SELF) || any_children_invalid
369    }
370
371    /// TODO(emilio): Reuse the bucket stuff from selectormap? That handles
372    /// :is() / :where() etc.
373    fn scan_component(
374        component: &Component<SelectorImpl>,
375        invalidation: &mut Option<Invalidation>,
376    ) {
377        match *component {
378            Component::LocalName(LocalName {
379                ref name,
380                ref lower_name,
381            }) => {
382                if invalidation.is_none() {
383                    *invalidation = Some(Invalidation::LocalName {
384                        name: name.clone(),
385                        lower_name: lower_name.clone(),
386                    });
387                }
388            },
389            Component::Class(ref class) => {
390                if invalidation.as_ref().map_or(true, |s| !s.is_id_or_class()) {
391                    *invalidation = Some(Invalidation::Class(class.clone()));
392                }
393            },
394            Component::ID(ref id) => {
395                if invalidation.as_ref().map_or(true, |s| !s.is_id()) {
396                    *invalidation = Some(Invalidation::ID(id.clone()));
397                }
398            },
399            _ => {
400                // Ignore everything else, at least for now.
401            },
402        }
403    }
404
405    /// Collect invalidations for a given selector.
406    ///
407    /// We look at the outermost local name, class, or ID selector to the left
408    /// of an ancestor combinator, in order to restyle only a given subtree.
409    ///
410    /// If the selector has no ancestor combinator, then we do the same for
411    /// the only sequence it has, but record it as an element invalidation
412    /// instead of a subtree invalidation.
413    ///
414    /// We prefer IDs to classs, and classes to local names, on the basis
415    /// that the former should be more specific than the latter. We also
416    /// prefer to generate subtree invalidations for the outermost part
417    /// of the selector, to reduce the amount of traversal we need to do
418    /// when flushing invalidations.
419    fn collect_invalidations(
420        &mut self,
421        selector: &Selector<SelectorImpl>,
422        quirks_mode: QuirksMode,
423    ) {
424        debug!(
425            "StylesheetInvalidationSet::collect_invalidations({:?})",
426            selector
427        );
428
429        let mut element_invalidation: Option<Invalidation> = None;
430        let mut subtree_invalidation: Option<Invalidation> = None;
431
432        let mut scan_for_element_invalidation = true;
433        let mut scan_for_subtree_invalidation = false;
434
435        let mut iter = selector.iter();
436
437        loop {
438            for component in &mut iter {
439                if scan_for_element_invalidation {
440                    Self::scan_component(component, &mut element_invalidation);
441                } else if scan_for_subtree_invalidation {
442                    Self::scan_component(component, &mut subtree_invalidation);
443                }
444            }
445            match iter.next_sequence() {
446                None => break,
447                Some(combinator) => {
448                    scan_for_subtree_invalidation = combinator.is_ancestor();
449                },
450            }
451            scan_for_element_invalidation = false;
452        }
453
454        if let Some(s) = subtree_invalidation {
455            debug!(" > Found subtree invalidation: {:?}", s);
456            if self.insert_invalidation(s, InvalidationKind::Scope, quirks_mode) {
457                return;
458            }
459        }
460
461        if let Some(s) = element_invalidation {
462            debug!(" > Found element invalidation: {:?}", s);
463            if self.insert_invalidation(s, InvalidationKind::Element, quirks_mode) {
464                return;
465            }
466        }
467
468        // The selector was of a form that we can't handle. Any element could
469        // match it, so let's just bail out.
470        debug!(" > Can't handle selector or OOMd, marking fully invalid");
471        self.invalidate_fully()
472    }
473
474    fn insert_invalidation(
475        &mut self,
476        invalidation: Invalidation,
477        kind: InvalidationKind,
478        quirks_mode: QuirksMode,
479    ) -> bool {
480        match invalidation {
481            Invalidation::Class(c) => {
482                let entry = match self.buckets.classes.try_entry(c.0, quirks_mode) {
483                    Ok(e) => e,
484                    Err(..) => return false,
485                };
486                *entry.or_insert(InvalidationKind::None) |= kind;
487            },
488            Invalidation::ID(i) => {
489                let entry = match self.buckets.ids.try_entry(i.0, quirks_mode) {
490                    Ok(e) => e,
491                    Err(..) => return false,
492                };
493                *entry.or_insert(InvalidationKind::None) |= kind;
494            },
495            Invalidation::LocalName { name, lower_name } => {
496                let insert_lower = name != lower_name;
497                if self.buckets.local_names.try_reserve(1).is_err() {
498                    return false;
499                }
500                let entry = self.buckets.local_names.entry(name);
501                *entry.or_insert(InvalidationKind::None) |= kind;
502                if insert_lower {
503                    if self.buckets.local_names.try_reserve(1).is_err() {
504                        return false;
505                    }
506                    let entry = self.buckets.local_names.entry(lower_name);
507                    *entry.or_insert(InvalidationKind::None) |= kind;
508                }
509            },
510        }
511
512        true
513    }
514
515    /// Collects invalidations for a given CSS rule, if not fully invalid
516    /// already.
517    ///
518    /// TODO(emilio): we can't check whether the rule is inside a non-effective
519    /// subtree, we potentially could do that.
520    pub fn rule_changed<S>(
521        &mut self,
522        stylesheet: &S,
523        rule: &CssRule,
524        guard: &SharedRwLockReadGuard,
525        device: &Device,
526        quirks_mode: QuirksMode,
527        change_kind: RuleChangeKind,
528    ) where
529        S: StylesheetInDocument,
530    {
531        debug!("StylesheetInvalidationSet::rule_changed");
532        if self.fully_invalid {
533            return;
534        }
535
536        if !stylesheet.enabled() || !stylesheet.is_effective_for_device(device, guard) {
537            debug!(" > Stylesheet was not effective");
538            return; // Nothing to do here.
539        }
540
541        // If the change is generic, we don't have the old rule information to know e.g., the old
542        // media condition, or the old selector text, so we might need to invalidate more
543        // aggressively. That only applies to the changed rules, for other rules we can just
544        // collect invalidations as normal.
545        let is_generic_change = change_kind == RuleChangeKind::Generic;
546        self.collect_invalidations_for_rule(rule, guard, device, quirks_mode, is_generic_change);
547        if self.fully_invalid {
548            return;
549        }
550
551        if !is_generic_change && !EffectiveRules::is_effective(guard, device, quirks_mode, rule) {
552            return;
553        }
554
555        let rules = EffectiveRulesIterator::effective_children(device, quirks_mode, guard, rule);
556        for rule in rules {
557            self.collect_invalidations_for_rule(
558                rule,
559                guard,
560                device,
561                quirks_mode,
562                /* is_generic_change = */ false,
563            );
564            if self.fully_invalid {
565                break;
566            }
567        }
568    }
569
570    /// Collects invalidations for a given CSS rule.
571    fn collect_invalidations_for_rule(
572        &mut self,
573        rule: &CssRule,
574        guard: &SharedRwLockReadGuard,
575        device: &Device,
576        quirks_mode: QuirksMode,
577        is_generic_change: bool,
578    ) {
579        use crate::stylesheets::CssRule::*;
580        debug!("StylesheetInvalidationSet::collect_invalidations_for_rule");
581        debug_assert!(!self.fully_invalid, "Not worth being here!");
582
583        match *rule {
584            Style(ref lock) => {
585                if is_generic_change {
586                    // TODO(emilio): We need to do this for selector / keyframe
587                    // name / font-face changes, because we don't have the old
588                    // selector / name.  If we distinguish those changes
589                    // specially, then we can at least use this invalidation for
590                    // style declaration changes.
591                    return self.invalidate_fully();
592                }
593
594                let style_rule = lock.read_with(guard);
595                for selector in style_rule.selectors.slice() {
596                    self.collect_invalidations(selector, quirks_mode);
597                    if self.fully_invalid {
598                        return;
599                    }
600                }
601            },
602            NestedDeclarations(..) => {
603                // Our containing style rule would handle invalidation for us.
604            },
605            Namespace(..) => {
606                // It's not clear what handling changes for this correctly would
607                // look like.
608            },
609            LayerStatement(..) => {
610                // Layer statement insertions might alter styling order, so we need to always
611                // invalidate fully.
612                return self.invalidate_fully();
613            },
614            Document(..) | Import(..) | Media(..) | Supports(..) | Container(..)
615            | LayerBlock(..) | StartingStyle(..) => {
616                // Do nothing, relevant nested rules are visited as part of rule iteration.
617            },
618            FontFace(..) => {
619                // Do nothing, @font-face doesn't affect computed style information on it's own.
620                // We'll restyle when the font face loads, if needed.
621            },
622            Page(..) | Margin(..) => {
623                // Do nothing, we don't support OM mutations on print documents, and page rules
624                // can't affect anything else.
625            },
626            Keyframes(ref lock) => {
627                if is_generic_change {
628                    return self.invalidate_fully();
629                }
630                let keyframes_rule = lock.read_with(guard);
631                if device.animation_name_may_be_referenced(&keyframes_rule.name) {
632                    debug!(
633                        " > Found @keyframes rule potentially referenced \
634                         from the page, marking the whole tree invalid."
635                    );
636                    self.invalidate_fully();
637                } else {
638                    // Do nothing, this animation can't affect the style of existing elements.
639                }
640            },
641            CounterStyle(..) | Property(..) | FontFeatureValues(..) | FontPaletteValues(..) => {
642                debug!(" > Found unsupported rule, marking the whole subtree invalid.");
643                self.invalidate_fully();
644            },
645            Scope(..) => {
646                // Addition/removal of @scope requires re-evaluation of scope proximity to properly
647                // figure out the styling order.
648                self.invalidate_fully();
649            },
650            PositionTry(..) => {
651                // Potential change in sizes/positions of anchored elements. TODO(dshin, bug 1910616):
652                // We should probably make an effort to see if this position-try is referenced.
653                self.invalidate_fully();
654            },
655        }
656    }
657}