From a108afe00e840e72f3876e5d45cc932d52e01dde Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20=C5=A0olt=C3=A9s?= Date: Sun, 20 Sep 2026 07:14:09 +0200 Subject: [PATCH 1/2] Implement retained CSS caret color --- .../code-oss-css-caret-color-804.md | 35 ++++++++++ .../native/css_property_metadata.json | 11 +-- .../webscene_css_property_identity.inc | 10 ++- .../generated/webscene_css_property_ids.inc | 1 + .../webscene_css_supported_properties.inc | 3 +- .../native/webscene_css_paint_values.h | 14 ++++ .../native/webscene_css_reset.h | 6 ++ .../native/webscene_css_specified_coverage.h | 1 + .../native/webscene_css_specified_ir.h | 3 + .../native/webscene_css_specified_value.h | 4 ++ .../native/webscene_native_dom.cpp | 9 +++ .../native/webscene_native_dom.h | 10 +++ .../native/webscene_native_dom_scene.inc | 2 +- .../native/webscene_v8_runtime_dom_core.inc | 1 + .../native/webscene_v8_runtime_style.inc | 68 ++++++++++++++++++- .../native/webscene_v8_runtime_support.inc | 5 ++ .../tests/css_property_identity_tests.cpp | 14 ++-- .../native_v8_runtime_css_layout_tests.inc | 40 +++++++++++ ...tive_v8_runtime_frame_scheduling_tests.inc | 3 +- .../tests/native_v8_runtime_tests.cpp | 8 +++ .../CssMutationInvalidationPlanner.cs | 5 +- src/WebScene.Css/CssPropertyCatalog.cs | 2 + .../CssPropertyMetadata.Generated.cs | 3 + tests/WebPlatformSubset/capabilities.json | 8 +++ .../contracts/css-caret-color.html | 24 +++++++ .../webscene-component-profile.json | 7 ++ .../CssMutationInvalidationPlannerTests.cs | 2 + .../CssPropertyCatalogTests.cs | 4 ++ 28 files changed, 286 insertions(+), 17 deletions(-) create mode 100644 docs/validation/code-oss-css-caret-color-804.md create mode 100644 tests/WebPlatformSubset/contracts/css-caret-color.html diff --git a/docs/validation/code-oss-css-caret-color-804.md b/docs/validation/code-oss-css-caret-color-804.md new file mode 100644 index 000000000..d5fc5d805 --- /dev/null +++ b/docs/validation/code-oss-css-caret-color-804.md @@ -0,0 +1,35 @@ +# Code OSS CSS caret-color implementation (#804) + +## Product trigger + +Unchanged Code OSS authors `caret-color: +var(--vscode-editorCursor-foreground)` on the EditContext text area. WebScene +previously painted the focused form-control caret with the text foreground. + +## Implemented slice + +- Adds generated native/managed identity and CSSOM for inherited + `caret-color`, including `auto`, `currentColor`, CSS-wide values, mutation, + removal, and cascade fallback. +- Retains the uncommon token in the existing sparse cold style map, so the + feature adds no hot per-node field. +- Resolves the existing kind-14 caret scene command independently from text + foreground while preserving element opacity. +- Treats mutation as paint-only and adds no layout invalidation, extra scene + node, timer, platform widget, document scan, browser shell, or Code OSS + patch. + +## Authored gates + +- `contracts/css-caret-color.html`: inheritance, currentColor, computed CSSOM, + mutation, and removal. +- The existing native focus/blink/caret contract now requires distinct text + `#123456` and caret `#abcdef` scene colors. +- `css-caret-color-performance`: 4,096 inputs, 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 12e45feef..5291b7528 100644 --- a/experiments/WebScene.NativeEngine.Probe/native/css_property_metadata.json +++ b/experiments/WebScene.NativeEngine.Probe/native/css_property_metadata.json @@ -158,7 +158,8 @@ {"id": "overscroll_behavior_y", "name": "overscroll-behavior-y", "aliases": ["overscrollbehaviory"]}, {"id": "isolation", "name": "isolation"}, {"id": "will_change", "name": "will-change", "aliases": ["willchange"]}, - {"id": "text_wrap", "name": "text-wrap", "aliases": ["textwrap"]} + {"id": "text_wrap", "name": "text-wrap", "aliases": ["textwrap"]}, + {"id": "caret_color", "name": "caret-color", "aliases": ["caretcolor"]} ], "nativeStorageOnlyProperties": [ "accent-color", "align-content", "animation-direction", "animation-fill-mode", "animation-play-state", @@ -211,20 +212,20 @@ "grid_column_end", "border", "border_top", "border_right", "border_bottom", "border_left", "border_inline", "border_block", "border_color", "outline", "transform", "transform_origin", "box_shadow", "background", "background_image", "overflow", "overflow_x", "overflow_y", "opacity", "fill", "stroke", "cursor", "font", "font_family", - "font_weight", "list_style" + "font_weight", "list_style", "caret_color" ] }, "nativeInheritedProperties": [ "all", "color", "cursor", "direction", "font", "font-family", "font-size", "font-style", "font-weight", "letter-spacing", "line-height", "list-style", "list-style-position", "list-style-type", - "text-align", "text-transform", "visibility", "white-space", "word-break", "overflow-wrap", "word-spacing", "-webkit-font-smoothing" + "text-align", "text-transform", "visibility", "white-space", "text-wrap", "word-break", "overflow-wrap", "word-spacing", "caret-color", "-webkit-font-smoothing" ], "nativeMasklessPropertyIds": [ "content", "aspect_ratio", "vertical_align", "grid_auto_columns", "grid_auto_flow", "table_layout", "border_inline", "border_block", "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", "will_change", "text_wrap" + "object_fit", "object_position", "user_select", "overscroll_behavior", "overscroll_behavior_x", "overscroll_behavior_y", "isolation", "will_change", "text_wrap", "caret_color" ], "managedKnownProperties": [ "align-content", "align-items", "align-self", "all", "background", "background-color", @@ -246,7 +247,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", "will-change", "text-wrap" + "overscroll-behavior", "overscroll-behavior-x", "overscroll-behavior-y", "isolation", "will-change", "text-wrap", "caret-color" ], "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 5b64760d9..6e3ad9a6a 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 @@ -236,6 +236,8 @@ inline constexpr std::array native_typed_property_identity_catalog{ native_typed_property_identity{"willchange", css_property_id::will_change}, native_typed_property_identity{"text-wrap", css_property_id::text_wrap}, native_typed_property_identity{"textwrap", css_property_id::text_wrap}, + native_typed_property_identity{"caret-color", css_property_id::caret_color}, + native_typed_property_identity{"caretcolor", css_property_id::caret_color}, }; inline constexpr std::array native_storage_only_property_catalog{ @@ -462,6 +464,7 @@ inline constexpr std::array native_modeled_property_mask_catalog{ false, false, false, + false, }; inline constexpr bool generated_property_has_modeled_mask( @@ -472,9 +475,10 @@ inline constexpr bool generated_property_has_modeled_mask( && native_modeled_property_mask_catalog[index]; } -inline constexpr std::array native_inherited_property_catalog{ +inline constexpr std::array native_inherited_property_catalog{ "-webkit-font-smoothing", "all", + "caret-color", "color", "cursor", "direction", @@ -491,6 +495,7 @@ inline constexpr std::array native_inherited_property_cata "overflow-wrap", "text-align", "text-transform", + "text-wrap", "visibility", "white-space", "word-break", @@ -667,6 +672,7 @@ inline constexpr std::array native_property_grammar_catalog{ native_property_grammar::keyword, native_property_grammar::keyword, native_property_grammar::keyword, + native_property_grammar::complex, }; inline constexpr native_property_grammar generated_property_grammar( @@ -911,5 +917,7 @@ inline css_property_id generated_property_id_lowercase(std::string_view name) no if (name == "willchange") return css_property_id::will_change; if (name == "text-wrap") return css_property_id::text_wrap; if (name == "textwrap") return css_property_id::text_wrap; + if (name == "caret-color") return css_property_id::caret_color; + if (name == "caretcolor") return css_property_id::caret_color; 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 6e9a122fc..17d75b74c 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 @@ -157,3 +157,4 @@ overscroll_behavior_y, isolation, will_change, text_wrap, +caret_color, 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 62a04d104..54ccd36a5 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 @@ -94,6 +94,7 @@ inline constexpr std::array cssom_supported_property_catalog{ cssom_supported_property_metadata{"bottom", "bottom"}, cssom_supported_property_metadata{"box-shadow", "boxShadow"}, cssom_supported_property_metadata{"box-sizing", "boxSizing"}, + cssom_supported_property_metadata{"caret-color", "caretColor"}, cssom_supported_property_metadata{"clear", "clear"}, cssom_supported_property_metadata{"clip-path", "clipPath"}, cssom_supported_property_metadata{"clip-rule", "clipRule"}, @@ -262,4 +263,4 @@ inline constexpr std::array cssom_supported_property_catalog{ cssom_supported_property_metadata{"zoom", "zoom"}, }; -inline constexpr auto cssom_style_template_property_accessor_count = 466U; +inline constexpr auto cssom_style_template_property_accessor_count = 468U; diff --git a/experiments/WebScene.NativeEngine.Probe/native/webscene_css_paint_values.h b/experiments/WebScene.NativeEngine.Probe/native/webscene_css_paint_values.h index 9f09ea796..c34d99c73 100644 --- a/experiments/WebScene.NativeEngine.Probe/native/webscene_css_paint_values.h +++ b/experiments/WebScene.NativeEngine.Probe/native/webscene_css_paint_values.h @@ -98,6 +98,20 @@ template bool apply_paint_value(dom_node& node,const std::string& name,const std::string& value, Decision& decision,Protected&& is_inline,LoadSvg&& load_svg) { + if (name == "caret-color") { + auto normalized = ascii_lower(trim_value(value)); + auto& effects = node.style.mutable_textual().effect_values; + if (normalized == "inherit" || normalized == "unset") { + effects.erase(name); + } else { + if (normalized == "initial" || normalized == "revert" + || normalized == "revert-layer") normalized = "auto"; + effects[name] = std::move(normalized); + } + decision.classification = "supported"; + decision.semantic_slice = "retained inherited caret paint"; + return true; + } if (name == "mask-image") { auto& effects = node.style.mutable_textual().effect_values; effects.erase("-webscene-mask-markup"); diff --git a/experiments/WebScene.NativeEngine.Probe/native/webscene_css_reset.h b/experiments/WebScene.NativeEngine.Probe/native/webscene_css_reset.h index 5832c8233..abd508f35 100644 --- a/experiments/WebScene.NativeEngine.Probe/native/webscene_css_reset.h +++ b/experiments/WebScene.NativeEngine.Probe/native/webscene_css_reset.h @@ -258,6 +258,12 @@ inline void apply_all_unset( reset.mutable_textual().will_change = previous.textual().will_change; reset.will_change_stacking_context = previous.will_change_stacking_context; } + if (has_inline({"caret-color"})) { + const auto value = previous.textual().effect_values.find("caret-color"); + if (value != previous.textual().effect_values.end()) { + reset.mutable_textual().effect_values["caret-color"] = value->second; + } + } // 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 9ebfa53ce..57d5df2e2 100644 --- a/experiments/WebScene.NativeEngine.Probe/native/webscene_css_specified_coverage.h +++ b/experiments/WebScene.NativeEngine.Probe/native/webscene_css_specified_coverage.h @@ -14,6 +14,7 @@ inline constexpr auto specified_property_samples = std::to_arrayparent) { + const auto& effects = current->style.textual().effect_values; + const auto value = effects.find("caret-color"); + if (value != effects.end()) return value->second; + } + return "auto"; +} + // Shared by retained layout/paint and the V8-free CSS service contract so the // contract exercises the production marker-suppression path. std::string list_marker_text(const dom_node& node); diff --git a/experiments/WebScene.NativeEngine.Probe/native/webscene_native_dom_scene.inc b/experiments/WebScene.NativeEngine.Probe/native/webscene_native_dom_scene.inc index e2821746d..06874b79c 100644 --- a/experiments/WebScene.NativeEngine.Probe/native/webscene_native_dom_scene.inc +++ b/experiments/WebScene.NativeEngine.Probe/native/webscene_native_dom_scene.inc @@ -2967,7 +2967,7 @@ void native_document::append_scene( text_y + 1.0F, caret_x, text_y + std::max(1.0F, line_height - 1.0F), - with_opacity(resolved_foreground(node)), + with_opacity(resolved_caret_color(node)), node.id}); } } 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 e628e950c..058741fb7 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 @@ -744,6 +744,7 @@ std::string_view{"user-select"}, std::string_view{"isolation"}, std::string_view{"will-change"}, + std::string_view{"caret-color"}, std::string_view{"pointer-events"}, std::string_view{"visibility"}, std::string_view{"-webkit-font-smoothing"} 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 92c6c767d..c38d3d460 100644 --- a/experiments/WebScene.NativeEngine.Probe/native/webscene_v8_runtime_style.inc +++ b/experiments/WebScene.NativeEngine.Probe/native/webscene_v8_runtime_style.inc @@ -51,6 +51,29 @@ return "normal"; } + static std::string serialized_caret_color(const dom_node& node) + { + const auto token = resolved_caret_color_token(node); + auto color = uint32_t{0xD1D4DCFFU}; + if (token == "auto" || token == "currentcolor") { + color = resolved_current_color_for_cssom(node); + } else { + color = native_document::parse_color(token); + } + std::ostringstream stream; + const auto red = static_cast((color >> 24U) & 0xFFU); + const auto green = static_cast((color >> 16U) & 0xFFU); + const auto blue = static_cast((color >> 8U) & 0xFFU); + const auto alpha = static_cast(color & 0xFFU); + if (alpha == 0xFFU) { + stream << "rgb(" << red << ", " << green << ", " << blue << ')'; + } else { + stream << "rgba(" << red << ", " << green << ", " << blue << ", " + << serialize_css_number(static_cast(alpha) / 255.0) << ')'; + } + return stream.str(); + } + static bool clear_inline_style(dom_node& node, const std::string& name) { const auto canonical_name = canonical_css_property_name(name); @@ -286,6 +309,10 @@ textual->will_change.clear(); } node.style.will_change_stacking_context = false; + } else if (name == "caretColor" || name == "caret-color") { + if (auto* textual = node.style.mutable_textual_if_present()) { + textual->effect_values.erase("caret-color"); + } } else if (name == "boxSizing" || name == "box-sizing") { node.style.border_box = false; node.style.inline_property_mask &= ~inline_box_sizing; @@ -781,7 +808,8 @@ self->apply_css_declaration( *node, {canonical_name, specified_value, important}, true); node->style.inline_property_mask |= property_mask; - } else if (value.find("var(") != std::string::npos && property_mask != 0U) { + } else if (value.find("var(") != std::string::npos + && (property_mask != 0U || canonical_name == "caret-color")) { // A CSSOM write is observable immediately. Resolve variable-backed // declarations through the same computed-value path used during a // cascade instead of feeding raw var() tokens to numeric parsers. @@ -1016,6 +1044,16 @@ std::string semantic_slice; } decision; css::apply_will_change_value(*node, name, value, decision); + } else if (name == "caret-color") { + auto normalized = ascii_lower(trim_value(specified_value)); + auto& effects = node->style.mutable_textual().effect_values; + if (normalized == "inherit" || normalized == "unset") { + effects.erase("caret-color"); + } else { + if (normalized == "initial" || normalized == "revert" + || normalized == "revert-layer") normalized = "auto"; + effects["caret-color"] = std::move(normalized); + } } else if (name == "box-sizing") { node->style.border_box = value == "border-box"; node->style.inline_property_mask |= inline_box_sizing; @@ -1648,6 +1686,15 @@ } else if (name == "will-change") { value = node->style.textual().will_change.empty() ? "auto" : node->style.textual().will_change; + } else if (name == "caret-color") { + if (computed) { + value = serialized_caret_color(*node); + } else { + const auto& declarations = node->authored_style().declarations; + const auto authored = declarations.find("caret-color"); + value = authored == declarations.end() + ? std::string{} : authored->second; + } } else if (name == "cursor") { value = resolved_cursor(*node); } else if (name == "font-size") { @@ -2129,6 +2176,15 @@ } else if (name == "willChange") { value = node->style.textual().will_change.empty() ? "auto" : node->style.textual().will_change; + } else if (name == "caretColor") { + if (computed) { + value = serialized_caret_color(*node); + } else { + const auto& declarations = node->authored_style().declarations; + const auto authored = declarations.find("caret-color"); + value = authored == declarations.end() + ? std::string{} : authored->second; + } } else if (name == "boxSizing") { value = node->style.border_box ? "border-box" : "content-box"; } else if (name == "borderRadius") { @@ -2743,6 +2799,16 @@ std::string semantic_slice; } decision; css::apply_will_change_value(*node, "will-change", value, decision); + } else if (name == "caretColor") { + auto normalized = ascii_lower(trim_value(specified_value)); + auto& effects = node->style.mutable_textual().effect_values; + if (normalized == "inherit" || normalized == "unset") { + effects.erase("caret-color"); + } else { + if (normalized == "initial" || normalized == "revert" + || normalized == "revert-layer") normalized = "auto"; + effects["caret-color"] = std::move(normalized); + } } 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 7bdff31b5..1fb8f3156 100644 --- a/experiments/WebScene.NativeEngine.Probe/native/webscene_v8_runtime_support.inc +++ b/experiments/WebScene.NativeEngine.Probe/native/webscene_v8_runtime_support.inc @@ -688,6 +688,11 @@ bool valid_cssom_declaration_value(std::string_view property_name, std::string_v if (canonical_name == "text-wrap") { return normalized_value == "wrap" || normalized_value == "nowrap"; } + if (canonical_name == "caret-color") { + const auto compiled = css::compile_specified_value( + css_property_id::caret_color, trimmed); + return compiled.valid; + } 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 ee1b508a9..7f1c467c6 100644 --- a/experiments/WebScene.NativeEngine.Probe/tests/css_property_identity_tests.cpp +++ b/experiments/WebScene.NativeEngine.Probe/tests/css_property_identity_tests.cpp @@ -105,10 +105,11 @@ int main() static_assert(static_cast(css_property_id::isolation) == 153U); static_assert(static_cast(css_property_id::will_change) == 154U); static_assert(static_cast(css_property_id::text_wrap) == 155U); - static_assert(native_typed_property_identity_catalog.size() == 231U); + static_assert(static_cast(css_property_id::caret_color) == 156U); + static_assert(native_typed_property_identity_catalog.size() == 233U); static_assert(native_storage_only_property_catalog.size() == 62U); - static_assert(cssom_supported_property_catalog.size() == 255U); - static_assert(cssom_style_template_property_accessor_count == 464U); + static_assert(cssom_supported_property_catalog.size() == 256U); + static_assert(cssom_style_template_property_accessor_count == 468U); 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); @@ -151,7 +152,8 @@ int main() && !variable_collection_contains(indexed_variables, std::string{"--missing"}), "variable membership supports ordered and indexed containers"); - std::array audited_mask_ids{}; + std::array(css_property_id::caret_color) + 1U> + audited_mask_ids{}; for (const auto& entry : native_typed_property_identity_catalog) { const auto index = static_cast(entry.id); if (audited_mask_ids[index]) continue; @@ -161,10 +163,12 @@ int main() "native mask coverage agrees with explicit property classification", entry.name); } - require(native_inherited_property_catalog.size() == 20U, + require(native_inherited_property_catalog.size() == 24U, "native inherited-property classification remains complete"); require(generated_property_inherits_by_default("color") && generated_property_inherits_by_default("direction") + && generated_property_inherits_by_default("text-wrap") + && generated_property_inherits_by_default("caret-color") && generated_property_inherits_by_default("-webkit-font-smoothing") && !generated_property_inherits_by_default("display") && !generated_property_inherits_by_default("width"), 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 14edfe407..fe66c0f90 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 @@ -366,6 +366,46 @@ void test_css_text_wrap_performance_gate(webscene_engine* engine) + std::to_string(elapsed) + " ms"); } +void test_css_caret_color_performance_gate(webscene_engine* engine) +{ + resize(engine, 1024, 768, 1U); + execute(engine, R"JS( + document.body.innerHTML = '
'; + globalThis.caretColorGrid = document.getElementById('caret-color-grid'); + const fragment = document.createDocumentFragment(); + for (let index = 0; index < 4096; ++index) { + fragment.appendChild(document.createElement('input')); + } + caretColorGrid.appendChild(fragment); + )JS", "native-css-caret-color-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 ? '#abcdef' : 'auto'; + for (const node of caretColorGrid.children) node.style.caretColor = value; + } + return [caretColorGrid.children.length, + caretColorGrid.lastElementChild.style.caretColor]; + })() + )JS", "native-css-caret-color-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 caret-color performance fixture regressed: " + result); + require(after.layout_passes == before.layout_passes, + "CSS caret-color transitions performed layout"); + require(after.published_scenes <= before.published_scenes + 1U, + "CSS caret-color transitions published unbounded scenes"); + require(elapsed < 5000.0, + "4,096-node CSS caret-color gate exceeded 5 seconds: " + + std::to_string(elapsed) + " ms"); +} + void test_modal_backdrop_scene(webscene_engine* engine) { resize(engine,400,300,1); diff --git a/experiments/WebScene.NativeEngine.Probe/tests/native_v8_runtime_frame_scheduling_tests.inc b/experiments/WebScene.NativeEngine.Probe/tests/native_v8_runtime_frame_scheduling_tests.inc index d005a628e..77fe3e94e 100644 --- a/experiments/WebScene.NativeEngine.Probe/tests/native_v8_runtime_frame_scheduling_tests.inc +++ b/experiments/WebScene.NativeEngine.Probe/tests/native_v8_runtime_frame_scheduling_tests.inc @@ -20,6 +20,7 @@ void test_native_text_input_focus_events_and_caret(webscene_engine* engine) input.style.letterSpacing = '2px'; input.style.wordSpacing = '3px'; input.style.color = 'rgb(18, 52, 86)'; + input.style.caretColor = 'rgb(171, 205, 239)'; const form = document.createElement('form'); document.body.appendChild(form); form.appendChild(input); @@ -108,7 +109,7 @@ void test_native_text_input_focus_events_and_caret(webscene_engine* engine) } } if (command.kind == 14U - && (command.rgba >> 8U) == 0x123456U + && (command.rgba >> 8U) == 0xABCDEFU && command.x >= 20.0F && command.x <= 200.0F && command.y >= 20.0F && command.y <= 52.0F) { observed_caret = true; 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 26d2521bc..1639fb828 100644 --- a/experiments/WebScene.NativeEngine.Probe/tests/native_v8_runtime_tests.cpp +++ b/experiments/WebScene.NativeEngine.Probe/tests/native_v8_runtime_tests.cpp @@ -437,6 +437,14 @@ int main() webscene_engine_destroy(focused_engine); return 0; } + if (selected == "css-caret-color-performance") { + auto* focused_engine = webscene_engine_create(0); + require(focused_engine != nullptr, + "CSS caret-color performance engine creation failed"); + test_css_caret_color_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"); diff --git a/src/WebScene.Css/CssMutationInvalidationPlanner.cs b/src/WebScene.Css/CssMutationInvalidationPlanner.cs index 56d542ee5..0c5897d7f 100644 --- a/src/WebScene.Css/CssMutationInvalidationPlanner.cs +++ b/src/WebScene.Css/CssMutationInvalidationPlanner.cs @@ -28,7 +28,8 @@ public static class CssMutationInvalidationPlanner "font-size", "font-style", "font-variant", "font-weight", "letter-spacing", "line-height", "list-style", "list-style-position", "list-style-type", "pointer-events", "stroke", "stroke-linecap", "stroke-linejoin", "stroke-opacity", "stroke-width", - "text-align", "text-indent", "text-transform", "visibility", "white-space", "word-break", + "text-align", "text-indent", "text-transform", "visibility", "white-space", "text-wrap", "word-break", + "caret-color", "overflow-wrap", "word-wrap", "word-spacing" }; @@ -41,7 +42,7 @@ public static class CssMutationInvalidationPlanner "grid-row-start", "grid-row-end", "grid-column", "grid-column-start", "grid-column-end", "align-content", "align-items", "align-self", "justify-content", "gap", "list-style", "list-style-position", "list-style-type", "order", "row-gap", "column-gap", "z-index", "white-space", - "word-break", "overflow-wrap", "word-wrap" + "text-wrap", "word-break", "overflow-wrap", "word-wrap" }; internal static IReadOnlySet InheritedProperties => s_inheritedProperties; diff --git a/src/WebScene.Css/CssPropertyCatalog.cs b/src/WebScene.Css/CssPropertyCatalog.cs index 84eb9b818..d78bd600f 100644 --- a/src/WebScene.Css/CssPropertyCatalog.cs +++ b/src/WebScene.Css/CssPropertyCatalog.cs @@ -93,6 +93,8 @@ public static bool IsValidCssomValue(string propertyName, string value) "isolation" => normalizedValue is "auto" or "isolate", "will-change" => IsWillChange(normalizedValue), "text-wrap" => normalizedValue is "wrap" or "nowrap", + "caret-color" => normalizedValue is "auto" or "currentcolor" + || CssColorParser.TryParseColor(trimmed, out _), "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", diff --git a/src/WebScene.Css/CssPropertyMetadata.Generated.cs b/src/WebScene.Css/CssPropertyMetadata.Generated.cs index 382ea1e75..acba5b7bf 100644 --- a/src/WebScene.Css/CssPropertyMetadata.Generated.cs +++ b/src/WebScene.Css/CssPropertyMetadata.Generated.cs @@ -122,6 +122,7 @@ internal static class CssGeneratedPropertyMetadata "isolation", "will-change", "text-wrap", + "caret-color", ]; internal static readonly string[] SupportedNames = @@ -215,6 +216,7 @@ internal static class CssGeneratedPropertyMetadata "bottom", "box-shadow", "box-sizing", + "caret-color", "clear", "clip-path", "clip-rule", @@ -504,6 +506,7 @@ internal static bool TryGetKnownId(string name, out int id) "isolation" => 114, "will-change" => 115, "text-wrap" => 116, + "caret-color" => 117, _ => -1, }; return id >= 0; diff --git a/tests/WebPlatformSubset/capabilities.json b/tests/WebPlatformSubset/capabilities.json index 4696769d6..6776d0357 100644 --- a/tests/WebPlatformSubset/capabilities.json +++ b/tests/WebPlatformSubset/capabilities.json @@ -456,6 +456,14 @@ "productEvidence": ["Code OSS diff and placeholder text", "Code OSS hover and Changes surfaces", "Code OSS Chat status, anchors, and code pills", "Code OSS notebook content"], "coverage": ["contracts/css-text-wrap-mode.html", "test_css_text_wrap_layout", "css-text-wrap-performance"], "gap": "Issue #802 implements the wrap/nowrap mode used by unchanged Code OSS while keeping computed white-space independent. The inherited cold token feeds the existing retained inline/layout/scene wrapping decision. Balance, pretty, stable, and the complete Text Level 4 shorthand remain outside this slice. Direct browser/native execution, exact product pixels, package regions, memory, lifecycle, and performance evidence remain qualification debt." + }, + { + "family": "css-caret-color", + "required": ["caret-color CSSOM", "inherited caret paint", "auto and currentColor", "cascade mutation and removal"], + "candidate": ["unchanged Code OSS editor caret pixels", "cross-RID performance qualification"], + "productEvidence": ["Code OSS EditContext text-area cursor", "Code OSS editor cursor theme color"], + "coverage": ["contracts/css-caret-color.html", "test_native_text_input_focus_events_and_caret", "css-caret-color-performance"], + "gap": "Issue #804 implements the caret-color slice used by unchanged Code OSS. The inherited token is stored only in sparse cold style state and drives the existing kind-14 retained caret command independently from text foreground, with no extra scene node. Direct browser/native execution, exact editor pixels, package regions, memory, lifecycle, and performance evidence remain qualification debt." } ] } diff --git a/tests/WebPlatformSubset/contracts/css-caret-color.html b/tests/WebPlatformSubset/contracts/css-caret-color.html new file mode 100644 index 000000000..b289d20a1 --- /dev/null +++ b/tests/WebPlatformSubset/contracts/css-caret-color.html @@ -0,0 +1,24 @@ + + +Caret color CSSOM and inheritance + + + +
+ diff --git a/tests/WebPlatformSubset/webscene-component-profile.json b/tests/WebPlatformSubset/webscene-component-profile.json index 30aa60e92..c21449dcf 100644 --- a/tests/WebPlatformSubset/webscene-component-profile.json +++ b/tests/WebPlatformSubset/webscene-component-profile.json @@ -2864,6 +2864,13 @@ "capabilities": ["text-wrap-cssom", "inherited-wrap-mode", "retained-line-wrapping", "text-wrap-mutation"], "evidence": ["focused-native-layout-contract-authored", "direct-chromium-contract-authored", "unchanged-code-oss-645f29c-inventory"], "reason": "Implementation-first WebScene #802 contract for 10 text-wrap declarations across 9 unchanged Code OSS CSS files. It covers wrap/nowrap CSSOM, inheritance, invalid writes, mutation/removal, overflow, and retained height changes while preserving independent computed white-space. Direct browser/native execution, exact product pixels, package regions, accessibility, performance, memory, and lifecycle evidence remain qualification debt." + }, + { + "path": "contracts/css-caret-color.html", + "type": "testharness", + "capabilities": ["caret-color-cssom", "inherited-caret-color", "currentcolor-caret", "retained-caret-paint"], + "evidence": ["focused-native-scene-contract-authored", "direct-chromium-contract-authored", "unchanged-code-oss-645f29c-inventory"], + "reason": "Implementation-first WebScene #804 contract for the unchanged Code OSS EditContext caret color. It covers inheritance, auto/currentColor, computed CSSOM, mutation/removal, and a distinct retained caret scene color without adding a second caret node. Direct browser/native execution, exact editor pixels, package regions, accessibility, performance, memory, and lifecycle evidence remain qualification debt." } ], "harnessBlocked": [], diff --git a/tests/WebScene.Css.Tests/CssMutationInvalidationPlannerTests.cs b/tests/WebScene.Css.Tests/CssMutationInvalidationPlannerTests.cs index 9ea9ae612..190820109 100644 --- a/tests/WebScene.Css.Tests/CssMutationInvalidationPlannerTests.cs +++ b/tests/WebScene.Css.Tests/CssMutationInvalidationPlannerTests.cs @@ -11,6 +11,8 @@ public sealed class CssMutationInvalidationPlannerTests [InlineData("--theme:red", CssMutationInvalidationScope.Target | CssMutationInvalidationScope.Descendants)] [InlineData("width:10px", CssMutationInvalidationScope.Target | CssMutationInvalidationScope.Layout)] [InlineData("flex-wrap:wrap", CssMutationInvalidationScope.Target | CssMutationInvalidationScope.Layout)] + [InlineData("text-wrap:nowrap", CssMutationInvalidationScope.Target | CssMutationInvalidationScope.Descendants | CssMutationInvalidationScope.Layout)] + [InlineData("caret-color:red", CssMutationInvalidationScope.Target | CssMutationInvalidationScope.Descendants)] [InlineData("background:red", CssMutationInvalidationScope.Target)] [InlineData("broken", CssMutationInvalidationScope.Target)] public void InlineStylePlanClassifiesPortableScope(string style, CssMutationInvalidationScope expected) diff --git a/tests/WebScene.Css.Tests/CssPropertyCatalogTests.cs b/tests/WebScene.Css.Tests/CssPropertyCatalogTests.cs index ba8c0528d..a4e349206 100644 --- a/tests/WebScene.Css.Tests/CssPropertyCatalogTests.cs +++ b/tests/WebScene.Css.Tests/CssPropertyCatalogTests.cs @@ -61,6 +61,7 @@ public void DoesNotMisclassifyNonColorValueProperties(string propertyName) [InlineData("isolation")] [InlineData("willChange")] [InlineData("textWrap")] + [InlineData("caretColor")] public void ExposesSupportedCssomProperties(string name) => Assert.True(CssPropertyCatalog.IsSupported(name)); @@ -131,6 +132,9 @@ public void DoesNotExposeUnknownOrCustomPropertiesAsIdlAttributes(string name) [InlineData("will-change", "auto, transform", false)] [InlineData("text-wrap", "nowrap", true)] [InlineData("text-wrap", "balance", false)] + [InlineData("caret-color", "#123456", true)] + [InlineData("caret-color", "currentColor", true)] + [InlineData("caret-color", "not-a-color", false)] public void ValidatesCssomValuesWithoutFrameworkKnowledge(string name, string value, bool expected) => Assert.Equal(expected, CssPropertyCatalog.IsValidCssomValue(name, value)); } From dc6b2178307f54b07f2f976c01910fc9a01e7b41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20=C5=A0olt=C3=A9s?= Date: Sun, 20 Sep 2026 07:15:45 +0200 Subject: [PATCH 2/2] Fix caret color parser input type --- .../WebScene.NativeEngine.Probe/native/webscene_native_dom.cpp | 2 +- .../native/webscene_v8_runtime_style.inc | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/experiments/WebScene.NativeEngine.Probe/native/webscene_native_dom.cpp b/experiments/WebScene.NativeEngine.Probe/native/webscene_native_dom.cpp index eaa2d9b42..d674c2318 100644 --- a/experiments/WebScene.NativeEngine.Probe/native/webscene_native_dom.cpp +++ b/experiments/WebScene.NativeEngine.Probe/native/webscene_native_dom.cpp @@ -729,7 +729,7 @@ uint32_t resolved_caret_color(const dom_node& node) if (value == "auto" || value == "currentcolor") { return resolved_foreground(node); } - return native_document::parse_color(value); + return native_document::parse_color(std::string(value)); } void append_xml_escaped(std::string_view value, std::string& output, bool attribute) 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 c38d3d460..6336a8665 100644 --- a/experiments/WebScene.NativeEngine.Probe/native/webscene_v8_runtime_style.inc +++ b/experiments/WebScene.NativeEngine.Probe/native/webscene_v8_runtime_style.inc @@ -58,7 +58,7 @@ if (token == "auto" || token == "currentcolor") { color = resolved_current_color_for_cssom(node); } else { - color = native_document::parse_color(token); + color = native_document::parse_color(std::string(token)); } std::ostringstream stream; const auto red = static_cast((color >> 24U) & 0xFFU);