summaryrefslogtreecommitdiffstats
path: root/servo/components/style/stylesheet_set.rs
diff options
context:
space:
mode:
Diffstat (limited to 'servo/components/style/stylesheet_set.rs')
-rw-r--r--servo/components/style/stylesheet_set.rs705
1 files changed, 705 insertions, 0 deletions
diff --git a/servo/components/style/stylesheet_set.rs b/servo/components/style/stylesheet_set.rs
new file mode 100644
index 0000000000..e2937e06e7
--- /dev/null
+++ b/servo/components/style/stylesheet_set.rs
@@ -0,0 +1,705 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+//! A centralized set of stylesheets for a document.
+
+use crate::dom::TElement;
+use crate::invalidation::stylesheets::{RuleChangeKind, StylesheetInvalidationSet};
+use crate::media_queries::Device;
+use crate::selector_parser::SnapshotMap;
+use crate::shared_lock::SharedRwLockReadGuard;
+use crate::stylesheets::{
+ CssRule, Origin, OriginSet, OriginSetIterator, PerOrigin, StylesheetInDocument,
+};
+use std::{mem, slice};
+
+/// Entry for a StylesheetSet.
+#[derive(MallocSizeOf)]
+struct StylesheetSetEntry<S>
+where
+ S: StylesheetInDocument + PartialEq + 'static,
+{
+ /// The sheet.
+ sheet: S,
+
+ /// Whether this sheet has been part of at least one flush.
+ committed: bool,
+}
+
+impl<S> StylesheetSetEntry<S>
+where
+ S: StylesheetInDocument + PartialEq + 'static,
+{
+ fn new(sheet: S) -> Self {
+ Self {
+ sheet,
+ committed: false,
+ }
+ }
+}
+
+/// A iterator over the stylesheets of a list of entries in the StylesheetSet.
+pub struct StylesheetCollectionIterator<'a, S>(slice::Iter<'a, StylesheetSetEntry<S>>)
+where
+ S: StylesheetInDocument + PartialEq + 'static;
+
+impl<'a, S> Clone for StylesheetCollectionIterator<'a, S>
+where
+ S: StylesheetInDocument + PartialEq + 'static,
+{
+ fn clone(&self) -> Self {
+ StylesheetCollectionIterator(self.0.clone())
+ }
+}
+
+impl<'a, S> Iterator for StylesheetCollectionIterator<'a, S>
+where
+ S: StylesheetInDocument + PartialEq + 'static,
+{
+ type Item = &'a S;
+
+ fn next(&mut self) -> Option<Self::Item> {
+ self.0.next().map(|entry| &entry.sheet)
+ }
+
+ fn size_hint(&self) -> (usize, Option<usize>) {
+ self.0.size_hint()
+ }
+}
+
+/// An iterator over the flattened view of the stylesheet collections.
+#[derive(Clone)]
+pub struct StylesheetIterator<'a, S>
+where
+ S: StylesheetInDocument + PartialEq + 'static,
+{
+ origins: OriginSetIterator,
+ collections: &'a PerOrigin<SheetCollection<S>>,
+ current: Option<(Origin, StylesheetCollectionIterator<'a, S>)>,
+}
+
+impl<'a, S> Iterator for StylesheetIterator<'a, S>
+where
+ S: StylesheetInDocument + PartialEq + 'static,
+{
+ type Item = (&'a S, Origin);
+
+ fn next(&mut self) -> Option<Self::Item> {
+ loop {
+ if self.current.is_none() {
+ let next_origin = self.origins.next()?;
+
+ self.current = Some((
+ next_origin,
+ self.collections.borrow_for_origin(&next_origin).iter(),
+ ));
+ }
+
+ {
+ let (origin, ref mut iter) = *self.current.as_mut().unwrap();
+ if let Some(s) = iter.next() {
+ return Some((s, origin));
+ }
+ }
+
+ self.current = None;
+ }
+ }
+}
+
+/// The validity of the data in a given cascade origin.
+#[derive(Clone, Copy, Debug, Eq, MallocSizeOf, Ord, PartialEq, PartialOrd)]
+pub enum DataValidity {
+ /// The origin is clean, all the data already there is valid, though we may
+ /// have new sheets at the end.
+ Valid = 0,
+
+ /// The cascade data is invalid, but not the invalidation data (which is
+ /// order-independent), and thus only the cascade data should be inserted.
+ CascadeInvalid = 1,
+
+ /// Everything needs to be rebuilt.
+ FullyInvalid = 2,
+}
+
+impl Default for DataValidity {
+ fn default() -> Self {
+ DataValidity::Valid
+ }
+}
+
+/// A struct to iterate over the different stylesheets to be flushed.
+pub struct DocumentStylesheetFlusher<'a, S>
+where
+ S: StylesheetInDocument + PartialEq + 'static,
+{
+ collections: &'a mut PerOrigin<SheetCollection<S>>,
+ had_invalidations: bool,
+}
+
+/// The type of rebuild that we need to do for a given stylesheet.
+#[derive(Clone, Copy, Debug)]
+pub enum SheetRebuildKind {
+ /// A full rebuild, of both cascade data and invalidation data.
+ Full,
+ /// A partial rebuild, of only the cascade data.
+ CascadeOnly,
+}
+
+impl SheetRebuildKind {
+ /// Whether the stylesheet invalidation data should be rebuilt.
+ pub fn should_rebuild_invalidation(&self) -> bool {
+ matches!(*self, SheetRebuildKind::Full)
+ }
+}
+
+impl<'a, S> DocumentStylesheetFlusher<'a, S>
+where
+ S: StylesheetInDocument + PartialEq + 'static,
+{
+ /// Returns a flusher for `origin`.
+ pub fn flush_origin(&mut self, origin: Origin) -> SheetCollectionFlusher<S> {
+ self.collections.borrow_mut_for_origin(&origin).flush()
+ }
+
+ /// Returns the list of stylesheets for `origin`.
+ ///
+ /// Only used for UA sheets.
+ pub fn origin_sheets(&mut self, origin: Origin) -> StylesheetCollectionIterator<S> {
+ self.collections.borrow_mut_for_origin(&origin).iter()
+ }
+
+ /// Returns whether any DOM invalidations were processed as a result of the
+ /// stylesheet flush.
+ #[inline]
+ pub fn had_invalidations(&self) -> bool {
+ self.had_invalidations
+ }
+}
+
+/// A flusher struct for a given collection, that takes care of returning the
+/// appropriate stylesheets that need work.
+pub struct SheetCollectionFlusher<'a, S>
+where
+ S: StylesheetInDocument + PartialEq + 'static,
+{
+ // TODO: This can be made an iterator again once
+ // https://github.com/rust-lang/rust/pull/82771 lands on stable.
+ entries: &'a mut [StylesheetSetEntry<S>],
+ validity: DataValidity,
+ dirty: bool,
+}
+
+impl<'a, S> SheetCollectionFlusher<'a, S>
+where
+ S: StylesheetInDocument + PartialEq + 'static,
+{
+ /// Whether the collection was originally dirty.
+ #[inline]
+ pub fn dirty(&self) -> bool {
+ self.dirty
+ }
+
+ /// What the state of the sheet data is.
+ #[inline]
+ pub fn data_validity(&self) -> DataValidity {
+ self.validity
+ }
+
+ /// Returns an iterator over the remaining list of sheets to consume.
+ pub fn sheets<'b>(&'b self) -> impl Iterator<Item = &'b S> {
+ self.entries.iter().map(|entry| &entry.sheet)
+ }
+}
+
+impl<'a, S> SheetCollectionFlusher<'a, S>
+where
+ S: StylesheetInDocument + PartialEq + 'static,
+{
+ /// Iterates over all sheets and values that we have to invalidate.
+ ///
+ /// TODO(emilio): This would be nicer as an iterator but we can't do that
+ /// until https://github.com/rust-lang/rust/pull/82771 stabilizes.
+ ///
+ /// Since we don't have a good use-case for partial iteration, this does the
+ /// trick for now.
+ pub fn each(self, mut callback: impl FnMut(&S, SheetRebuildKind) -> bool) {
+ for potential_sheet in self.entries.iter_mut() {
+ let committed = mem::replace(&mut potential_sheet.committed, true);
+ let rebuild_kind = if !committed {
+ // If the sheet was uncommitted, we need to do a full rebuild
+ // anyway.
+ SheetRebuildKind::Full
+ } else {
+ match self.validity {
+ DataValidity::Valid => continue,
+ DataValidity::CascadeInvalid => SheetRebuildKind::CascadeOnly,
+ DataValidity::FullyInvalid => SheetRebuildKind::Full,
+ }
+ };
+
+ if !callback(&potential_sheet.sheet, rebuild_kind) {
+ return;
+ }
+ }
+ }
+}
+
+#[derive(MallocSizeOf)]
+struct SheetCollection<S>
+where
+ S: StylesheetInDocument + PartialEq + 'static,
+{
+ /// The actual list of stylesheets.
+ ///
+ /// This is only a list of top-level stylesheets, and as such it doesn't
+ /// include recursive `@import` rules.
+ entries: Vec<StylesheetSetEntry<S>>,
+
+ /// The validity of the data that was already there for a given origin.
+ ///
+ /// Note that an origin may appear on `origins_dirty`, but still have
+ /// `DataValidity::Valid`, if only sheets have been appended into it (in
+ /// which case the existing data is valid, but the origin needs to be
+ /// rebuilt).
+ data_validity: DataValidity,
+
+ /// Whether anything in the collection has changed. Note that this is
+ /// different from `data_validity`, in the sense that after a sheet append,
+ /// the data validity is still `Valid`, but we need to be marked as dirty.
+ dirty: bool,
+}
+
+impl<S> Default for SheetCollection<S>
+where
+ S: StylesheetInDocument + PartialEq + 'static,
+{
+ fn default() -> Self {
+ Self {
+ entries: vec![],
+ data_validity: DataValidity::Valid,
+ dirty: false,
+ }
+ }
+}
+
+impl<S> SheetCollection<S>
+where
+ S: StylesheetInDocument + PartialEq + 'static,
+{
+ /// Returns the number of stylesheets in the set.
+ fn len(&self) -> usize {
+ self.entries.len()
+ }
+
+ /// Returns the `index`th stylesheet in the set if present.
+ fn get(&self, index: usize) -> Option<&S> {
+ self.entries.get(index).map(|e| &e.sheet)
+ }
+
+ fn remove(&mut self, sheet: &S) {
+ let index = self.entries.iter().position(|entry| entry.sheet == *sheet);
+ if cfg!(feature = "gecko") && index.is_none() {
+ // FIXME(emilio): Make Gecko's PresShell::AddUserSheet not suck.
+ return;
+ }
+ let sheet = self.entries.remove(index.unwrap());
+ // Removing sheets makes us tear down the whole cascade and invalidation
+ // data, but only if the sheet has been involved in at least one flush.
+ // Checking whether the sheet has been committed allows us to avoid
+ // rebuilding the world when sites quickly append and remove a
+ // stylesheet.
+ //
+ // See bug 1434756.
+ if sheet.committed {
+ self.set_data_validity_at_least(DataValidity::FullyInvalid);
+ } else {
+ self.dirty = true;
+ }
+ }
+
+ fn contains(&self, sheet: &S) -> bool {
+ self.entries.iter().any(|e| e.sheet == *sheet)
+ }
+
+ /// Appends a given sheet into the collection.
+ fn append(&mut self, sheet: S) {
+ debug_assert!(!self.contains(&sheet));
+ self.entries.push(StylesheetSetEntry::new(sheet));
+ // Appending sheets doesn't alter the validity of the existing data, so
+ // we don't need to change `data_validity` here.
+ //
+ // But we need to be marked as dirty, otherwise we'll never add the new
+ // sheet!
+ self.dirty = true;
+ }
+
+ fn insert_before(&mut self, sheet: S, before_sheet: &S) {
+ debug_assert!(!self.contains(&sheet));
+
+ let index = self
+ .entries
+ .iter()
+ .position(|entry| entry.sheet == *before_sheet)
+ .expect("`before_sheet` stylesheet not found");
+
+ // Inserting stylesheets somewhere but at the end changes the validity
+ // of the cascade data, but not the invalidation data.
+ self.set_data_validity_at_least(DataValidity::CascadeInvalid);
+ self.entries.insert(index, StylesheetSetEntry::new(sheet));
+ }
+
+ fn set_data_validity_at_least(&mut self, validity: DataValidity) {
+ use std::cmp;
+
+ debug_assert_ne!(validity, DataValidity::Valid);
+
+ self.dirty = true;
+ self.data_validity = cmp::max(validity, self.data_validity);
+ }
+
+ /// Returns an iterator over the current list of stylesheets.
+ fn iter(&self) -> StylesheetCollectionIterator<S> {
+ StylesheetCollectionIterator(self.entries.iter())
+ }
+
+ fn flush(&mut self) -> SheetCollectionFlusher<S> {
+ let dirty = mem::replace(&mut self.dirty, false);
+ let validity = mem::replace(&mut self.data_validity, DataValidity::Valid);
+
+ SheetCollectionFlusher {
+ entries: &mut self.entries,
+ dirty,
+ validity,
+ }
+ }
+}
+
+/// The set of stylesheets effective for a given document.
+#[cfg_attr(feature = "servo", derive(MallocSizeOf))]
+pub struct DocumentStylesheetSet<S>
+where
+ S: StylesheetInDocument + PartialEq + 'static,
+{
+ /// The collections of sheets per each origin.
+ collections: PerOrigin<SheetCollection<S>>,
+
+ /// The invalidations for stylesheets added or removed from this document.
+ invalidations: StylesheetInvalidationSet,
+}
+
+/// This macro defines methods common to DocumentStylesheetSet and
+/// AuthorStylesheetSet.
+///
+/// We could simplify the setup moving invalidations to SheetCollection, but
+/// that would imply not sharing invalidations across origins of the same
+/// documents, which is slightly annoying.
+macro_rules! sheet_set_methods {
+ ($set_name:expr) => {
+ fn collect_invalidations_for(
+ &mut self,
+ device: Option<&Device>,
+ sheet: &S,
+ guard: &SharedRwLockReadGuard,
+ ) {
+ if let Some(device) = device {
+ self.invalidations
+ .collect_invalidations_for(device, sheet, guard);
+ }
+ }
+
+ /// Appends a new stylesheet to the current set.
+ ///
+ /// No device implies not computing invalidations.
+ pub fn append_stylesheet(
+ &mut self,
+ device: Option<&Device>,
+ sheet: S,
+ guard: &SharedRwLockReadGuard,
+ ) {
+ debug!(concat!($set_name, "::append_stylesheet"));
+ self.collect_invalidations_for(device, &sheet, guard);
+ let collection = self.collection_for(&sheet);
+ collection.append(sheet);
+ }
+
+ /// Insert a given stylesheet before another stylesheet in the document.
+ pub fn insert_stylesheet_before(
+ &mut self,
+ device: Option<&Device>,
+ sheet: S,
+ before_sheet: S,
+ guard: &SharedRwLockReadGuard,
+ ) {
+ debug!(concat!($set_name, "::insert_stylesheet_before"));
+ self.collect_invalidations_for(device, &sheet, guard);
+
+ let collection = self.collection_for(&sheet);
+ collection.insert_before(sheet, &before_sheet);
+ }
+
+ /// Remove a given stylesheet from the set.
+ pub fn remove_stylesheet(
+ &mut self,
+ device: Option<&Device>,
+ sheet: S,
+ guard: &SharedRwLockReadGuard,
+ ) {
+ debug!(concat!($set_name, "::remove_stylesheet"));
+ self.collect_invalidations_for(device, &sheet, guard);
+
+ let collection = self.collection_for(&sheet);
+ collection.remove(&sheet)
+ }
+
+ /// Notify the set that a rule from a given stylesheet has changed
+ /// somehow.
+ pub fn rule_changed(
+ &mut self,
+ device: Option<&Device>,
+ sheet: &S,
+ rule: &CssRule,
+ guard: &SharedRwLockReadGuard,
+ change_kind: RuleChangeKind,
+ ) {
+ if let Some(device) = device {
+ let quirks_mode = device.quirks_mode();
+ self.invalidations.rule_changed(
+ sheet,
+ rule,
+ guard,
+ device,
+ quirks_mode,
+ change_kind,
+ );
+ }
+
+ let validity = match change_kind {
+ // Insertion / Removals need to rebuild both the cascade and
+ // invalidation data. For generic changes this is conservative,
+ // could be optimized on a per-case basis.
+ RuleChangeKind::Generic | RuleChangeKind::Insertion | RuleChangeKind::Removal => {
+ DataValidity::FullyInvalid
+ },
+ // TODO(emilio): This, in theory, doesn't need to invalidate
+ // style data, if the rule we're modifying is actually in the
+ // CascadeData already.
+ //
+ // But this is actually a bit tricky to prove, because when we
+ // copy-on-write a stylesheet we don't bother doing a rebuild,
+ // so we may still have rules from the original stylesheet
+ // instead of the cloned one that we're modifying. So don't
+ // bother for now and unconditionally rebuild, it's no worse
+ // than what we were already doing anyway.
+ //
+ // Maybe we could record whether we saw a clone in this flush,
+ // and if so do the conservative thing, otherwise just
+ // early-return.
+ RuleChangeKind::StyleRuleDeclarations => DataValidity::FullyInvalid,
+ };
+
+ let collection = self.collection_for(&sheet);
+ collection.set_data_validity_at_least(validity);
+ }
+ };
+}
+
+impl<S> DocumentStylesheetSet<S>
+where
+ S: StylesheetInDocument + PartialEq + 'static,
+{
+ /// Create a new empty DocumentStylesheetSet.
+ pub fn new() -> Self {
+ Self {
+ collections: Default::default(),
+ invalidations: StylesheetInvalidationSet::new(),
+ }
+ }
+
+ fn collection_for(&mut self, sheet: &S) -> &mut SheetCollection<S> {
+ let origin = sheet.contents().origin;
+ self.collections.borrow_mut_for_origin(&origin)
+ }
+
+ sheet_set_methods!("DocumentStylesheetSet");
+
+ /// Returns the number of stylesheets in the set.
+ pub fn len(&self) -> usize {
+ self.collections
+ .iter_origins()
+ .fold(0, |s, (item, _)| s + item.len())
+ }
+
+ /// Returns the count of stylesheets for a given origin.
+ #[inline]
+ pub fn sheet_count(&self, origin: Origin) -> usize {
+ self.collections.borrow_for_origin(&origin).len()
+ }
+
+ /// Returns the `index`th stylesheet in the set for the given origin.
+ #[inline]
+ pub fn get(&self, origin: Origin, index: usize) -> Option<&S> {
+ self.collections.borrow_for_origin(&origin).get(index)
+ }
+
+ /// Returns whether the given set has changed from the last flush.
+ pub fn has_changed(&self) -> bool {
+ !self.invalidations.is_empty() ||
+ self.collections
+ .iter_origins()
+ .any(|(collection, _)| collection.dirty)
+ }
+
+ /// Flush the current set, unmarking it as dirty, and returns a
+ /// `DocumentStylesheetFlusher` in order to rebuild the stylist.
+ pub fn flush<E>(
+ &mut self,
+ document_element: Option<E>,
+ snapshots: Option<&SnapshotMap>,
+ ) -> DocumentStylesheetFlusher<S>
+ where
+ E: TElement,
+ {
+ debug!("DocumentStylesheetSet::flush");
+
+ let had_invalidations = self.invalidations.flush(document_element, snapshots);
+
+ DocumentStylesheetFlusher {
+ collections: &mut self.collections,
+ had_invalidations,
+ }
+ }
+
+ /// Flush stylesheets, but without running any of the invalidation passes.
+ #[cfg(feature = "servo")]
+ pub fn flush_without_invalidation(&mut self) -> OriginSet {
+ debug!("DocumentStylesheetSet::flush_without_invalidation");
+
+ let mut origins = OriginSet::empty();
+ self.invalidations.clear();
+
+ for (collection, origin) in self.collections.iter_mut_origins() {
+ if collection.flush().dirty() {
+ origins |= origin;
+ }
+ }
+
+ origins
+ }
+
+ /// Return an iterator over the flattened view of all the stylesheets.
+ pub fn iter(&self) -> StylesheetIterator<S> {
+ StylesheetIterator {
+ origins: OriginSet::all().iter(),
+ collections: &self.collections,
+ current: None,
+ }
+ }
+
+ /// Mark the stylesheets for the specified origin as dirty, because
+ /// something external may have invalidated it.
+ pub fn force_dirty(&mut self, origins: OriginSet) {
+ self.invalidations.invalidate_fully();
+ for origin in origins.iter() {
+ // We don't know what happened, assume the worse.
+ self.collections
+ .borrow_mut_for_origin(&origin)
+ .set_data_validity_at_least(DataValidity::FullyInvalid);
+ }
+ }
+}
+
+/// The set of stylesheets effective for a given Shadow Root.
+#[derive(MallocSizeOf)]
+pub struct AuthorStylesheetSet<S>
+where
+ S: StylesheetInDocument + PartialEq + 'static,
+{
+ /// The actual style sheets.
+ collection: SheetCollection<S>,
+ /// The set of invalidations scheduled for this collection.
+ invalidations: StylesheetInvalidationSet,
+}
+
+/// A struct to flush an author style sheet collection.
+pub struct AuthorStylesheetFlusher<'a, S>
+where
+ S: StylesheetInDocument + PartialEq + 'static,
+{
+ /// The actual flusher for the collection.
+ pub sheets: SheetCollectionFlusher<'a, S>,
+ /// Whether any sheet invalidation matched.
+ pub had_invalidations: bool,
+}
+
+impl<S> AuthorStylesheetSet<S>
+where
+ S: StylesheetInDocument + PartialEq + 'static,
+{
+ /// Create a new empty AuthorStylesheetSet.
+ #[inline]
+ pub fn new() -> Self {
+ Self {
+ collection: Default::default(),
+ invalidations: StylesheetInvalidationSet::new(),
+ }
+ }
+
+ /// Whether anything has changed since the last time this was flushed.
+ pub fn dirty(&self) -> bool {
+ self.collection.dirty
+ }
+
+ /// Whether the collection is empty.
+ pub fn is_empty(&self) -> bool {
+ self.collection.len() == 0
+ }
+
+ /// Returns the `index`th stylesheet in the collection of author styles if present.
+ pub fn get(&self, index: usize) -> Option<&S> {
+ self.collection.get(index)
+ }
+
+ /// Returns the number of author stylesheets.
+ pub fn len(&self) -> usize {
+ self.collection.len()
+ }
+
+ fn collection_for(&mut self, _sheet: &S) -> &mut SheetCollection<S> {
+ &mut self.collection
+ }
+
+ sheet_set_methods!("AuthorStylesheetSet");
+
+ /// Iterate over the list of stylesheets.
+ pub fn iter(&self) -> StylesheetCollectionIterator<S> {
+ self.collection.iter()
+ }
+
+ /// Mark the sheet set dirty, as appropriate.
+ pub fn force_dirty(&mut self) {
+ self.invalidations.invalidate_fully();
+ self.collection
+ .set_data_validity_at_least(DataValidity::FullyInvalid);
+ }
+
+ /// Flush the stylesheets for this author set.
+ ///
+ /// `host` is the root of the affected subtree, like the shadow host, for
+ /// example.
+ pub fn flush<E>(
+ &mut self,
+ host: Option<E>,
+ snapshots: Option<&SnapshotMap>,
+ ) -> AuthorStylesheetFlusher<S>
+ where
+ E: TElement,
+ {
+ let had_invalidations = self.invalidations.flush(host, snapshots);
+ AuthorStylesheetFlusher {
+ sheets: self.collection.flush(),
+ had_invalidations,
+ }
+ }
+}