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