fix(style): retain media sheets in tree scopes

This commit is contained in:
ldm0
2026-08-24 04:11:54 +08:00
parent a9fd8775de
commit 87f07d70ca
12 changed files with 562 additions and 96 deletions
@@ -3,7 +3,6 @@ use std::rc::Rc;
use moli_web_mime::is_stylesheet_type_attribute;
use crate::{
context_bootstrap::evaluate_match_media_query_list_with_viewport,
document_runtime::DomHandle,
dom::native::{DomHost, Node},
style_engine::{
@@ -304,7 +303,6 @@ pub(super) fn active_stylesheet_handles(
&& is_stylesheet_type_attribute(
runtime.dom_host().get_attribute(*handle, "type").as_deref(),
)
&& stylesheet_media_matches(runtime, *handle)
})
.collect::<Vec<_>>();
let preferred_title = handles
@@ -337,8 +335,13 @@ pub(super) fn stylesheet_text(runtime: &JsContextHost, handle: DomHandle) -> Str
}
fn stylesheet_source(runtime: &JsContextHost, handle: DomHandle) -> StyloStylesheetSource {
let media = runtime
.dom_host()
.get_attribute(handle, "media")
.unwrap_or_default();
let Some(element) = runtime.dom_host().node(handle).and_then(Node::as_element) else {
return StyloStylesheetSource::new(String::new(), style_base_url(runtime, handle));
return StyloStylesheetSource::new(String::new(), style_base_url(runtime, handle))
.with_owner_media_text(&media);
};
if element.is_html_element("link") {
return runtime
@@ -349,27 +352,18 @@ fn stylesheet_source(runtime: &JsContextHost, handle: DomHandle) -> StyloStylesh
.with_source_id(StyleSourceId::linked_style_sheet(
runtime.dom_host(),
handle,
));
))
.with_owner_media_text(&media);
}
if element.is_inline_style_element()
&& let Some(source) = runtime.owner_style_sheet_source(handle)
{
return source.with_source_id(StyleSourceId::owner_style_sheet(runtime.dom_host(), handle));
return source
.with_source_id(StyleSourceId::owner_style_sheet(runtime.dom_host(), handle))
.with_owner_media_text(&media);
}
StyloStylesheetSource::new(String::new(), style_base_url(runtime, handle))
}
fn stylesheet_media_matches(runtime: &JsContextHost, handle: DomHandle) -> bool {
let Some(media) = runtime.dom_host().get_attribute(handle, "media") else {
return true;
};
let media = media.trim();
media.is_empty()
|| evaluate_match_media_query_list_with_viewport(
media,
Some(runtime.emulated_media()),
runtime.style_viewport(),
)
.with_owner_media_text(&media)
}
fn link_stylesheet_is_enabled(runtime: &JsContextHost, handle: DomHandle) -> bool {
@@ -440,8 +440,6 @@ pub(super) fn computed_style_snapshot_from_current_observation(
host,
&world.document_state,
owner_document,
viewport,
environment,
quirks_mode,
tree_scope_versions,
));
+28 -12
View File
@@ -39,6 +39,7 @@ pub(super) struct RetainedStyleInvalidations {
pub(super) shadow_scopes: Vec<(DomHandle, StylesheetInvalidationSet)>,
pub(super) shadow_scope_fallbacks: Vec<DomHandle>,
pub(super) removed_shadow_scopes: Vec<DomHandle>,
pub(super) viewport_size_changed: bool,
}
/// Compares an explicit full-world snapshot with the canonical retained
@@ -74,6 +75,7 @@ impl RetainedStyleInvalidations {
shadow_scopes: Vec::new(),
shadow_scope_fallbacks: Vec::new(),
removed_shadow_scopes: Vec::new(),
viewport_size_changed: false,
}
}
}
@@ -201,6 +203,7 @@ pub(super) fn update_retained_style_system(
);
invalidations.document = document_update.invalidations;
invalidations.document_scope_fallback = document_update.scope_fallback;
invalidations.viewport_size_changed = document_update.viewport_size_changed;
let shadow_reconciliation = reconcile_shadow_scopes(
retained,
@@ -260,6 +263,7 @@ pub(super) fn update_retained_style_system_incrementally(
);
invalidations.document = document_update.invalidations;
invalidations.document_scope_fallback = document_update.scope_fallback;
invalidations.viewport_size_changed = document_update.viewport_size_changed;
let shadow_reconciliation = reconcile_dirty_shadow_scopes(
retained,
@@ -274,15 +278,19 @@ pub(super) fn update_retained_style_system_incrementally(
invalidations.removed_shadow_scopes = shadow_reconciliation.removed_roots;
let mut dirty_source_scopes = full_source_projection_scopes.clone();
if document_update.device_changed {
if document_update
.device_affected_origins
.contains(OriginSet::ORIGIN_AUTHOR)
{
dirty_source_scopes.insert(StyleScopeId::Document(document));
dirty_source_scopes.extend(
retained
.shadow_scopes
.iter()
.map(|scope| StyleScopeId::ShadowRoot(scope.root())),
);
}
dirty_source_scopes.extend(
shadow_reconciliation
.device_affected_roots
.iter()
.copied()
.map(StyleScopeId::ShadowRoot),
);
dirty_source_scopes.extend(
invalidations
.removed_shadow_scopes
@@ -296,6 +304,7 @@ pub(super) fn update_retained_style_system_incrementally(
retained_source_records,
dirty_source_ids,
&dirty_source_scopes,
document_update.device_changed,
|source| install_active_stylesheet(host, shared_lock, source, key.quirks_mode),
);
refresh_retained_derived_state(
@@ -311,6 +320,8 @@ struct DocumentScopeUpdate {
invalidations: Option<StylesheetInvalidationSet>,
scope_fallback: bool,
device_changed: bool,
viewport_size_changed: bool,
device_affected_origins: OriginSet,
stylesheets_changed: bool,
}
@@ -353,11 +364,13 @@ fn update_document_scope(
.force_stylesheet_origins_dirty(OriginSet::ORIGIN_AUTHOR);
}
let device_changed = retained.key.viewport_width_bits != key.viewport_width_bits
|| retained.key.viewport_height_bits != key.viewport_height_bits
let viewport_size_changed = retained.key.viewport_width_bits != key.viewport_width_bits
|| retained.key.viewport_height_bits != key.viewport_height_bits;
let device_changed = viewport_size_changed
|| retained.key.screen_width_bits != key.screen_width_bits
|| retained.key.screen_height_bits != key.screen_height_bits
|| retained.key.environment != key.environment;
let mut device_affected_origins = OriginSet::empty();
if device_changed {
let device = new_style_device_with_viewport_bits(
key.viewport_width_bits,
@@ -369,10 +382,10 @@ fn update_document_scope(
);
let guard = shared_lock.read();
let guards = StylesheetGuards::same(&guard);
let affected_origins = retained.stylist.set_device(device, &guards);
device_affected_origins = retained.stylist.set_device(device, &guards);
retained
.stylist
.force_stylesheet_origins_dirty(affected_origins);
.force_stylesheet_origins_dirty(device_affected_origins);
}
let stylesheet_reconciliation = stylesheet_sources.and_then(|sources| {
@@ -409,7 +422,8 @@ fn update_document_scope(
}
let stylesheets_changed = stylesheet_reconciliation.is_some();
let must_flush = device_changed || stylesheets_changed || custom_properties_changed;
let must_flush =
!device_affected_origins.is_empty() || stylesheets_changed || custom_properties_changed;
let invalidations = must_flush.then(|| {
let guard = shared_lock.read();
retained.stylist.flush(&StylesheetGuards::same(&guard))
@@ -418,6 +432,8 @@ fn update_document_scope(
invalidations,
scope_fallback,
device_changed,
viewport_size_changed,
device_affected_origins,
stylesheets_changed,
}
}
@@ -83,6 +83,7 @@ pub(super) struct ShadowScopeReconciliation {
pub(super) invalidations: Vec<(DomHandle, StylesheetInvalidationSet)>,
pub(super) removed_roots: Vec<DomHandle>,
pub(super) scope_fallbacks: Vec<DomHandle>,
pub(super) device_affected_roots: Vec<DomHandle>,
pub(super) collections_changed: bool,
}
@@ -100,6 +101,7 @@ pub(super) fn reconcile_dirty_shadow_scopes(
let mut invalidations = Vec::new();
let mut removed_roots = Vec::new();
let mut scope_fallbacks = Vec::new();
let mut device_affected_roots = Vec::new();
let mut collections_changed = false;
let mut newly_built_roots = HashSet::new();
@@ -135,6 +137,18 @@ pub(super) fn reconcile_dirty_shadow_scopes(
retained.shadow_scopes = next_scopes;
}
if device_changed {
let guard = shared_lock.read();
device_affected_roots.extend(
retained
.shadow_scopes
.iter()
.filter(|scope| !newly_built_roots.contains(&scope.root()))
.filter(|scope| shadow_scope_media_changed(scope, &retained.stylist, &guard))
.map(ShadowScopeStyles::root),
);
}
for (root, sources) in dirty_scopes {
if newly_built_roots.contains(root) {
continue;
@@ -170,11 +184,12 @@ pub(super) fn reconcile_dirty_shadow_scopes(
if newly_built_roots.contains(&scope.root()) {
continue;
}
let must_flush = dirty_roots.contains(&scope.root()) || device_changed;
let device_affected = device_affected_roots.contains(&scope.root());
let must_flush = dirty_roots.contains(&scope.root()) || device_affected;
if !must_flush {
continue;
}
if device_changed {
if device_affected {
scope.author_styles_mut().stylesheets.force_dirty();
}
let guard = shared_lock.read();
@@ -186,6 +201,7 @@ pub(super) fn reconcile_dirty_shadow_scopes(
invalidations,
removed_roots,
scope_fallbacks,
device_affected_roots,
collections_changed,
}
}
@@ -203,13 +219,14 @@ pub(super) fn reconcile_shadow_scopes(
let mut next_scopes = Vec::with_capacity(desired_scopes.len());
let mut invalidations = Vec::new();
let mut scope_fallbacks = Vec::new();
let mut device_affected_roots = Vec::new();
let mut collections_changed = false;
for (root, sources) in desired_scopes {
let mut scope = if let Some(index) = previous_scopes
let (mut scope, retained_scope) = if let Some(index) = previous_scopes
.iter()
.position(|scope| scope.root() == *root)
{
previous_scopes.remove(index)
(previous_scopes.remove(index), true)
} else {
collections_changed = true;
let (scope, scope_invalidations) = build_shadow_scope(
@@ -220,9 +237,17 @@ pub(super) fn reconcile_shadow_scopes(
&mut install,
);
invalidations.push((*root, scope_invalidations));
scope
(scope, false)
};
let device_affected = device_changed && retained_scope && {
let guard = shared_lock.read();
shadow_scope_media_changed(&scope, &retained.stylist, &guard)
};
if device_affected {
device_affected_roots.push(*root);
}
let reconciliation = scope
.active_stylesheets_mut()
.reconcile(sources, &mut install);
@@ -237,10 +262,10 @@ pub(super) fn reconcile_shadow_scopes(
scope_fallbacks.push(*root);
}
}
if device_changed {
if device_affected {
scope.author_styles_mut().stylesheets.force_dirty();
}
if reconciliation.is_some() || device_changed {
if reconciliation.is_some() || device_affected {
let guard = shared_lock.read();
invalidations.push((*root, scope.flush(&mut retained.stylist, &guard)));
}
@@ -257,10 +282,26 @@ pub(super) fn reconcile_shadow_scopes(
invalidations,
removed_roots,
scope_fallbacks,
device_affected_roots,
collections_changed,
}
}
fn shadow_scope_media_changed(
scope: &ShadowScopeStyles,
stylist: &Stylist,
guard: &SharedRwLockReadGuard<'_>,
) -> bool {
scope.active_stylesheets().entries().iter().any(|entry| {
!scope.author_styles().data.media_feature_affected_matches(
entry.stylesheet(),
guard,
stylist.device(),
stylist.quirks_mode(),
)
})
}
fn build_shadow_scope(
stylist: &mut Stylist,
shared_lock: &SharedRwLock,
@@ -6,6 +6,7 @@ use std::{
use moli_crypto::Sha256Context;
use moli_selector::StyloSourceDependencySummary;
use style::stylesheets::{CssRule, StylesheetInDocument};
use style_traits::ToCss;
use super::super::{
source_id::{StyleScopeId, StyleSourceId, StyleSourceKind},
@@ -35,6 +36,12 @@ pub(crate) struct StyloStylesheetSource {
cache_key: StyleSourceKey,
source_id: Option<StyleSourceId>,
adopted_client_id: Option<u64>,
/// Top-level media list attached by the stylesheet owner.
///
/// Live stylesheets carry the same list in Stylo's `Stylesheet::media`.
/// Keeping the serialized value on the source also preserves it when a
/// live sheet is projected into an independently parsed test/query world.
media_text: StdArc<str>,
}
#[derive(Clone, Debug)]
@@ -93,6 +100,7 @@ impl StyloStylesheetSource {
cache_key,
source_id: None,
adopted_client_id: None,
media_text: StdArc::from(""),
}
}
@@ -100,9 +108,14 @@ impl StyloStylesheetSource {
stylesheet: &crate::live_stylesheet::LiveStylesheetRef,
) -> Self {
let base_url = stylesheet.base_url().clone();
let parsed_stylesheet = stylesheet.stylesheet();
let media_text = {
let guard = parsed_stylesheet.shared_lock.read();
parsed_stylesheet.media.read_with(&guard).to_css_string()
};
Self {
contents: StyloStylesheetSourceContents::Live {
stylesheet: stylesheet.stylesheet(),
stylesheet: parsed_stylesheet,
id: stylesheet.id(),
contents_revision: stylesheet.contents_revision(),
cascade_generation: stylesheet.cascade_generation(),
@@ -119,6 +132,7 @@ impl StyloStylesheetSource {
),
source_id: None,
adopted_client_id: None,
media_text: StdArc::from(media_text),
}
}
@@ -142,6 +156,22 @@ impl StyloStylesheetSource {
self
}
/// Installs an owner attribute media list on a text-backed source.
///
/// A live stylesheet is already authoritative for CSSOM media mutations,
/// so owner projection must not overwrite the list captured from it.
pub(crate) fn with_owner_media_text(mut self, media_text: &str) -> Self {
if matches!(&self.contents, StyloStylesheetSourceContents::Text { .. }) {
self.media_text = StdArc::from(media_text);
}
self
}
fn with_media_text(mut self, media_text: StdArc<str>) -> Self {
self.media_text = media_text;
self
}
pub(crate) fn input_css_text(&self) -> Option<&str> {
match &self.contents {
StyloStylesheetSourceContents::Text { shared } => Some(shared.css_text()),
@@ -180,7 +210,8 @@ impl StyloStylesheetSource {
)
.with_source_id(self.source_id.clone())
.with_origin_clean(self.origin_clean)
.with_sheet_url(self.sheet_url.as_ref().clone());
.with_sheet_url(self.sheet_url.as_ref().clone())
.with_media_text(StdArc::clone(&self.media_text));
projection.adopted_client_id = self.adopted_client_id;
projection
}
@@ -365,6 +396,14 @@ impl StyloStylesheetSource {
self.adopted_client_id
}
pub(in crate::style_engine) fn media_text(&self) -> &str {
self.media_text.as_ref()
}
pub(in crate::style_engine) fn has_authoritative_runtime_media(&self) -> bool {
matches!(&self.contents, StyloStylesheetSourceContents::Live { .. })
}
pub(in crate::style_engine) fn has_same_installation_identity(&self, other: &Self) -> bool {
match (self.live_stylesheet_id(), other.live_stylesheet_id()) {
(Some(left), Some(right)) => left == right,
@@ -383,6 +422,7 @@ impl StyloStylesheetSource {
&& self.sheet_url == other.sheet_url
&& self.origin_clean == other.origin_clean
&& self.cache_key == other.cache_key
&& self.media_text == other.media_text
&& match (&self.contents, &other.contents) {
(
StyloStylesheetSourceContents::Text { shared: left },
@@ -431,6 +471,7 @@ impl PartialEq for StyloStylesheetSource {
&& self.cache_key == other.cache_key
&& self.source_id == other.source_id
&& self.adopted_client_id == other.adopted_client_id
&& self.media_text == other.media_text
&& match (&self.contents, &other.contents) {
(
StyloStylesheetSourceContents::Text { shared: left },
@@ -455,6 +496,7 @@ impl Hash for StyloStylesheetSource {
self.cache_key.hash(state);
self.source_id.hash(state);
self.adopted_client_id.hash(state);
self.media_text.hash(state);
match &self.contents {
StyloStylesheetSourceContents::Text { shared } => {
0_u8.hash(state);
@@ -544,6 +586,9 @@ pub(in crate::style_engine) fn stylesheet_sources_cache_key(
hasher.update(sources.len().to_le_bytes());
for source in sources {
hasher.update(source.cache_key().fingerprint);
let media_text = source.media_text().as_bytes();
hasher.update(media_text.len().to_le_bytes());
hasher.update(media_text);
update_style_source_identity_hash(&mut hasher, source.source_id());
match source.adopted_client_id() {
Some(client_id) => {
@@ -74,6 +74,9 @@ pub(super) fn build_source_cascade_data(
(previous_keys.get(&source_id) == Some(&key))
.then(|| previous_data.get(&source_id).cloned())
.flatten()
.filter(|data| {
source_cascade_matches_device(data, &stylesheets, stylist, shared_lock)
})
});
let data = retained_data
.unwrap_or_else(|| build_author_cascade_data(stylist, shared_lock, &stylesheets));
@@ -91,35 +94,47 @@ pub(super) fn update_source_cascade_data_for_scopes(
retained_source_records: &[RetainedStylesheetSourceRecord<'_>],
dirty_source_ids: &HashSet<StyleSourceId>,
dirty_scopes: &HashSet<StyleScopeId>,
device_changed: bool,
mut install: impl FnMut(&StyloStylesheetSource) -> ActiveStylesheet,
) {
if dirty_source_ids.is_empty() && dirty_scopes.is_empty() {
if dirty_source_ids.is_empty() && dirty_scopes.is_empty() && !device_changed {
return;
}
let previous_data = std::mem::take(&mut retained.source_cascade_data);
let previous_keys = std::mem::take(&mut retained.source_cascade_keys);
let mut next_data = retain_clean_entries(&previous_data, dirty_source_ids, dirty_scopes);
let mut next_keys = retain_clean_entries(&previous_keys, dirty_source_ids, dirty_scopes);
let mut next_data = if device_changed {
HashMap::new()
} else {
retain_clean_entries(&previous_data, dirty_source_ids, dirty_scopes)
};
let mut next_keys = if device_changed {
HashMap::new()
} else {
retain_clean_entries(&previous_keys, dirty_source_ids, dirty_scopes)
};
let source_filter = (!device_changed).then_some((dirty_source_ids, dirty_scopes));
let mut sources_by_id = installed_sources_by_id(
&retained.document_stylesheets,
&retained.shadow_scopes,
Some(dirty_source_ids),
Some(dirty_scopes),
source_filter.map(|(source_ids, _)| source_ids),
source_filter.map(|(_, scopes)| scopes),
);
add_retained_source_records(
&mut sources_by_id,
retained_source_records,
Some(dirty_source_ids),
Some(dirty_scopes),
source_filter.map(|(source_ids, _)| source_ids),
source_filter.map(|(_, scopes)| scopes),
&mut install,
);
for (source_id, (sources, stylesheets)) in sources_by_id {
let key = stylesheet_sources_cache_key(&sources);
let data = if previous_keys.get(&source_id) == Some(&key) {
previous_data.get(&source_id).cloned()
previous_data.get(&source_id).cloned().filter(|data| {
source_cascade_matches_device(data, &stylesheets, &retained.stylist, shared_lock)
})
} else {
None
}
@@ -133,6 +148,23 @@ pub(super) fn update_source_cascade_data_for_scopes(
retained.source_cascade_keys = next_keys;
}
fn source_cascade_matches_device(
data: &CascadeData,
stylesheets: &[DocumentStyleSheet],
stylist: &Stylist,
shared_lock: &SharedRwLock,
) -> bool {
let guard = shared_lock.read();
stylesheets.iter().all(|stylesheet| {
data.media_feature_affected_matches(
stylesheet,
&guard,
stylist.device(),
stylist.quirks_mode(),
)
})
}
type SourceCascadeData = HashMap<StyleSourceId, ServoArc<CascadeData>>;
type SourceCascadeKeys = HashMap<StyleSourceId, StyleSourceSetKey>;
@@ -21,13 +21,20 @@ pub(crate) fn link_rel_qualifies_as_stylesheet(rel: Option<&str>, title: Option<
pub(super) fn stylesheet_owner_is_stylesheet_source_enabled(
host: &DomHost,
handle: DomHandle,
media_text: &str,
emulated_media: &EmulatedMediaOverrides,
viewport: StyleViewport,
) -> bool {
host.node(handle).is_some_and(|node| {
node.as_element()
.is_some_and(|element| element.is_inline_style_element())
&& style_element_is_stylesheet_source_enabled(host, handle, emulated_media, viewport)
&& style_element_is_stylesheet_source_enabled(
host,
handle,
media_text,
emulated_media,
viewport,
)
})
}
@@ -46,6 +53,7 @@ pub(super) fn stylesheet_source_base_url(host: &DomHost, handle: DomHandle) -> u
fn style_element_is_stylesheet_source_enabled(
host: &DomHost,
handle: DomHandle,
media_text: &str,
emulated_media: &EmulatedMediaOverrides,
viewport: StyleViewport,
) -> bool {
@@ -57,28 +65,23 @@ fn style_element_is_stylesheet_source_enabled(
&& moli_web_mime::is_stylesheet_type_attribute(
host.get_attribute(handle, "type").as_deref(),
)
&& stylesheet_media_matches_for_stylesheet_source(host, handle, emulated_media, viewport)
&& stylesheet_media_matches_for_stylesheet_source(media_text, emulated_media, viewport)
}
pub(super) fn linked_stylesheet_media_matches_for_stylesheet_source(
host: &DomHost,
handle: DomHandle,
media_text: &str,
emulated_media: &EmulatedMediaOverrides,
viewport: StyleViewport,
) -> bool {
stylesheet_media_matches_for_stylesheet_source(host, handle, emulated_media, viewport)
stylesheet_media_matches_for_stylesheet_source(media_text, emulated_media, viewport)
}
fn stylesheet_media_matches_for_stylesheet_source(
host: &DomHost,
handle: DomHandle,
media_text: &str,
emulated_media: &EmulatedMediaOverrides,
viewport: StyleViewport,
) -> bool {
let Some(media) = host.get_attribute(handle, "media") else {
return true;
};
let media = media.trim();
let media = media_text.trim();
media.is_empty()
|| evaluate_match_media_query_list_with_viewport(media, Some(emulated_media), viewport)
}
@@ -122,16 +122,23 @@ impl<'a> RetainedStylesheetSourceRecord<'a> {
}
fn dependency_summary(&self) -> Arc<StyloSourceDependencySummary> {
self.source().source_dependency_summary()
}
fn source(&self) -> &StyloStylesheetSource {
match &self.source {
RetainedStylesheetSourceRecordSource::OwnerStyleSheet(source) => {
source.source_dependency_summary()
}
RetainedStylesheetSourceRecordSource::Registered(source) => {
source.source_dependency_summary()
}
RetainedStylesheetSourceRecordSource::OwnerStyleSheet(source)
| RetainedStylesheetSourceRecordSource::Registered(source) => source,
}
}
fn effective_media_text(&self, host: &DomHost, owner: DomHandle) -> String {
if self.source().has_authoritative_runtime_media() {
return self.source().media_text().to_owned();
}
host.get_attribute(owner, "media").unwrap_or_default()
}
pub(super) fn is_dependency_source_enabled(
&self,
host: &DomHost,
@@ -140,12 +147,17 @@ impl<'a> RetainedStylesheetSourceRecord<'a> {
) -> bool {
match self.id.kind {
StyleSourceKind::OwnerStyleSheet { owner } => {
stylesheet_owner_is_stylesheet_source_enabled(host, owner, emulated_media, viewport)
stylesheet_owner_is_stylesheet_source_enabled(
host,
owner,
&self.effective_media_text(host, owner),
emulated_media,
viewport,
)
}
StyleSourceKind::LinkedStyleSheet { owner } => {
linked_stylesheet_media_matches_for_stylesheet_source(
host,
owner,
&self.effective_media_text(host, owner),
emulated_media,
viewport,
)
@@ -11,7 +11,6 @@ use style::{
device::{Device, servo::FontMetricsProvider},
font_face::FontFaceRule,
font_metrics::FontMetrics,
media_queries::MediaList,
properties::{ComputedValues, style_structs::Font},
servo::media_features::PointerCapabilities,
servo_arc::Arc as ServoArc,
@@ -36,6 +35,7 @@ use crate::{document_runtime::DomHandle, dom::native::DomHost};
use super::{
StyleViewport, StyloStyleEnvironment,
active_stylesheets::{ActiveStylesheet, ActiveStylesheetCollection},
media_list::parse_media_query_list_with_context,
source::store::{StyleSourceMetadata, StyloStylesheetSource},
source_id::{StyleSourceId, StyleSourceKind},
ua::HTML_STYLESHEET as MOLI_UA_STYLESHEET,
@@ -285,6 +285,7 @@ fn style_source_metadata_for_css_text_with_origin(
css_text,
origin,
QuirksMode::NoQuirks,
"",
)));
let guard = shared_lock.read();
let stylist = new_stylist_with_viewport_bits(
@@ -434,7 +435,7 @@ pub(super) fn append_stylesheet_to_stylist(
origin: Origin,
quirks_mode: QuirksMode,
) {
let stylesheet = parse_stylesheet(shared_lock, base_url, css_text, origin, quirks_mode);
let stylesheet = parse_stylesheet(shared_lock, base_url, css_text, origin, quirks_mode, "");
let guard = shared_lock.read();
stylist.append_stylesheet(DocumentStyleSheet::new(ServoArc::new(stylesheet)), &guard);
}
@@ -458,6 +459,7 @@ fn author_stylesheet_for_source(
css_text,
Origin::Author,
quirks_mode,
source.media_text(),
))
}
@@ -467,8 +469,10 @@ fn parse_stylesheet(
css_text: &str,
origin: Origin,
quirks_mode: QuirksMode,
media_text: &str,
) -> Stylesheet {
let media = ServoArc::new(shared_lock.wrap(MediaList::empty()));
let media = parse_media_query_list_with_context(media_text, base_url, quirks_mode);
let media = ServoArc::new(shared_lock.wrap(media));
Stylesheet::from_str(
css_text,
UrlExtraData::from(base_url.clone()),
@@ -5720,6 +5720,259 @@ fn one_shadow_stylesheet_change_preserves_the_other_scope_cascade_data() {
);
}
#[test]
fn device_changes_keep_media_sheets_installed_and_flush_only_affected_tree_scopes() {
reset_source_cascade_rebuild_count_for_test();
let mut host = test_host();
let document = host.document_handle();
let document_target = host.create_element("div");
assert!(host.set_attribute(document_target, "class", "document-target"));
assert!(host.set_attribute(document_target, "style", "width: 50vw"));
assert!(host.append_child(document, document_target));
let first_host = host.create_element("section");
let second_host = host.create_element("article");
assert!(host.append_child(document, first_host));
assert!(host.append_child(document, second_host));
let first_root = host
.attach_shadow_root(first_host, "open")
.expect("first host should accept a shadow root");
let second_root = host
.attach_shadow_root(second_host, "open")
.expect("second host should accept a shadow root");
let first_target = host.create_element("span");
let second_target = host.create_element("span");
assert!(host.set_attribute(first_target, "class", "first-target"));
assert!(host.set_attribute(first_target, "style", "height: 50vh"));
assert!(host.set_attribute(second_target, "class", "second-target"));
assert!(host.append_child(first_root, first_target));
assert!(host.append_child(second_root, second_target));
let engine = MoliStyleEngine::new();
let document_url = url::Url::parse("https://example.test/shadow-media.html").unwrap();
let mut inputs = FullStyleWorldSnapshot::default();
inputs.document_stylesheet_sources.push(
StyloStylesheetSource::new(
".document-target { color: rgb(7, 8, 9); }".into(),
document_url.clone(),
)
.with_owner_media_text("(max-width: 600px)"),
);
inputs.shadow_stylesheet_sources.push((
first_root,
vec![
StyloStylesheetSource::new(
".first-target { color: rgb(1, 2, 3); }".into(),
document_url.clone(),
)
.with_source_id(Some(StyleSourceId::shadow_root_adopted_style_sheet(
first_root, 71,
)))
.with_owner_media_text("(max-width: 600px)"),
],
));
inputs.shadow_stylesheet_sources.push((
second_root,
vec![
StyloStylesheetSource::new(
".second-target { color: rgb(4, 5, 6); }".into(),
document_url.clone(),
)
.with_source_id(Some(StyleSourceId::shadow_root_adopted_style_sheet(
second_root,
72,
)))
.with_owner_media_text("(min-width: 100px)"),
],
));
let viewport_800 = StyleViewport::new(Some(800.0), Some(600.0));
assert_eq!(
engine.computed_style_property_value(
&host,
&document_url,
document_target,
"color",
None,
&inputs,
viewport_800,
),
Some("rgb(0, 0, 0)".into()),
);
assert_eq!(
engine.computed_style_property_value(
&host,
&document_url,
first_target,
"color",
None,
&inputs,
viewport_800,
),
Some("rgb(0, 0, 0)".into()),
);
assert_eq!(
engine.computed_style_property_value(
&host,
&document_url,
second_target,
"color",
None,
&inputs,
viewport_800,
),
Some("rgb(4, 5, 6)".into()),
);
assert_eq!(
engine.computed_style_property_value(
&host,
&document_url,
document_target,
"width",
None,
&inputs,
viewport_800,
),
Some("400px".into()),
);
assert_eq!(
engine.computed_style_property_value(
&host,
&document_url,
first_target,
"height",
None,
&inputs,
viewport_800,
),
Some("300px".into()),
);
engine.with_retained_style_system_for_document_for_test(document, |retained| {
assert_eq!(
retained.document_stylesheets.entries().len(),
1,
"a non-matching owner MediaList must not remove its sheet from the Document scope",
);
assert_eq!(retained.shadow_scopes.len(), 2);
assert!(
retained
.shadow_scopes
.iter()
.all(|scope| scope.active_stylesheets().entries().len() == 1),
"a non-matching owner MediaList must not remove its sheet from the TreeScope",
);
});
let document_flushes = engine.retained_stylist_flush_count_for_document_for_test(document);
let first_flushes = engine
.retained_shadow_scope_flush_count_for_document_for_test(document, first_root)
.expect("first scope should be retained");
let second_flushes = engine
.retained_shadow_scope_flush_count_for_document_for_test(document, second_root)
.expect("second scope should be retained");
let source_rebuilds = source_cascade_rebuild_count_for_test();
let viewport_700 = StyleViewport::new(Some(700.0), Some(500.0));
assert_eq!(
engine.computed_style_property_value(
&host,
&document_url,
first_target,
"color",
None,
&inputs,
viewport_700,
),
Some("rgb(0, 0, 0)".into()),
);
assert_eq!(
engine.retained_shadow_scope_flush_count_for_document_for_test(document, first_root),
Some(first_flushes),
"a viewport change that crosses no media boundary must not flush the first scope",
);
assert_eq!(
engine.retained_shadow_scope_flush_count_for_document_for_test(document, second_root),
Some(second_flushes),
"a viewport change that crosses no media boundary must not flush the second scope",
);
assert_eq!(source_cascade_rebuild_count_for_test(), source_rebuilds);
assert_eq!(
engine.computed_style_property_value(
&host,
&document_url,
document_target,
"width",
None,
&inputs,
viewport_700,
),
Some("350px".into()),
"viewport units must be invalidated without a Document stylesheet flush",
);
assert_eq!(
engine.computed_style_property_value(
&host,
&document_url,
first_target,
"height",
None,
&inputs,
viewport_700,
),
Some("250px".into()),
"viewport units inside ShadowRoot must be invalidated without an AuthorStyles flush",
);
assert_eq!(
engine.retained_stylist_flush_count_for_document_for_test(document),
document_flushes,
"a viewport change that crosses no media boundary must not flush the Document Stylist",
);
let viewport_500 = StyleViewport::new(Some(500.0), Some(600.0));
assert_eq!(
engine.computed_style_property_value(
&host,
&document_url,
document_target,
"color",
None,
&inputs,
viewport_500,
),
Some("rgb(7, 8, 9)".into()),
);
assert_eq!(
engine.computed_style_property_value(
&host,
&document_url,
first_target,
"color",
None,
&inputs,
viewport_500,
),
Some("rgb(1, 2, 3)".into()),
);
assert_eq!(
engine.retained_shadow_scope_flush_count_for_document_for_test(document, first_root),
Some(first_flushes + 1),
"only the scope whose top-level MediaList changed must flush",
);
assert_eq!(
engine.retained_shadow_scope_flush_count_for_document_for_test(document, second_root),
Some(second_flushes),
"an unrelated matching MediaList must preserve its AuthorStyles",
);
assert_eq!(
engine.retained_stylist_flush_count_for_document_for_test(document),
document_flushes + 1,
"the Document Stylist must flush once when its installed MediaList changes match state",
);
assert_eq!(
source_cascade_rebuild_count_for_test(),
source_rebuilds + 1,
"only the affected scope's source-local cascade projection must rebuild",
);
}
#[test]
fn style_subtree_invalidation_clears_only_affected_shadow_cascade_data() {
let mut host = test_host();
@@ -598,6 +598,59 @@ fn source_metadata_respects_style_element_media() {
)
);
}
#[test]
fn source_metadata_uses_the_stylesheet_runtime_media_list() {
use style::{context::QuirksMode, stylesheets::AllowImportRules};
let mut host = test_host();
let style = host.create_element("style");
assert!(host.set_attribute(style, "media", "print"));
assert!(host.append_child(host.document_handle(), style));
let mut engine = MoliStyleEngine::new();
engine.set_owner_style_sheet_text_with_host(
&host,
style,
"body:has(.marker) .target { color: green; }".into(),
);
let registry = crate::live_stylesheet::LiveStylesheetRegistry::default();
let stylesheet = registry.create(
"body:has(.marker) .target { color: green; }",
url::Url::parse("https://example.test/runtime-media.css").unwrap(),
QuirksMode::NoQuirks,
AllowImportRules::No,
engine.author_shared_lock(),
);
stylesheet.set_media_text("screen");
assert!(engine.install_owner_live_stylesheet_with_host(&host, style, stylesheet.clone()));
let media = crate::protocol_types::EmulatedMediaOverrides::default();
let source_scope = StyleSourceScope::for_document(host.document_handle());
assert_eq!(host.get_attribute(style, "media").as_deref(), Some("print"));
assert!(
engine.test_author_sources_have_relative_selector_dependency_for_document(
&host,
host.document_handle(),
&source_scope,
&media,
),
"CSSOM runtime media must be authoritative even when the owner attribute differs",
);
stylesheet.set_media_text("print");
assert!(engine.refresh_owner_live_stylesheet_with_host(&host, style, stylesheet.id(),));
assert!(
!engine.test_author_sources_have_relative_selector_dependency_for_document(
&host,
host.document_handle(),
&source_scope,
&media,
),
"an inactive runtime MediaList must not participate in dependency invalidation",
);
}
#[test]
fn stylo_source_dependency_summary_reads_invalidation_metadata() {
let base_url = url::Url::parse("https://example.test/app.css").unwrap();
@@ -6,8 +6,7 @@ use moli_selector::StyloDomStyleAdapter;
use style::context::QuirksMode;
use super::{
FullStyleWorldSnapshot, StyleTreeScopeVersions, StyleViewport, StyleWorldEnvironment,
StyloStyleEnvironment,
FullStyleWorldSnapshot, StyleTreeScopeVersions, StyleWorldEnvironment,
cleanup::StyleCacheCleanup,
retained::{
RetainedStyleInvalidations, build_retained_style_system,
@@ -28,21 +27,16 @@ pub(super) fn retained_style_world_update_plan(
host: &DomHost,
document_state: &StyleDocumentState,
document: DomHandle,
viewport: StyleViewport,
environment: StyloStyleEnvironment,
quirks_mode: QuirksMode,
tree_scope_versions: StyleTreeScopeVersions,
) -> StyleWorldUpdatePlan {
let source_dirty_scope = document_state.source_dirty_scope_snapshot();
let Some((replace_world, device_changed, tree_scopes_changed, retained_shadow_roots)) =
document_state.try_with_retained_style_system(|retained| {
let Some((replace_world, tree_scopes_changed, retained_shadow_roots)) = document_state
.try_with_retained_style_system(|retained| {
(
retained
.key
.requires_replacement_for_observation(quirks_mode),
retained
.key
.device_differs_from_observation(viewport, environment),
retained.key.tree_scope_versions != Some(tree_scope_versions),
retained
.shadow_scopes
@@ -77,22 +71,8 @@ pub(super) fn retained_style_world_update_plan(
);
}
// The bridge currently models <style media> and <link media> by active-list
// membership instead of installing their MediaList on the Stylo sheet.
// A device change therefore has to refresh those ordered lists. This is
// still scope-local and parse-free for unchanged installations.
if device_changed {
shadow_stylesheet_roots.extend(
connected_shadow_roots
.as_deref()
.unwrap_or(&retained_shadow_roots)
.iter()
.copied(),
);
}
StyleWorldUpdatePlan::Incremental(IncrementalStyleWorldUpdatePlan::new(
source_dirty_scope.refreshes_document_stylesheets(document) || device_changed,
source_dirty_scope.refreshes_document_stylesheets(document),
shadow_stylesheet_roots.into_iter().collect(),
connected_shadow_roots,
source_dirty_scope.refreshes_custom_property_registrations(document),
@@ -319,6 +299,7 @@ fn apply_retained_stylesheet_invalidations(
shadow_scopes,
shadow_scope_fallbacks,
removed_shadow_scopes,
viewport_size_changed,
} = invalidations;
if let Some(invalidations) = document_invalidations {
// HTML documents normally have one document element. The native DOM
@@ -352,6 +333,9 @@ fn apply_retained_stylesheet_invalidations(
}
dom_adapter.process_stylesheet_invalidations(host, &roots, &invalidations);
}
if viewport_size_changed {
invalidate_viewport_unit_styles(host, dom_adapter, document);
}
cache_cleanup.invalidate_stylesheet_dirty_subtrees(host);
if document_scope_fallback {
cache_cleanup.invalidate_subtrees(host, [document]);
@@ -363,3 +347,34 @@ fn apply_retained_stylesheet_invalidations(
}
cache_cleanup.invalidate_subtrees(host, removed_shadow_scopes);
}
fn invalidate_viewport_unit_styles(
host: &DomHost,
dom_adapter: &StyloDomStyleAdapter,
document: DomHandle,
) {
let mut roots = host
.child_handles(document)
.filter(|handle| {
host.node(*handle)
.is_some_and(|node| node.as_element().is_some())
})
.collect::<Vec<_>>();
for shadow_root in host
.snapshot_connected_shadow_roots()
.into_iter()
.filter(|root| host.owner_document_handle(*root) == Some(document))
{
roots.extend(host.child_handles(shadow_root).filter(|handle| {
host.node(*handle)
.is_some_and(|node| node.as_element().is_some())
}));
}
dom_adapter.with_bound_host(host, |binding| {
for root in roots {
if let Some(root) = binding.element(host, root) {
style::invalidation::viewport_units::invalidate(root);
}
}
});
}