Source code
Revision control
Copy as Markdown
Other Tools
/* 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
//! 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
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
///
/// 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(usize, &S, SheetRebuildKind) -> bool) {
for (index, potential_sheet) in self.entries.iter_mut().enumerate() {
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(index, &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.
//
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_origins(),
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_origins() {
// 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,
}
}
}