Skip to main content

style/
stylesheet_set.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 centralized set of stylesheets for a document.
6
7use crate::derives::*;
8use crate::device::Device;
9use crate::invalidation::stylesheets::{RuleChangeKind, StylesheetInvalidationSet};
10use crate::shared_lock::{SharedRwLockReadGuard, StylesheetGuards};
11use crate::stylesheets::{
12    CssRule, CssRuleRef, CustomMediaMap, Origin, OriginSet, PerOrigin, StylesheetInDocument,
13};
14use std::mem;
15
16/// Entry for a StylesheetSet.
17#[derive(MallocSizeOf)]
18struct StylesheetSetEntry<S>
19where
20    S: StylesheetInDocument + PartialEq + 'static,
21{
22    /// The sheet.
23    sheet: S,
24
25    /// Whether this sheet has been part of at least one flush.
26    committed: bool,
27}
28
29impl<S> StylesheetSetEntry<S>
30where
31    S: StylesheetInDocument + PartialEq + 'static,
32{
33    fn new(sheet: S) -> Self {
34        Self {
35            sheet,
36            committed: false,
37        }
38    }
39}
40
41/// The validity of the data in a given cascade origin.
42#[derive(Clone, Copy, Debug, Default, Eq, MallocSizeOf, Ord, PartialEq, PartialOrd)]
43pub enum DataValidity {
44    /// The origin is clean, all the data already there is valid, though we may
45    /// have new sheets at the end.
46    #[default]
47    Valid = 0,
48
49    /// The cascade data is invalid, but not the invalidation data (which is
50    /// order-independent), and thus only the cascade data should be inserted.
51    CascadeInvalid = 1,
52
53    /// Everything needs to be rebuilt.
54    FullyInvalid = 2,
55}
56
57/// A struct to iterate over the different stylesheets to be flushed.
58pub struct DocumentStylesheetFlusher<'a, S>
59where
60    S: StylesheetInDocument + PartialEq + 'static,
61{
62    collections: &'a mut PerOrigin<SheetCollection<S>>,
63}
64
65/// The type of rebuild that we need to do for a given stylesheet.
66#[derive(Clone, Copy, Debug)]
67pub enum SheetRebuildKind {
68    /// A full rebuild, of both cascade data and invalidation data.
69    Full,
70    /// A partial rebuild, of only the cascade data.
71    CascadeOnly,
72}
73
74impl SheetRebuildKind {
75    /// Whether the stylesheet invalidation data should be rebuilt.
76    pub fn should_rebuild_invalidation(&self) -> bool {
77        matches!(*self, SheetRebuildKind::Full)
78    }
79}
80
81impl<'a, S> DocumentStylesheetFlusher<'a, S>
82where
83    S: StylesheetInDocument + PartialEq + 'static,
84{
85    /// Returns a flusher for `origin`.
86    pub fn flush_origin(&mut self, origin: Origin) -> SheetCollectionFlusher<'_, S> {
87        self.collections.borrow_mut_for_origin(&origin).flush()
88    }
89
90    /// Returns the list of stylesheets for `origin`.
91    ///
92    /// Only used for UA sheets.
93    pub fn origin_sheets(&self, origin: Origin) -> impl Iterator<Item = &S> {
94        self.collections.borrow_for_origin(&origin).iter()
95    }
96}
97
98/// A flusher struct for a given collection, that takes care of returning the
99/// appropriate stylesheets that need work.
100pub struct SheetCollectionFlusher<'a, S>
101where
102    S: StylesheetInDocument + PartialEq + 'static,
103{
104    // TODO: This can be made an iterator again once
105    // https://github.com/rust-lang/rust/pull/82771 lands on stable.
106    entries: &'a mut [StylesheetSetEntry<S>],
107    validity: DataValidity,
108    dirty: bool,
109}
110
111impl<'a, S> SheetCollectionFlusher<'a, S>
112where
113    S: StylesheetInDocument + PartialEq + 'static,
114{
115    /// Whether the collection was originally dirty.
116    #[inline]
117    pub fn dirty(&self) -> bool {
118        self.dirty
119    }
120
121    /// What the state of the sheet data is.
122    #[inline]
123    pub fn data_validity(&self) -> DataValidity {
124        self.validity
125    }
126
127    /// Returns an iterator over the remaining list of sheets to consume.
128    pub fn sheets(&self) -> impl Iterator<Item = &S> {
129        self.entries.iter().map(|entry| &entry.sheet)
130    }
131}
132
133impl<'a, S> SheetCollectionFlusher<'a, S>
134where
135    S: StylesheetInDocument + PartialEq + 'static,
136{
137    /// Iterates over all sheets and values that we have to invalidate.
138    ///
139    /// TODO(emilio): This would be nicer as an iterator but we can't do that
140    /// until https://github.com/rust-lang/rust/pull/82771 stabilizes.
141    ///
142    /// Since we don't have a good use-case for partial iteration, this does the
143    /// trick for now.
144    pub fn each(self, mut callback: impl FnMut(usize, &S, SheetRebuildKind) -> bool) {
145        for (index, potential_sheet) in self.entries.iter_mut().enumerate() {
146            let committed = mem::replace(&mut potential_sheet.committed, true);
147            let rebuild_kind = if !committed {
148                // If the sheet was uncommitted, we need to do a full rebuild
149                // anyway.
150                SheetRebuildKind::Full
151            } else {
152                match self.validity {
153                    DataValidity::Valid => continue,
154                    DataValidity::CascadeInvalid => SheetRebuildKind::CascadeOnly,
155                    DataValidity::FullyInvalid => SheetRebuildKind::Full,
156                }
157            };
158
159            if !callback(index, &potential_sheet.sheet, rebuild_kind) {
160                return;
161            }
162        }
163    }
164}
165
166#[derive(MallocSizeOf)]
167struct SheetCollection<S>
168where
169    S: StylesheetInDocument + PartialEq + 'static,
170{
171    /// The actual list of stylesheets.
172    ///
173    /// This is only a list of top-level stylesheets, and as such it doesn't
174    /// include recursive `@import` rules.
175    entries: Vec<StylesheetSetEntry<S>>,
176
177    /// The validity of the data that was already there for a given origin.
178    ///
179    /// Note that an origin may appear on `origins_dirty`, but still have
180    /// `DataValidity::Valid`, if only sheets have been appended into it (in
181    /// which case the existing data is valid, but the origin needs to be
182    /// rebuilt).
183    data_validity: DataValidity,
184
185    /// Whether anything in the collection has changed. Note that this is
186    /// different from `data_validity`, in the sense that after a sheet append,
187    /// the data validity is still `Valid`, but we need to be marked as dirty.
188    dirty: bool,
189}
190
191impl<S> Default for SheetCollection<S>
192where
193    S: StylesheetInDocument + PartialEq + 'static,
194{
195    fn default() -> Self {
196        Self {
197            entries: vec![],
198            data_validity: DataValidity::Valid,
199            dirty: false,
200        }
201    }
202}
203
204impl<S> SheetCollection<S>
205where
206    S: StylesheetInDocument + PartialEq + 'static,
207{
208    /// Returns the number of stylesheets in the set.
209    fn len(&self) -> usize {
210        self.entries.len()
211    }
212
213    /// Returns the `index`th stylesheet in the set if present.
214    fn get(&self, index: usize) -> Option<&S> {
215        self.entries.get(index).map(|e| &e.sheet)
216    }
217
218    fn find_sheet_index(&self, sheet: &S) -> Option<usize> {
219        let rev_pos = self
220            .entries
221            .iter()
222            .rev()
223            .position(|entry| entry.sheet == *sheet);
224        rev_pos.map(|i| self.entries.len() - i - 1)
225    }
226
227    /// Removes a sheet from the collection, returning whether the sheet had
228    /// been committed (that is, whether it has been part of a flush).
229    fn remove(&mut self, sheet: &S) -> bool {
230        let index = self.find_sheet_index(sheet);
231        if cfg!(feature = "gecko") && index.is_none() {
232            // FIXME(emilio): Make Gecko's PresShell::AddUserSheet not suck.
233            return false;
234        }
235        let sheet = self.entries.remove(index.unwrap());
236        // Removing sheets makes us tear down the whole cascade and invalidation
237        // data, but only if the sheet has been involved in at least one flush.
238        // Checking whether the sheet has been committed allows us to avoid
239        // rebuilding the world when sites quickly append and remove a
240        // stylesheet.
241        //
242        // See bug 1434756.
243        if sheet.committed {
244            self.set_data_validity_at_least(DataValidity::FullyInvalid);
245        } else {
246            self.dirty = true;
247        }
248        sheet.committed
249    }
250
251    fn contains(&self, sheet: &S) -> bool {
252        self.entries.iter().any(|e| e.sheet == *sheet)
253    }
254
255    /// Appends a given sheet into the collection.
256    fn append(&mut self, sheet: S) {
257        debug_assert!(!self.contains(&sheet));
258        self.entries.push(StylesheetSetEntry::new(sheet));
259        // Appending sheets doesn't alter the validity of the existing data, so
260        // we don't need to change `data_validity` here.
261        //
262        // But we need to be marked as dirty, otherwise we'll never add the new
263        // sheet!
264        self.dirty = true;
265    }
266
267    fn insert_before(&mut self, sheet: S, before_sheet: &S) {
268        debug_assert!(!self.contains(&sheet));
269
270        let index = self
271            .find_sheet_index(before_sheet)
272            .expect("`before_sheet` stylesheet not found");
273
274        // Inserting stylesheets somewhere but at the end changes the validity
275        // of the cascade data, but not the invalidation data.
276        self.set_data_validity_at_least(DataValidity::CascadeInvalid);
277        self.entries.insert(index, StylesheetSetEntry::new(sheet));
278    }
279
280    fn set_data_validity_at_least(&mut self, validity: DataValidity) {
281        use std::cmp;
282
283        debug_assert_ne!(validity, DataValidity::Valid);
284
285        self.dirty = true;
286        self.data_validity = cmp::max(validity, self.data_validity);
287    }
288
289    /// Returns an iterator over the current list of stylesheets.
290    fn iter(&self) -> impl Iterator<Item = &S> {
291        self.entries.iter().map(|e| &e.sheet)
292    }
293
294    /// Returns a mutable iterator over the current list of stylesheets.
295    fn iter_mut(&mut self) -> impl Iterator<Item = &mut S> {
296        self.entries.iter_mut().map(|e| &mut e.sheet)
297    }
298
299    /// Collects invalidations for the sheets that have been added (i.e. sheets not yet committed)
300    /// since the last flush.
301    fn collect_pending_invalidations(
302        &self,
303        invalidations: &mut StylesheetInvalidationSet,
304        device: &Device,
305        custom_media: &CustomMediaMap,
306        guard: &SharedRwLockReadGuard,
307    ) {
308        if !self.dirty {
309            return;
310        }
311        for entry in &self.entries {
312            if entry.committed {
313                continue;
314            }
315            invalidations.collect_invalidations_for(device, custom_media, &entry.sheet, guard);
316        }
317    }
318
319    fn flush(&mut self) -> SheetCollectionFlusher<'_, S> {
320        let dirty = mem::replace(&mut self.dirty, false);
321        let validity = mem::replace(&mut self.data_validity, DataValidity::Valid);
322
323        SheetCollectionFlusher {
324            entries: &mut self.entries,
325            dirty,
326            validity,
327        }
328    }
329}
330
331/// The set of stylesheets effective for a given document.
332#[derive(MallocSizeOf)]
333pub struct DocumentStylesheetSet<S>
334where
335    S: StylesheetInDocument + PartialEq + 'static,
336{
337    /// The collections of sheets per each origin.
338    collections: PerOrigin<SheetCollection<S>>,
339
340    /// The invalidations for stylesheets added or removed from this document.
341    invalidations: StylesheetInvalidationSet,
342}
343
344/// This macro defines methods common to DocumentStylesheetSet and
345/// AuthorStylesheetSet.
346///
347/// We could simplify the setup moving invalidations to SheetCollection, but
348/// that would imply not sharing invalidations across origins of the same
349/// documents, which is slightly annoying.
350macro_rules! sheet_set_methods {
351    ($set_name:expr) => {
352        fn collect_invalidations_for(
353            &mut self,
354            device: Option<&Device>,
355            custom_media: &CustomMediaMap,
356            sheet: &S,
357            guard: &SharedRwLockReadGuard,
358        ) {
359            if let Some(device) = device {
360                self.invalidations
361                    .collect_invalidations_for(device, custom_media, sheet, guard);
362            }
363        }
364
365        /// Appends a new stylesheet to the current set.
366        ///
367        /// Invalidations for the new sheet are collected when flushing.
368        pub fn append_stylesheet(&mut self, sheet: S, guard: &SharedRwLockReadGuard) {
369            debug!(concat!($set_name, "::append_stylesheet"));
370            let collection = self.collection_for(&sheet, guard);
371            collection.append(sheet);
372        }
373
374        /// Insert a given stylesheet before another stylesheet in the document.
375        ///
376        /// Invalidations for the new sheet are collected when flushing.
377        pub fn insert_stylesheet_before(
378            &mut self,
379            sheet: S,
380            before_sheet: S,
381            guard: &SharedRwLockReadGuard,
382        ) {
383            debug!(concat!($set_name, "::insert_stylesheet_before"));
384            let collection = self.collection_for(&sheet, guard);
385            collection.insert_before(sheet, &before_sheet);
386        }
387
388        /// Remove a given stylesheet from the set.
389        ///
390        /// No device implies not computing invalidations.
391        pub fn remove_stylesheet(
392            &mut self,
393            device: Option<&Device>,
394            custom_media: &CustomMediaMap,
395            sheet: S,
396            guard: &SharedRwLockReadGuard,
397        ) {
398            debug!(concat!($set_name, "::remove_stylesheet"));
399            let collection = self.collection_for(&sheet, guard);
400            // If the sheet never made it to a flush, it never affected the document styles, so
401            // nothing to invalidate.
402            if collection.remove(&sheet) {
403                self.collect_invalidations_for(device, custom_media, &sheet, guard);
404            }
405        }
406
407        /// Notify the set that a rule from a given stylesheet has changed
408        /// somehow.
409        pub fn rule_changed(
410            &mut self,
411            device: Option<&Device>,
412            custom_media: &CustomMediaMap,
413            sheet: &S,
414            rule: &CssRule,
415            guard: &SharedRwLockReadGuard,
416            change_kind: RuleChangeKind,
417            ancestors: &[CssRuleRef],
418        ) {
419            if let Some(device) = device {
420                let quirks_mode = device.quirks_mode();
421                self.invalidations.rule_changed(
422                    sheet,
423                    rule,
424                    guard,
425                    device,
426                    quirks_mode,
427                    custom_media,
428                    change_kind,
429                    ancestors,
430                );
431            }
432
433            let validity = match change_kind {
434                // Insertion / Removals need to rebuild both the cascade and
435                // invalidation data. For generic changes this is conservative,
436                // could be optimized on a per-case basis.
437                RuleChangeKind::Generic | RuleChangeKind::Insertion | RuleChangeKind::Removal => {
438                    DataValidity::FullyInvalid
439                },
440                // TODO(emilio): This, in theory, doesn't need to invalidate
441                // style data, if the rule we're modifying is actually in the
442                // CascadeData already.
443                //
444                // But this is actually a bit tricky to prove, because when we
445                // copy-on-write a stylesheet we don't bother doing a rebuild,
446                // so we may still have rules from the original stylesheet
447                // instead of the cloned one that we're modifying. So don't
448                // bother for now and unconditionally rebuild, it's no worse
449                // than what we were already doing anyway.
450                //
451                // Maybe we could record whether we saw a clone in this flush,
452                // and if so do the conservative thing, otherwise just
453                // early-return.
454                RuleChangeKind::PositionTryDeclarations | RuleChangeKind::StyleRuleDeclarations => {
455                    DataValidity::FullyInvalid
456                },
457            };
458
459            let collection = self.collection_for(&sheet, guard);
460            collection.set_data_validity_at_least(validity);
461        }
462    };
463}
464
465impl<S> Default for DocumentStylesheetSet<S>
466where
467    S: StylesheetInDocument + PartialEq + 'static,
468{
469    fn default() -> Self {
470        Self::new()
471    }
472}
473
474impl<S> DocumentStylesheetSet<S>
475where
476    S: StylesheetInDocument + PartialEq + 'static,
477{
478    /// Create a new empty DocumentStylesheetSet.
479    pub fn new() -> Self {
480        Self {
481            collections: Default::default(),
482            invalidations: StylesheetInvalidationSet::new(),
483        }
484    }
485
486    fn collection_for(
487        &mut self,
488        sheet: &S,
489        guard: &SharedRwLockReadGuard,
490    ) -> &mut SheetCollection<S> {
491        let origin = sheet.contents(guard).origin;
492        self.collections.borrow_mut_for_origin(&origin)
493    }
494
495    sheet_set_methods!("DocumentStylesheetSet");
496
497    /// Returns the number of stylesheets in the set.
498    pub fn len(&self) -> usize {
499        self.collections
500            .iter_origins()
501            .fold(0, |s, (item, _)| s + item.len())
502    }
503
504    /// Returns the count of stylesheets for a given origin.
505    #[inline]
506    pub fn sheet_count(&self, origin: Origin) -> usize {
507        self.collections.borrow_for_origin(&origin).len()
508    }
509
510    /// Returns the `index`th stylesheet in the set for the given origin.
511    #[inline]
512    pub fn get(&self, origin: Origin, index: usize) -> Option<&S> {
513        self.collections.borrow_for_origin(&origin).get(index)
514    }
515
516    /// Returns whether the given set has changed from the last flush.
517    pub fn has_changed(&self) -> bool {
518        !self.invalidations.is_empty()
519            || self
520                .collections
521                .iter_origins()
522                .any(|(collection, _)| collection.dirty)
523    }
524
525    /// Flush the current set, unmarking it as dirty, and returns a `DocumentStylesheetFlusher` in
526    /// order to rebuild the stylist and the invalidation set.
527    ///
528    /// `custom_media_for_origin` is used to compute the invalidations for the sheets added since
529    /// the last flush.
530    pub fn flush<'a>(
531        &mut self,
532        device: &Device,
533        guards: &StylesheetGuards,
534        custom_media_for_origin: impl Fn(Origin) -> &'a CustomMediaMap,
535    ) -> (DocumentStylesheetFlusher<'_, S>, StylesheetInvalidationSet) {
536        debug!("DocumentStylesheetSet::flush");
537        for (collection, origin) in self.collections.iter_origins() {
538            collection.collect_pending_invalidations(
539                &mut self.invalidations,
540                device,
541                custom_media_for_origin(origin),
542                guards.for_origin(origin),
543            );
544        }
545        (
546            DocumentStylesheetFlusher {
547                collections: &mut self.collections,
548            },
549            std::mem::take(&mut self.invalidations),
550        )
551    }
552
553    /// Flush stylesheets, but without running any of the invalidation passes.
554    #[cfg(feature = "servo")]
555    pub fn flush_without_invalidation(&mut self) -> OriginSet {
556        debug!("DocumentStylesheetSet::flush_without_invalidation");
557
558        let mut origins = OriginSet::empty();
559        std::mem::take(&mut self.invalidations);
560
561        for (collection, origin) in self.collections.iter_mut_origins() {
562            if collection.flush().dirty() {
563                origins |= origin;
564            }
565        }
566
567        origins
568    }
569
570    /// Return an iterator over the flattened view of all the stylesheets.
571    pub fn iter(&self) -> impl Iterator<Item = (&S, Origin)> {
572        self.collections
573            .iter_origins()
574            .flat_map(|(c, o)| c.iter().map(move |s| (s, o)))
575    }
576
577    /// Return an iterator over the flattened view of all the stylesheets, mutably.
578    pub fn iter_mut(&mut self) -> impl Iterator<Item = (&mut S, Origin)> {
579        self.collections
580            .iter_mut_origins()
581            .flat_map(|(c, o)| c.iter_mut().map(move |s| (s, o)))
582    }
583
584    /// Mark the stylesheets for the specified origin as dirty, because
585    /// something external may have invalidated it.
586    pub fn force_dirty(&mut self, origins: OriginSet) {
587        self.invalidations.invalidate_fully();
588        for origin in origins.iter_origins() {
589            // We don't know what happened, assume the worse.
590            self.collections
591                .borrow_mut_for_origin(&origin)
592                .set_data_validity_at_least(DataValidity::FullyInvalid);
593        }
594    }
595}
596
597/// The set of stylesheets effective for a given Shadow Root.
598#[derive(MallocSizeOf)]
599pub struct AuthorStylesheetSet<S>
600where
601    S: StylesheetInDocument + PartialEq + 'static,
602{
603    /// The actual style sheets.
604    collection: SheetCollection<S>,
605    /// The set of invalidations scheduled for this collection.
606    invalidations: StylesheetInvalidationSet,
607}
608
609/// A struct to flush an author style sheet collection.
610pub struct AuthorStylesheetFlusher<'a, S>
611where
612    S: StylesheetInDocument + PartialEq + 'static,
613{
614    /// The actual flusher for the collection.
615    pub sheets: SheetCollectionFlusher<'a, S>,
616}
617
618impl<S> Default for AuthorStylesheetSet<S>
619where
620    S: StylesheetInDocument + PartialEq + 'static,
621{
622    fn default() -> Self {
623        Self::new()
624    }
625}
626
627impl<S> AuthorStylesheetSet<S>
628where
629    S: StylesheetInDocument + PartialEq + 'static,
630{
631    /// Create a new empty AuthorStylesheetSet.
632    #[inline]
633    pub fn new() -> Self {
634        Self {
635            collection: Default::default(),
636            invalidations: StylesheetInvalidationSet::new(),
637        }
638    }
639
640    /// Whether anything has changed since the last time this was flushed.
641    pub fn dirty(&self) -> bool {
642        self.collection.dirty
643    }
644
645    /// Whether the collection is empty.
646    pub fn is_empty(&self) -> bool {
647        self.collection.len() == 0
648    }
649
650    /// Returns the `index`th stylesheet in the collection of author styles if present.
651    pub fn get(&self, index: usize) -> Option<&S> {
652        self.collection.get(index)
653    }
654
655    /// Returns the number of author stylesheets.
656    pub fn len(&self) -> usize {
657        self.collection.len()
658    }
659
660    fn collection_for(&mut self, _: &S, _: &SharedRwLockReadGuard) -> &mut SheetCollection<S> {
661        &mut self.collection
662    }
663
664    sheet_set_methods!("AuthorStylesheetSet");
665
666    /// Iterate over the list of stylesheets.
667    pub fn iter(&self) -> impl Iterator<Item = &S> {
668        self.collection.iter()
669    }
670
671    /// Returns a mutable iterator over the current list of stylesheets.
672    pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut S> {
673        self.collection.iter_mut()
674    }
675
676    /// Mark the sheet set dirty, as appropriate.
677    pub fn force_dirty(&mut self) {
678        self.invalidations.invalidate_fully();
679        self.collection
680            .set_data_validity_at_least(DataValidity::FullyInvalid);
681    }
682
683    /// Flush the stylesheets for this author set.
684    ///
685    /// No device implies not computing invalidations.
686    pub fn flush(
687        &mut self,
688        device: Option<&Device>,
689        custom_media: &CustomMediaMap,
690        guard: &SharedRwLockReadGuard,
691    ) -> (AuthorStylesheetFlusher<'_, S>, StylesheetInvalidationSet) {
692        if let Some(device) = device {
693            self.collection.collect_pending_invalidations(
694                &mut self.invalidations,
695                device,
696                custom_media,
697                guard,
698            );
699        }
700        (
701            AuthorStylesheetFlusher {
702                sheets: self.collection.flush(),
703            },
704            std::mem::take(&mut self.invalidations),
705        )
706    }
707}