From 87f07d70ca490dca4cd0801cb5cf2ebda828c47d Mon Sep 17 00:00:00 2001 From: ldm0 Date: Mon, 24 Aug 2026 04:11:54 +0800 Subject: [PATCH] fix(style): retain media sheets in tree scopes --- .../element/styles/declaration/style_world.rs | 30 +-- moli-renderer-v8/src/style_engine/computed.rs | 2 - moli-renderer-v8/src/style_engine/retained.rs | 40 ++- .../src/style_engine/shadow_scopes.rs | 55 +++- .../src/style_engine/source/store.rs | 49 +++- .../src/style_engine/source_cascade.rs | 48 +++- .../src/style_engine/source_owner.rs | 25 +- .../src/style_engine/source_record.rs | 30 ++- .../src/style_engine/stylesheet.rs | 10 +- .../src/style_engine/tests/subtree_cache.rs | 253 ++++++++++++++++++ .../src/style_engine/tests/subtree_context.rs | 53 ++++ .../src/style_engine/world_lifecycle.rs | 63 +++-- 12 files changed, 562 insertions(+), 96 deletions(-) diff --git a/moli-renderer-v8/src/native_bridge/element/styles/declaration/style_world.rs b/moli-renderer-v8/src/native_bridge/element/styles/declaration/style_world.rs index d74dbf2fa6..ac2e280f3d 100644 --- a/moli-renderer-v8/src/native_bridge/element/styles/declaration/style_world.rs +++ b/moli-renderer-v8/src/native_bridge/element/styles/declaration/style_world.rs @@ -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::>(); 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 { diff --git a/moli-renderer-v8/src/style_engine/computed.rs b/moli-renderer-v8/src/style_engine/computed.rs index 2b61d368f3..80105f515e 100644 --- a/moli-renderer-v8/src/style_engine/computed.rs +++ b/moli-renderer-v8/src/style_engine/computed.rs @@ -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, )); diff --git a/moli-renderer-v8/src/style_engine/retained.rs b/moli-renderer-v8/src/style_engine/retained.rs index d0446fb50e..f9bc310540 100644 --- a/moli-renderer-v8/src/style_engine/retained.rs +++ b/moli-renderer-v8/src/style_engine/retained.rs @@ -39,6 +39,7 @@ pub(super) struct RetainedStyleInvalidations { pub(super) shadow_scopes: Vec<(DomHandle, StylesheetInvalidationSet)>, pub(super) shadow_scope_fallbacks: Vec, pub(super) removed_shadow_scopes: Vec, + 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, 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, } } diff --git a/moli-renderer-v8/src/style_engine/shadow_scopes.rs b/moli-renderer-v8/src/style_engine/shadow_scopes.rs index ab7e4cfa88..8bd76957d7 100644 --- a/moli-renderer-v8/src/style_engine/shadow_scopes.rs +++ b/moli-renderer-v8/src/style_engine/shadow_scopes.rs @@ -83,6 +83,7 @@ pub(super) struct ShadowScopeReconciliation { pub(super) invalidations: Vec<(DomHandle, StylesheetInvalidationSet)>, pub(super) removed_roots: Vec, pub(super) scope_fallbacks: Vec, + pub(super) device_affected_roots: Vec, 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, diff --git a/moli-renderer-v8/src/style_engine/source/store.rs b/moli-renderer-v8/src/style_engine/source/store.rs index b05a3796bf..a84604f2df 100644 --- a/moli-renderer-v8/src/style_engine/source/store.rs +++ b/moli-renderer-v8/src/style_engine/source/store.rs @@ -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, adopted_client_id: Option, + /// 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, } #[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) -> 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) => { diff --git a/moli-renderer-v8/src/style_engine/source_cascade.rs b/moli-renderer-v8/src/style_engine/source_cascade.rs index 06b03c53c7..9eaf6a1336 100644 --- a/moli-renderer-v8/src/style_engine/source_cascade.rs +++ b/moli-renderer-v8/src/style_engine/source_cascade.rs @@ -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, dirty_scopes: &HashSet, + 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>; type SourceCascadeKeys = HashMap; diff --git a/moli-renderer-v8/src/style_engine/source_owner.rs b/moli-renderer-v8/src/style_engine/source_owner.rs index dd963c78a6..0aa3923afb 100644 --- a/moli-renderer-v8/src/style_engine/source_owner.rs +++ b/moli-renderer-v8/src/style_engine/source_owner.rs @@ -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) } diff --git a/moli-renderer-v8/src/style_engine/source_record.rs b/moli-renderer-v8/src/style_engine/source_record.rs index 5d4c3e1718..bb44db30bb 100644 --- a/moli-renderer-v8/src/style_engine/source_record.rs +++ b/moli-renderer-v8/src/style_engine/source_record.rs @@ -122,16 +122,23 @@ impl<'a> RetainedStylesheetSourceRecord<'a> { } fn dependency_summary(&self) -> Arc { + 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, ) diff --git a/moli-renderer-v8/src/style_engine/stylesheet.rs b/moli-renderer-v8/src/style_engine/stylesheet.rs index 2bc535e7b7..691464a877 100644 --- a/moli-renderer-v8/src/style_engine/stylesheet.rs +++ b/moli-renderer-v8/src/style_engine/stylesheet.rs @@ -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()), diff --git a/moli-renderer-v8/src/style_engine/tests/subtree_cache.rs b/moli-renderer-v8/src/style_engine/tests/subtree_cache.rs index a2159d2e9c..107a746f36 100644 --- a/moli-renderer-v8/src/style_engine/tests/subtree_cache.rs +++ b/moli-renderer-v8/src/style_engine/tests/subtree_cache.rs @@ -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(); diff --git a/moli-renderer-v8/src/style_engine/tests/subtree_context.rs b/moli-renderer-v8/src/style_engine/tests/subtree_context.rs index 7141edc144..ad46a32088 100644 --- a/moli-renderer-v8/src/style_engine/tests/subtree_context.rs +++ b/moli-renderer-v8/src/style_engine/tests/subtree_context.rs @@ -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(); diff --git a/moli-renderer-v8/src/style_engine/world_lifecycle.rs b/moli-renderer-v8/src/style_engine/world_lifecycle.rs index 35dd4c8313..5d288157c3 100644 --- a/moli-renderer-v8/src/style_engine/world_lifecycle.rs +++ b/moli-renderer-v8/src/style_engine/world_lifecycle.rs @@ -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