From 8d6574d45ffda06ab98bc51c59ff2bf545d03fc3 Mon Sep 17 00:00:00 2001 From: lanyue-llk <270302213+lanyue-llk@users.noreply.github.com> Date: Tue, 29 Sep 2026 23:19:54 +0800 Subject: [PATCH] fix(content): consume resolved style semantics --- Cargo.lock | 26 +-- Cargo.toml | 14 +- .../context_host/host_environment.rs | 23 +- moli-renderer-v8/src/native_bridge/element.rs | 2 +- .../src/native_bridge/element/styles.rs | 6 +- .../element/styles/declaration/values.rs | 6 +- moli-renderer-v8/src/runtime/page_dump.rs | 40 +--- .../src/runtime/page_dump/visibility.rs | 210 ++---------------- .../runtime/page_vm/tests/markdown_dump.rs | 29 +++ moli-renderer-v8/src/script_vm/layout.rs | 17 +- moli-renderer-v8/src/style_engine/computed.rs | 162 +++++++++----- moli-renderer-v8/src/style_engine/mod.rs | 60 +---- .../src/style_engine/stylesheet.rs | 19 -- 13 files changed, 205 insertions(+), 409 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 85928ba0ca..19b1f81c36 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4489,7 +4489,7 @@ dependencies = [ "errno 0.3.14", "libc", "linux-raw-sys 0.4.15", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -4502,7 +4502,7 @@ dependencies = [ "errno 0.3.14", "libc", "linux-raw-sys 0.12.1", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -4624,7 +4624,7 @@ dependencies = [ [[package]] name = "selectors" version = "0.40.0" -source = "git+https://github.com/lexmount/stylo?branch=moli#3c8bbb450a52f9a4dcb2de51061216a1d2727def" +source = "git+https://github.com/lexmount/stylo?rev=49506ee7365dac8708b314733bc2d99736c2a24a#49506ee7365dac8708b314733bc2d99736c2a24a" dependencies = [ "bitflags", "cssparser", @@ -4756,7 +4756,7 @@ dependencies = [ [[package]] name = "servo_arc" version = "0.4.3" -source = "git+https://github.com/lexmount/stylo?branch=moli#3c8bbb450a52f9a4dcb2de51061216a1d2727def" +source = "git+https://github.com/lexmount/stylo?rev=49506ee7365dac8708b314733bc2d99736c2a24a#49506ee7365dac8708b314733bc2d99736c2a24a" dependencies = [ "serde", "stable_deref_trait", @@ -5009,7 +5009,7 @@ dependencies = [ [[package]] name = "stylo" version = "0.20.0" -source = "git+https://github.com/lexmount/stylo?branch=moli#3c8bbb450a52f9a4dcb2de51061216a1d2727def" +source = "git+https://github.com/lexmount/stylo?rev=49506ee7365dac8708b314733bc2d99736c2a24a#49506ee7365dac8708b314733bc2d99736c2a24a" dependencies = [ "app_units", "arrayvec", @@ -5065,7 +5065,7 @@ dependencies = [ [[package]] name = "stylo_atoms" version = "0.20.0" -source = "git+https://github.com/lexmount/stylo?branch=moli#3c8bbb450a52f9a4dcb2de51061216a1d2727def" +source = "git+https://github.com/lexmount/stylo?rev=49506ee7365dac8708b314733bc2d99736c2a24a#49506ee7365dac8708b314733bc2d99736c2a24a" dependencies = [ "string_cache", "string_cache_codegen", @@ -5074,7 +5074,7 @@ dependencies = [ [[package]] name = "stylo_derive" version = "0.20.0" -source = "git+https://github.com/lexmount/stylo?branch=moli#3c8bbb450a52f9a4dcb2de51061216a1d2727def" +source = "git+https://github.com/lexmount/stylo?rev=49506ee7365dac8708b314733bc2d99736c2a24a#49506ee7365dac8708b314733bc2d99736c2a24a" dependencies = [ "darling", "proc-macro2", @@ -5086,7 +5086,7 @@ dependencies = [ [[package]] name = "stylo_dom" version = "0.20.0" -source = "git+https://github.com/lexmount/stylo?branch=moli#3c8bbb450a52f9a4dcb2de51061216a1d2727def" +source = "git+https://github.com/lexmount/stylo?rev=49506ee7365dac8708b314733bc2d99736c2a24a#49506ee7365dac8708b314733bc2d99736c2a24a" dependencies = [ "bitflags", "stylo_malloc_size_of", @@ -5095,7 +5095,7 @@ dependencies = [ [[package]] name = "stylo_malloc_size_of" version = "0.20.0" -source = "git+https://github.com/lexmount/stylo?branch=moli#3c8bbb450a52f9a4dcb2de51061216a1d2727def" +source = "git+https://github.com/lexmount/stylo?rev=49506ee7365dac8708b314733bc2d99736c2a24a#49506ee7365dac8708b314733bc2d99736c2a24a" dependencies = [ "app_units", "cssparser", @@ -5112,7 +5112,7 @@ dependencies = [ [[package]] name = "stylo_static_prefs" version = "0.20.0" -source = "git+https://github.com/lexmount/stylo?branch=moli#3c8bbb450a52f9a4dcb2de51061216a1d2727def" +source = "git+https://github.com/lexmount/stylo?rev=49506ee7365dac8708b314733bc2d99736c2a24a#49506ee7365dac8708b314733bc2d99736c2a24a" dependencies = [ "toml", ] @@ -5130,7 +5130,7 @@ dependencies = [ [[package]] name = "stylo_traits" version = "0.20.0" -source = "git+https://github.com/lexmount/stylo?branch=moli#3c8bbb450a52f9a4dcb2de51061216a1d2727def" +source = "git+https://github.com/lexmount/stylo?rev=49506ee7365dac8708b314733bc2d99736c2a24a#49506ee7365dac8708b314733bc2d99736c2a24a" dependencies = [ "app_units", "bitflags", @@ -5430,7 +5430,7 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "to_shmem" version = "0.5.0" -source = "git+https://github.com/lexmount/stylo?branch=moli#3c8bbb450a52f9a4dcb2de51061216a1d2727def" +source = "git+https://github.com/lexmount/stylo?rev=49506ee7365dac8708b314733bc2d99736c2a24a#49506ee7365dac8708b314733bc2d99736c2a24a" dependencies = [ "cssparser", "servo_arc", @@ -5443,7 +5443,7 @@ dependencies = [ [[package]] name = "to_shmem_derive" version = "0.1.0" -source = "git+https://github.com/lexmount/stylo?branch=moli#3c8bbb450a52f9a4dcb2de51061216a1d2727def" +source = "git+https://github.com/lexmount/stylo?rev=49506ee7365dac8708b314733bc2d99736c2a24a#49506ee7365dac8708b314733bc2d99736c2a24a" dependencies = [ "darling", "proc-macro2", diff --git a/Cargo.toml b/Cargo.toml index bedd190767..f9924654de 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -103,13 +103,13 @@ curl-sys = { git = "https://github.com/lexmount/curl-rust", rev = "a0ea59f5ca1b1 cookie = { git = "https://github.com/ldm0/cookie-rs", branch = "priority" } v8 = { path = "vendor/v8-152.2.0" } deno_v8 = { path = "vendor/deno_v8-0.3.0" } -selectors = { git = "https://github.com/lexmount/stylo", branch = "moli" } -servo_arc = { git = "https://github.com/lexmount/stylo", branch = "moli" } -stylo = { git = "https://github.com/lexmount/stylo", branch = "moli" } -stylo_atoms = { git = "https://github.com/lexmount/stylo", branch = "moli" } -stylo_dom = { git = "https://github.com/lexmount/stylo", branch = "moli" } -stylo_static_prefs = { git = "https://github.com/lexmount/stylo", branch = "moli" } -stylo_traits = { git = "https://github.com/lexmount/stylo", branch = "moli" } +selectors = { git = "https://github.com/lexmount/stylo", rev = "49506ee7365dac8708b314733bc2d99736c2a24a" } +servo_arc = { git = "https://github.com/lexmount/stylo", rev = "49506ee7365dac8708b314733bc2d99736c2a24a" } +stylo = { git = "https://github.com/lexmount/stylo", rev = "49506ee7365dac8708b314733bc2d99736c2a24a" } +stylo_atoms = { git = "https://github.com/lexmount/stylo", rev = "49506ee7365dac8708b314733bc2d99736c2a24a" } +stylo_dom = { git = "https://github.com/lexmount/stylo", rev = "49506ee7365dac8708b314733bc2d99736c2a24a" } +stylo_static_prefs = { git = "https://github.com/lexmount/stylo", rev = "49506ee7365dac8708b314733bc2d99736c2a24a" } +stylo_traits = { git = "https://github.com/lexmount/stylo", rev = "49506ee7365dac8708b314733bc2d99736c2a24a" } taffy = { git = "https://github.com/ldm0/taffy", rev = "645bf5a49df607a8bff34ec90c5343901d04f10e" } [workspace.lints.clippy] diff --git a/moli-renderer-v8/src/native_bridge/context_host/host_environment.rs b/moli-renderer-v8/src/native_bridge/context_host/host_environment.rs index 0acb97ca40..2f9cc2a634 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/host_environment.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/host_environment.rs @@ -1478,26 +1478,13 @@ impl JsContextHost { .retained_stylesheet_query_snapshot_for_document(document) } - pub(crate) fn style_engine_author_shared_lock(&self) -> style::shared_lock::SharedRwLock { - self.style_engine.author_shared_lock() - } - - pub(crate) fn effective_keyframe_rule_texts_for_elements( + pub(crate) fn final_opacity_animation_names_for_elements( &self, document: DomHandle, - elements: impl IntoIterator, - ) -> ( - Vec>, - std::collections::HashMap, Vec>, - ) { - let tree_scopes: Vec<_> = elements - .into_iter() - .map(|element| self.dom_host().containing_shadow_root(element)) - .collect(); - let rules = self - .style_engine - .effective_keyframe_rule_texts_for_tree_scopes(document, tree_scopes.iter().copied()); - (tree_scopes, rules) + elements: impl IntoIterator)>, + ) -> std::collections::HashMap> { + self.style_engine + .final_opacity_animation_names_for_elements(self.dom_host(), document, elements) } #[cfg(test)] diff --git a/moli-renderer-v8/src/native_bridge/element.rs b/moli-renderer-v8/src/native_bridge/element.rs index b7b286e74d..51ec35f309 100644 --- a/moli-renderer-v8/src/native_bridge/element.rs +++ b/moli-renderer-v8/src/native_bridge/element.rs @@ -121,7 +121,7 @@ pub(crate) use shadow_dom::{ }; pub(crate) use styles::{ StyleMode, active_css_animation_transform_value, - computed_style_own_inherited_property_values_for_document_snapshot, + computed_style_own_visibility_values_for_document_snapshot, computed_style_properties_for_inspector_handle, computed_style_property_values_for_document_snapshot, css_animation_start_applies, cssom_style_entry_requires_structured_parser, diff --git a/moli-renderer-v8/src/native_bridge/element/styles.rs b/moli-renderer-v8/src/native_bridge/element/styles.rs index c532f2f4e6..ede75d2ee2 100644 --- a/moli-renderer-v8/src/native_bridge/element/styles.rs +++ b/moli-renderer-v8/src/native_bridge/element/styles.rs @@ -617,13 +617,11 @@ pub(crate) fn computed_style_property_values_for_document_snapshot( .collect() } -pub(crate) fn computed_style_own_inherited_property_values_for_document_snapshot( +pub(crate) fn computed_style_own_visibility_values_for_document_snapshot( host: &JsContextHost, handles: impl IntoIterator, - property: &str, ) -> Vec { let mut observation = StyleObservation::new(host); - let shared_lock = host.style_engine_author_shared_lock(); handles .into_iter() .map(|handle| { @@ -634,7 +632,7 @@ pub(crate) fn computed_style_own_inherited_property_values_for_document_snapshot }; observation .read(handle) - .computed_style_has_own_inherited_longhand_value(property, &shared_lock) + .computed_style_has_own_visibility_value() }) .collect() } diff --git a/moli-renderer-v8/src/native_bridge/element/styles/declaration/values.rs b/moli-renderer-v8/src/native_bridge/element/styles/declaration/values.rs index 13773ed135..9b1c3925a0 100644 --- a/moli-renderer-v8/src/native_bridge/element/styles/declaration/values.rs +++ b/moli-renderer-v8/src/native_bridge/element/styles/declaration/values.rs @@ -224,14 +224,12 @@ impl<'a> ComputedStyleRead<'a> { Some(facts) } - pub(in crate::native_bridge::element) fn computed_style_has_own_inherited_longhand_value( + pub(in crate::native_bridge::element) fn computed_style_has_own_visibility_value( &self, - property: &str, - shared_lock: &style::shared_lock::SharedRwLock, ) -> bool { self.stylo_style .as_ref() - .is_some_and(|style| style.has_own_inherited_longhand_value(property, shared_lock)) + .is_some_and(StyloComputedStyleSnapshot::has_own_visibility_value) } pub(crate) fn computed_values(&self) -> Option> { diff --git a/moli-renderer-v8/src/runtime/page_dump.rs b/moli-renderer-v8/src/runtime/page_dump.rs index 16845bf53b..7c0e6e9c9a 100644 --- a/moli-renderer-v8/src/runtime/page_dump.rs +++ b/moli-renderer-v8/src/runtime/page_dump.rs @@ -97,40 +97,20 @@ impl PageVm { ); let own_visibility_values = self .vm() - .computed_style_own_inherited_property_values_for_document_snapshot( - nodes.iter().copied(), - "visibility", - ); + .computed_style_own_visibility_values_for_document_snapshot(nodes.iter().copied()); let own_visibility: HashMap<_, _> = nodes.iter().copied().zip(own_visibility_values).collect(); - let (keyframe_scopes, keyframe_rules) = self - .vm() - .effective_keyframe_rule_texts_for_element_snapshots(nodes.iter().copied()); - let final_opacity_by_scope: HashMap<_, _> = keyframe_rules - .into_iter() - .map(|(scope, stylesheet_texts)| { - ( - scope, - visibility::final_opacity_animation_names( - stylesheet_texts.iter().map(String::as_str), - ), - ) - }) - .collect(); - let final_opacity_animations: HashMap<_, _> = nodes + let animation_candidates = nodes .iter() .copied() - .zip(keyframe_scopes) - .map(|(node, scope)| { - ( - node, - final_opacity_by_scope - .get(&scope) - .cloned() - .unwrap_or_default(), - ) - }) - .collect(); + .zip(values.iter()) + .filter(|(_, values)| values.get(2).is_some_and(|value| value == "0")) + .map(|(node, values)| { + (node, visibility::computed_property_animation_names(values)) + }); + let final_opacity_animations = self + .vm() + .final_opacity_animation_names_for_element_snapshots(animation_candidates); let styles = nodes .into_iter() .zip(values) diff --git a/moli-renderer-v8/src/runtime/page_dump/visibility.rs b/moli-renderer-v8/src/runtime/page_dump/visibility.rs index c0dadc9f56..e29e0fff44 100644 --- a/moli-renderer-v8/src/runtime/page_dump/visibility.rs +++ b/moli-renderer-v8/src/runtime/page_dump/visibility.rs @@ -415,10 +415,7 @@ fn has_bounded_final_opacity( values: &[String], final_opacity_animations: &HashSet, ) -> bool { - let names: Vec<_> = values - .get(14) - .map(|value| value.split(',').map(str::trim).collect()) - .unwrap_or_default(); + let names = parse_animation_names(values.get(14)); let durations: Vec<_> = values .get(15) .map(|value| value.split(',').map(str::trim).collect()) @@ -444,7 +441,7 @@ fn has_bounded_final_opacity( .map(|value| value.split(',').map(str::trim).collect()) .unwrap_or_default(); names.iter().enumerate().any(|(index, name)| { - final_opacity_animations.contains(*name) + final_opacity_animations.contains(name) && !durations.is_empty() && animation_duration_is_positive(durations[index % durations.len()]) && !fill_modes.is_empty() @@ -461,6 +458,23 @@ fn has_bounded_final_opacity( }) } +pub(super) fn computed_property_animation_names(values: &[String]) -> Vec { + parse_animation_names(values.get(8)) +} + +fn parse_animation_names(value: Option<&String>) -> Vec { + value + .map(|value| { + value + .split(',') + .map(str::trim) + .filter(|name| !name.is_empty() && *name != "none") + .map(str::to_owned) + .collect() + }) + .unwrap_or_default() +} + fn animation_duration_is_positive(duration: &str) -> bool { animation_time_seconds(duration).is_some_and(|duration| duration > 0.0) } @@ -477,79 +491,6 @@ fn animation_time_seconds(value: &str) -> Option { }) } -/// Return only animations whose declared final keyframe paints a nonzero -/// opacity. The caller still requires a finite, forward, running animation -/// before using that final authored state for the static document snapshot. -pub(super) fn final_opacity_animation_names<'a>( - stylesheets: impl IntoIterator, -) -> HashSet { - let mut result = HashSet::new(); - for source in stylesheets { - let folded = source.to_ascii_lowercase(); - for offset in code_marker_offsets(&folded, "@keyframes") { - let Some(relative_open) = source[offset..].find('{') else { - continue; - }; - let open = offset + relative_open; - let Some(close) = matching_delimiter(source, open, '{', '}') else { - continue; - }; - let Some(rule) = - moli_css_parse::parse_keyframes_rule_view_with_stylo(&source[offset..=close]) - else { - continue; - }; - let Some(canonical_open) = rule.css_text.find('{') else { - continue; - }; - let Some(canonical_close) = - matching_delimiter(&rule.css_text, canonical_open, '{', '}') - else { - continue; - }; - let name = rule.name; - if final_keyframe_reveals(&rule.css_text[canonical_open + 1..canonical_close]) { - result.insert(name); - } else { - result.remove(&name); - } - } - } - result -} - -fn final_keyframe_reveals(body: &str) -> bool { - let mut offset = 0; - while let Some(relative_open) = body[offset..].find('{') { - let open = offset + relative_open; - let selector = body[offset..open].trim(); - let Some(close) = matching_delimiter(body, open, '{', '}') else { - return false; - }; - let is_final = selector - .split(',') - .map(str::trim) - .any(|value| matches!(value, "to" | "100%" | "100.0%")); - if is_final { - for declaration in body[open + 1..close].split(';') { - let Some((name, value)) = declaration.split_once(':') else { - continue; - }; - if name.trim().eq_ignore_ascii_case("opacity") - && value - .trim() - .parse::() - .is_ok_and(|opacity| opacity > 0.0) - { - return true; - } - } - } - offset = close + 1; - } - false -} - fn target_is_disclosure( dom: &D, targets: &HashMap<&str, D::NodeId>, @@ -564,119 +505,6 @@ fn target_is_disclosure( }) } -fn code_marker_offsets(value: &str, marker: &str) -> Vec { - let mut result = Vec::new(); - let mut offset = 0; - let mut quote = None; - let mut escaped = false; - let mut line_comment = false; - let mut block_comment = false; - while offset < value.len() { - let tail = &value[offset..]; - if line_comment { - if tail.starts_with('\n') { - line_comment = false; - } - } else if block_comment { - if tail.starts_with("*/") { - block_comment = false; - offset += 2; - continue; - } - } else if let Some(delimiter) = quote { - let character = tail.chars().next().expect("offset is within value"); - if escaped { - escaped = false; - } else if character == '\\' { - escaped = true; - } else if character == delimiter { - quote = None; - } - } else if tail.starts_with("//") { - line_comment = true; - offset += 2; - continue; - } else if tail.starts_with("/*") { - block_comment = true; - offset += 2; - continue; - } else { - let character = tail.chars().next().expect("offset is within value"); - if matches!(character, '\'' | '"' | '`') { - quote = Some(character); - } else if tail.starts_with(marker) { - result.push(offset); - offset += marker.len(); - continue; - } - } - offset += tail - .chars() - .next() - .expect("offset is within value") - .len_utf8(); - } - result -} - -fn matching_delimiter(value: &str, start: usize, open: char, close: char) -> Option { - let mut depth = 0; - let mut quote = None; - let mut escaped = false; - let mut line_comment = false; - let mut block_comment = false; - let mut offset = start; - while offset < value.len() { - let tail = &value[offset..]; - if line_comment { - if tail.starts_with('\n') { - line_comment = false; - } - } else if block_comment { - if tail.starts_with("*/") { - block_comment = false; - offset += 2; - continue; - } - } else if let Some(delimiter) = quote { - let character = tail.chars().next().expect("offset is within value"); - if escaped { - escaped = false; - } else if character == '\\' { - escaped = true; - } else if character == delimiter { - quote = None; - } - } else if tail.starts_with("//") { - line_comment = true; - offset += 2; - continue; - } else if tail.starts_with("/*") { - block_comment = true; - offset += 2; - continue; - } else { - let character = tail.chars().next().expect("offset is within value"); - if matches!(character, '\'' | '"' | '`') { - quote = Some(character); - } else if character == open { - depth += 1; - } else if character == close { - depth -= 1; - if depth == 0 { - return Some(offset); - } - } - } - offset += tail - .chars() - .next() - .expect("offset is within value") - .len_utf8(); - } - None -} - fn px(value: &str) -> Option { value.strip_suffix("px")?.trim().parse().ok() } diff --git a/moli-renderer-v8/src/runtime/page_vm/tests/markdown_dump.rs b/moli-renderer-v8/src/runtime/page_vm/tests/markdown_dump.rs index 32283a9bb4..6cda0534b0 100644 --- a/moli-renderer-v8/src/runtime/page_vm/tests/markdown_dump.rs +++ b/moli-renderer-v8/src/runtime/page_vm/tests/markdown_dump.rs @@ -184,16 +184,23 @@ async fn markdown_uses_live_visibility_and_preserves_disclosure_content() { document.head.innerHTML = ''; document.body.innerHTML = `
+

Independent variable hidden content

+

Hidden fallback content

Invalid variable inherits the veil

Variable fallback inherits the veil

Origin revert inherits the veil

Layer revert inherits the veil

`; "#).unwrap(); + assert_eq!(page.vm_mut().eval("getComputedStyle(document.getElementById('variable-hidden')).visibility").unwrap(), "hidden"); + assert_eq!(page.vm_mut().eval("getComputedStyle(document.getElementById('fallback-hidden')).visibility").unwrap(), "hidden"); let inherited_visibility = page.render_page_dump(options(false)); for text in ["Invalid variable inherits the veil", "Variable fallback inherits the veil", "Origin revert inherits the veil", "Layer revert inherits the veil"] { assert!(inherited_visibility.contains(text), "missing {text}: {inherited_visibility}"); } + for text in ["Independent variable hidden content", "Hidden fallback content"] { + assert!(!inherited_visibility.contains(text), "leaked {text}: {inherited_visibility}"); + } page.vm_mut().eval(r#" document.body.style.visibility = ''; document.head.innerHTML = ''; @@ -216,6 +223,28 @@ async fn markdown_uses_live_visibility_and_preserves_disclosure_content() { let scoped_keyframes = page.render_page_dump(options(false)); assert!(scoped_keyframes.contains("Scoped count 2"), "{scoped_keyframes}"); assert!(!scoped_keyframes.contains("Scoped count 992"), "{scoped_keyframes}"); + page.vm_mut().eval(r#" + document.head.innerHTML = ''; + document.body.innerHTML = '

Layered count 992

Repeated count 992

'; + "#).unwrap(); + for id in ["layered-count", "repeated-count"] { + assert_eq!(page.vm_mut().eval(&format!("getComputedStyle(document.getElementById('{id}')).opacity")).unwrap(), "0"); + } + let keyframe_priority = page.render_page_dump(options(false)); + assert!(keyframe_priority.contains("Layered count 2"), "{keyframe_priority}"); + assert!(keyframe_priority.contains("Repeated count 2"), "{keyframe_priority}"); + assert!(!keyframe_priority.contains("Layered count 992"), "{keyframe_priority}"); + assert!(!keyframe_priority.contains("Repeated count 992"), "{keyframe_priority}"); + page.vm_mut().eval(r#" + const unrelatedRules = Array.from({length:128}, (_, index) => `@keyframes unused${index}{to{opacity:1}}`).join(''); + const unrelatedNodes = Array.from({length:128}, (_, index) => `hidden${index}`).join(''); + document.head.innerHTML = ``; + document.body.innerHTML = `

Scale ${unrelatedNodes}kept

`; + "#).unwrap(); + crate::style_engine::reset_final_opacity_animation_query_count_for_test(); + let scaled_keyframes = page.render_page_dump(options(false)); + assert!(scaled_keyframes.contains("Scale kept"), "{scaled_keyframes}"); + assert_eq!(crate::style_engine::final_opacity_animation_query_count_for_test(), 1); page.vm_mut().eval(r##" document.body.innerHTML = ` diff --git a/moli-renderer-v8/src/script_vm/layout.rs b/moli-renderer-v8/src/script_vm/layout.rs index 6bf8477e1b..b43e6ac01c 100644 --- a/moli-renderer-v8/src/script_vm/layout.rs +++ b/moli-renderer-v8/src/script_vm/layout.rs @@ -1,16 +1,13 @@ use super::*; impl ScriptVm { - pub(crate) fn effective_keyframe_rule_texts_for_element_snapshots( + pub(crate) fn final_opacity_animation_names_for_element_snapshots( &self, - elements: impl IntoIterator, - ) -> ( - Vec>, - std::collections::HashMap, Vec>, - ) { + elements: impl IntoIterator)>, + ) -> std::collections::HashMap> { let host = self._context_host.borrow(); let document = host.document_handle(); - host.effective_keyframe_rule_texts_for_elements(document, elements) + host.final_opacity_animation_names_for_elements(document, elements) } pub(crate) fn sync_live_document_style_sources(&mut self) { @@ -32,15 +29,13 @@ impl ScriptVm { ) } - pub(crate) fn computed_style_own_inherited_property_values_for_document_snapshot( + pub(crate) fn computed_style_own_visibility_values_for_document_snapshot( &self, handles: impl IntoIterator, - property: &str, ) -> Vec { - crate::native_bridge::element::computed_style_own_inherited_property_values_for_document_snapshot( + crate::native_bridge::element::computed_style_own_visibility_values_for_document_snapshot( &self._context_host.borrow(), handles, - property, ) } diff --git a/moli-renderer-v8/src/style_engine/computed.rs b/moli-renderer-v8/src/style_engine/computed.rs index 4702687db4..1e8af6294d 100644 --- a/moli-renderer-v8/src/style_engine/computed.rs +++ b/moli-renderer-v8/src/style_engine/computed.rs @@ -1,10 +1,12 @@ use std::collections::HashSet; +#[cfg(test)] +use std::sync::atomic::{AtomicUsize, Ordering}; use dom::ElementState as StyloElementState; use style::{ Atom, animation::DocumentAnimationSet, - applicable_declarations::{CascadePriority, RevertKind}, + computed_value_flags::ComputedValueFlags, context::{ QuirksMode, RegisteredSpeculativePainter, RegisteredSpeculativePainters, SharedStyleContext, StyleContext, StyleSystemOptions, ThreadLocalStyleContext, @@ -12,7 +14,7 @@ use style::{ data::ElementStyles, dom::{TElement, TNode}, properties::{ - CSSWideKeyword, ComputedValues, PropertyDeclaration, PropertyDeclarationId, PropertyId, + ComputedValues, PropertyId, longhands::{ text_wrap_mode::computed_value::T as StyloTextWrapMode, visibility::computed_value::T as ComputedVisibility, @@ -21,8 +23,10 @@ use style::{ parse_style_attribute, }, selector_parser::{PseudoElement, SnapshotMap}, + servo::animation::final_keyframe_opacity, servo_arc::Arc as ServoArc, shared_lock::StylesheetGuards, + style_resolver::{PseudoElementResolution, StyleResolverForElement}, stylesheets::{CssRuleType, UrlExtraData}, stylist::RuleInclusion, thread_state::{self, ThreadState}, @@ -44,6 +48,19 @@ use crate::{ use moli_selector::StyloElement; +#[cfg(test)] +static FINAL_OPACITY_ANIMATION_QUERY_COUNT: AtomicUsize = AtomicUsize::new(0); + +#[cfg(test)] +pub(crate) fn reset_final_opacity_animation_query_count_for_test() { + FINAL_OPACITY_ANIMATION_QUERY_COUNT.store(0, Ordering::Relaxed); +} + +#[cfg(test)] +pub(crate) fn final_opacity_animation_query_count_for_test() -> usize { + FINAL_OPACITY_ANIMATION_QUERY_COUNT.load(Ordering::Relaxed) +} + use super::{ FullStyleWorldSnapshot, MoliStyleEngine, PreparedStyleWorldUpdate, StyleViewport, StyleWorldUpdate, StyleWorldUpdatePlan, @@ -283,65 +300,12 @@ impl StyloComputedStyleSnapshot { serialize_raw_computed_property(&self.primary, property_id) } - /// Whether this element's cascade supplies a value for an inherited - /// longhand instead of taking the value from its parent. - /// - /// Computed values alone cannot distinguish `visibility:hidden` declared - /// on an element from the same value inherited from an ancestor. Consumers - /// that selectively expose an otherwise hidden subtree need that source - /// distinction, but should not parse inline style or stylesheet text a - /// second time. - pub(crate) fn has_own_inherited_longhand_value( - &self, - property: &str, - shared_lock: &style::shared_lock::SharedRwLock, - ) -> bool { - let Ok(property_id) = PropertyId::parse_enabled_for_all_content(property) else { - return false; - }; - let Some(longhand) = property_id.longhand_id() else { - return false; - }; - let guard = shared_lock.read(); - let guards = StylesheetGuards::same(&guard); - let mut reverted: Option<(CascadePriority, RevertKind)> = None; - for node in self.primary.rules().self_and_ancestors() { - let priority = node.cascade_priority(); - if reverted.is_some_and(|(reverted_priority, kind)| { - !reverted_priority.allows_when_reverted(&priority, kind) - }) { - continue; - } - let Some(source) = node.style_source() else { - continue; - }; - for (declaration, importance) in source - .read(node.cascade_level().guard(&guards)) - .declaration_importance_iter() - .rev() - { - if importance.important() != node.importance().important() - || declaration.id() != PropertyDeclarationId::Longhand(longhand) - { - continue; - } - match declaration.get_css_wide_keyword() { - Some(CSSWideKeyword::Inherit | CSSWideKeyword::Unset) => return false, - Some(keyword) if keyword.revert_kind().is_some() => { - reverted = Some((priority, keyword.revert_kind().unwrap())); - continue; - } - _ => {} - } - // Variable substitution happens inside Stylo's cascade. A raw - // `WithVariables` declaration cannot establish an independent - // hidden boundary here: it may be invalid at computed-value - // time or resolve to `inherit`. Treat it conservatively as - // inherited instead of recreating the substitution engine. - return !matches!(declaration, PropertyDeclaration::WithVariables(_)); - } - } - false + /// Whether Stylo's completed cascade established `visibility` on this + /// element instead of inheriting it from the parent. + pub(crate) fn has_own_visibility_value(&self) -> bool { + self.primary + .flags + .contains(ComputedValueFlags::HAS_OWN_VISIBILITY) } pub(crate) fn resolved_property_value(&self, property: &str) -> Option { @@ -370,6 +334,82 @@ pub(super) fn retained_current_element_state( })? } +pub(super) fn retained_final_opacity_animation_names( + engine: &MoliStyleEngine, + host: &DomHost, + document: DomHandle, + elements: impl IntoIterator)>, +) -> std::collections::HashMap> { + let elements: Vec<_> = elements.into_iter().collect(); + if elements.is_empty() { + return Default::default(); + } + let Some(world) = engine.document_worlds.active_world(document) else { + return Default::default(); + }; + engine.dom_adapter.with_bound_host(host, |dom_adapter| { + install_shadow_cascade_data_for_resolution(&world, dom_adapter); + let shared_lock = dom_adapter.shared_lock().clone(); + let guard = shared_lock.read(); + let guards = StylesheetGuards::same(&guard); + let snapshot_map = SnapshotMap::new(); + let empty_painters = EmptyRegisteredSpeculativePainters; + world.document_state.with_retained_style_system(|retained| { + let shared = SharedStyleContext { + stylist: &retained.stylist, + visited_styles_enabled: false, + options: StyleSystemOptions::default(), + guards, + current_time_for_animations: 0.0, + traversal_flags: TraversalFlags::empty(), + snapshot_map: &snapshot_map, + animations: DocumentAnimationSet::default(), + registered_speculative_painters: &empty_painters, + }; + let _layout_thread_state = StyloLayoutThreadStateGuard::enter(); + let mut thread_local = ThreadLocalStyleContext::new(); + let mut context = StyleContext { + shared: &shared, + thread_local: &mut thread_local, + }; + elements + .into_iter() + .filter_map(|(handle, names)| { + let element = dom_adapter.element(host, handle)?; + let base_style = element.borrow_data()?.styles.primary().clone(); + let revealing = names + .into_iter() + .filter(|name| { + #[cfg(test)] + FINAL_OPACITY_ANIMATION_QUERY_COUNT.fetch_add(1, Ordering::Relaxed); + let atom = Atom::from(name.as_str()); + let Some(animation) = retained.stylist.lookup_keyframes(&atom, element) + else { + return false; + }; + let mut resolver = StyleResolverForElement::new( + element, + &mut context, + RuleInclusion::All, + PseudoElementResolution::IfApplicable, + ); + let opacity = final_keyframe_opacity( + element, + animation, + &shared, + &base_style, + &mut resolver, + ); + opacity.is_some_and(|opacity| opacity > 0.0) + }) + .collect::>(); + (!revealing.is_empty()).then_some((handle, revealing)) + }) + .collect() + }) + }) +} + pub(super) fn computed_style_property_value( engine: &MoliStyleEngine, host: &DomHost, diff --git a/moli-renderer-v8/src/style_engine/mod.rs b/moli-renderer-v8/src/style_engine/mod.rs index 2fa684fe61..8f6d49a0ff 100644 --- a/moli-renderer-v8/src/style_engine/mod.rs +++ b/moli-renderer-v8/src/style_engine/mod.rs @@ -35,6 +35,11 @@ mod cache; mod cause; mod cleanup; mod computed; +#[cfg(test)] +pub(crate) use computed::{ + final_opacity_animation_query_count_for_test, + reset_final_opacity_animation_query_count_for_test, +}; mod document_world; mod drain; mod eligibility; @@ -300,58 +305,13 @@ impl MoliStyleEngine { }) } - pub(crate) fn effective_keyframe_rule_texts_for_tree_scopes( + pub(crate) fn final_opacity_animation_names_for_elements( &self, + host: &DomHost, document: DomHandle, - tree_scopes: impl IntoIterator>, - ) -> HashMap, Vec> { - let Some(world) = self.document_worlds.active_world(document) else { - return HashMap::new(); - }; - world - .document_state - .try_with_retained_style_system(|retained| { - let device = retained.stylist.device(); - let mut document_rules = Vec::new(); - let document_custom_media = retained - .stylist - .cascade_data() - .borrow_for_origin(style::stylesheets::Origin::Author) - .custom_media_map(); - for entry in retained.document_stylesheets.entries() { - document_rules.extend(stylesheet::native_effective_keyframe_rule_texts( - entry.stylesheet(), - device, - document_custom_media, - )); - } - let mut shadow_rules = HashMap::new(); - for scope in &retained.shadow_scopes { - let rules = shadow_rules.entry(scope.root()).or_insert_with(Vec::new); - for entry in scope.active_stylesheets().entries() { - rules.extend(stylesheet::native_effective_keyframe_rule_texts( - entry.stylesheet(), - device, - scope.author_styles().data.custom_media_map(), - )); - } - } - tree_scopes - .into_iter() - .collect::>() - .into_iter() - .map(|tree_scope| { - let mut rules = document_rules.clone(); - if let Some(tree_scope) = tree_scope - && let Some(scoped_rules) = shadow_rules.get(&tree_scope) - { - rules.extend(scoped_rules.iter().cloned()); - } - (tree_scope, rules) - }) - .collect() - }) - .unwrap_or_default() + elements: impl IntoIterator)>, + ) -> HashMap> { + computed::retained_final_opacity_animation_names(self, host, document, elements) } #[cfg(debug_assertions)] diff --git a/moli-renderer-v8/src/style_engine/stylesheet.rs b/moli-renderer-v8/src/style_engine/stylesheet.rs index acff86ae15..aa403d1887 100644 --- a/moli-renderer-v8/src/style_engine/stylesheet.rs +++ b/moli-renderer-v8/src/style_engine/stylesheet.rs @@ -218,25 +218,6 @@ pub(super) fn native_effective_font_face_rule_addresses( native_effective_font_face_rule_addresses_with_guard(stylesheet, device, custom_media, &guard) } -pub(super) fn native_effective_keyframe_rule_texts( - stylesheet: &DocumentStyleSheet, - device: &Device, - custom_media: &CustomMediaMap, -) -> Vec { - let guard = stylesheet.0.shared_lock.read(); - if !stylesheet.enabled() || !stylesheet.is_effective_for_device(device, custom_media, &guard) { - return Vec::new(); - } - stylesheet - .contents(&guard) - .effective_rules(device, custom_media, &guard) - .filter_map(|rule| match rule { - CssRule::Keyframes(rule) => Some(rule.read_with(&guard).to_css_string(&guard)), - _ => None, - }) - .collect() -} - fn native_effective_font_face_rule_addresses_with_guard( stylesheet: &DocumentStyleSheet, device: &Device,