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