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