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;
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    fn remove(&mut self, sheet: &S) {
228        let index = self.find_sheet_index(sheet);
229        if cfg!(feature = "gecko") && index.is_none() {
230            // FIXME(emilio): Make Gecko's PresShell::AddUserSheet not suck.
231            return;
232        }
233        let sheet = self.entries.remove(index.unwrap());
234        // Removing sheets makes us tear down the whole cascade and invalidation
235        // data, but only if the sheet has been involved in at least one flush.
236        // Checking whether the sheet has been committed allows us to avoid
237        // rebuilding the world when sites quickly append and remove a
238        // stylesheet.
239        //
240        // See bug 1434756.
241        if sheet.committed {
242            self.set_data_validity_at_least(DataValidity::FullyInvalid);
243        } else {
244            self.dirty = true;
245        }
246    }
247
248    fn contains(&self, sheet: &S) -> bool {
249        self.entries.iter().any(|e| e.sheet == *sheet)
250    }
251
252    /// Appends a given sheet into the collection.
253    fn append(&mut self, sheet: S) {
254        debug_assert!(!self.contains(&sheet));
255        self.entries.push(StylesheetSetEntry::new(sheet));
256        // Appending sheets doesn't alter the validity of the existing data, so
257        // we don't need to change `data_validity` here.
258        //
259        // But we need to be marked as dirty, otherwise we'll never add the new
260        // sheet!
261        self.dirty = true;
262    }
263
264    fn insert_before(&mut self, sheet: S, before_sheet: &S) {
265        debug_assert!(!self.contains(&sheet));
266
267        let index = self
268            .find_sheet_index(before_sheet)
269            .expect("`before_sheet` stylesheet not found");
270
271        // Inserting stylesheets somewhere but at the end changes the validity
272        // of the cascade data, but not the invalidation data.
273        self.set_data_validity_at_least(DataValidity::CascadeInvalid);
274        self.entries.insert(index, StylesheetSetEntry::new(sheet));
275    }
276
277    fn set_data_validity_at_least(&mut self, validity: DataValidity) {
278        use std::cmp;
279
280        debug_assert_ne!(validity, DataValidity::Valid);
281
282        self.dirty = true;
283        self.data_validity = cmp::max(validity, self.data_validity);
284    }
285
286    /// Returns an iterator over the current list of stylesheets.
287    fn iter(&self) -> impl Iterator<Item = &S> {
288        self.entries.iter().map(|e| &e.sheet)
289    }
290
291    /// Returns a mutable iterator over the current list of stylesheets.
292    fn iter_mut(&mut self) -> impl Iterator<Item = &mut S> {
293        self.entries.iter_mut().map(|e| &mut e.sheet)
294    }
295
296    fn flush(&mut self) -> SheetCollectionFlusher<'_, S> {
297        let dirty = mem::replace(&mut self.dirty, false);
298        let validity = mem::replace(&mut self.data_validity, DataValidity::Valid);
299
300        SheetCollectionFlusher {
301            entries: &mut self.entries,
302            dirty,
303            validity,
304        }
305    }
306}
307
308/// The set of stylesheets effective for a given document.
309#[derive(MallocSizeOf)]
310pub struct DocumentStylesheetSet<S>
311where
312    S: StylesheetInDocument + PartialEq + 'static,
313{
314    /// The collections of sheets per each origin.
315    collections: PerOrigin<SheetCollection<S>>,
316
317    /// The invalidations for stylesheets added or removed from this document.
318    invalidations: StylesheetInvalidationSet,
319}
320
321/// This macro defines methods common to DocumentStylesheetSet and
322/// AuthorStylesheetSet.
323///
324/// We could simplify the setup moving invalidations to SheetCollection, but
325/// that would imply not sharing invalidations across origins of the same
326/// documents, which is slightly annoying.
327macro_rules! sheet_set_methods {
328    ($set_name:expr) => {
329        fn collect_invalidations_for(
330            &mut self,
331            device: Option<&Device>,
332            custom_media: &CustomMediaMap,
333            sheet: &S,
334            guard: &SharedRwLockReadGuard,
335        ) {
336            if let Some(device) = device {
337                self.invalidations
338                    .collect_invalidations_for(device, custom_media, sheet, guard);
339            }
340        }
341
342        /// Appends a new stylesheet to the current set.
343        ///
344        /// No device implies not computing invalidations.
345        pub fn append_stylesheet(
346            &mut self,
347            device: Option<&Device>,
348            custom_media: &CustomMediaMap,
349            sheet: S,
350            guard: &SharedRwLockReadGuard,
351        ) {
352            debug!(concat!($set_name, "::append_stylesheet"));
353            self.collect_invalidations_for(device, custom_media, &sheet, guard);
354            let collection = self.collection_for(&sheet, guard);
355            collection.append(sheet);
356        }
357
358        /// Insert a given stylesheet before another stylesheet in the document.
359        pub fn insert_stylesheet_before(
360            &mut self,
361            device: Option<&Device>,
362            custom_media: &CustomMediaMap,
363            sheet: S,
364            before_sheet: S,
365            guard: &SharedRwLockReadGuard,
366        ) {
367            debug!(concat!($set_name, "::insert_stylesheet_before"));
368            self.collect_invalidations_for(device, custom_media, &sheet, guard);
369
370            let collection = self.collection_for(&sheet, guard);
371            collection.insert_before(sheet, &before_sheet);
372        }
373
374        /// Remove a given stylesheet from the set.
375        pub fn remove_stylesheet(
376            &mut self,
377            device: Option<&Device>,
378            custom_media: &CustomMediaMap,
379            sheet: S,
380            guard: &SharedRwLockReadGuard,
381        ) {
382            debug!(concat!($set_name, "::remove_stylesheet"));
383            self.collect_invalidations_for(device, custom_media, &sheet, guard);
384
385            let collection = self.collection_for(&sheet, guard);
386            collection.remove(&sheet)
387        }
388
389        /// Notify the set that a rule from a given stylesheet has changed
390        /// somehow.
391        pub fn rule_changed(
392            &mut self,
393            device: Option<&Device>,
394            custom_media: &CustomMediaMap,
395            sheet: &S,
396            rule: &CssRule,
397            guard: &SharedRwLockReadGuard,
398            change_kind: RuleChangeKind,
399            ancestors: &[CssRuleRef],
400        ) {
401            if let Some(device) = device {
402                let quirks_mode = device.quirks_mode();
403                self.invalidations.rule_changed(
404                    sheet,
405                    rule,
406                    guard,
407                    device,
408                    quirks_mode,
409                    custom_media,
410                    change_kind,
411                    ancestors,
412                );
413            }
414
415            let validity = match change_kind {
416                // Insertion / Removals need to rebuild both the cascade and
417                // invalidation data. For generic changes this is conservative,
418                // could be optimized on a per-case basis.
419                RuleChangeKind::Generic | RuleChangeKind::Insertion | RuleChangeKind::Removal => {
420                    DataValidity::FullyInvalid
421                },
422                // TODO(emilio): This, in theory, doesn't need to invalidate
423                // style data, if the rule we're modifying is actually in the
424                // CascadeData already.
425                //
426                // But this is actually a bit tricky to prove, because when we
427                // copy-on-write a stylesheet we don't bother doing a rebuild,
428                // so we may still have rules from the original stylesheet
429                // instead of the cloned one that we're modifying. So don't
430                // bother for now and unconditionally rebuild, it's no worse
431                // than what we were already doing anyway.
432                //
433                // Maybe we could record whether we saw a clone in this flush,
434                // and if so do the conservative thing, otherwise just
435                // early-return.
436                RuleChangeKind::PositionTryDeclarations | RuleChangeKind::StyleRuleDeclarations => {
437                    DataValidity::FullyInvalid
438                },
439            };
440
441            let collection = self.collection_for(&sheet, guard);
442            collection.set_data_validity_at_least(validity);
443        }
444    };
445}
446
447impl<S> Default for DocumentStylesheetSet<S>
448where
449    S: StylesheetInDocument + PartialEq + 'static,
450{
451    fn default() -> Self {
452        Self::new()
453    }
454}
455
456impl<S> DocumentStylesheetSet<S>
457where
458    S: StylesheetInDocument + PartialEq + 'static,
459{
460    /// Create a new empty DocumentStylesheetSet.
461    pub fn new() -> Self {
462        Self {
463            collections: Default::default(),
464            invalidations: StylesheetInvalidationSet::new(),
465        }
466    }
467
468    fn collection_for(
469        &mut self,
470        sheet: &S,
471        guard: &SharedRwLockReadGuard,
472    ) -> &mut SheetCollection<S> {
473        let origin = sheet.contents(guard).origin;
474        self.collections.borrow_mut_for_origin(&origin)
475    }
476
477    sheet_set_methods!("DocumentStylesheetSet");
478
479    /// Returns the number of stylesheets in the set.
480    pub fn len(&self) -> usize {
481        self.collections
482            .iter_origins()
483            .fold(0, |s, (item, _)| s + item.len())
484    }
485
486    /// Returns the count of stylesheets for a given origin.
487    #[inline]
488    pub fn sheet_count(&self, origin: Origin) -> usize {
489        self.collections.borrow_for_origin(&origin).len()
490    }
491
492    /// Returns the `index`th stylesheet in the set for the given origin.
493    #[inline]
494    pub fn get(&self, origin: Origin, index: usize) -> Option<&S> {
495        self.collections.borrow_for_origin(&origin).get(index)
496    }
497
498    /// Returns whether the given set has changed from the last flush.
499    pub fn has_changed(&self) -> bool {
500        !self.invalidations.is_empty()
501            || self
502                .collections
503                .iter_origins()
504                .any(|(collection, _)| collection.dirty)
505    }
506
507    /// Flush the current set, unmarking it as dirty, and returns a `DocumentStylesheetFlusher` in
508    /// order to rebuild the stylist and the invalidation set.
509    pub fn flush(&mut self) -> (DocumentStylesheetFlusher<'_, S>, StylesheetInvalidationSet) {
510        debug!("DocumentStylesheetSet::flush");
511        (
512            DocumentStylesheetFlusher {
513                collections: &mut self.collections,
514            },
515            std::mem::take(&mut self.invalidations),
516        )
517    }
518
519    /// Flush stylesheets, but without running any of the invalidation passes.
520    #[cfg(feature = "servo")]
521    pub fn flush_without_invalidation(&mut self) -> OriginSet {
522        debug!("DocumentStylesheetSet::flush_without_invalidation");
523
524        let mut origins = OriginSet::empty();
525        std::mem::take(&mut self.invalidations);
526
527        for (collection, origin) in self.collections.iter_mut_origins() {
528            if collection.flush().dirty() {
529                origins |= origin;
530            }
531        }
532
533        origins
534    }
535
536    /// Return an iterator over the flattened view of all the stylesheets.
537    pub fn iter(&self) -> impl Iterator<Item = (&S, Origin)> {
538        self.collections
539            .iter_origins()
540            .flat_map(|(c, o)| c.iter().map(move |s| (s, o)))
541    }
542
543    /// Return an iterator over the flattened view of all the stylesheets, mutably.
544    pub fn iter_mut(&mut self) -> impl Iterator<Item = (&mut S, Origin)> {
545        self.collections
546            .iter_mut_origins()
547            .flat_map(|(c, o)| c.iter_mut().map(move |s| (s, o)))
548    }
549
550    /// Mark the stylesheets for the specified origin as dirty, because
551    /// something external may have invalidated it.
552    pub fn force_dirty(&mut self, origins: OriginSet) {
553        self.invalidations.invalidate_fully();
554        for origin in origins.iter_origins() {
555            // We don't know what happened, assume the worse.
556            self.collections
557                .borrow_mut_for_origin(&origin)
558                .set_data_validity_at_least(DataValidity::FullyInvalid);
559        }
560    }
561}
562
563/// The set of stylesheets effective for a given Shadow Root.
564#[derive(MallocSizeOf)]
565pub struct AuthorStylesheetSet<S>
566where
567    S: StylesheetInDocument + PartialEq + 'static,
568{
569    /// The actual style sheets.
570    collection: SheetCollection<S>,
571    /// The set of invalidations scheduled for this collection.
572    invalidations: StylesheetInvalidationSet,
573}
574
575/// A struct to flush an author style sheet collection.
576pub struct AuthorStylesheetFlusher<'a, S>
577where
578    S: StylesheetInDocument + PartialEq + 'static,
579{
580    /// The actual flusher for the collection.
581    pub sheets: SheetCollectionFlusher<'a, S>,
582}
583
584impl<S> Default for AuthorStylesheetSet<S>
585where
586    S: StylesheetInDocument + PartialEq + 'static,
587{
588    fn default() -> Self {
589        Self::new()
590    }
591}
592
593impl<S> AuthorStylesheetSet<S>
594where
595    S: StylesheetInDocument + PartialEq + 'static,
596{
597    /// Create a new empty AuthorStylesheetSet.
598    #[inline]
599    pub fn new() -> Self {
600        Self {
601            collection: Default::default(),
602            invalidations: StylesheetInvalidationSet::new(),
603        }
604    }
605
606    /// Whether anything has changed since the last time this was flushed.
607    pub fn dirty(&self) -> bool {
608        self.collection.dirty
609    }
610
611    /// Whether the collection is empty.
612    pub fn is_empty(&self) -> bool {
613        self.collection.len() == 0
614    }
615
616    /// Returns the `index`th stylesheet in the collection of author styles if present.
617    pub fn get(&self, index: usize) -> Option<&S> {
618        self.collection.get(index)
619    }
620
621    /// Returns the number of author stylesheets.
622    pub fn len(&self) -> usize {
623        self.collection.len()
624    }
625
626    fn collection_for(&mut self, _: &S, _: &SharedRwLockReadGuard) -> &mut SheetCollection<S> {
627        &mut self.collection
628    }
629
630    sheet_set_methods!("AuthorStylesheetSet");
631
632    /// Iterate over the list of stylesheets.
633    pub fn iter(&self) -> impl Iterator<Item = &S> {
634        self.collection.iter()
635    }
636
637    /// Returns a mutable iterator over the current list of stylesheets.
638    pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut S> {
639        self.collection.iter_mut()
640    }
641
642    /// Mark the sheet set dirty, as appropriate.
643    pub fn force_dirty(&mut self) {
644        self.invalidations.invalidate_fully();
645        self.collection
646            .set_data_validity_at_least(DataValidity::FullyInvalid);
647    }
648
649    /// Flush the stylesheets for this author set.
650    pub fn flush(&mut self) -> (AuthorStylesheetFlusher<'_, S>, StylesheetInvalidationSet) {
651        (
652            AuthorStylesheetFlusher {
653                sheets: self.collection.flush(),
654            },
655            std::mem::take(&mut self.invalidations),
656        )
657    }
658}