diff --git a/docs/validation/code-oss-css-will-change-800.md b/docs/validation/code-oss-css-will-change-800.md new file mode 100644 index 00000000..e9982c8f --- /dev/null +++ b/docs/validation/code-oss-css-will-change-800.md @@ -0,0 +1,35 @@ +# Code OSS CSS will-change implementation (#800) + +## Product trigger + +Unchanged Code OSS has 16 `will-change` declarations across 12 CSS files. +Transform/opacity hints cover Quick Input, Action Widget, sessions/mobile, +Chat, counters, voice glow, and onboarding. Other hints include +background-position, box-shadow, left, and top. + +## Implemented slice + +- Adds generated native/managed identity, bounded validation, and CSSOM for + `auto` or up to eight comma-separated ASCII custom identifiers. +- Retains the normalized authored list in cold textual style and derives one + packed hot bit when a hinted non-initial property establishes a stacking + context. +- Reuses the retained atomic stacking-context predicate for paint and hit + ordering. It does not preallocate a compositor layer. +- Treats mutation as paint-only and adds no layout invalidation, visual-tree + node, timer, frame participant, platform widget, browser shell, or Code OSS + patch. + +## Authored gates + +- `contracts/css-will-change-stacking-context.html`: CSSOM, invalid writes, + mutation/removal, and overlapping descendant/sibling order. +- `test_css_will_change_stacking_context`: native hit-order mutation contract. +- `css-will-change-performance`: 4,096 nodes, ten transitions, zero added + layout, bounded scene publication, and a five-second guardrail. + +## Evidence state + +Implementation and gates are authored from source review. Per the active fast +merge direction, no build, test, WPT, visual, package, memory, lifecycle, or +performance command was executed. Evidence remains zero until full validation. diff --git a/experiments/WebScene.NativeEngine.Probe/native/css_property_metadata.json b/experiments/WebScene.NativeEngine.Probe/native/css_property_metadata.json index 15a86271..3a51ec48 100644 --- a/experiments/WebScene.NativeEngine.Probe/native/css_property_metadata.json +++ b/experiments/WebScene.NativeEngine.Probe/native/css_property_metadata.json @@ -156,7 +156,8 @@ {"id": "overscroll_behavior", "name": "overscroll-behavior", "aliases": ["overscrollbehavior"]}, {"id": "overscroll_behavior_x", "name": "overscroll-behavior-x", "aliases": ["overscrollbehaviorx"]}, {"id": "overscroll_behavior_y", "name": "overscroll-behavior-y", "aliases": ["overscrollbehaviory"]}, - {"id": "isolation", "name": "isolation"} + {"id": "isolation", "name": "isolation"}, + {"id": "will_change", "name": "will-change", "aliases": ["willchange"]} ], "nativeStorageOnlyProperties": [ "accent-color", "align-content", "animation-direction", "animation-fill-mode", "animation-play-state", @@ -178,7 +179,7 @@ "justify_content", "box_sizing", "vertical_align", "grid_auto_flow", "border_collapse", "table_layout", "border_style", "background_repeat", "visibility", "pointer_events", "text_anchor", "font_smoothing", "text_align", "text_transform", "white_space", "word_break", "overflow_wrap", "list_style_position", "list_style_type", "scrollbar_width", "container", "container_type", "content_visibility", "object_fit", "user_select", - "overscroll_behavior", "overscroll_behavior_x", "overscroll_behavior_y", "isolation" + "overscroll_behavior", "overscroll_behavior_x", "overscroll_behavior_y", "isolation", "will_change" ], "componentList": [ "container_name", "background_position", "background_size", "transition", "transition_property", "transition_duration", @@ -222,7 +223,7 @@ "outline", "outline_width", "outline_color", "animation", "animation_name", "animation_duration", "animation_delay", "animation_timing_function", "animation_iteration_count", "background_repeat", "background_position", "background_size", "font", "text_transform", "word_break", "overflow_wrap", "list_style", "list_style_position", "list_style_type", - "object_fit", "object_position", "user_select", "overscroll_behavior", "overscroll_behavior_x", "overscroll_behavior_y", "isolation" + "object_fit", "object_position", "user_select", "overscroll_behavior", "overscroll_behavior_x", "overscroll_behavior_y", "isolation", "will_change" ], "managedKnownProperties": [ "align-content", "align-items", "align-self", "all", "background", "background-color", @@ -244,7 +245,7 @@ "top", "transform", "visibility", "white-space", "word-break", "overflow-wrap", "width", "word-spacing", "z-index", "outline", "outline-color", "outline-offset", "outline-style", "outline-width", "grid-template-areas", "aspect-ratio", "object-fit", "object-position", "user-select", - "overscroll-behavior", "overscroll-behavior-x", "overscroll-behavior-y", "isolation" + "overscroll-behavior", "overscroll-behavior-x", "overscroll-behavior-y", "isolation", "will-change" ], "supportedPropertyExtras": [ "animation", "animation-delay", "animation-direction", "animation-duration", "animation-fill-mode", diff --git a/experiments/WebScene.NativeEngine.Probe/native/generated/webscene_css_property_identity.inc b/experiments/WebScene.NativeEngine.Probe/native/generated/webscene_css_property_identity.inc index 18ed80d2..49b28f06 100644 --- a/experiments/WebScene.NativeEngine.Probe/native/generated/webscene_css_property_identity.inc +++ b/experiments/WebScene.NativeEngine.Probe/native/generated/webscene_css_property_identity.inc @@ -232,6 +232,8 @@ inline constexpr std::array native_typed_property_identity_catalog{ native_typed_property_identity{"overscroll-behavior-y", css_property_id::overscroll_behavior_y}, native_typed_property_identity{"overscrollbehaviory", css_property_id::overscroll_behavior_y}, native_typed_property_identity{"isolation", css_property_id::isolation}, + native_typed_property_identity{"will-change", css_property_id::will_change}, + native_typed_property_identity{"willchange", css_property_id::will_change}, }; inline constexpr std::array native_storage_only_property_catalog{ @@ -456,6 +458,7 @@ inline constexpr std::array native_modeled_property_mask_catalog{ false, false, false, + false, }; inline constexpr bool generated_property_has_modeled_mask( @@ -659,6 +662,7 @@ inline constexpr std::array native_property_grammar_catalog{ native_property_grammar::keyword, native_property_grammar::keyword, native_property_grammar::keyword, + native_property_grammar::keyword, }; inline constexpr native_property_grammar generated_property_grammar( @@ -899,5 +903,7 @@ inline css_property_id generated_property_id_lowercase(std::string_view name) no if (name == "overscroll-behavior-y") return css_property_id::overscroll_behavior_y; if (name == "overscrollbehaviory") return css_property_id::overscroll_behavior_y; if (name == "isolation") return css_property_id::isolation; + if (name == "will-change") return css_property_id::will_change; + if (name == "willchange") return css_property_id::will_change; return css_property_id::unknown; } diff --git a/experiments/WebScene.NativeEngine.Probe/native/generated/webscene_css_property_ids.inc b/experiments/WebScene.NativeEngine.Probe/native/generated/webscene_css_property_ids.inc index 194b5afc..873f5aed 100644 --- a/experiments/WebScene.NativeEngine.Probe/native/generated/webscene_css_property_ids.inc +++ b/experiments/WebScene.NativeEngine.Probe/native/generated/webscene_css_property_ids.inc @@ -155,3 +155,4 @@ overscroll_behavior, overscroll_behavior_x, overscroll_behavior_y, isolation, +will_change, diff --git a/experiments/WebScene.NativeEngine.Probe/native/generated/webscene_css_supported_properties.inc b/experiments/WebScene.NativeEngine.Probe/native/generated/webscene_css_supported_properties.inc index a8d4b9ce..71ebed87 100644 --- a/experiments/WebScene.NativeEngine.Probe/native/generated/webscene_css_supported_properties.inc +++ b/experiments/WebScene.NativeEngine.Probe/native/generated/webscene_css_supported_properties.inc @@ -253,6 +253,7 @@ inline constexpr std::array cssom_supported_property_catalog{ cssom_supported_property_metadata{"white-space", "whiteSpace"}, cssom_supported_property_metadata{"widows", "widows"}, cssom_supported_property_metadata{"width", "width"}, + cssom_supported_property_metadata{"will-change", "willChange"}, cssom_supported_property_metadata{"word-break", "wordBreak"}, cssom_supported_property_metadata{"word-spacing", "wordSpacing"}, cssom_supported_property_metadata{"word-wrap", "wordWrap"}, @@ -260,4 +261,4 @@ inline constexpr std::array cssom_supported_property_catalog{ cssom_supported_property_metadata{"zoom", "zoom"}, }; -inline constexpr auto cssom_style_template_property_accessor_count = 462U; +inline constexpr auto cssom_style_template_property_accessor_count = 464U; diff --git a/experiments/WebScene.NativeEngine.Probe/native/webscene_css_application.h b/experiments/WebScene.NativeEngine.Probe/native/webscene_css_application.h index 76d1af64..1661e566 100644 --- a/experiments/WebScene.NativeEngine.Probe/native/webscene_css_application.h +++ b/experiments/WebScene.NativeEngine.Probe/native/webscene_css_application.h @@ -261,6 +261,7 @@ void apply_resolved_declaration(native_document& document,dom_node& node, } else if (css::apply_user_select_value(node,name,value,decision)) { } else if (css::apply_overscroll_behavior_value(node,name,value,decision)) { } else if (css::apply_isolation_value(node,name,value,decision)) { + } else if (css::apply_will_change_value(node,name,value,decision)) { } else if (css::apply_text_value(node,name,value,decision,is_inline)) { } else if (name == "border-style") { decision.classification = "partially-supported"; 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 f8843c7b..065615fe 100644 --- a/experiments/WebScene.NativeEngine.Probe/native/webscene_css_cascade_reset.h +++ b/experiments/WebScene.NativeEngine.Probe/native/webscene_css_cascade_reset.h @@ -229,11 +229,13 @@ inline void reset_cascaded_style(dom_node& node, textual->object_fit.clear(); textual->object_position.clear(); textual->user_select.clear(); + textual->will_change.clear(); textual->overscroll_x = overscroll_behavior::automatic; textual->overscroll_y = overscroll_behavior::automatic; } if ((node.style.inline_property_mask & inline_font_weight) == 0U) node.style.font_weight = 0; node.style.isolation_stacking_context = false; + node.style.will_change_stacking_context = false; if ((node.style.inline_property_mask & inline_line_height) == 0U) node.style.line_height = -1; if ((node.style.inline_property_mask & inline_letter_spacing) == 0U) { node.style.letter_spacing = 0; diff --git a/experiments/WebScene.NativeEngine.Probe/native/webscene_css_interaction_values.h b/experiments/WebScene.NativeEngine.Probe/native/webscene_css_interaction_values.h index 0f23e83e..a260b284 100644 --- a/experiments/WebScene.NativeEngine.Probe/native/webscene_css_interaction_values.h +++ b/experiments/WebScene.NativeEngine.Probe/native/webscene_css_interaction_values.h @@ -1,5 +1,6 @@ #pragma once #include "webscene_css_property_mask.h" +#include #include #include @@ -131,4 +132,76 @@ bool apply_isolation_value(dom_node& node,const std::string& name, node.style.isolation_stacking_context = value == "isolate"; return true; } + +inline std::optional normalize_will_change_value( + std::string_view raw_value) +{ + auto value = ascii_lower(trim_value(raw_value)); + if (value == "auto") return std::string{}; + if (value.empty() || value.size() > 256U) return std::nullopt; + std::string result; + auto start = size_t{0}; + auto count = size_t{0}; + while (start <= value.size()) { + const auto comma = value.find(',', start); + const auto token = trim_value(value.substr( + start, comma == std::string::npos ? std::string::npos : comma - start)); + if (token.empty() || token.size() > 64U || ++count > 8U) return std::nullopt; + const auto reserved = token == "auto" || token == "none" + || token == "default" || token == "initial" || token == "inherit" + || token == "unset" || token == "revert" || token == "revert-layer"; + const auto identifier = std::all_of(token.begin(), token.end(), [](unsigned char c) { + return std::isalnum(c) != 0 || c == '-' || c == '_'; + }) && (std::isalpha(static_cast(token.front())) != 0 + || token.front() == '-' || token.front() == '_'); + if (reserved || !identifier) return std::nullopt; + if (!result.empty()) result += ", "; + result += token; + if (comma == std::string::npos) break; + start = comma + 1U; + } + return result; +} + +inline bool will_change_establishes_stacking_context(std::string_view value) +{ + auto start = size_t{0}; + while (start <= value.size()) { + const auto comma = value.find(',', start); + const auto token = trim_value(value.substr( + start, comma == std::string::npos ? std::string::npos : comma - start)); + if (token == "transform" || token == "opacity" || token == "filter" + || token == "perspective" || token == "clip-path" + || token == "mask" || token == "mask-image" + || token == "backdrop-filter") return true; + if (comma == std::string::npos) break; + start = comma + 1U; + } + return false; +} + +template +bool apply_will_change_value(dom_node& node,const std::string& name, + const std::string& raw_value,Decision& decision) +{ + if (canonical_property_name(name) != "will-change") return false; + auto value = ascii_lower(trim_value(raw_value)); + if (value == "inherit") { + value = node.parent == nullptr + ? std::string{} : node.parent->style.textual().will_change; + } else if (value == "initial" || value == "unset" || value == "revert" + || value == "revert-layer") { + value = "auto"; + } + const auto normalized = normalize_will_change_value(value.empty() ? "auto" : value); + if (!normalized.has_value()) { + decision.classification = "invalid-authoring"; + return true; + } + node.style.will_change_stacking_context = + will_change_establishes_stacking_context(*normalized); + if (normalized->empty() && !node.style.has_textual_data()) return true; + node.style.mutable_textual().will_change = *normalized; + return true; +} } // namespace webscene_native::css diff --git a/experiments/WebScene.NativeEngine.Probe/native/webscene_css_reset.h b/experiments/WebScene.NativeEngine.Probe/native/webscene_css_reset.h index e06ff67c..08e10742 100644 --- a/experiments/WebScene.NativeEngine.Probe/native/webscene_css_reset.h +++ b/experiments/WebScene.NativeEngine.Probe/native/webscene_css_reset.h @@ -251,6 +251,10 @@ inline void apply_all_unset( if (has_inline({"isolation"})) { reset.isolation_stacking_context = previous.isolation_stacking_context; } + if (has_inline({"will-change"})) { + reset.mutable_textual().will_change = previous.textual().will_change; + reset.will_change_stacking_context = previous.will_change_stacking_context; + } // These modeled properties do not yet have dedicated inline-mask // bits, so preserve their applied values by authored declaration. diff --git a/experiments/WebScene.NativeEngine.Probe/native/webscene_css_specified_coverage.h b/experiments/WebScene.NativeEngine.Probe/native/webscene_css_specified_coverage.h index bf748e7e..91000519 100644 --- a/experiments/WebScene.NativeEngine.Probe/native/webscene_css_specified_coverage.h +++ b/experiments/WebScene.NativeEngine.Probe/native/webscene_css_specified_coverage.h @@ -11,7 +11,8 @@ inline constexpr auto specified_property_samples = std::to_arraywill_change.clear(); + } + node.style.will_change_stacking_context = false; } else if (name == "boxSizing" || name == "box-sizing") { node.style.border_box = false; node.style.inline_property_mask &= ~inline_box_sizing; @@ -1001,6 +1006,12 @@ std::string semantic_slice; } decision; css::apply_isolation_value(*node, name, value, decision); + } else if (name == "will-change") { + struct interaction_decision final { + std::string classification{"supported"}; + std::string semantic_slice; + } decision; + css::apply_will_change_value(*node, name, value, decision); } else if (name == "box-sizing") { node->style.border_box = value == "border-box"; node->style.inline_property_mask |= inline_box_sizing; @@ -1622,6 +1633,9 @@ node->style.textual().overscroll_y); } else if (name == "isolation") { value = node->style.isolation_stacking_context ? "isolate" : "auto"; + } else if (name == "will-change") { + value = node->style.textual().will_change.empty() + ? "auto" : node->style.textual().will_change; } else if (name == "cursor") { value = resolved_cursor(*node); } else if (name == "font-size") { @@ -2100,6 +2114,9 @@ node->style.textual().overscroll_y); } else if (name == "isolation") { value = node->style.isolation_stacking_context ? "isolate" : "auto"; + } else if (name == "willChange") { + value = node->style.textual().will_change.empty() + ? "auto" : node->style.textual().will_change; } else if (name == "boxSizing") { value = node->style.border_box ? "border-box" : "content-box"; } else if (name == "borderRadius") { @@ -2705,6 +2722,12 @@ std::string semantic_slice; } decision; css::apply_isolation_value(*node, name, value, decision); + } else if (name == "willChange") { + struct interaction_decision final { + std::string classification{"supported"}; + std::string semantic_slice; + } decision; + css::apply_will_change_value(*node, "will-change", value, decision); } else if (name == "boxSizing") { node->style.border_box = value == "border-box"; node->style.inline_property_mask |= inline_box_sizing; diff --git a/experiments/WebScene.NativeEngine.Probe/native/webscene_v8_runtime_support.inc b/experiments/WebScene.NativeEngine.Probe/native/webscene_v8_runtime_support.inc index 385ac3ba..7c69dbf6 100644 --- a/experiments/WebScene.NativeEngine.Probe/native/webscene_v8_runtime_support.inc +++ b/experiments/WebScene.NativeEngine.Probe/native/webscene_v8_runtime_support.inc @@ -682,6 +682,9 @@ bool valid_cssom_declaration_value(std::string_view property_name, std::string_v if (canonical_name == "isolation") { return normalized_value == "auto" || normalized_value == "isolate"; } + if (canonical_name == "will-change") { + return css::normalize_will_change_value(normalized_value).has_value(); + } if (canonical_name == "position") { return normalized_value == "static" || normalized_value == "relative" diff --git a/experiments/WebScene.NativeEngine.Probe/tests/css_property_identity_tests.cpp b/experiments/WebScene.NativeEngine.Probe/tests/css_property_identity_tests.cpp index dff09362..f48ca814 100644 --- a/experiments/WebScene.NativeEngine.Probe/tests/css_property_identity_tests.cpp +++ b/experiments/WebScene.NativeEngine.Probe/tests/css_property_identity_tests.cpp @@ -103,10 +103,11 @@ int main() static_assert(static_cast(css_property_id::overscroll_behavior_x) == 151U); static_assert(static_cast(css_property_id::overscroll_behavior_y) == 152U); static_assert(static_cast(css_property_id::isolation) == 153U); - static_assert(native_typed_property_identity_catalog.size() == 227U); + static_assert(static_cast(css_property_id::will_change) == 154U); + static_assert(native_typed_property_identity_catalog.size() == 229U); static_assert(native_storage_only_property_catalog.size() == 62U); - static_assert(cssom_supported_property_catalog.size() == 253U); - static_assert(cssom_style_template_property_accessor_count == 462U); + static_assert(cssom_supported_property_catalog.size() == 254U); + static_assert(cssom_style_template_property_accessor_count == 463U); for (const auto& entry : native_typed_property_identity_catalog) { require(property_id(entry.name) == entry.id, "typed name maps to its generated id", entry.name); diff --git a/experiments/WebScene.NativeEngine.Probe/tests/native_v8_runtime_css_layout_tests.inc b/experiments/WebScene.NativeEngine.Probe/tests/native_v8_runtime_css_layout_tests.inc index 068f3576..21662e98 100644 --- a/experiments/WebScene.NativeEngine.Probe/tests/native_v8_runtime_css_layout_tests.inc +++ b/experiments/WebScene.NativeEngine.Probe/tests/native_v8_runtime_css_layout_tests.inc @@ -281,6 +281,46 @@ void test_css_isolation_performance_gate(webscene_engine* engine) + std::to_string(elapsed) + " ms"); } +void test_css_will_change_performance_gate(webscene_engine* engine) +{ + resize(engine, 1024, 768, 1U); + execute(engine, R"JS( + document.body.innerHTML = '
'; + globalThis.willChangeGrid = document.getElementById('will-change-grid'); + const fragment = document.createDocumentFragment(); + for (let index = 0; index < 4096; ++index) { + fragment.appendChild(document.createElement('div')); + } + willChangeGrid.appendChild(fragment); + )JS", "native-css-will-change-performance-setup.js"); + webscene_engine_metrics before{}; + webscene_engine_get_metrics(engine, &before); + const auto started = std::chrono::steady_clock::now(); + const auto result = evaluate(engine, R"JS( + (() => { + for (let cycle = 0; cycle < 10; ++cycle) { + const value = cycle % 2 === 0 ? 'transform, opacity' : 'auto'; + for (const node of willChangeGrid.children) node.style.willChange = value; + } + return [willChangeGrid.children.length, + willChangeGrid.lastElementChild.style.willChange]; + })() + )JS", "native-css-will-change-performance-gate.js"); + const auto elapsed = std::chrono::duration( + std::chrono::steady_clock::now() - started).count(); + webscene_engine_metrics after{}; + webscene_engine_get_metrics(engine, &after); + require(result == R"JSON([4096,"auto"])JSON", + "CSS will-change performance fixture regressed: " + result); + require(after.layout_passes == before.layout_passes, + "CSS will-change transitions performed layout"); + require(after.published_scenes <= before.published_scenes + 1U, + "CSS will-change transitions published unbounded scenes"); + require(elapsed < 5000.0, + "4,096-node CSS will-change gate exceeded 5 seconds: " + + std::to_string(elapsed) + " ms"); +} + void test_modal_backdrop_scene(webscene_engine* engine) { resize(engine,400,300,1); @@ -5053,6 +5093,36 @@ void test_css_isolation_stacking_context(webscene_engine* engine) "CSS isolation auto mutation retained an atomic context: " + released); } +void test_css_will_change_stacking_context(webscene_engine* engine) +{ + resize(engine, 320, 180, 79'600U); + const auto hinted = evaluate(engine, R"JS( + (() => { + document.body.innerHTML = `
`; + return [getComputedStyle(document.getElementById('hinted')).willChange, + document.elementFromPoint(10,10).id]; + })() + )JS", "native-css-will-change-stacking.js"); + require(hinted == R"JSON(["transform, opacity","sibling"])JSON", + "CSS will-change did not establish an atomic context: " + hinted); + const auto released = evaluate(engine, R"JS( + (() => { + const hinted = document.getElementById('hinted'); + hinted.style.willChange = 'background-position'; + return [getComputedStyle(hinted).willChange, + document.elementFromPoint(10,10).id]; + })() + )JS", "native-css-will-change-auto.js"); + require(released == R"JSON(["background-position","escape"])JSON", + "Non-context will-change hint retained an atomic context: " + released); +} + void test_z_index_orders_positioned_siblings_in_scene(webscene_engine* engine) { execute(engine, R"JS( 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 eddac1e0..3375fee5 100644 --- a/experiments/WebScene.NativeEngine.Probe/tests/native_v8_runtime_tests.cpp +++ b/experiments/WebScene.NativeEngine.Probe/tests/native_v8_runtime_tests.cpp @@ -421,6 +421,14 @@ int main() webscene_engine_destroy(focused_engine); return 0; } + if (selected == "css-will-change-performance") { + auto* focused_engine = webscene_engine_create(0); + require(focused_engine != nullptr, + "CSS will-change performance engine creation failed"); + test_css_will_change_performance_gate(focused_engine); + webscene_engine_destroy(focused_engine); + return 0; + } if(selected=="placeholder-pseudo") { auto* focused_engine=webscene_engine_create(0); require(focused_engine!=nullptr,"placeholder pseudo engine creation failed"); @@ -1539,6 +1547,7 @@ int main() } test_z_index_orders_positioned_siblings_in_scene(engine); test_css_isolation_stacking_context(engine); + test_css_will_change_stacking_context(engine); test_popup_portal_tooltip_escapes_non_stacking_positioned_wrapper(engine); test_fixed_portal_descendant_stays_in_ancestor_stacking_context(engine); test_transform_origin_keywords_cascade_independently_from_inline_transform(engine); diff --git a/src/WebScene.Css/CssPropertyCatalog.cs b/src/WebScene.Css/CssPropertyCatalog.cs index c8398694..5eeaca60 100644 --- a/src/WebScene.Css/CssPropertyCatalog.cs +++ b/src/WebScene.Css/CssPropertyCatalog.cs @@ -91,6 +91,7 @@ public static bool IsValidCssomValue(string propertyName, string value) => normalizedValue is "auto" or "contain" or "none", "overscroll-behavior" => IsOverscrollBehavior(normalizedValue), "isolation" => normalizedValue is "auto" or "isolate", + "will-change" => IsWillChange(normalizedValue), "font-size" => IsFontSize(normalizedValue), "color-scheme" => normalizedValue is "normal" or "light" or "dark" or "light dark" or "dark light" or "only light" or "only dark", @@ -169,6 +170,27 @@ private static bool IsOverscrollBehavior(string value) && tokens.All(static token => token is "auto" or "contain" or "none"); } + private static bool IsWillChange(string value) + { + if (value == "auto") return true; + if (value.Length is 0 or > 256) return false; + var tokens = value.Split(',', StringSplitOptions.TrimEntries); + if (tokens.Length is 0 or > 8) return false; + foreach (var token in tokens) + { + if (token.Length is 0 or > 64 + || token is "auto" or "none" or "default" or "initial" or "inherit" + or "unset" or "revert" or "revert-layer" + || !(char.IsLetter(token[0]) || token[0] is '-' or '_') + || token.Any(static character => + !(char.IsLetterOrDigit(character) || character is '-' or '_'))) + { + return false; + } + } + return true; + } + private static bool IsObjectPosition(string value) { var tokens = value.Split([' ', '\t', '\r', '\n'], diff --git a/src/WebScene.Css/CssPropertyMetadata.Generated.cs b/src/WebScene.Css/CssPropertyMetadata.Generated.cs index 17e0851c..156925d7 100644 --- a/src/WebScene.Css/CssPropertyMetadata.Generated.cs +++ b/src/WebScene.Css/CssPropertyMetadata.Generated.cs @@ -120,6 +120,7 @@ internal static class CssGeneratedPropertyMetadata "overscroll-behavior-x", "overscroll-behavior-y", "isolation", + "will-change", ]; internal static readonly string[] SupportedNames = @@ -372,6 +373,7 @@ internal static class CssGeneratedPropertyMetadata "white-space", "widows", "width", + "will-change", "word-break", "word-spacing", "word-wrap", @@ -498,6 +500,7 @@ internal static bool TryGetKnownId(string name, out int id) "overscroll-behavior-x" => 112, "overscroll-behavior-y" => 113, "isolation" => 114, + "will-change" => 115, _ => -1, }; return id >= 0; diff --git a/tests/WebPlatformSubset/capabilities.json b/tests/WebPlatformSubset/capabilities.json index b230d94f..50582cc9 100644 --- a/tests/WebPlatformSubset/capabilities.json +++ b/tests/WebPlatformSubset/capabilities.json @@ -440,6 +440,14 @@ "productEvidence": ["Code OSS pixel spinner", "Code OSS sessions workbench and part", "Code OSS Chat session container", "Code OSS voice glow"], "coverage": ["contracts/css-isolation-stacking-context.html", "test_css_isolation_stacking_context", "css-isolation-performance"], "gap": "Issue #795 implements the exact isolation:isolate slice used by unchanged Code OSS. One packed computed-style bit feeds the retained atomic stacking-context predicate, so a positive-z descendant remains scoped below a later sibling and CSSOM mutation changes paint and hit order without relayout. Direct browser/native execution, exact product pixels, package regions, memory, lifecycle, and performance evidence remain qualification debt." + }, + { + "family": "css-will-change-stacking-context", + "required": ["will-change auto and bounded custom-ident list CSSOM", "hinted atomic stacking context", "retained paint and hit-order mutation", "cascade removal fallback"], + "candidate": ["unchanged Code OSS product pixels", "cross-RID performance qualification"], + "productEvidence": ["Code OSS Quick Input", "Code OSS Action Widget", "Code OSS sessions and mobile views", "Code OSS Chat, counter, voice, and onboarding animation surfaces"], + "coverage": ["contracts/css-will-change-stacking-context.html", "test_css_will_change_stacking_context", "css-will-change-performance"], + "gap": "Issue #800 implements the exact will-change slice used by unchanged Code OSS. A normalized bounded list is cold state; one packed derived bit feeds the retained atomic stacking-context predicate for transform, opacity, and other context-establishing hints without allocating a compositor layer. Direct browser/native execution, exact product pixels, package regions, memory, lifecycle, and performance evidence remain qualification debt." } ] } diff --git a/tests/WebPlatformSubset/contracts/css-will-change-stacking-context.html b/tests/WebPlatformSubset/contracts/css-will-change-stacking-context.html new file mode 100644 index 00000000..8fb395a0 --- /dev/null +++ b/tests/WebPlatformSubset/contracts/css-will-change-stacking-context.html @@ -0,0 +1,26 @@ + + +Will-change establishes the hinted stacking context + + + +
+
+ diff --git a/tests/WebPlatformSubset/webscene-component-profile.json b/tests/WebPlatformSubset/webscene-component-profile.json index 39006ead..33bc0315 100644 --- a/tests/WebPlatformSubset/webscene-component-profile.json +++ b/tests/WebPlatformSubset/webscene-component-profile.json @@ -2850,6 +2850,13 @@ "capabilities": ["isolation-cssom", "atomic-stacking-context", "descendant-z-index-containment", "paint-order-mutation"], "evidence": ["focused-native-hit-order-contract-authored", "direct-chromium-contract-authored", "unchanged-code-oss-645f29c-inventory"], "reason": "Implementation-first WebScene #795 contract for the five isolation:isolate declarations in unchanged Code OSS. It covers initial/computed CSSOM, mutation/removal, invalid writes, descendant z-index containment, and retained hit order without relayout. Direct browser/native execution, exact product pixels, package regions, accessibility, performance, memory, and lifecycle evidence remain qualification debt." + }, + { + "path": "contracts/css-will-change-stacking-context.html", + "type": "testharness", + "capabilities": ["will-change-cssom", "hinted-atomic-stacking-context", "descendant-z-index-containment", "paint-order-mutation"], + "evidence": ["focused-native-hit-order-contract-authored", "direct-chromium-contract-authored", "unchanged-code-oss-645f29c-inventory"], + "reason": "Implementation-first WebScene #800 contract for 16 will-change declarations across 12 unchanged Code OSS CSS files. It covers bounded list CSSOM, mutation/removal, invalid writes, hinted descendant z-index containment, and retained hit order without relayout or eager layer allocation. Direct browser/native execution, exact product pixels, package regions, accessibility, performance, memory, and lifecycle evidence remain qualification debt." } ], "harnessBlocked": [], diff --git a/tests/WebScene.Css.Tests/CssPropertyCatalogTests.cs b/tests/WebScene.Css.Tests/CssPropertyCatalogTests.cs index b2e79ef1..f98d1961 100644 --- a/tests/WebScene.Css.Tests/CssPropertyCatalogTests.cs +++ b/tests/WebScene.Css.Tests/CssPropertyCatalogTests.cs @@ -59,6 +59,7 @@ public void DoesNotMisclassifyNonColorValueProperties(string propertyName) [InlineData("overscrollBehaviorX")] [InlineData("overscrollBehaviorY")] [InlineData("isolation")] + [InlineData("willChange")] public void ExposesSupportedCssomProperties(string name) => Assert.True(CssPropertyCatalog.IsSupported(name)); @@ -125,6 +126,8 @@ public void DoesNotExposeUnknownOrCustomPropertiesAsIdlAttributes(string name) [InlineData("overscroll-behavior-y", "bounce", false)] [InlineData("isolation", "isolate", true)] [InlineData("isolation", "group", false)] + [InlineData("will-change", "transform, opacity", true)] + [InlineData("will-change", "auto, transform", false)] public void ValidatesCssomValuesWithoutFrameworkKnowledge(string name, string value, bool expected) => Assert.Equal(expected, CssPropertyCatalog.IsValidCssomValue(name, value)); }