fix(cssom): serialize computed background layers

Serialize computed background from the shared retained style read, using the image list to coordinate layers, repeating or truncating other component lists, and emitting color only in the final layer. Include computed initial components instead of using specified-value shorthand elision.

When Stylo cannot serialize a computed shorthand, reify the shared CSSOM result as one property-associated opaque CSSStyleValue. Preserve native realm allocation, snapshot immutability and longhand Typed OM types. Cover multiple layers, currentcolor, list coordination, private native writes and cross-realm reads.
This commit is contained in:
ldm0
2026-10-21 16:43:26 +08:00
committed by Donough Liu
parent 8c5ffea821
commit 330a1bc99c
5 changed files with 116 additions and 0 deletions
@@ -1,5 +1,7 @@
use super::*;
mod background;
pub(in crate::native_bridge::element::styles) fn style_property_value_with_context(
runtime: &JsContextHost,
handle: DomHandle,
@@ -105,6 +107,9 @@ pub(super) fn computed_style_property_value_after_style_update(
if property == "font-variant" {
return computed_font_variant_shorthand_value(runtime, handle, resolution);
}
if property == "background" {
return background::computed_shorthand(runtime, handle, resolution);
}
if property == "mask" {
return computed_mask_shorthand_value(runtime, handle, resolution);
}
@@ -0,0 +1,67 @@
use super::*;
/// CSSOM serializes the coordinated background layers, including initial
/// components. Specified-value shorthand serialization would omit those parts.
pub(super) fn computed_shorthand(
runtime: &JsContextHost,
handle: DomHandle,
resolution: StyleResolutionContext<'_>,
) -> String {
let [
Some(images),
Some(repeats),
Some(attachments),
Some(positions_x),
Some(positions_y),
Some(sizes),
Some(origins),
Some(clips),
] = [
"background-image",
"background-repeat",
"background-attachment",
"background-position-x",
"background-position-y",
"background-size",
"background-origin",
"background-clip",
]
.map(|property| {
top_level_comma_separated_component_values(
&resolution.computed_property(runtime, handle, property),
)
.filter(|values| !values.is_empty())
})
else {
return String::new();
};
let color = resolution.computed_property(runtime, handle, "background-color");
images
.iter()
.enumerate()
.map(|(index, image)| {
// The image list controls the layer count. Other lists repeat or
// truncate to that count, and only the final layer carries color.
let color = if index + 1 == images.len() {
format!("{color} ")
} else {
String::new()
};
format!(
"{color}{image} {} {} {} {} / {} {} {}",
layer_value(&repeats, index),
layer_value(&attachments, index),
layer_value(&positions_x, index),
layer_value(&positions_y, index),
layer_value(&sizes, index),
layer_value(&origins, index),
layer_value(&clips, index),
)
})
.collect::<Vec<_>>()
.join(", ")
}
fn layer_value(values: &[String], index: usize) -> &str {
&values[index % values.len()]
}
@@ -58,6 +58,7 @@ mod advanced_style_values;
mod computed_style_access;
mod content_and_invalidation;
mod cross_document_and_animations;
mod css_computed_background;
mod css_computed_map_enumeration;
mod nested_document_invalidation;
mod properties_and_selectors;
@@ -0,0 +1,33 @@
(() => {
const check = (ok, message) => {if (!ok) throw new Error(message);};
const cases = [
['', 'rgba(0, 0, 0, 0) none repeat scroll 0% 0% / auto padding-box border-box'],
['background:blue', 'rgb(0, 0, 255) none repeat scroll 0% 0% / auto padding-box border-box'],
['color:red;background:currentcolor', 'rgb(255, 0, 0) none repeat scroll 0% 0% / auto padding-box border-box'],
['background:blue content-box', 'rgb(0, 0, 255) none repeat scroll 0% 0% / auto content-box content-box'],
['background:blue;background-repeat:no-repeat,repeat', 'rgb(0, 0, 255) none no-repeat scroll 0% 0% / auto padding-box border-box'],
['background-image:none,none;background-color:blue', 'none repeat scroll 0% 0% / auto padding-box border-box, rgb(0, 0, 255) none repeat scroll 0% 0% / auto padding-box border-box'],
['background-image:none,none;background-size:10px,20px,30px', 'none repeat scroll 0% 0% / 10px auto padding-box border-box, rgba(0, 0, 0, 0) none repeat scroll 0% 0% / 20px auto padding-box border-box'],
['background:blue;background-position-x:10%;background-position-y:20%', 'rgb(0, 0, 255) none repeat scroll 10% 20% / auto padding-box border-box'],
['background-image:linear-gradient(red,blue),linear-gradient(black,white);background-repeat:no-repeat', 'linear-gradient(rgb(255, 0, 0), rgb(0, 0, 255)) no-repeat scroll 0% 0% / auto padding-box border-box, rgba(0, 0, 0, 0) linear-gradient(rgb(0, 0, 0), rgb(255, 255, 255)) no-repeat scroll 0% 0% / auto padding-box border-box'],
];
for (const [text, expected] of cases) {
const element = document.createElement('div');
element.style.cssText = text;
document.body.append(element);
const style = getComputedStyle(element);
check(style.background === expected, text + ': ' + style.background);
element.remove();
}
const element = document.createElement('div');
element.style.background = 'blue'; document.body.append(element);
const computed = getComputedStyle(element);
const previous = computed.background;
element.style.backgroundColor = 'red';
check(String(previous).startsWith('rgb(0, 0, 255) '), 'retained value is immutable');
check(computed.background.startsWith('rgb(255, 0, 0) '), 'retained declaration observes mutations');
check(computed.backgroundImage === 'none', 'longhand value preserved');
element.remove();
check(computed.background === '', 'inactive owner is empty');
return true;
})()
@@ -0,0 +1,10 @@
use super::*;
#[test]
fn css_computed_background_preserves_coordinated_layers() {
let mut vm = new_parsed_test_vm("https://computed-background.test/", "<!doctype html><body>");
assert_eq!(
vm.eval(include_str!("css_computed_background.js")).unwrap(),
"true"
);
}