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