From 2c92bd5859b93250e44433bd6f2d4e0744e68864 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20=C5=A0olt=C3=A9s?= Date: Sat, 19 Sep 2026 23:43:15 +0200 Subject: [PATCH] Implement typed registered custom property animations --- ...egistered-custom-property-animation-748.md | 38 +++++ .../native/html_parser/src/lib.rs | 1 + .../native/webscene_css_application.h | 9 ++ .../native/webscene_css_cascade_reset.h | 5 +- .../native/webscene_css_native_cascade.h | 46 +++++- .../native/webscene_css_pseudo_application.h | 6 + .../native/webscene_css_state.h | 19 +++ .../native/webscene_css_stylesheet.h | 5 + .../native/webscene_css_stylesheet_data.h | 1 + .../native/webscene_css_stylesheet_owner.h | 4 + .../native/webscene_css_stylesheet_sink.h | 100 +++++++++++- .../native/webscene_css_transitions.h | 87 +++++++++- .../native/webscene_css_variables.h | 84 +++++++++- .../native/webscene_native_dom.h | 17 +- .../native/webscene_native_dom_animations.inc | 150 +++++++++++++++++- .../native/webscene_native_dom_metrics.inc | 9 +- .../webscene_stylesheet_cssom_compatibility.h | 23 +++ .../native/webscene_v8_runtime.cpp | 29 ++++ .../webscene_v8_runtime_cache_and_frames.inc | 1 + .../webscene_v8_runtime_css_cascade.inc | 42 ++++- .../webscene_v8_runtime_css_parsing.inc | 12 ++ .../native/webscene_v8_runtime_dom_core.inc | 4 + .../native/webscene_v8_runtime_lifecycle.inc | 12 ++ .../native/webscene_v8_runtime_navigation.inc | 1 + .../native/webscene_v8_runtime_state.inc | 2 + .../native/webscene_v8_runtime_style.inc | 13 +- .../tests/css_parser_tests.cpp | 22 +++ ...ative_v8_runtime_animation_cssom_tests.inc | 88 ++++++++++ .../tests/native_v8_runtime_tests.cpp | 1 + tests/NativeWeb/css_service.cpp | 86 ++++++++++ tests/WebPlatformSubset/capabilities.json | 8 + ...-registered-custom-property-animation.html | 58 +++++++ .../webscene-component-profile.json | 7 + 33 files changed, 954 insertions(+), 36 deletions(-) create mode 100644 docs/validation/css-registered-custom-property-animation-748.md create mode 100644 tests/WebPlatformSubset/contracts/css-registered-custom-property-animation.html diff --git a/docs/validation/css-registered-custom-property-animation-748.md b/docs/validation/css-registered-custom-property-animation-748.md new file mode 100644 index 000000000..f66ac98db --- /dev/null +++ b/docs/validation/css-registered-custom-property-animation-748.md @@ -0,0 +1,38 @@ +# Registered custom-property animation contract (#748) + +WebScene retains document-level `@property` registrations for the `` and +`` grammars used by unchanged Code OSS. Registrations are bounded to +64 per document; names and values are bounded to 128 bytes. A keyframe definition +retains at most eight registered custom-property tracks and 64 stops per track, +inside the existing eight-animation admission budget. + +The shared cssparser stream now emits `@property` descriptors as declarations. +The prepared stylesheet and live runtime retain syntax, inheritance, initial +value, owner identity, and typed keyframe stops. Invalid or incomplete rules are +rejected atomically. Non-inheriting registrations materialize their initial value +only on elements whose declarations consume them; computed style supplies the +same initial value without allocating custom-property state on unrelated nodes. +Invalid authored values remain visible through inline CSSOM and compute to the +registered initial value. + +The host-clock sampler interpolates registered values only while an admitted +track contributes. It updates the computed custom-property value and retained +background/mask/border-image tokens only when the serialized sample changes. +Paused and settled tracks request no idle frames. Stylesheet removal, navigation, +cache reset, and document teardown release registrations and sampled track state. + +Coverage: + +- `webscene_css_parser_tests` proves the Rust syntax stream emits all three + descriptors for `@property`. +- `native_web_css_service` proves preparation, ownership, non-inheriting initial + values, zero allocation across 256 unrelated nodes, midpoint interpolation, + dependent gradient-token updates, and removal. +- `test_registered_custom_property_keyframes_update_dependent_paint` covers live + CSSOM identity, percentage/angle sampling, forwards fill, and computed paint. +- `css-registered-custom-property-animation.html` is the browser/native candidate + for CSSPropertyRule reflection and a deterministic paused midpoint. + +Conic-gradient rasterization, border-image gradient rasterization, arbitrary +registered syntaxes, `CSS.registerProperty()`, and additive composition remain +separate paint and Properties & Values work. diff --git a/experiments/WebScene.NativeEngine.Probe/native/html_parser/src/lib.rs b/experiments/WebScene.NativeEngine.Probe/native/html_parser/src/lib.rs index 9246b2913..1b0a95e82 100644 --- a/experiments/WebScene.NativeEngine.Probe/native/html_parser/src/lib.rs +++ b/experiments/WebScene.NativeEngine.Probe/native/html_parser/src/lib.rs @@ -964,6 +964,7 @@ mod css_syntax { let mut declarations = 0usize; if prelude.name.eq_ignore_ascii_case("font-face") || prelude.name.eq_ignore_ascii_case("page") + || prelude.name.eq_ignore_ascii_case("property") { declarations = parse_css_stream_declaration_list( input, diff --git a/experiments/WebScene.NativeEngine.Probe/native/webscene_css_application.h b/experiments/WebScene.NativeEngine.Probe/native/webscene_css_application.h index 46f81190e..f1318dfc7 100644 --- a/experiments/WebScene.NativeEngine.Probe/native/webscene_css_application.h +++ b/experiments/WebScene.NativeEngine.Probe/native/webscene_css_application.h @@ -284,6 +284,15 @@ void apply_declaration(native_document& document,dom_node& node, decision.semantic_slice="unresolved custom property at computed-value time"; return; } + if (contains_variable + && (declaration.name == "background" + || declaration.name == "background-image" + || declaration.name == "mask" + || declaration.name == "mask-image" + || declaration.name == "border-image")) { + node.style.mutable_custom_properties().dependent_values[ + declaration.name] = declaration.value; + } on_resolved(contains_variable); apply_resolved_declaration( document, diff --git a/experiments/WebScene.NativeEngine.Probe/native/webscene_css_cascade_reset.h b/experiments/WebScene.NativeEngine.Probe/native/webscene_css_cascade_reset.h index 2a10727b4..23c0fba6e 100644 --- a/experiments/WebScene.NativeEngine.Probe/native/webscene_css_cascade_reset.h +++ b/experiments/WebScene.NativeEngine.Probe/native/webscene_css_cascade_reset.h @@ -9,7 +9,8 @@ namespace webscene_native::css { // Clear previously cascaded fields before matching a new rule set. Authored // inline metadata remains owned by the node and is preserved/reseeded here. inline void reset_cascaded_style(dom_node& node, - const std::unordered_map& variables) + const std::unordered_map& variables, + const std::unordered_map* registrations = nullptr) { node.style.important_property_mask = 0; node.style.important_margin_sides = 0; @@ -232,7 +233,7 @@ inline void reset_cascaded_style(dom_node& node, node.style.word_spacing = 0; node.style.word_spacing_specified = false; } - css::seed_inline_custom_properties(node); + css::seed_inline_custom_properties(node, registrations); node.style.clear_pseudo_elements(); // Recompute stylesheet visibility from the current class/selector set. // React reuses toolbar nodes while replacing their responsive classes; diff --git a/experiments/WebScene.NativeEngine.Probe/native/webscene_css_native_cascade.h b/experiments/WebScene.NativeEngine.Probe/native/webscene_css_native_cascade.h index 9ef8f49d1..b0c7e5122 100644 --- a/experiments/WebScene.NativeEngine.Probe/native/webscene_css_native_cascade.h +++ b/experiments/WebScene.NativeEngine.Probe/native/webscene_css_native_cascade.h @@ -39,15 +39,42 @@ bool apply_native_cascade(native_document& document,dom_node& node, if(declaration.important) authored.important_declarations.insert(declaration.name); } } - reset_cascaded_style(node,variables); + reset_cascaded_style( + node, variables, &sheets.state().registered_custom_properties); auto indices=sheets.candidates(node,focused); auto matched=match_candidates(document,node,sheets.state().rules,indices, [&](const auto& subject,const auto& rule,const auto&) { return query.matches_prepared(subject,rule.payload->compiled_pseudo_origin); }, [&](const auto& subject,const auto& rule) { return query.matches_prepared(subject,rule.compiled_selector()); }); + seed_relevant_registered_custom_properties( + node, sheets.state().registered_custom_properties, + [&](const std::string& name, const std::string& variable_token) { + const auto references = [&](const css_rule* rule) { + return std::any_of( + rule->declarations().begin(), + rule->declarations().end(), + [&](const css_declaration& declaration) { + return declaration.name == name + || declaration.value.find(variable_token) + != std::string::npos; + }); + }; + return std::any_of( + matched.ordinary.begin(), matched.ordinary.end(), references) + || std::any_of( + matched.pseudo.begin(), matched.pseudo.end(), + [&](const auto& entry) { return references(entry.second); }); + }); const cascaded_rule_order cascade_order(matched.ordinary); apply_matched_declarations(node,cascade_order,[&](const css_declaration& declaration,bool inline_origin) { property_result result; - apply_declaration(document,node,declaration,variables,inline_origin,result,load_svg,[](bool) {}); + if (declaration.name.starts_with("--")) { + apply_custom_property( + node, declaration, + &sheets.state().registered_custom_properties); + result.classification = "supported"; + } else { + apply_declaration(document,node,declaration,variables,inline_origin,result,load_svg,[](bool) {}); + } observe(declaration,result); }); if(inline_attributes) { @@ -94,14 +121,23 @@ bool apply_native_cascade(native_document& document,dom_node& node, node.style.mutable_before_pseudo().layout=previous.before_pseudo().layout; if(node.style.after_pseudo().generated && previous.after_pseudo().generated) node.style.mutable_after_pseudo().layout=previous.after_pseudo().layout; - configure_keyframes(node.style,sheets.state().opacity_keyframes); + configure_keyframes( + node.style, + sheets.state().opacity_keyframes, + sheets.state().registered_custom_properties); if(node.style.has_pseudo_elements()) { auto& before=node.style.mutable_before_pseudo(); auto& after=node.style.mutable_after_pseudo(); if(before.has_animation_data()) - configure_keyframes(before.mutable_animations(),sheets.state().opacity_keyframes); + configure_keyframes( + before.mutable_animations(), + sheets.state().opacity_keyframes, + sheets.state().registered_custom_properties); if(after.has_animation_data()) - configure_keyframes(after.mutable_animations(),sheets.state().opacity_keyframes); + configure_keyframes( + after.mutable_animations(), + sheets.state().opacity_keyframes, + sheets.state().registered_custom_properties); } document.update_style_animations(node); } diff --git a/experiments/WebScene.NativeEngine.Probe/native/webscene_css_pseudo_application.h b/experiments/WebScene.NativeEngine.Probe/native/webscene_css_pseudo_application.h index b7e8921b9..4cb3ae764 100644 --- a/experiments/WebScene.NativeEngine.Probe/native/webscene_css_pseudo_application.h +++ b/experiments/WebScene.NativeEngine.Probe/native/webscene_css_pseudo_application.h @@ -297,6 +297,12 @@ void apply_pseudo_declaration(dom_node& node,node_style::pseudo_element& pseudo, return; } const auto& name = declaration.name; + if (contains_variable + && (name == "background" || name == "background-image" + || name == "mask" || name == "mask-image" + || name == "border-image")) { + pseudo.variable_dependent_values[name] = declaration.value; + } on_resolved(contains_variable); if (name == "font-family" && pseudo.font_family_important diff --git a/experiments/WebScene.NativeEngine.Probe/native/webscene_css_state.h b/experiments/WebScene.NativeEngine.Probe/native/webscene_css_state.h index 5ccdb45aa..a3c7aaf26 100644 --- a/experiments/WebScene.NativeEngine.Probe/native/webscene_css_state.h +++ b/experiments/WebScene.NativeEngine.Probe/native/webscene_css_state.h @@ -252,11 +252,28 @@ struct compiled_css_selector final { }; struct css_opacity_keyframes final { + struct custom_property_stop final { + float offset{0}; + std::string value; + }; std::vector opacity_stops; std::vector translation_stops; std::vector scale_stops; std::vector rotation_stops; std::vector filter_stops; + std::unordered_map> + custom_property_stops; + }; + + enum class registered_property_syntax : uint8_t { angle, percentage }; + + struct registered_custom_property final { + std::string name; + registered_property_syntax syntax{registered_property_syntax::angle}; + std::string initial_value; + float initial_number{0}; + bool inherits{false}; + uint32_t stylesheet_owner_id{0}; }; struct css_inheritance_candidate_rule final { @@ -310,6 +327,8 @@ struct compiled_css_selector final { std::vector active_dependencies; std::unordered_map variables; std::unordered_set important_variables; + std::unordered_map + registered_custom_properties; std::unordered_map cascade_layer_orders; std::unordered_map> stylesheet_layer_names; diff --git a/experiments/WebScene.NativeEngine.Probe/native/webscene_css_stylesheet.h b/experiments/WebScene.NativeEngine.Probe/native/webscene_css_stylesheet.h index 50b8c1592..50ef73887 100644 --- a/experiments/WebScene.NativeEngine.Probe/native/webscene_css_stylesheet.h +++ b/experiments/WebScene.NativeEngine.Probe/native/webscene_css_stylesheet.h @@ -31,6 +31,11 @@ std::optional prepare_stylesheet(std::string_view text, output.cascade_layers.push_back(name); return static_cast(output.cascade_layers.size()); } + void register_custom_property(const registered_custom_property& property) { + if (output.registered_custom_properties.size() < 64U) { + output.registered_custom_properties.push_back(property); + } + } void record_feature(std::string_view, const std::string& feature, std::string_view classification, const std::string& detail, std::string_view) { // Font bytes/registration are handled by a resource host, which this diff --git a/experiments/WebScene.NativeEngine.Probe/native/webscene_css_stylesheet_data.h b/experiments/WebScene.NativeEngine.Probe/native/webscene_css_stylesheet_data.h index b0a7efc37..9ce09493a 100644 --- a/experiments/WebScene.NativeEngine.Probe/native/webscene_css_stylesheet_data.h +++ b/experiments/WebScene.NativeEngine.Probe/native/webscene_css_stylesheet_data.h @@ -14,6 +14,7 @@ struct prepared_stylesheet { std::string source_address; std::vector> rules; std::unordered_map keyframes; + std::vector registered_custom_properties; std::vector diagnostics; // First-appearance order. Empty entries are distinct anonymous layers; // named entries are unified by the consuming document across stylesheets. diff --git a/experiments/WebScene.NativeEngine.Probe/native/webscene_css_stylesheet_owner.h b/experiments/WebScene.NativeEngine.Probe/native/webscene_css_stylesheet_owner.h index b7e0bc369..4e472ef4d 100644 --- a/experiments/WebScene.NativeEngine.Probe/native/webscene_css_stylesheet_owner.h +++ b/experiments/WebScene.NativeEngine.Probe/native/webscene_css_stylesheet_owner.h @@ -51,6 +51,10 @@ class stylesheet_owner { } for(const auto& [name,definition]:entry.sheet.keyframes) state_.opacity_keyframes[name]=definition; + for(auto property:entry.sheet.registered_custom_properties) { + property.stylesheet_owner_id=entry.id; + state_.registered_custom_properties[property.name]=std::move(property); + } } } public: diff --git a/experiments/WebScene.NativeEngine.Probe/native/webscene_css_stylesheet_sink.h b/experiments/WebScene.NativeEngine.Probe/native/webscene_css_stylesheet_sink.h index a18669cb9..31adb98da 100644 --- a/experiments/WebScene.NativeEngine.Probe/native/webscene_css_stylesheet_sink.h +++ b/experiments/WebScene.NativeEngine.Probe/native/webscene_css_stylesheet_sink.h @@ -84,6 +84,11 @@ class stylesheet_sink final : public css_syntax_sink { if (name == "keyframes" || name == "-webkit-keyframes") { current.keyframes = true; current.prelude = std::string(prelude); + } else if (name == "property" && stack_.empty() && has_block) { + current.property_rule = true; + current.prelude = trim_value(prelude); + current.declarations.reserve(3U); + current.children_active = false; } else if (name == "font-face") { current.children_active = false; owner_.record_feature( @@ -185,7 +190,8 @@ class stylesheet_sink final : public css_syntax_sink { if (stack_.empty()) return false; auto& current = stack_.back(); ++current.observed_declarations; - if (current.active && current.kind == css_syntax_style_rule) { + if (current.active + && (current.kind == css_syntax_style_rule || current.property_rule)) { append_declaration( current.declarations, name, value, important); } @@ -251,6 +257,20 @@ class stylesheet_sink final : public css_syntax_sink { completed_keyframes_.emplace_back( std::move(current.prelude), std::move(current.keyframe_definition)); + } else if (current.property_rule) { + const auto parsed = parse_registered_property(current); + if (parsed.has_value()) { + owner_.register_custom_property(*parsed); + owner_.record_feature( + "css", "at-rule:@property", "supported", + "bounded and registrations", + "stylesheet-parser"); + } else { + owner_.record_feature( + "css", "at-rule:@property", "invalid-authoring", + "requires a custom name, syntax, inherits and typed initial-value", + "stylesheet-parser"); + } } return true; } @@ -321,8 +341,86 @@ class stylesheet_sink final : public css_syntax_sink { bool active{false}; bool children_active{false}; bool keyframes{false}; + bool property_rule{false}; }; + static std::optional parse_typed_number( + std::string_view value, + registered_property_syntax syntax) + { + const auto source = ascii_lower(trim_value(value)); + const auto unit = syntax == registered_property_syntax::angle + ? std::string_view{"deg"} : std::string_view{"%"}; + if (!source.ends_with(unit)) return std::nullopt; + const auto number = source.substr(0U, source.size() - unit.size()); + if (number.empty() || number.size() > 64U) return std::nullopt; + char* end = nullptr; + const auto parsed = std::strtof(number.c_str(), &end); + if (end != number.c_str() + number.size() || !std::isfinite(parsed)) { + return std::nullopt; + } + return parsed; + } + + static std::optional parse_registered_property( + const frame& source) + { + if (!source.prelude.starts_with("--") || source.prelude.size() > 128U + || source.prelude.size() < 3U) return std::nullopt; + for (const auto value : std::string_view(source.prelude).substr(2U)) { + const auto byte = static_cast(value); + if (std::isalnum(byte) == 0 && value != '-' && value != '_') { + return std::nullopt; + } + } + const css_declaration* syntax = nullptr; + const css_declaration* inherits = nullptr; + const css_declaration* initial = nullptr; + for (const auto& declaration : source.declarations) { + if (declaration.important) return std::nullopt; + if (declaration.name == "syntax") { + if (syntax != nullptr) return std::nullopt; + syntax = &declaration; + } else if (declaration.name == "inherits") { + if (inherits != nullptr) return std::nullopt; + inherits = &declaration; + } else if (declaration.name == "initial-value") { + if (initial != nullptr) return std::nullopt; + initial = &declaration; + } + } + if (syntax == nullptr || inherits == nullptr || initial == nullptr) { + return std::nullopt; + } + auto syntax_value = trim_value(syntax->value); + if (syntax_value.size() >= 2U + && ((syntax_value.front() == '\'' && syntax_value.back() == '\'') + || (syntax_value.front() == '"' && syntax_value.back() == '"'))) { + syntax_value = syntax_value.substr(1U, syntax_value.size() - 2U); + } + registered_custom_property result; + result.name = source.prelude; + if (syntax_value == "") { + result.syntax = registered_property_syntax::angle; + } else if (syntax_value == "") { + result.syntax = registered_property_syntax::percentage; + } else { + return std::nullopt; + } + const auto inherits_value = ascii_lower(trim_value(inherits->value)); + if (inherits_value != "true" && inherits_value != "false") { + return std::nullopt; + } + result.inherits = inherits_value == "true"; + const auto number = parse_typed_number(initial->value, result.syntax); + if (!number.has_value() || initial->value.size() > 128U) { + return std::nullopt; + } + result.initial_number = *number; + result.initial_value = trim_value(initial->value); + return result; + } + frame* nearest_style_frame() noexcept { for (auto iterator = stack_.rbegin(); iterator != stack_.rend(); ++iterator) { diff --git a/experiments/WebScene.NativeEngine.Probe/native/webscene_css_transitions.h b/experiments/WebScene.NativeEngine.Probe/native/webscene_css_transitions.h index ee3c9371a..894b4e27c 100644 --- a/experiments/WebScene.NativeEngine.Probe/native/webscene_css_transitions.h +++ b/experiments/WebScene.NativeEngine.Probe/native/webscene_css_transitions.h @@ -549,8 +549,27 @@ inline std::string serialize_animation_shorthand( return result; } +inline std::optional parse_registered_custom_number( + std::string_view value, + registered_property_syntax syntax) +{ + const auto source = ascii_lower(trim_value(value)); + const auto unit = syntax == registered_property_syntax::angle + ? std::string_view{"deg"} : std::string_view{"%"}; + if (!source.ends_with(unit)) return std::nullopt; + const auto number = source.substr(0U, source.size() - unit.size()); + if (number.empty()) return std::nullopt; + char* end = nullptr; + const auto parsed = std::strtof(number.c_str(), &end); + if (end != number.c_str() + number.size() || !std::isfinite(parsed)) { + return std::nullopt; + } + return parsed; +} + inline void configure_keyframes(node_style::animation_data& animations, - const std::unordered_map& definitions) + const std::unordered_map& definitions, + const std::unordered_map& registrations = {}) { animations.keyframe_animations.clear(); const auto names = split_css_component_list(animations.animation_name_value, ','); @@ -625,13 +644,34 @@ inline void configure_keyframes(node_style::animation_data& animations, track.scale_keyframes = definition->second.scale_stops; track.rotation_keyframes = definition->second.rotation_stops; track.filter_keyframes = definition->second.filter_stops; + for (const auto& [property_name, raw_stops] + : definition->second.custom_property_stops) { + const auto registration = registrations.find(property_name); + if (registration == registrations.end()) continue; + node_style::custom_property_animation custom; + custom.name = property_name; + custom.unit = registration->second.syntax + == registered_property_syntax::angle + ? "deg" : "%"; + for (const auto& raw : raw_stops) { + const auto number = parse_registered_custom_number( + raw.value, registration->second.syntax); + if (number.has_value()) { + custom.keyframes.push_back({raw.offset, *number}); + } + } + if (custom.keyframes.size() >= 2U) { + track.custom_property_animations.push_back(std::move(custom)); + } + } } const auto has_supported_effect = track.opacity_keyframes.size() >= 2U || track.translation_keyframes.size() >= 2U || track.scale_keyframes.size() >= 2U || track.rotation_keyframes.size() >= 2U - || track.filter_keyframes.size() >= 2U; + || track.filter_keyframes.size() >= 2U + || !track.custom_property_animations.empty(); if (has_supported_effect) { std::ostringstream signature; signature << normalized_name << '|' << track.duration_ms << '|' @@ -656,6 +696,12 @@ inline void configure_keyframes(node_style::animation_data& animations, signature << "|r" << stop.offset << ':' << stop.degrees; for (const auto& stop : track.filter_keyframes) signature << "|f" << stop.offset << ':' << stop.value; + for (const auto& custom : track.custom_property_animations) { + signature << "|c" << custom.name << ':' << custom.unit; + for (const auto& stop : custom.keyframes) { + signature << ',' << stop.offset << ':' << stop.value; + } + } track.signature = signature.str(); } animations.keyframe_animations.push_back(std::move(track)); @@ -663,10 +709,11 @@ inline void configure_keyframes(node_style::animation_data& animations, } inline void configure_keyframes(node_style& style, - const std::unordered_map& definitions) + const std::unordered_map& definitions, + const std::unordered_map& registrations = {}) { if (!style.has_animation_data()) return; - configure_keyframes(style.mutable_animations(), definitions); + configure_keyframes(style.mutable_animations(), definitions, registrations); } inline void append_keyframe( @@ -686,6 +733,12 @@ inline void append_keyframe( declarations.begin(), declarations.end(), [](const auto& declaration) { return declaration.name == "filter"; }); + std::vector custom_properties; + for (const auto& declaration : declarations) { + if (declaration.name.starts_with("--")) { + custom_properties.push_back(&declaration); + } + } const auto transform_value = transform == declarations.end() ? std::optional{} : parse_keyframe_transform(transform->value); @@ -715,6 +768,22 @@ inline void append_keyframe( if (filter != declarations.end()) { definition.filter_stops.push_back({offset, filter->value}); } + for (const auto* custom : custom_properties) { + if (custom->name.size() > 128U || custom->value.size() > 128U) { + continue; + } + auto found = definition.custom_property_stops.find(custom->name); + if (found == definition.custom_property_stops.end()) { + if (definition.custom_property_stops.size() >= 8U) continue; + found = definition.custom_property_stops.emplace( + custom->name, + std::vector{}) + .first; + } + if (found->second.size() < 64U) { + found->second.push_back({offset, custom->value}); + } + } } } @@ -744,6 +813,10 @@ inline void finish_keyframes( normalize(definition.scale_stops); normalize(definition.rotation_stops); normalize(definition.filter_stops); + for (auto& [name, stops] : definition.custom_property_stops) { + static_cast(name); + normalize(stops); + } if (definition.rotation_stops.size() == 1U && definition.rotation_stops.front().offset > 0) { definition.rotation_stops.insert(definition.rotation_stops.begin(), {0, 0}); @@ -763,7 +836,11 @@ inline void finish_keyframes( || definition.translation_stops.size() >= 2U || definition.scale_stops.size() >= 2U || definition.rotation_stops.size() >= 2U - || definition.filter_stops.size() >= 2U) { + || definition.filter_stops.size() >= 2U + || std::any_of( + definition.custom_property_stops.begin(), + definition.custom_property_stops.end(), + [](const auto& entry) { return entry.second.size() >= 2U; })) { definitions[ascii_lower(trim_value(std::move(name)))] = std::move(definition); } diff --git a/experiments/WebScene.NativeEngine.Probe/native/webscene_css_variables.h b/experiments/WebScene.NativeEngine.Probe/native/webscene_css_variables.h index ceea223f3..f992eb697 100644 --- a/experiments/WebScene.NativeEngine.Probe/native/webscene_css_variables.h +++ b/experiments/WebScene.NativeEngine.Probe/native/webscene_css_variables.h @@ -112,19 +112,80 @@ inline std::string resolve_value(const dom_node& node,std::string value, return resolved.valid ? resolved.value : std::string{}; } -inline void seed_inline_custom_properties(dom_node& node) { - node.style.clear_custom_properties(); - node.authored_style().for_each_declaration( - [&](const std::string& name, const std::string& value) { +inline bool registered_custom_value_is_valid( + std::string_view value, + registered_property_syntax syntax); + +inline void seed_inline_custom_properties( + dom_node& node, + const std::unordered_map* registrations = nullptr) { + node.style.clear_custom_properties(); + node.authored_style().for_each_declaration( + [&](const std::string& name, const std::string& value) { if (!name.starts_with("--")) return; auto& custom = node.style.mutable_custom_properties(); - custom.values[name] = value; + auto computed_value = value; + if (registrations != nullptr) { + const auto registration = registrations->find(name); + if (registration != registrations->end() + && !registered_custom_value_is_valid( + value, registration->second.syntax)) { + computed_value = registration->second.initial_value; + } + } + custom.values[name] = std::move(computed_value); if (node.authored_style().important_declarations.contains(name)) { custom.important.insert(name); } }); } -inline bool apply_custom_property(dom_node& node,const css_declaration& declaration) { +inline bool registered_custom_value_is_valid( + std::string_view value, + registered_property_syntax syntax) +{ + const auto source = ascii_lower(trim_value(value)); + const auto unit = syntax == registered_property_syntax::angle + ? std::string_view{"deg"} : std::string_view{"%"}; + if (!source.ends_with(unit)) return false; + const auto number = source.substr(0U, source.size() - unit.size()); + if (number.empty()) return false; + char* end = nullptr; + const auto parsed = std::strtof(number.c_str(), &end); + return end == number.c_str() + number.size() && std::isfinite(parsed); +} + +template +inline void seed_relevant_registered_custom_properties( + dom_node& node, + const std::unordered_map& registrations, + RuleReferences&& rule_references) +{ + for (const auto& [name, registration] : registrations) { + if (registration.inherits + || node.style.custom_properties().values.contains(name)) continue; + const auto variable_token = "var(" + name; + auto relevant = node.authored_style().declarations.contains(name); + if (!relevant) { + for (const auto& [property, value] + : node.authored_style().declarations) { + static_cast(property); + if (value.find(variable_token) != std::string::npos) { + relevant = true; + break; + } + } + } + relevant = relevant || rule_references(name, variable_token); + if (!relevant) continue; + node.style.mutable_custom_properties().values.emplace( + name, registration.initial_value); + } +} + +inline bool apply_custom_property( + dom_node& node, + const css_declaration& declaration, + const std::unordered_map* registrations = nullptr) { const auto& name=declaration.name; if(!name.starts_with("--")) return false; const auto& custom=node.style.custom_properties(); @@ -133,7 +194,16 @@ inline bool apply_custom_property(dom_node& node,const css_declaration& declarat const bool inline_important=existing_inline && node.authored_style().important_declarations.contains(name); if(inline_important || (!declaration.important && (existing_important || existing_inline))) return false; auto& values=node.style.mutable_custom_properties(); - values.values[name]=declaration.value; + auto value=declaration.value; + if (registrations != nullptr) { + const auto registration=registrations->find(name); + if (registration != registrations->end() + && !registered_custom_value_is_valid( + value, registration->second.syntax)) { + value=registration->second.initial_value; + } + } + values.values[name]=std::move(value); if(declaration.important) values.important.insert(name); else values.important.erase(name); return true; diff --git a/experiments/WebScene.NativeEngine.Probe/native/webscene_native_dom.h b/experiments/WebScene.NativeEngine.Probe/native/webscene_native_dom.h index c3f681f7a..ee8fc65c0 100644 --- a/experiments/WebScene.NativeEngine.Probe/native/webscene_native_dom.h +++ b/experiments/WebScene.NativeEngine.Probe/native/webscene_native_dom.h @@ -189,6 +189,15 @@ struct node_style final { float offset{0}; std::string value; }; + struct custom_property_keyframe final { + float offset{0}; + float value{0}; + }; + struct custom_property_animation final { + std::string name; + std::string unit; + std::vector keyframes; + }; struct transition_timing final { enum class function_kind : uint8_t { cubic_bezier, steps }; @@ -255,6 +264,7 @@ struct node_style final { std::vector scale_keyframes; std::vector rotation_keyframes; std::vector filter_keyframes; + std::vector custom_property_animations; float duration_ms{0}; float delay_ms{0}; float iterations{1}; @@ -308,6 +318,7 @@ struct node_style final { struct custom_property_data final { std::unordered_map values; std::unordered_set important; + std::unordered_map dependent_values; }; const custom_property_data& custom_properties() const noexcept @@ -331,7 +342,8 @@ struct node_style final { { return custom_property_state != nullptr && (!custom_property_state->values.empty() - || !custom_property_state->important.empty()); + || !custom_property_state->important.empty() + || !custom_property_state->dependent_values.empty()); } const custom_property_data* custom_property_data_identity() const noexcept @@ -535,6 +547,7 @@ struct node_style final { std::string font_family; bool font_family_important{false}; std::string content; + std::unordered_map variable_dependent_values; bool generated{false}; bool display_none{false}; bool visibility_hidden{false}; @@ -1556,6 +1569,8 @@ struct dom_node final { std::vector filter_keyframes; std::vector filter_underlying; std::vector painted_filter_functions; + std::unordered_map painted_custom_properties; + std::unordered_map underlying_custom_properties; double started_ms{0}; double paused_at_ms{0}; float painted_opacity{1}; diff --git a/experiments/WebScene.NativeEngine.Probe/native/webscene_native_dom_animations.inc b/experiments/WebScene.NativeEngine.Probe/native/webscene_native_dom_animations.inc index f341d8e60..95826a16c 100644 --- a/experiments/WebScene.NativeEngine.Probe/native/webscene_native_dom_animations.inc +++ b/experiments/WebScene.NativeEngine.Probe/native/webscene_native_dom_animations.inc @@ -571,12 +571,24 @@ void native_document::update_style_animations(dom_node& node) runtime.filter_keyframes.clear(); runtime.filter_underlying.clear(); runtime.painted_filter_functions.clear(); + runtime.painted_custom_properties.clear(); + runtime.underlying_custom_properties.clear(); runtime.painted_opacity = node.style.opacity; runtime.painted_translation_x = node.style.transform_translate_x; runtime.painted_translation_y = node.style.transform_translate_y; runtime.painted_scale_x = node.style.transform_scale_x; runtime.painted_scale_y = node.style.transform_scale_y; runtime.painted_rotation_degrees = node.style.transform_rotate_degrees; + for (const auto& custom : configured.custom_property_animations) { + const auto found = node.style.custom_properties().values.find( + custom.name); + const auto underlying = found == node.style.custom_properties().values.end() + ? std::string{} : found->second; + runtime.underlying_custom_properties.emplace( + custom.name, underlying); + runtime.painted_custom_properties.emplace( + custom.name, underlying); + } if (!configured.filter_keyframes.empty()) { auto valid = configured.filter_keyframes.size() >= 2U; size_t longest = 0U; @@ -608,7 +620,9 @@ void native_document::update_style_animations(dom_node& node) if (configured.opacity_keyframes.size() < 2U && configured.translation_keyframes.size() < 2U && configured.scale_keyframes.size() < 2U - && configured.rotation_keyframes.size() < 2U && !valid) { + && configured.rotation_keyframes.size() < 2U + && configured.custom_property_animations.empty() + && !valid) { runtime.active = false; } } @@ -706,12 +720,27 @@ void native_document::update_style_animations(dom_node& node) runtime.filter_keyframes.clear(); runtime.filter_underlying.clear(); runtime.painted_filter_functions.clear(); + runtime.painted_custom_properties.clear(); + runtime.underlying_custom_properties.clear(); runtime.painted_opacity = pseudo.opacity; runtime.painted_translation_x = {0, length_unit::pixels}; runtime.painted_translation_y = {0, length_unit::pixels}; runtime.painted_scale_x = 1; runtime.painted_scale_y = 1; runtime.painted_rotation_degrees = 0; + for (const auto& custom : source.custom_property_animations) { + auto underlying = std::string{}; + const auto registration_value = + node.style.custom_properties().values.find(custom.name); + if (registration_value + != node.style.custom_properties().values.end()) { + underlying = registration_value->second; + } + runtime.underlying_custom_properties.emplace( + custom.name, underlying); + runtime.painted_custom_properties.emplace( + custom.name, underlying); + } if (!source.filter_keyframes.empty()) { auto valid = source.filter_keyframes.size() >= 2U; size_t longest = 0U; @@ -743,7 +772,9 @@ void native_document::update_style_animations(dom_node& node) if (source.opacity_keyframes.size() < 2U && source.translation_keyframes.size() < 2U && source.scale_keyframes.size() < 2U - && source.rotation_keyframes.size() < 2U && !valid) { + && source.rotation_keyframes.size() < 2U + && source.custom_property_animations.empty() + && !valid) { runtime.active = false; } } @@ -1161,6 +1192,48 @@ bool native_document::advance_animations() noexcept }; auto advanced = false; auto layout_changed = false; + const auto serialize_custom_number = [](float value, std::string_view unit) { + std::ostringstream stream; + stream << value << unit; + return stream.str(); + }; + const auto replace_token = [](std::string& value, + const std::string& previous, + const std::string& current) { + if (previous.empty() || previous == current) return false; + auto changed = false; + size_t offset = 0U; + while ((offset = value.find(previous, offset)) != std::string::npos) { + value.replace(offset, previous.size(), current); + offset += current.size(); + changed = true; + } + return changed; + }; + const auto replace_node_dependent_tokens = [&](dom_node& node, + const std::string& previous, + const std::string& current) { + auto changed = false; + if (node.style.has_background_image_data()) { + changed = replace_token( + node.style.mutable_background_image().image_value, + previous, current) || changed; + } + if (auto* textual = node.style.mutable_textual_if_present(); + textual != nullptr) { + for (auto& [name, value] : textual->effect_values) { + static_cast(name); + changed = replace_token(value, previous, current) || changed; + } + } + return changed; + }; + const auto replace_pseudo_dependent_tokens = [&](node_style::pseudo_element& pseudo, + const std::string& previous, + const std::string& current) { + return replace_token( + pseudo.background_image.image_value, previous, current); + }; for (auto& owner : nodes_) { auto& node = *owner; auto* animation_pointer = node.animation_runtime(); @@ -1679,6 +1752,40 @@ bool native_document::advance_animations() noexcept winning_filter = &runtime.painted_filter_functions; animation.keyframe_filter_override = true; } + for (const auto& custom : configured.custom_property_animations) { + if (custom.keyframes.size() < 2U) continue; + auto sampled = runtime.underlying_custom_properties[custom.name]; + if (contributes) { + auto right = std::lower_bound( + custom.keyframes.begin(), custom.keyframes.end(), progress, + [](const auto& stop, float value) { + return stop.offset < value; + }); + auto number = 0.0F; + if (right == custom.keyframes.begin()) { + number = right->value; + } else if (right == custom.keyframes.end()) { + number = custom.keyframes.back().value; + } else { + const auto& from = *(right - 1); + const auto local = std::clamp( + (progress - from.offset) + / std::max(0.0001F, right->offset - from.offset), + 0.0F, 1.0F); + const auto eased = eased_segment(local); + number = from.value + (right->value - from.value) * eased; + } + sampled = serialize_custom_number(number, custom.unit); + } + auto& previous = runtime.painted_custom_properties[custom.name]; + if (sampled != previous) { + const auto token_changed = replace_node_dependent_tokens( + node, previous, sampled); + node.style.mutable_custom_properties().values[custom.name] = sampled; + previous = sampled; + advanced = advanced || token_changed || contributes; + } + } advanced = advanced || (contributes && keyframe_paint_visible); if (completed && !runtime.end_event_sent) { if (!animation_events_) { @@ -1903,7 +2010,7 @@ bool native_document::advance_animations() noexcept } const auto advance_pseudo = [&] ( - const node_style::pseudo_element& pseudo, + node_style::pseudo_element& pseudo, dom_node::animation_runtime_data::pseudo_animation_runtime& pseudo_runtime, std::string_view pseudo_name) { if (!pseudo.generated || pseudo.display_none) { @@ -2110,6 +2217,39 @@ bool native_document::advance_animations() noexcept winning_filter = &runtime.painted_filter_functions; filter_override = true; } + for (const auto& custom : configured.custom_property_animations) { + if (custom.keyframes.size() < 2U) continue; + auto sampled = runtime.underlying_custom_properties[custom.name]; + if (contributes) { + auto right = std::lower_bound( + custom.keyframes.begin(), custom.keyframes.end(), progress, + [](const auto& stop, float value) { + return stop.offset < value; + }); + auto number = 0.0F; + if (right == custom.keyframes.begin()) { + number = right->value; + } else if (right == custom.keyframes.end()) { + number = custom.keyframes.back().value; + } else { + const auto& from = *(right - 1); + const auto eased = eased_segment(std::clamp( + (progress - from.offset) + / std::max(0.0001F, right->offset - from.offset), + 0.0F, 1.0F)); + number = from.value + + (right->value - from.value) * eased; + } + sampled = serialize_custom_number(number, custom.unit); + } + auto& previous = runtime.painted_custom_properties[custom.name]; + if (sampled != previous) { + const auto token_changed = replace_pseudo_dependent_tokens( + pseudo, previous, sampled); + previous = sampled; + advanced = advanced || token_changed || contributes; + } + } advanced = advanced || (contributes && keyframe_paint_visible); if (completed && !runtime.end_event_sent) { if (!animation_events_) { @@ -2141,9 +2281,9 @@ bool native_document::advance_animations() noexcept } }; advance_pseudo( - node.style.before_pseudo(), animation.before_pseudo, "::before"); + node.style.mutable_before_pseudo(), animation.before_pseudo, "::before"); advance_pseudo( - node.style.after_pseudo(), animation.after_pseudo, "::after"); + node.style.mutable_after_pseudo(), animation.after_pseudo, "::after"); } // Opacity, colors, filters, scale, and rotation are paint-only. // Their interpolated values are consumed directly by build_scene(), so they diff --git a/experiments/WebScene.NativeEngine.Probe/native/webscene_native_dom_metrics.inc b/experiments/WebScene.NativeEngine.Probe/native/webscene_native_dom_metrics.inc index 1e5fcfe49..8b21e109d 100644 --- a/experiments/WebScene.NativeEngine.Probe/native/webscene_native_dom_metrics.inc +++ b/experiments/WebScene.NativeEngine.Probe/native/webscene_native_dom_metrics.inc @@ -92,7 +92,7 @@ native_document::allocation_metrics native_document::read_allocation_metrics() c const auto& custom = node->style.custom_properties(); ++result.custom_property_node_count; result.custom_property_entry_count += - custom.values.size(); + custom.values.size() + custom.dependent_values.size(); const auto map_hash_bytes = [](const auto& values) { using value_type = typename std::decay_t::value_type; return values.bucket_count() * sizeof(void*) @@ -101,7 +101,8 @@ native_document::allocation_metrics native_document::read_allocation_metrics() c result.custom_property_storage_bytes += sizeof(node_style::custom_property_data) + 2U * sizeof(void*) + map_hash_bytes(custom.values) - + map_hash_bytes(custom.important); + + map_hash_bytes(custom.important) + + map_hash_bytes(custom.dependent_values); for (const auto& [name, value] : custom.values) { result.custom_property_storage_bytes += name.capacity() + value.capacity() + 2U; @@ -109,6 +110,10 @@ native_document::allocation_metrics native_document::read_allocation_metrics() c for (const auto& name : custom.important) { result.custom_property_storage_bytes += name.capacity() + 1U; } + for (const auto& [name, value] : custom.dependent_values) { + result.custom_property_storage_bytes += + name.capacity() + value.capacity() + 2U; + } } if (node != nullptr && node->style.has_background_image_data()) { background_allocations.insert( diff --git a/experiments/WebScene.NativeEngine.Probe/native/webscene_stylesheet_cssom_compatibility.h b/experiments/WebScene.NativeEngine.Probe/native/webscene_stylesheet_cssom_compatibility.h index ecd68018f..c3b949d17 100644 --- a/experiments/WebScene.NativeEngine.Probe/native/webscene_stylesheet_cssom_compatibility.h +++ b/experiments/WebScene.NativeEngine.Probe/native/webscene_stylesheet_cssom_compatibility.h @@ -39,6 +39,7 @@ inline constexpr std::array cssCompatibilityScriptParts{R"J const layerStatementRuleInstances = new WeakSet(); const keyframesRuleInstances = new WeakSet(); const keyframeRuleInstances = new WeakSet(); + const propertyRuleInstances = new WeakSet(); const nestedDeclarationsInstances = new WeakSet(); const mediaListInstances = new WeakSet(); const ruleListInstances = new WeakSet(); @@ -124,6 +125,8 @@ inline constexpr std::array cssCompatibilityScriptParts{R"J 'CSSKeyframesRule', keyframesRuleInstances, CSSRuleInterface); const CSSKeyframeRuleInterface = interfaceConstructor( 'CSSKeyframeRule', keyframeRuleInstances, CSSRuleInterface); + const CSSPropertyRuleInterface = interfaceConstructor( + 'CSSPropertyRule', propertyRuleInstances, CSSRuleInterface); const CSSNestedDeclarationsInterface = interfaceConstructor( 'CSSNestedDeclarations', nestedDeclarationsInstances, CSSRuleInterface); const MediaListInterface = interfaceConstructor('MediaList', mediaListInstances); @@ -145,6 +148,7 @@ inline constexpr std::array cssCompatibilityScriptParts{R"J ['CSSLayerStatementRule', CSSLayerStatementRuleInterface], ['CSSKeyframesRule', CSSKeyframesRuleInterface], ['CSSKeyframeRule', CSSKeyframeRuleInterface], + ['CSSPropertyRule', CSSPropertyRuleInterface], ['CSSNestedDeclarations', CSSNestedDeclarationsInterface], ['MediaList', MediaListInterface], ['CSSRuleList', CSSRuleListInterface] @@ -980,6 +984,7 @@ R"JS( source = source.slice(identifier[0].length).trim(); const containerMatch = /^@container(?:\s+([^\{]*?))?\s*\{/i.exec(parsed.cssText); const layerBlockMatch = /^@layer(?:\s+([^\{]*?))?\s*\{/i.exec(parsed.cssText); const keyframesSpec = parseKeyframesPrelude(parsed); + const propertyMatch = /^@property\s+(--[-_a-zA-Z0-9]+)\s*\{/i.exec(parsed.cssText); const layerStatementNames = parsed.body === undefined ? parseLayerStatementNames(parsed.cssText) : undefined; const groupingMatch = mediaMatch || supportsMatch || containerMatch || layerBlockMatch; @@ -1039,6 +1044,9 @@ R"JS( if (importSpec) { } else if (keyframesSpec) { keyframesRuleInstances.add(rule); Object.setPrototypeOf(rule, CSSKeyframesRuleInterface.prototype); + } else if (propertyMatch) { + propertyRuleInstances.add(rule); + Object.setPrototypeOf(rule, CSSPropertyRuleInterface.prototype); } else if (layerStatementNames) { layerStatementRuleInstances.add(rule); Object.setPrototypeOf(rule, CSSLayerStatementRuleInterface.prototype); @@ -1496,6 +1504,21 @@ R"JS( deleteRule: { writable: true, value(index) { } Object.defineProperties(rule, descriptors); serialize(); + } else if (propertyMatch) { + const descriptors = makeConstructedDeclaration(parsed.body || '', () => {}); + let syntax = descriptors.getPropertyValue('syntax').trim(); + if (syntax.length >= 2 && (syntax[0] === '"' || syntax[0] === "'") + && syntax.at(-1) === syntax[0]) syntax = syntax.slice(1, -1); + const inherits = descriptors.getPropertyValue('inherits').trim().toLowerCase(); + const initialValue = descriptors.getPropertyValue('initial-value').trim(); + Object.defineProperties(rule, { + type: { enumerable: true, value: 0 }, + name: { enumerable: true, value: propertyMatch[1] }, + syntax: { enumerable: true, value: syntax }, + inherits: { enumerable: true, value: inherits === 'true' }, + initialValue: { enumerable: true, value: initialValue }, + detach: { value: () => { parent = null; } } + }); } else if (layerStatementNames) { Object.defineProperties(rule, { type: { enumerable: true, value: 0 }, diff --git a/experiments/WebScene.NativeEngine.Probe/native/webscene_v8_runtime.cpp b/experiments/WebScene.NativeEngine.Probe/native/webscene_v8_runtime.cpp index 7629ebf1a..44a9d50de 100644 --- a/experiments/WebScene.NativeEngine.Probe/native/webscene_v8_runtime.cpp +++ b/experiments/WebScene.NativeEngine.Probe/native/webscene_v8_runtime.cpp @@ -9242,6 +9242,19 @@ v8_dom_runtime::memory_metrics v8_dom_runtime::read_memory_metrics() const noexc for (const auto& stop : keyframes.filter_stops) { result.native_css_rule_storage_bytes += string_bytes(stop.value); } + for (const auto& [property, stops] : keyframes.custom_property_stops) { + result.native_css_rule_storage_bytes += string_bytes(property) + + sizeof(decltype(keyframes.custom_property_stops)::value_type) + + stops.capacity() * sizeof(css_opacity_keyframes::custom_property_stop); + for (const auto& stop : stops) { + result.native_css_rule_storage_bytes += string_bytes(stop.value); + } + } + } + for (const auto& [name, property] : impl_->registered_custom_properties) { + result.native_css_rule_storage_bytes += string_bytes(name) + + sizeof(decltype(impl_->registered_custom_properties)::value_type) + + string_bytes(property.name) + string_bytes(property.initial_value); } for (const auto& [root, cascade] : impl_->inactive_css_cascades) { static_cast(root); @@ -9261,6 +9274,22 @@ v8_dom_runtime::memory_metrics v8_dom_runtime::read_memory_metrics() const noexc for (const auto& stop : keyframes.filter_stops) { result.native_css_rule_storage_bytes += string_bytes(stop.value); } + for (const auto& [property, stops] : keyframes.custom_property_stops) { + result.native_css_rule_storage_bytes += string_bytes(property) + + sizeof(decltype(keyframes.custom_property_stops)::value_type) + + stops.capacity() + * sizeof(css_opacity_keyframes::custom_property_stop); + for (const auto& stop : stops) { + result.native_css_rule_storage_bytes += string_bytes(stop.value); + } + } + } + for (const auto& [name, property] + : cascade.registered_custom_properties) { + result.native_css_rule_storage_bytes += string_bytes(name) + + sizeof(decltype(cascade.registered_custom_properties)::value_type) + + string_bytes(property.name) + + string_bytes(property.initial_value); } } { diff --git a/experiments/WebScene.NativeEngine.Probe/native/webscene_v8_runtime_cache_and_frames.inc b/experiments/WebScene.NativeEngine.Probe/native/webscene_v8_runtime_cache_and_frames.inc index 0e795050f..598f638eb 100644 --- a/experiments/WebScene.NativeEngine.Probe/native/webscene_v8_runtime_cache_and_frames.inc +++ b/experiments/WebScene.NativeEngine.Probe/native/webscene_v8_runtime_cache_and_frames.inc @@ -2774,6 +2774,7 @@ activate_css_cascade(&frame_body); css_rules.clear(); opacity_keyframes.clear(); + registered_custom_properties.clear(); css_rules_by_class.clear(); css_rules_by_id.clear(); css_rules_by_tag.clear(); diff --git a/experiments/WebScene.NativeEngine.Probe/native/webscene_v8_runtime_css_cascade.inc b/experiments/WebScene.NativeEngine.Probe/native/webscene_v8_runtime_css_cascade.inc index d54bcadea..2b42f4cf9 100644 --- a/experiments/WebScene.NativeEngine.Probe/native/webscene_v8_runtime_css_cascade.inc +++ b/experiments/WebScene.NativeEngine.Probe/native/webscene_v8_runtime_css_cascade.inc @@ -182,7 +182,8 @@ void configure_style_keyframe_animation(dom_node& node) { - css::configure_keyframes(node.style,opacity_keyframes); + css::configure_keyframes( + node.style, opacity_keyframes, registered_custom_properties); for (const auto& track : node.style.animations().keyframe_animations) { if (track.opacity_keyframes.size() >= 2U) record_composition("css-opacity-keyframe-animation",node.id); @@ -196,7 +197,9 @@ } const auto configure_pseudo = [&](node_style::pseudo_element& pseudo) { if (!pseudo.has_animation_data()) return; - css::configure_keyframes(pseudo.mutable_animations(), opacity_keyframes); + css::configure_keyframes( + pseudo.mutable_animations(), opacity_keyframes, + registered_custom_properties); for (const auto& track : pseudo.animations().keyframe_animations) { if (track.opacity_keyframes.size() >= 2U) record_composition("css-pseudo-opacity-keyframe-animation",node.id); @@ -258,7 +261,8 @@ return; } if (name.starts_with("--")) { - css::apply_custom_property(node,declaration); + css::apply_custom_property( + node, declaration, ®istered_custom_properties); record_feature("css","custom-property","supported",{},"style-application"); return; } @@ -344,7 +348,8 @@ ++css_cascade_applications; #endif const auto previous_style = node.style; - css::reset_cascaded_style(node,css_variables); + css::reset_cascaded_style( + node, css_variables, ®istered_custom_properties); #if defined(WEBSCENE_NATIVE_ENGINE_CERTIFICATION) const auto reset_completed = trace_css ? std::chrono::steady_clock::now() @@ -390,6 +395,25 @@ }); auto& matched_rules=matching.ordinary; auto& matched_pseudo_rules=matching.pseudo; + css::seed_relevant_registered_custom_properties( + node, registered_custom_properties, + [&](const std::string& name, const std::string& variable_token) { + const auto references = [&](const css_rule* rule) { + return std::any_of( + rule->declarations().begin(), + rule->declarations().end(), + [&](const css_declaration& declaration) { + return declaration.name == name + || declaration.value.find(variable_token) + != std::string::npos; + }); + }; + return std::any_of( + matched_rules.begin(), matched_rules.end(), references) + || std::any_of( + matched_pseudo_rules.begin(), matched_pseudo_rules.end(), + [&](const auto& entry) { return references(entry.second); }); + }); #if defined(WEBSCENE_NATIVE_ENGINE_CERTIFICATION) if (trace_css) css_profile_matched_rules += matched_rules.size(); const auto matching_completed = trace_css @@ -4260,12 +4284,20 @@ if (node.tag == "style" || node.tag == "link") owners.insert(node.id); }); if (owners.empty()) return false; + const auto previous_registration_size = registered_custom_properties.size(); + std::erase_if(registered_custom_properties, [&](const auto& entry) { + return entry.second.stylesheet_owner_id != 0U + && owners.contains(entry.second.stylesheet_owner_id); + }); const auto previous_size = css_rules.size(); std::erase_if(css_rules, [&](const css_rule& rule) { return rule.stylesheet_owner_id != 0 && owners.contains(rule.stylesheet_owner_id); }); - if (css_rules.size() == previous_size) return false; + if (css_rules.size() == previous_size + && registered_custom_properties.size() == previous_registration_size) { + return false; + } rebuild_css_rule_indexes_and_root_variables(); if (recascade) recascade_after_stylesheet_change(); return true; diff --git a/experiments/WebScene.NativeEngine.Probe/native/webscene_v8_runtime_css_parsing.inc b/experiments/WebScene.NativeEngine.Probe/native/webscene_v8_runtime_css_parsing.inc index 26dfecd28..6540f901a 100644 --- a/experiments/WebScene.NativeEngine.Probe/native/webscene_v8_runtime_css_parsing.inc +++ b/experiments/WebScene.NativeEngine.Probe/native/webscene_v8_runtime_css_parsing.inc @@ -1386,6 +1386,14 @@ } #if defined(WEBSCENE_NATIVE_ENGINE_CSSPARSER) + void register_custom_property(registered_custom_property property) + { + if (registered_custom_properties.size() >= 64U + && !registered_custom_properties.contains(property.name)) return; + property.stylesheet_owner_id = active_stylesheet_owner_id; + registered_custom_properties[property.name] = std::move(property); + } + void finish_css_syntax_keyframes(std::string name, css_opacity_keyframes definition) { css::finish_keyframes(opacity_keyframes,std::move(name),std::move(definition)); } @@ -1436,6 +1444,10 @@ css::rebuild_root_variables( css_rules, css_variables, important_css_variables); for (const auto& [name, frames] : sheet.keyframes) opacity_keyframes[name] = frames; + for (auto property : sheet.registered_custom_properties) { + property.stylesheet_owner_id = active_stylesheet_owner_id; + registered_custom_properties[property.name] = std::move(property); + } for (const auto& diagnostic : sheet.diagnostics) record_feature("css", diagnostic.feature, diagnostic.classification, diagnostic.detail, "compiled-stylesheet"); diff --git a/experiments/WebScene.NativeEngine.Probe/native/webscene_v8_runtime_dom_core.inc b/experiments/WebScene.NativeEngine.Probe/native/webscene_v8_runtime_dom_core.inc index 4dab83e61..214506068 100644 --- a/experiments/WebScene.NativeEngine.Probe/native/webscene_v8_runtime_dom_core.inc +++ b/experiments/WebScene.NativeEngine.Probe/native/webscene_v8_runtime_dom_core.inc @@ -791,6 +791,8 @@ state.stylesheet_owner_id = active_stylesheet_owner_id; state.stylesheet_shadow_scope_root_id = active_stylesheet_shadow_scope_root_id; state.opacity_keyframes = std::move(opacity_keyframes); + state.registered_custom_properties = + std::move(registered_custom_properties); state.rules_by_class = std::move(css_rules_by_class); state.rules_by_id = std::move(css_rules_by_id); state.rules_by_tag = std::move(css_rules_by_tag); @@ -828,6 +830,8 @@ active_stylesheet_shadow_scope_root_id = state.stylesheet_shadow_scope_root_id; opacity_keyframes = std::move(state.opacity_keyframes); + registered_custom_properties = + std::move(state.registered_custom_properties); css_rules_by_class = std::move(state.rules_by_class); css_rules_by_id = std::move(state.rules_by_id); css_rules_by_tag = std::move(state.rules_by_tag); diff --git a/experiments/WebScene.NativeEngine.Probe/native/webscene_v8_runtime_lifecycle.inc b/experiments/WebScene.NativeEngine.Probe/native/webscene_v8_runtime_lifecycle.inc index 79fbda895..b00f1d57b 100644 --- a/experiments/WebScene.NativeEngine.Probe/native/webscene_v8_runtime_lifecycle.inc +++ b/experiments/WebScene.NativeEngine.Probe/native/webscene_v8_runtime_lifecycle.inc @@ -1219,6 +1219,12 @@ for (auto& stop : keyframes.filter_stops) { stop.value.shrink_to_fit(); } + for (auto& [property, stops] + : keyframes.custom_property_stops) { + static_cast(property); + stops.shrink_to_fit(); + for (auto& stop : stops) stop.value.shrink_to_fit(); + } } const auto compact_index = [](auto& index) { for (auto& [key, values] : index) { @@ -1251,6 +1257,12 @@ for (auto& stop : keyframes.filter_stops) { stop.value.shrink_to_fit(); } + for (auto& [property, stops] + : keyframes.custom_property_stops) { + static_cast(property); + stops.shrink_to_fit(); + for (auto& stop : stops) stop.value.shrink_to_fit(); + } } compact_index(cascade.rules_by_class); compact_index(cascade.rules_by_id); diff --git a/experiments/WebScene.NativeEngine.Probe/native/webscene_v8_runtime_navigation.inc b/experiments/WebScene.NativeEngine.Probe/native/webscene_v8_runtime_navigation.inc index bb6a47b44..77d9d79a4 100644 --- a/experiments/WebScene.NativeEngine.Probe/native/webscene_v8_runtime_navigation.inc +++ b/experiments/WebScene.NativeEngine.Probe/native/webscene_v8_runtime_navigation.inc @@ -660,6 +660,7 @@ if (!execute_document_start_scripts(local_context, false)) return false; css_rules.clear(); opacity_keyframes.clear(); + registered_custom_properties.clear(); css_rules_by_class.clear(); css_rules_by_id.clear(); css_rules_by_tag.clear(); diff --git a/experiments/WebScene.NativeEngine.Probe/native/webscene_v8_runtime_state.inc b/experiments/WebScene.NativeEngine.Probe/native/webscene_v8_runtime_state.inc index 689f7eaab..e3d8ea0c1 100644 --- a/experiments/WebScene.NativeEngine.Probe/native/webscene_v8_runtime_state.inc +++ b/experiments/WebScene.NativeEngine.Probe/native/webscene_v8_runtime_state.inc @@ -560,6 +560,8 @@ uint32_t active_stylesheet_owner_id{0}; uint32_t active_stylesheet_shadow_scope_root_id{0}; std::unordered_map opacity_keyframes; + std::unordered_map + registered_custom_properties; css_class_index_map> css_rules_by_class; css_index_string_map> css_rules_by_id; css_index_string_map> css_rules_by_tag; diff --git a/experiments/WebScene.NativeEngine.Probe/native/webscene_v8_runtime_style.inc b/experiments/WebScene.NativeEngine.Probe/native/webscene_v8_runtime_style.inc index e6935f970..2fc7cac72 100644 --- a/experiments/WebScene.NativeEngine.Probe/native/webscene_v8_runtime_style.inc +++ b/experiments/WebScene.NativeEngine.Probe/native/webscene_v8_runtime_style.inc @@ -1140,6 +1140,10 @@ else if (authored != authored_style.end()) value = authored->second; } else if (name.starts_with("--")) { const std::string* known_value = nullptr; + const auto registration = self->registered_custom_properties.find(name); + const auto inherits = registration + == self->registered_custom_properties.end() + || registration->second.inherits; for (auto* current_node = node; current_node != nullptr; current_node = current_node->parent) { const auto& custom = current_node->style.custom_properties().values; @@ -1148,13 +1152,16 @@ known_value = &known->second; break; } + if (!inherits) break; } - if (known_value == nullptr) { - auto* self = current(info.GetIsolate()); + if (known_value == nullptr && inherits) { const auto known = self->css_variables.find(name); if (known != self->css_variables.end()) known_value = &known->second; } - value = known_value == nullptr ? "" : *known_value; + if (known_value != nullptr) value = *known_value; + else if (registration != self->registered_custom_properties.end()) { + value = registration->second.initial_value; + } } else { const auto format_length = [](const css_length& length) { if (length.unit == length_unit::automatic) return std::string {}; diff --git a/experiments/WebScene.NativeEngine.Probe/tests/css_parser_tests.cpp b/experiments/WebScene.NativeEngine.Probe/tests/css_parser_tests.cpp index 654bf34bb..f9f957fe7 100644 --- a/experiments/WebScene.NativeEngine.Probe/tests/css_parser_tests.cpp +++ b/experiments/WebScene.NativeEngine.Probe/tests/css_parser_tests.cpp @@ -101,6 +101,27 @@ void invalid_utf8_is_rejected() require(!parsed, "invalid UTF-8 should fail explicitly"); } +void property_descriptors_stream_as_declarations() +{ + const auto parsed = parse_css_syntax_stylesheet(R"CSS( + @property --progress { + syntax: ''; + inherits: false; + initial-value: 0%; + } + )CSS"); + require(static_cast(parsed), parsed.error); + require(parsed.rules.size() == 1U && parsed.rules[0].name == "property", + "@property remains a document-level at-rule"); + require(parsed.rules[0].declaration_count == 3U + && parsed.declarations.size() == 3U, + "@property descriptors stream through declaration callbacks"); + require(parsed.declarations[0].name == "syntax" + && parsed.declarations[1].name == "inherits" + && parsed.declarations[2].name == "initial-value", + "@property descriptor names preserve source order"); +} + class direct_sink final : public css_syntax_sink { public: bool begin_rule( @@ -190,6 +211,7 @@ int main() declaration_syntax(); stylesheet_structure(); invalid_utf8_is_rejected(); + property_descriptors_stream_as_declarations(); direct_streaming_sink(); nested_style_rule_stream(); std::cout << "CSS parser tests passed\n"; diff --git a/experiments/WebScene.NativeEngine.Probe/tests/native_v8_runtime_animation_cssom_tests.inc b/experiments/WebScene.NativeEngine.Probe/tests/native_v8_runtime_animation_cssom_tests.inc index 3ad88f429..f92c29d2e 100644 --- a/experiments/WebScene.NativeEngine.Probe/tests/native_v8_runtime_animation_cssom_tests.inc +++ b/experiments/WebScene.NativeEngine.Probe/tests/native_v8_runtime_animation_cssom_tests.inc @@ -471,6 +471,94 @@ void test_opacity_keyframes_use_host_clock_with_staggered_infinite_delays( "detached CSS animations retained host-frame demand"); } +void test_registered_custom_property_keyframes_update_dependent_paint() +{ + auto* engine = webscene_engine_create(0); + require(engine != nullptr, "typed custom-property engine creation failed"); + execute_and_wait(engine, R"JS( + (() => { + document.body.innerHTML = ` + +
+
+
+
`; + })() + )JS", "typed-custom-property-setup.js"); + const auto reflected = evaluate(engine, R"JS( + (() => { + const rules = document.getElementById('typed-sheet').sheet.cssRules; + const dynamic = new CSSStyleSheet(); + dynamic.replaceSync("@property --dynamic { syntax: ''; inherits: false; initial-value: 0deg; }"); + const dynamicName = dynamic.cssRules[0].name; + dynamic.deleteRule(0); + dynamic.insertRule("@property --replacement { syntax: ''; inherits: false; initial-value: 5%; }"); + return [rules[0].constructor.name, rules[0] instanceof CSSPropertyRule, + rules[0].name, rules[0].syntax, rules[0].inherits, + rules[0].initialValue, rules[1].syntax, dynamicName, + dynamic.cssRules[0].name]; + })() + )JS", "typed-custom-property-cssom.js"); + require( + reflected == R"(["CSSPropertyRule",true,"--progress","",false,"0%","","--dynamic","--replacement"])", + "CSSPropertyRule reflection diverged: " + reflected); + const auto initial_semantics = evaluate(engine, R"JS( + (() => { + const child = document.getElementById('registered-child'); + const initial = getComputedStyle(child).getPropertyValue('--progress'); + child.style.setProperty('--progress', 'invalid'); + return [initial, child.style.getPropertyValue('--progress'), + getComputedStyle(child).getPropertyValue('--progress')]; + })() + )JS", "typed-custom-property-initial.js"); + require(initial_semantics == R"(["0%","invalid","0%"])", + "registered inheritance/invalid-value semantics diverged: " + + initial_semantics); + + animation_frame_and_wait(engine, 500.0, 7481U); + const auto midpoint = evaluate(engine, R"JS( + (() => { + const style = getComputedStyle(document.getElementById('typed')); + return [style.getPropertyValue('--progress'), + style.getPropertyValue('--angle'), style.backgroundImage]; + })() + )JS", "typed-custom-property-midpoint.js"); + require( + midpoint.find("50%") != std::string::npos + && midpoint.find("315deg") != std::string::npos + && midpoint.find("linear-gradient") != std::string::npos, + "typed custom-property midpoint did not reach computed paint: " + midpoint); + + animation_frame_and_wait(engine, 1000.0, 7482U); + const auto terminal = evaluate(engine, R"JS( + getComputedStyle(document.getElementById('typed')) + .getPropertyValue('--progress') + )JS", "typed-custom-property-terminal.js"); + require(terminal == R"("100%")", + "typed custom-property forwards fill did not retain terminal value: " + + terminal); + webscene_engine_destroy(engine); +} + void test_rotation_keyframes_use_host_clock_and_wrap_continuously( webscene_engine* engine) { diff --git a/experiments/WebScene.NativeEngine.Probe/tests/native_v8_runtime_tests.cpp b/experiments/WebScene.NativeEngine.Probe/tests/native_v8_runtime_tests.cpp index c72c5af3c..b0bda0d88 100644 --- a/experiments/WebScene.NativeEngine.Probe/tests/native_v8_runtime_tests.cpp +++ b/experiments/WebScene.NativeEngine.Probe/tests/native_v8_runtime_tests.cpp @@ -1654,6 +1654,7 @@ int main() test_opacity_and_color_transitions_use_host_clock_and_dispatch_events(engine); test_inline_transition_longhands_survive_dynamic_parse_and_recascade(engine); test_opacity_keyframes_use_host_clock_with_staggered_infinite_delays(engine); + test_registered_custom_property_keyframes_update_dependent_paint(); test_rotation_keyframes_use_host_clock_and_wrap_continuously(engine); test_filter_keyframes_use_host_clock_and_retained_paint(engine); test_clipped_offscreen_keyframes_do_not_keep_host_frame_clock_alive(engine); diff --git a/tests/NativeWeb/css_service.cpp b/tests/NativeWeb/css_service.cpp index ad14ed271..c2a75429c 100644 --- a/tests/NativeWeb/css_service.cpp +++ b/tests/NativeWeb/css_service.cpp @@ -47,6 +47,8 @@ struct stylesheet_test_host { std::vector rules; std::vector unsupported; std::vector layers; + std::vector + registered_custom_properties; bool inventory_media_query(const std::string&) { return true; } uint32_t register_cascade_layer(const std::string& name) { if(!name.empty()) { @@ -57,6 +59,10 @@ struct stylesheet_test_host { layers.push_back(name); return static_cast(layers.size()); } + void register_custom_property( + const webscene_native::css::registered_custom_property& property) { + registered_custom_properties.push_back(property); + } void record_feature(std::string_view, const std::string& name, std::string_view classification, const std::string&, std::string_view) { if(classification=="unsupported") unsupported.push_back(name); @@ -1355,5 +1361,85 @@ int main(int argc,char** argv) { session.set_environment({1000,600,false}); if(session_flush()) return 164; if(!session.remove(1) || !session_flush() || tree_child.style.width.value==100) return 165; + auto invalid_typed_sheet=webscene_native::css::prepare_stylesheet(R"CSS( + @property --wrong-unit { + syntax: ''; + inherits: false; + initial-value: 10%; + } + @property --missing-inherits { + syntax: ''; + initial-value: 0%; + } + @property wrong-name { + syntax: ''; + inherits: false; + initial-value: 0%; + } + )CSS","asset://app/invalid-typed.css",[](const auto&) {return true;}); + if(!invalid_typed_sheet + || !invalid_typed_sheet->registered_custom_properties.empty()) return 184; + auto typed_sheet=webscene_native::css::prepare_stylesheet(R"CSS( + @property --progress { + syntax: ''; + inherits: false; + initial-value: 0%; + } + @keyframes typed-progress { + from { --progress: 0%; } + to { --progress: 100%; } + } + .typed { + background-image: linear-gradient(90deg, red var(--progress), blue); + animation: typed-progress 1000ms linear 1 forwards; + } + )CSS","asset://app/typed.css",[](const auto&) {return true;}); + if(!typed_sheet || typed_sheet->registered_custom_properties.size()!=1U + || typed_sheet->registered_custom_properties[0].name!="--progress" + || typed_sheet->keyframes["typed-progress"] + .custom_property_stops["--progress"].size()!=2U) { + std::cerr<<"typed preparation registrations=" + <<(typed_sheet?typed_sheet->registered_custom_properties.size():0U) + <<" keyframes="<<(typed_sheet?typed_sheet->keyframes.size():0U) + <<" diagnostics="<<(typed_sheet?typed_sheet->diagnostics.size():0U)<<'\n'; + if(typed_sheet) for(const auto& diagnostic:typed_sheet->diagnostics) + std::cerr< unrelated_nodes; + unrelated_nodes.reserve(256U); + for(size_t index=0;index<256U;++index) { + auto& unrelated=typed_document.create_element("div"); + unrelated.class_name="unrelated"; + typed_document.append_child(typed_document.body(),unrelated); + unrelated_nodes.push_back(&unrelated); + } + auto& typed_node=typed_document.create_element("div"); + typed_node.class_name="typed"; + typed_document.append_child(typed_document.body(),typed_node); + webscene_native::css::query_host typed_query(typed_document); + webscene_native::css::stylesheet_owner typed_sheets; + typed_sheets.replace(748,std::move(*typed_sheet)); + webscene_native::css::apply_native_document_cascade( + typed_document,typed_sheets,typed_query, + [](const auto&,auto&,auto&,auto&) {return false;}, + [](const auto&,const auto&) {}); + if(typed_node.style.custom_properties().values.at("--progress")!="0%" + || typed_sheets.state().registered_custom_properties.size()!=1U) return 180; + if(std::any_of(unrelated_nodes.begin(),unrelated_nodes.end(),[](const auto* node) { + return node->style.custom_property_data_identity()!=nullptr; + })) return 183; + typed_document.signal_animation_frame(0); + typed_document.advance_animations(); + typed_document.signal_animation_frame(500); + typed_document.advance_animations(); + const auto sampled=typed_node.style.custom_properties().values.at("--progress"); + if(sampled!="50%" + || typed_node.style.background_image().image_value.find("50%") + ==std::string::npos) return 181; + if(!typed_sheets.remove(748) + || !typed_sheets.state().registered_custom_properties.empty()) return 182; std::cout<<"V8-free shared CSS declaration service passed\n"; } diff --git a/tests/WebPlatformSubset/capabilities.json b/tests/WebPlatformSubset/capabilities.json index 03a21a60b..ee5bd7b78 100644 --- a/tests/WebPlatformSubset/capabilities.json +++ b/tests/WebPlatformSubset/capabilities.json @@ -378,6 +378,14 @@ "productEvidence": ["mixed-weight status prose", "compact labels beside pseudo-painted fixed-height timeline segments"], "coverage": ["contracts/system-ui-inline-text-paint.html", "contracts/inline-generated-content-timeline-alignment.html", "webscene-retina-text-profile.json", "NativeTextShapingTests", "test_merged_inline_fragment_uses_contextual_host_advance", "test_flattened_inline_fragment_honors_text_alignment"], "gap": "The candidate visual lanes retain native and Chromium screenshots for the same deterministic system-UI fixture at both 1x and 2x, including normal and bold colored runs, light body copy, generated whitespace, and compact-label alignment against a positioned pseudo-element translated with its flex-baseline subtree. Glyph antialiasing is presenter-dependent, so exact whole-frame pixel identity remains diagnostic; broader script fallback, variable-font optical axes, complex shaping and bidirectional text remain outside this bounded claim." + }, + { + "family": "registered-custom-property-animation", + "required": ["document-level @property parsing", "bounded and registrations", "inherits:false initial values", "typed host-clock keyframe interpolation", "dependent retained paint-token updates", "CSSPropertyRule identity and descriptors", "stylesheet and teardown cleanup"], + "candidate": ["browser/native deterministic midpoint comparison", "conic-gradient raster consumer", "border-image gradient raster consumer"], + "productEvidence": ["Code OSS chat input and session banner border beams", "inline and terminal chat progress borders", "voice processing ring", "chat last-edit highlight"], + "coverage": ["contracts/css-registered-custom-property-animation.html", "webscene_css_parser_tests", "native_web_css_service", "test_registered_custom_property_keyframes_update_dependent_paint"], + "gap": "Issue #748 implements the exact / registration and interpolation slice used by unchanged Code OSS, with bounded registration/track/stop storage and change-only dependent token updates. Direct browser/native profile execution and product pixels remain candidate evidence. Conic-gradient and border-image gradient rasterization, arbitrary syntaxes, CSS.registerProperty(), and additive composition remain separate work." } ] } diff --git a/tests/WebPlatformSubset/contracts/css-registered-custom-property-animation.html b/tests/WebPlatformSubset/contracts/css-registered-custom-property-animation.html new file mode 100644 index 000000000..26090e5b4 --- /dev/null +++ b/tests/WebPlatformSubset/contracts/css-registered-custom-property-animation.html @@ -0,0 +1,58 @@ + + +Registered custom properties interpolate into retained paint + + + +
+ diff --git a/tests/WebPlatformSubset/webscene-component-profile.json b/tests/WebPlatformSubset/webscene-component-profile.json index e93a66d2f..9a28f6ad0 100644 --- a/tests/WebPlatformSubset/webscene-component-profile.json +++ b/tests/WebPlatformSubset/webscene-component-profile.json @@ -2751,6 +2751,13 @@ "evidence": ["wpt-service-workers-fetch-event", "chromium-153-direct-product-fixture", "vscode-oss-645f29c-unchanged-service-worker"], "reason": "Cumulative connected-resource acceptance for WebScene #266/#308. The unchanged VS Code 645f29c webview Service Worker performs CacheStorage empty-match, client load-resource messaging, and bounded chunk streaming for prelude script, extension stylesheet, element SVG, and stylesheet CSS url() SVG while the fixture renders content from the copied current vscode-demo README. The native companion gate covers controller-first CSS image routing, unhandled host fallback, handled failure closure, stale-style response rejection, teardown, 100 warm cycles at p95 <= 100 ms, <= 8 MiB retained V8 heap, <= 64 MiB post-warm RSS growth, and <= 256 MiB absolute high-water growth.", "nativeNavigation": true + }, + { + "path": "contracts/css-registered-custom-property-animation.html", + "type": "testharness", + "capabilities": ["css-property-rule", "registered-angle-property", "registered-percentage-property", "typed-custom-property-keyframes", "non-inheriting-initial-value", "dependent-gradient-token-update"], + "evidence": ["native-parser-contract", "v8-free-native-cascade-contract", "focused-live-runtime-contract", "unchanged-code-oss-645f29c-inventory"], + "reason": "Implementation-first WebScene #748 contract for the bounded / @property slice used by unchanged Code OSS. It checks CSSPropertyRule reflection, inherits:false initial values, a deterministic paused midpoint, and the dependent gradient token. Browser/native execution and exact product pixels remain qualification debt; conic-gradient and border-image raster consumers are separate paint work." } ], "harnessBlocked": [],