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::data::ElementData;
12use crate::derives::*;
13use crate::dom::{TDocument, TElement, TNode};
14use crate::invalidation::element::element_wrapper::{ElementSnapshot, ElementWrapper};
15use crate::invalidation::element::restyle_hints::RestyleHint;
16use crate::media_queries::Device;
17use crate::selector_map::PrecomputedHashSet;
18use crate::selector_parser::{SelectorImpl, Snapshot, SnapshotMap};
19use crate::shared_lock::SharedRwLockReadGuard;
20use crate::simple_buckets_map::SimpleBucketsMap;
21use crate::stylesheets::{
22    CssRule, CssRuleRef, CustomMediaMap, EffectiveRules, EffectiveRulesIterator,
23    StylesheetInDocument,
24};
25use crate::stylist::CascadeDataDifference;
26use crate::values::specified::position::PositionTryFallbacksItem;
27use crate::values::AtomIdent;
28use crate::Atom;
29use crate::LocalName as SelectorLocalName;
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(Debug, Eq, Hash, MallocSizeOf, PartialEq)]
54enum Invalidation {
55    /// An element with a given id.
56    ID(AtomIdent),
57    /// An element with a given class name.
58    Class(AtomIdent),
59    /// An element with a given local name.
60    LocalName {
61        name: SelectorLocalName,
62        lower_name: 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(ref 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(ref 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        self.process_invalidations_in_subtree(root, snapshots, quirks_mode)
281    }
282
283    /// Process style invalidations in a given subtree. This traverses the
284    /// subtree looking for elements that match the invalidations in our hash
285    /// map members.
286    ///
287    /// Returns whether it invalidated at least one element's style.
288    #[allow(unsafe_code)]
289    fn process_invalidations_in_subtree<E>(
290        &self,
291        element: E,
292        snapshots: Option<&SnapshotMap>,
293        quirks_mode: QuirksMode,
294    ) -> bool
295    where
296        E: TElement,
297    {
298        debug!("process_invalidations_in_subtree({:?})", element);
299        let mut data = match element.mutate_data() {
300            Some(data) => data,
301            None => return false,
302        };
303
304        if !data.has_styles() {
305            return false;
306        }
307
308        if data.hint.contains_subtree() {
309            debug!(
310                "process_invalidations_in_subtree: {:?} was already invalid",
311                element
312            );
313            return false;
314        }
315
316        let element_wrapper = snapshots.map(|s| ElementWrapper::new(element, s));
317        let snapshot = element_wrapper.as_ref().and_then(|e| e.snapshot());
318
319        match self.invalidation_kind_for(element, snapshot, quirks_mode) {
320            InvalidationKind::None => {},
321            InvalidationKind::Element => {
322                debug!(
323                    "process_invalidations_in_subtree: {:?} matched self",
324                    element
325                );
326                data.hint.insert(RestyleHint::RESTYLE_SELF);
327            },
328            InvalidationKind::Scope => {
329                debug!(
330                    "process_invalidations_in_subtree: {:?} matched subtree",
331                    element
332                );
333                data.hint.insert(RestyleHint::restyle_subtree());
334                return true;
335            },
336        }
337
338        let mut any_children_invalid = false;
339
340        for child in element.traversal_children() {
341            let child = match child.as_element() {
342                Some(e) => e,
343                None => continue,
344            };
345
346            any_children_invalid |=
347                self.process_invalidations_in_subtree(child, snapshots, quirks_mode);
348        }
349
350        if any_children_invalid {
351            debug!(
352                "Children of {:?} changed, setting dirty descendants",
353                element
354            );
355            unsafe { element.set_dirty_descendants() }
356        }
357
358        data.hint.contains(RestyleHint::RESTYLE_SELF) || any_children_invalid
359    }
360
361    /// TODO(emilio): Reuse the bucket stuff from selectormap? That handles :is() / :where() etc.
362    fn scan_component(
363        component: &Component<SelectorImpl>,
364        invalidation: &mut Option<Invalidation>,
365    ) {
366        match *component {
367            Component::LocalName(LocalName {
368                ref name,
369                ref lower_name,
370            }) => {
371                if invalidation.is_none() {
372                    *invalidation = Some(Invalidation::LocalName {
373                        name: name.clone(),
374                        lower_name: lower_name.clone(),
375                    });
376                }
377            },
378            Component::Class(ref class) => {
379                if invalidation.as_ref().map_or(true, |s| !s.is_id_or_class()) {
380                    *invalidation = Some(Invalidation::Class(class.clone()));
381                }
382            },
383            Component::ID(ref id) => {
384                if invalidation.as_ref().map_or(true, |s| !s.is_id()) {
385                    *invalidation = Some(Invalidation::ID(id.clone()));
386                }
387            },
388            _ => {
389                // Ignore everything else, at least for now.
390            },
391        }
392    }
393
394    /// Collect invalidations for a given selector.
395    ///
396    /// We look at the outermost local name, class, or ID selector to the left
397    /// of an ancestor combinator, in order to restyle only a given subtree.
398    ///
399    /// If the selector has no ancestor combinator, then we do the same for
400    /// the only sequence it has, but record it as an element invalidation
401    /// instead of a subtree invalidation.
402    ///
403    /// We prefer IDs to classs, and classes to local names, on the basis
404    /// that the former should be more specific than the latter. We also
405    /// prefer to generate subtree invalidations for the outermost part
406    /// of the selector, to reduce the amount of traversal we need to do
407    /// when flushing invalidations.
408    fn collect_invalidations(
409        &mut self,
410        selector: &Selector<SelectorImpl>,
411        quirks_mode: QuirksMode,
412    ) {
413        debug!(
414            "StylesheetInvalidationSet::collect_invalidations({:?})",
415            selector
416        );
417
418        let mut element_invalidation: Option<Invalidation> = None;
419        let mut subtree_invalidation: Option<Invalidation> = None;
420
421        let mut scan_for_element_invalidation = true;
422        let mut scan_for_subtree_invalidation = false;
423
424        let mut iter = selector.iter();
425
426        loop {
427            for component in &mut iter {
428                if scan_for_element_invalidation {
429                    Self::scan_component(component, &mut element_invalidation);
430                } else if scan_for_subtree_invalidation {
431                    Self::scan_component(component, &mut subtree_invalidation);
432                }
433            }
434            match iter.next_sequence() {
435                None => break,
436                Some(combinator) => {
437                    scan_for_subtree_invalidation = combinator.is_ancestor();
438                },
439            }
440            scan_for_element_invalidation = false;
441        }
442
443        if let Some(s) = subtree_invalidation {
444            debug!(" > Found subtree invalidation: {:?}", s);
445            if self.insert_invalidation(s, InvalidationKind::Scope, quirks_mode) {
446                return;
447            }
448        }
449
450        if let Some(s) = element_invalidation {
451            debug!(" > Found element invalidation: {:?}", s);
452            if self.insert_invalidation(s, InvalidationKind::Element, quirks_mode) {
453                return;
454            }
455        }
456
457        // The selector was of a form that we can't handle. Any element could
458        // match it, so let's just bail out.
459        debug!(" > Can't handle selector or OOMd, marking fully invalid");
460        self.invalidate_fully()
461    }
462
463    fn insert_invalidation(
464        &mut self,
465        invalidation: Invalidation,
466        kind: InvalidationKind,
467        quirks_mode: QuirksMode,
468    ) -> bool {
469        match invalidation {
470            Invalidation::Class(c) => {
471                let entry = match self.buckets.classes.try_entry(c.0, quirks_mode) {
472                    Ok(e) => e,
473                    Err(..) => return false,
474                };
475                *entry.or_insert(InvalidationKind::None) |= kind;
476            },
477            Invalidation::ID(i) => {
478                let entry = match self.buckets.ids.try_entry(i.0, quirks_mode) {
479                    Ok(e) => e,
480                    Err(..) => return false,
481                };
482                *entry.or_insert(InvalidationKind::None) |= kind;
483            },
484            Invalidation::LocalName { name, lower_name } => {
485                let insert_lower = name != lower_name;
486                if self.buckets.local_names.try_reserve(1).is_err() {
487                    return false;
488                }
489                let entry = self.buckets.local_names.entry(name);
490                *entry.or_insert(InvalidationKind::None) |= kind;
491                if insert_lower {
492                    if self.buckets.local_names.try_reserve(1).is_err() {
493                        return false;
494                    }
495                    let entry = self.buckets.local_names.entry(lower_name);
496                    *entry.or_insert(InvalidationKind::None) |= kind;
497                }
498            },
499        }
500
501        true
502    }
503
504    /// Collects invalidations for a given CSS rule, if not fully invalid already.
505    pub fn rule_changed<S>(
506        &mut self,
507        stylesheet: &S,
508        rule: &CssRule,
509        guard: &SharedRwLockReadGuard,
510        device: &Device,
511        quirks_mode: QuirksMode,
512        custom_media: &CustomMediaMap,
513        change_kind: RuleChangeKind,
514        ancestors: &[CssRuleRef],
515    ) where
516        S: StylesheetInDocument,
517    {
518        debug!("StylesheetInvalidationSet::rule_changed");
519        if !stylesheet.enabled() || !stylesheet.is_effective_for_device(device, custom_media, guard)
520        {
521            debug!(" > Stylesheet was not effective");
522            return; // Nothing to do here.
523        }
524
525        if ancestors
526            .iter()
527            .any(|r| !EffectiveRules::is_effective(guard, device, quirks_mode, custom_media, r))
528        {
529            debug!(" > Ancestor rules not effective");
530            return;
531        }
532
533        if change_kind == RuleChangeKind::PositionTryDeclarations {
534            // @position-try declaration changes need to be dealt explicitly, since the
535            // declarations are mutable and we can't otherwise detect changes to them.
536            match *rule {
537                CssRule::PositionTry(ref pt) => {
538                    self.cascade_data_difference
539                        .changed_position_try_names
540                        .insert(pt.read_with(guard).name.0.clone());
541                },
542                _ => debug_assert!(false, "how did position-try decls change on anything else?"),
543            }
544            return;
545        }
546
547        if self.style_fully_invalid {
548            return;
549        }
550
551        // If the change is generic, we don't have the old rule information to know e.g., the old
552        // media condition, or the old selector text, so we might need to invalidate more
553        // aggressively. That only applies to the changed rules, for other rules we can just
554        // collect invalidations as normal.
555        let is_generic_change = change_kind == RuleChangeKind::Generic;
556        self.collect_invalidations_for_rule(
557            rule,
558            guard,
559            device,
560            quirks_mode,
561            is_generic_change,
562            ancestors,
563        );
564        if self.style_fully_invalid {
565            return;
566        }
567
568        if !is_generic_change
569            && !EffectiveRules::is_effective(guard, device, quirks_mode, custom_media, &rule.into())
570        {
571            return;
572        }
573
574        let rules = EffectiveRulesIterator::effective_children(
575            device,
576            quirks_mode,
577            custom_media,
578            guard,
579            rule,
580        );
581        for rule in rules {
582            self.collect_invalidations_for_rule(
583                rule,
584                guard,
585                device,
586                quirks_mode,
587                /* is_generic_change = */ false,
588                // 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.
589                &[],
590            );
591            if self.style_fully_invalid {
592                break;
593            }
594        }
595    }
596
597    /// Collects invalidations for a given CSS rule.
598    fn collect_invalidations_for_rule(
599        &mut self,
600        rule: &CssRule,
601        guard: &SharedRwLockReadGuard,
602        device: &Device,
603        quirks_mode: QuirksMode,
604        is_generic_change: bool,
605        ancestors: &[CssRuleRef],
606    ) {
607        use crate::stylesheets::CssRule::*;
608        debug!("StylesheetInvalidationSet::collect_invalidations_for_rule");
609        debug_assert!(!self.style_fully_invalid, "Not worth being here!");
610
611        match *rule {
612            Style(ref lock) => {
613                if is_generic_change {
614                    // TODO(emilio): We need to do this for selector / keyframe
615                    // name / font-face changes, because we don't have the old
616                    // selector / name.  If we distinguish those changes
617                    // specially, then we can at least use this invalidation for
618                    // style declaration changes.
619                    return self.invalidate_fully();
620                }
621
622                let style_rule = lock.read_with(guard);
623                for selector in style_rule.selectors.slice() {
624                    self.collect_invalidations(selector, quirks_mode);
625                    if self.style_fully_invalid {
626                        return;
627                    }
628                }
629            },
630            NestedDeclarations(..) => {
631                if ancestors.iter().any(|r| matches!(r, CssRuleRef::Scope(_))) {
632                    self.invalidate_fully();
633                }
634            },
635            Namespace(..) => {
636                // It's not clear what handling changes for this correctly would
637                // look like.
638            },
639            LayerStatement(..) => {
640                // Layer statement insertions might alter styling order, so we need to always
641                // invalidate fully.
642                return self.invalidate_fully();
643            },
644            Document(..) | Import(..) | Media(..) | Supports(..) | Container(..)
645            | LayerBlock(..) | StartingStyle(..) => {
646                // Do nothing, relevant nested rules are visited as part of rule iteration.
647            },
648            FontFace(..) => {
649                // Do nothing, @font-face doesn't affect computed style information on it's own.
650                // We'll restyle when the font face loads, if needed.
651            },
652            Page(..) | Margin(..) => {
653                // Do nothing, we don't support OM mutations on print documents, and page rules
654                // can't affect anything else.
655            },
656            Keyframes(ref lock) => {
657                if is_generic_change {
658                    return self.invalidate_fully();
659                }
660                let keyframes_rule = lock.read_with(guard);
661                if device.animation_name_may_be_referenced(&keyframes_rule.name) {
662                    debug!(
663                        " > Found @keyframes rule potentially referenced \
664                         from the page, marking the whole tree invalid."
665                    );
666                    self.invalidate_fully();
667                } else {
668                    // Do nothing, this animation can't affect the style of existing elements.
669                }
670            },
671            CounterStyle(..) | Property(..) | FontFeatureValues(..) | FontPaletteValues(..) => {
672                debug!(" > Found unsupported rule, marking the whole subtree invalid.");
673                self.invalidate_fully();
674            },
675            Scope(..) => {
676                // Addition/removal of @scope requires re-evaluation of scope proximity to properly
677                // figure out the styling order.
678                self.invalidate_fully();
679            },
680            PositionTry(..) => {
681                // @position-try changes doesn't change style-time information (only layout
682                // information) and is handled by invalidate_position_try. So do nothing.
683            },
684            CustomMedia(..) => {
685                // @custom-media might be referenced by other rules which we can't get a hand on in
686                // here, so we don't know which elements are affected.
687                //
688                // TODO: Maybe track referenced custom-media rules like we do for @keyframe?
689                self.invalidate_fully();
690            },
691        }
692    }
693}
694
695/// Invalidates for any absolutely positioned element that references the given @position-try fallback names.
696pub fn invalidate_position_try<E>(
697    element: E,
698    changed_names: &PrecomputedHashSet<Atom>,
699    invalidate_self: &mut impl FnMut(E, &mut ElementData),
700    invalidated_descendants: &mut impl FnMut(E),
701) -> bool
702where
703    E: TElement,
704{
705    debug_assert!(
706        !changed_names.is_empty(),
707        "Don't call me if there's nothing to do"
708    );
709    let mut data = match element.mutate_data() {
710        Some(data) => data,
711        None => return false,
712    };
713
714    let mut self_invalid = false;
715    let style = data.styles.primary();
716    if style.clone_position().is_absolutely_positioned() {
717        let fallbacks = style.clone_position_try_fallbacks();
718        let referenced = fallbacks.0.iter().any(|f| match f {
719            PositionTryFallbacksItem::IdentAndOrTactic(ident_or_tactic) => {
720                changed_names.contains(&ident_or_tactic.ident.0)
721            },
722            PositionTryFallbacksItem::PositionArea(..) => false,
723        });
724
725        if referenced {
726            self_invalid = true;
727            invalidate_self(element, &mut data);
728        }
729    }
730    let mut any_children_invalid = false;
731    for child in element.traversal_children() {
732        let Some(e) = child.as_element() else {
733            continue;
734        };
735        any_children_invalid |=
736            invalidate_position_try(e, changed_names, invalidate_self, invalidated_descendants);
737    }
738    if any_children_invalid {
739        invalidated_descendants(element);
740    }
741    self_invalid || any_children_invalid
742}