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