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