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