fix(content): consume resolved style semantics

This commit is contained in:
lanyue-llk
2026-09-29 23:19:54 +08:00
parent 7060a0b35b
commit 8d6574d45f
13 changed files with 205 additions and 409 deletions
Generated
+13 -13
View File
@@ -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",
+7 -7
View File
@@ -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]
@@ -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<Item = DomHandle>,
) -> (
Vec<Option<DomHandle>>,
std::collections::HashMap<Option<DomHandle>, Vec<String>>,
) {
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<Item = (DomHandle, Vec<String>)>,
) -> std::collections::HashMap<DomHandle, std::collections::HashSet<String>> {
self.style_engine
.final_opacity_animation_names_for_elements(self.dom_host(), document, elements)
}
#[cfg(test)]
@@ -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,
@@ -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<Item = DomHandle>,
property: &str,
) -> Vec<bool> {
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()
}
@@ -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<ServoArc<ComputedValues>> {
+10 -30
View File
@@ -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)
@@ -415,10 +415,7 @@ fn has_bounded_final_opacity(
values: &[String],
final_opacity_animations: &HashSet<String>,
) -> 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<String> {
parse_animation_names(values.get(8))
}
fn parse_animation_names(value: Option<&String>) -> Vec<String> {
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<f32> {
})
}
/// 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<Item = &'a str>,
) -> HashSet<String> {
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::<f32>()
.is_ok_and(|opacity| opacity > 0.0)
{
return true;
}
}
}
offset = close + 1;
}
false
}
fn target_is_disclosure<D: Dom + ?Sized>(
dom: &D,
targets: &HashMap<&str, D::NodeId>,
@@ -564,119 +505,6 @@ fn target_is_disclosure<D: Dom + ?Sized>(
})
}
fn code_marker_offsets(value: &str, marker: &str) -> Vec<usize> {
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<usize> {
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<f32> {
value.strip_suffix("px")?.trim().parse().ok()
}
@@ -184,16 +184,23 @@ async fn markdown_uses_live_visibility_and_preserves_disclosure_content() {
document.head.innerHTML = '<style>.reverted{visibility:hidden}.reverted{visibility:revert}@layer base{.layered{visibility:hidden}.layered{visibility:revert-layer}}</style>';
document.body.innerHTML = `
<main>
<p id="variable-hidden" style="--v:hidden;visibility:var(--v)">Independent variable hidden content</p>
<p id="fallback-hidden" style="visibility:var(--missing, hidden)">Hidden fallback content</p>
<p style="visibility:var(--missing)">Invalid variable inherits the veil</p>
<p style="visibility:var(--missing, inherit)">Variable fallback inherits the veil</p>
<p class="reverted">Origin revert inherits the veil</p>
<p class="layered">Layer revert inherits the veil</p>
</main>`;
"#).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 = '<style>@keyframes state{to{opacity:0}}@media (min-width:999999px){@keyframes state{to{opacity:1}}}</style>';
@@ -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 = '<style>@layer low,high;@layer high{@keyframes layered{to{opacity:0}}}@layer low{@keyframes layered{to{opacity:1}}}@keyframes repeated{to{opacity:1}to{opacity:0}}</style>';
document.body.innerHTML = '<p>Layered count <span id="layered-count" style="opacity:0;animation:layered 1s forwards">99</span>2</p><p>Repeated count <span id="repeated-count" style="opacity:0;animation:repeated 1s forwards">99</span>2</p>';
"#).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) => `<span style="opacity:0">hidden${index}</span>`).join('');
document.head.innerHTML = `<style>${unrelatedRules}@keyframes used{to{opacity:1}}</style>`;
document.body.innerHTML = `<p>Scale ${unrelatedNodes}<span style="opacity:0;animation:used 1s forwards">kept</span></p>`;
"#).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 = `
<button onclick="console.log('tracking')">Save</button>
+6 -11
View File
@@ -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<Item = DomHandle>,
) -> (
Vec<Option<DomHandle>>,
std::collections::HashMap<Option<DomHandle>, Vec<String>>,
) {
elements: impl IntoIterator<Item = (DomHandle, Vec<String>)>,
) -> std::collections::HashMap<DomHandle, std::collections::HashSet<String>> {
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<Item = DomHandle>,
property: &str,
) -> Vec<bool> {
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,
)
}
+101 -61
View File
@@ -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<String> {
@@ -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<Item = (DomHandle, Vec<String>)>,
) -> std::collections::HashMap<DomHandle, HashSet<String>> {
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::<HashSet<_>>();
(!revealing.is_empty()).then_some((handle, revealing))
})
.collect()
})
})
}
pub(super) fn computed_style_property_value(
engine: &MoliStyleEngine,
host: &DomHost,
+10 -50
View File
@@ -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<Item = Option<DomHandle>>,
) -> HashMap<Option<DomHandle>, Vec<String>> {
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::<HashSet<_>>()
.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<Item = (DomHandle, Vec<String>)>,
) -> HashMap<DomHandle, HashSet<String>> {
computed::retained_final_opacity_animation_names(self, host, document, elements)
}
#[cfg(debug_assertions)]
@@ -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<String> {
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,