Skip to main content

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