From 82a42d86b21069c30120ff7d6a0abf806b8056b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20=C5=A0olt=C3=A9s?= Date: Sun, 20 Sep 2026 06:59:22 +0200 Subject: [PATCH] Implement CSS text-wrap layout mode --- docs/validation/code-oss-css-text-wrap-802.md | 37 ++++++++++ .../native/css_property_metadata.json | 9 ++- .../webscene_css_property_identity.inc | 6 ++ .../generated/webscene_css_property_ids.inc | 1 + .../webscene_css_supported_properties.inc | 3 +- .../native/webscene_css_cascade_reset.h | 1 + .../native/webscene_css_reset.h | 3 + .../native/webscene_css_specified_coverage.h | 1 + .../native/webscene_css_text_values.h | 10 +++ .../native/webscene_native_dom.cpp | 1 + .../native/webscene_native_dom.h | 12 +++ .../native/webscene_native_dom_metrics.inc | 1 + .../native/webscene_v8_runtime_style.inc | 21 ++++++ .../native/webscene_v8_runtime_support.inc | 3 + .../tests/css_property_identity_tests.cpp | 7 +- .../native_v8_runtime_css_layout_tests.inc | 73 +++++++++++++++++++ .../tests/native_v8_runtime_tests.cpp | 9 +++ src/WebScene.Css/CssPropertyCatalog.cs | 1 + .../CssPropertyMetadata.Generated.cs | 3 + tests/WebPlatformSubset/capabilities.json | 8 ++ .../contracts/css-text-wrap-mode.html | 25 +++++++ .../webscene-component-profile.json | 7 ++ .../CssPropertyCatalogTests.cs | 3 + 23 files changed, 237 insertions(+), 8 deletions(-) create mode 100644 docs/validation/code-oss-css-text-wrap-802.md create mode 100644 tests/WebPlatformSubset/contracts/css-text-wrap-mode.html diff --git a/docs/validation/code-oss-css-text-wrap-802.md b/docs/validation/code-oss-css-text-wrap-802.md new file mode 100644 index 00000000..73c02c13 --- /dev/null +++ b/docs/validation/code-oss-css-text-wrap-802.md @@ -0,0 +1,37 @@ +# Code OSS CSS text-wrap implementation (#802) + +## Product trigger + +Unchanged Code OSS has 10 `text-wrap` declarations across 9 CSS files. The +bounded values are `nowrap`, `wrap`, and one `initial` reset across diff and +placeholder text, hovers, Changes, Chat status/anchors/code pills, and notebook +content. + +## Implemented slice + +- Adds generated native/managed identity, validation, and CSSOM for the + product-used `wrap|nowrap` mode plus CSS-wide reset/inheritance behavior. +- Retains the inherited mode in cold textual style independently from + `white-space`; computed `white-space` therefore remains browser-shaped. +- Composes the mode with the existing retained inline/layout/scene wrap + decision, including word-break and overflow-wrap behavior. +- Uses ordinary coalesced layout invalidation and adds no timer, frame + participant, platform text widget, document scan, browser shell, or Code OSS + patch. + +`balance`, `pretty`, `stable`, and the complete Text Level 4 shorthand grammar +remain outside this bounded slice. + +## Authored gates + +- `contracts/css-text-wrap-mode.html`: inheritance, computed CSSOM, invalid + writes, mutation/removal, overflow, and height change. +- `test_css_text_wrap_layout`: native inheritance and retained geometry. +- `css-text-wrap-performance`: 4,096 text nodes, ten forced layout checkpoints, + bounded layout/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 3a51ec48..12e45fee 100644 --- a/experiments/WebScene.NativeEngine.Probe/native/css_property_metadata.json +++ b/experiments/WebScene.NativeEngine.Probe/native/css_property_metadata.json @@ -157,7 +157,8 @@ {"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": "will_change", "name": "will-change", "aliases": ["willchange"]} + {"id": "will_change", "name": "will-change", "aliases": ["willchange"]}, + {"id": "text_wrap", "name": "text-wrap", "aliases": ["textwrap"]} ], "nativeStorageOnlyProperties": [ "accent-color", "align-content", "animation-direction", "animation-fill-mode", "animation-play-state", @@ -179,7 +180,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", "will_change" + "overscroll_behavior", "overscroll_behavior_x", "overscroll_behavior_y", "isolation", "will_change", "text_wrap" ], "componentList": [ "container_name", "background_position", "background_size", "transition", "transition_property", "transition_duration", @@ -223,7 +224,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", "will_change" + "object_fit", "object_position", "user_select", "overscroll_behavior", "overscroll_behavior_x", "overscroll_behavior_y", "isolation", "will_change", "text_wrap" ], "managedKnownProperties": [ "align-content", "align-items", "align-self", "all", "background", "background-color", @@ -245,7 +246,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" + "overscroll-behavior", "overscroll-behavior-x", "overscroll-behavior-y", "isolation", "will-change", "text-wrap" ], "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 49b28f06..5b64760d 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 @@ -234,6 +234,8 @@ inline constexpr std::array native_typed_property_identity_catalog{ 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}, + native_typed_property_identity{"text-wrap", css_property_id::text_wrap}, + native_typed_property_identity{"textwrap", css_property_id::text_wrap}, }; inline constexpr std::array native_storage_only_property_catalog{ @@ -459,6 +461,7 @@ inline constexpr std::array native_modeled_property_mask_catalog{ false, false, false, + false, }; inline constexpr bool generated_property_has_modeled_mask( @@ -663,6 +666,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( @@ -905,5 +909,7 @@ inline css_property_id generated_property_id_lowercase(std::string_view name) no 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; + if (name == "text-wrap") return css_property_id::text_wrap; + if (name == "textwrap") return css_property_id::text_wrap; 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 873f5aed..6e9a122f 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 @@ -156,3 +156,4 @@ overscroll_behavior_x, overscroll_behavior_y, isolation, will_change, +text_wrap, 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 71ebed87..62a04d10 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 @@ -236,6 +236,7 @@ inline constexpr std::array cssom_supported_property_catalog{ cssom_supported_property_metadata{"text-indent", "textIndent"}, cssom_supported_property_metadata{"text-overflow", "textOverflow"}, cssom_supported_property_metadata{"text-transform", "textTransform"}, + cssom_supported_property_metadata{"text-wrap", "textWrap"}, cssom_supported_property_metadata{"top", "top"}, cssom_supported_property_metadata{"transform", "transform"}, cssom_supported_property_metadata{"transform-origin", "transformOrigin"}, @@ -261,4 +262,4 @@ inline constexpr std::array cssom_supported_property_catalog{ cssom_supported_property_metadata{"zoom", "zoom"}, }; -inline constexpr auto cssom_style_template_property_accessor_count = 464U; +inline constexpr auto cssom_style_template_property_accessor_count = 466U; 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 065615fe..2ebf46d7 100644 --- a/experiments/WebScene.NativeEngine.Probe/native/webscene_css_cascade_reset.h +++ b/experiments/WebScene.NativeEngine.Probe/native/webscene_css_cascade_reset.h @@ -226,6 +226,7 @@ inline void reset_cascaded_style(dom_node& node, } textual->word_break.clear(); textual->overflow_wrap.clear(); + textual->text_wrap.clear(); textual->object_fit.clear(); textual->object_position.clear(); textual->user_select.clear(); diff --git a/experiments/WebScene.NativeEngine.Probe/native/webscene_css_reset.h b/experiments/WebScene.NativeEngine.Probe/native/webscene_css_reset.h index 08e10742..5832c823 100644 --- a/experiments/WebScene.NativeEngine.Probe/native/webscene_css_reset.h +++ b/experiments/WebScene.NativeEngine.Probe/native/webscene_css_reset.h @@ -231,6 +231,9 @@ inline void apply_all_unset( reset.mutable_textual().overflow_wrap = previous.textual().overflow_wrap; } + if (has_inline({"text-wrap"})) { + reset.mutable_textual().text_wrap = previous.textual().text_wrap; + } if (has_inline({"object-fit"})) { reset.mutable_textual().object_fit = previous.textual().object_fit; 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 91000519..9ebfa53c 100644 --- a/experiments/WebScene.NativeEngine.Probe/native/webscene_css_specified_coverage.h +++ b/experiments/WebScene.NativeEngine.Probe/native/webscene_css_specified_coverage.h @@ -13,6 +13,7 @@ inline constexpr auto specified_property_samples = std::to_arrayparent) { if (current->style.textual().white_space.empty()) continue; return current->style.textual().white_space != "nowrap" diff --git a/experiments/WebScene.NativeEngine.Probe/native/webscene_native_dom.h b/experiments/WebScene.NativeEngine.Probe/native/webscene_native_dom.h index 3384dddc..bc81bfde 100644 --- a/experiments/WebScene.NativeEngine.Probe/native/webscene_native_dom.h +++ b/experiments/WebScene.NativeEngine.Probe/native/webscene_native_dom.h @@ -791,6 +791,9 @@ struct node_style final { std::string vertical_align; std::string text_transform; std::string white_space; + // CSS Text Level 4 wrap mode is inherited independently from the + // white-space shorthand. Empty means inherit; root resolves to wrap. + std::string text_wrap; // Inherited line-breaking policy. Empty values inherit without adding // state to descendants; the root resolves both properties to normal. std::string word_break; @@ -2233,6 +2236,15 @@ inline bool resolved_right_to_left(const dom_node& node) noexcept return false; } +inline std::string_view resolved_text_wrap_mode(const dom_node& node) noexcept +{ + for (auto* current = &node; current != nullptr; current = current->parent) { + const auto& value = current->style.textual().text_wrap; + if (!value.empty()) return value; + } + return "wrap"; +} + // 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_metrics.inc b/experiments/WebScene.NativeEngine.Probe/native/webscene_native_dom_metrics.inc index 132fb69d..7f34a72e 100644 --- a/experiments/WebScene.NativeEngine.Probe/native/webscene_native_dom_metrics.inc +++ b/experiments/WebScene.NativeEngine.Probe/native/webscene_native_dom_metrics.inc @@ -341,6 +341,7 @@ native_document::allocation_metrics native_document::read_allocation_metrics() c + textual->vertical_align.capacity() + 1U + textual->text_transform.capacity() + 1U + textual->white_space.capacity() + 1U + + textual->text_wrap.capacity() + 1U + textual->word_break.capacity() + 1U + textual->overflow_wrap.capacity() + 1U + textual->object_fit.capacity() + 1U 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 44c0bca4..92c6c767 100644 --- a/experiments/WebScene.NativeEngine.Probe/native/webscene_v8_runtime_style.inc +++ b/experiments/WebScene.NativeEngine.Probe/native/webscene_v8_runtime_style.inc @@ -449,6 +449,10 @@ textual->white_space.clear(); } node.style.inline_property_mask &= ~inline_white_space; + } else if (name == "textWrap" || name == "text-wrap") { + if (auto* textual = node.style.mutable_textual_if_present()) { + textual->text_wrap.clear(); + } } else if (name == "wordBreak" || name == "word-break") { if (auto* textual = node.style.mutable_textual_if_present()) { textual->word_break.clear(); @@ -1174,6 +1178,12 @@ } else if (name == "white-space") { node->style.mutable_textual().white_space = value; node->style.inline_property_mask |= inline_white_space; + } else if (name == "text-wrap") { + auto mode = value; + if (mode == "inherit" || mode == "unset") mode.clear(); + else if (mode == "initial" || mode == "revert" + || mode == "revert-layer") mode = "wrap"; + node->style.mutable_textual().text_wrap = std::move(mode); } else if (name == "word-break") { node->style.mutable_textual().word_break = value == "inherit" || value == "unset" ? std::string{} : value; @@ -1610,6 +1620,8 @@ value = computed_effect_value(*node, name); } else if (name == "white-space") { value = node->style.textual().white_space; + } else if (name == "text-wrap") { + value = resolved_text_wrap_mode(*node); } else if (name == "word-break") { value = resolved_line_break_property(*node, false); } else if (name == "overflow-wrap") { @@ -2367,6 +2379,9 @@ value = node->style.textual().text_align; } else if (name == "whiteSpace") { value = node->style.textual().white_space; + } else if (name == "textWrap") { + value = computed ? std::string(resolved_text_wrap_mode(*node)) + : node->style.textual().text_wrap; } else if (name == "wordBreak") { value = computed ? resolved_line_break_property(*node, false) @@ -2915,6 +2930,12 @@ } else if (name == "whiteSpace") { node->style.mutable_textual().white_space = value; node->style.inline_property_mask |= inline_white_space; + } else if (name == "textWrap") { + auto mode = value; + if (mode == "inherit" || mode == "unset") mode.clear(); + else if (mode == "initial" || mode == "revert" + || mode == "revert-layer") mode = "wrap"; + node->style.mutable_textual().text_wrap = std::move(mode); } else if (name == "wordBreak") { node->style.mutable_textual().word_break = value == "inherit" || value == "unset" ? std::string{} : value; 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 7c69dbf6..7bdff31b 100644 --- a/experiments/WebScene.NativeEngine.Probe/native/webscene_v8_runtime_support.inc +++ b/experiments/WebScene.NativeEngine.Probe/native/webscene_v8_runtime_support.inc @@ -685,6 +685,9 @@ bool valid_cssom_declaration_value(std::string_view property_name, std::string_v if (canonical_name == "will-change") { return css::normalize_will_change_value(normalized_value).has_value(); } + if (canonical_name == "text-wrap") { + return normalized_value == "wrap" || normalized_value == "nowrap"; + } 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 f48ca814..ee1b508a 100644 --- a/experiments/WebScene.NativeEngine.Probe/tests/css_property_identity_tests.cpp +++ b/experiments/WebScene.NativeEngine.Probe/tests/css_property_identity_tests.cpp @@ -104,10 +104,11 @@ int main() static_assert(static_cast(css_property_id::overscroll_behavior_y) == 152U); static_assert(static_cast(css_property_id::isolation) == 153U); static_assert(static_cast(css_property_id::will_change) == 154U); - static_assert(native_typed_property_identity_catalog.size() == 229U); + static_assert(static_cast(css_property_id::text_wrap) == 155U); + static_assert(native_typed_property_identity_catalog.size() == 231U); static_assert(native_storage_only_property_catalog.size() == 62U); - static_assert(cssom_supported_property_catalog.size() == 254U); - static_assert(cssom_style_template_property_accessor_count == 463U); + static_assert(cssom_supported_property_catalog.size() == 255U); + static_assert(cssom_style_template_property_accessor_count == 464U); 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 21662e98..14edfe40 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 @@ -321,6 +321,51 @@ void test_css_will_change_performance_gate(webscene_engine* engine) + std::to_string(elapsed) + " ms"); } +void test_css_text_wrap_performance_gate(webscene_engine* engine) +{ + resize(engine, 1024, 768, 1U); + execute(engine, R"JS( + document.body.innerHTML = '
'; + globalThis.textWrapGrid = document.getElementById('text-wrap-grid'); + const fragment = document.createDocumentFragment(); + for (let index = 0; index < 4096; ++index) { + const node = document.createElement('div'); + node.style.width = '40px'; + node.textContent = 'alpha beta'; + fragment.appendChild(node); + } + textWrapGrid.appendChild(fragment); + )JS", "native-css-text-wrap-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( + (() => { + let checksum = 0; + for (let cycle = 0; cycle < 10; ++cycle) { + const value = cycle % 2 === 0 ? 'nowrap' : 'wrap'; + for (const node of textWrapGrid.children) node.style.textWrap = value; + checksum += textWrapGrid.scrollHeight; + } + return [textWrapGrid.children.length, + textWrapGrid.lastElementChild.style.textWrap, checksum > 0]; + })() + )JS", "native-css-text-wrap-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,"wrap",true])JSON", + "CSS text-wrap performance fixture regressed: " + result); + require(after.layout_passes <= before.layout_passes + 10U, + "CSS text-wrap transitions performed unbounded layout"); + require(after.published_scenes <= before.published_scenes + 10U, + "CSS text-wrap transitions published unbounded scenes"); + require(elapsed < 5000.0, + "4,096-node CSS text-wrap gate exceeded 5 seconds: " + + std::to_string(elapsed) + " ms"); +} + void test_modal_backdrop_scene(webscene_engine* engine) { resize(engine,400,300,1); @@ -5123,6 +5168,34 @@ void test_css_will_change_stacking_context(webscene_engine* engine) "Non-context will-change hint retained an atomic context: " + released); } +void test_css_text_wrap_layout(webscene_engine* engine) +{ + resize(engine, 320, 180, 79'700U); + const auto result = evaluate(engine, R"JS( + (() => { + document.body.innerHTML = `
alpha beta
`; + const sample = document.getElementById('sample'); + const nowrap = [getComputedStyle(sample).textWrap, + sample.clientHeight, sample.scrollWidth > sample.clientWidth]; + sample.style.textWrap = 'wrap'; + const wrapped = [getComputedStyle(sample).textWrap, + sample.clientHeight > nowrap[1], sample.scrollWidth <= sample.clientWidth]; + sample.style.removeProperty('text-wrap'); + return [nowrap, wrapped, getComputedStyle(sample).textWrap]; + })() + )JS", "native-css-text-wrap-layout.js"); + require(result.find(R"JSON([["nowrap")JSON") == 0, + "CSS text-wrap did not inherit nowrap: " + result); + require(result.find(R"JSON(["wrap",true,true])JSON") != std::string::npos, + "CSS text-wrap mutation did not restore wrapping geometry: " + result); + require(result.ends_with(R"JSON("nowrap"])JSON"), + "CSS text-wrap removal did not restore cascade: " + result); +} + 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 3375fee5..26d2521b 100644 --- a/experiments/WebScene.NativeEngine.Probe/tests/native_v8_runtime_tests.cpp +++ b/experiments/WebScene.NativeEngine.Probe/tests/native_v8_runtime_tests.cpp @@ -429,6 +429,14 @@ int main() webscene_engine_destroy(focused_engine); return 0; } + if (selected == "css-text-wrap-performance") { + auto* focused_engine = webscene_engine_create(0); + require(focused_engine != nullptr, + "CSS text-wrap performance engine creation failed"); + test_css_text_wrap_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"); @@ -1548,6 +1556,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_css_text_wrap_layout(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 5eeaca60..84eb9b81 100644 --- a/src/WebScene.Css/CssPropertyCatalog.cs +++ b/src/WebScene.Css/CssPropertyCatalog.cs @@ -92,6 +92,7 @@ public static bool IsValidCssomValue(string propertyName, string value) "overscroll-behavior" => IsOverscrollBehavior(normalizedValue), "isolation" => normalizedValue is "auto" or "isolate", "will-change" => IsWillChange(normalizedValue), + "text-wrap" => normalizedValue is "wrap" or "nowrap", "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 156925d7..382ea1e7 100644 --- a/src/WebScene.Css/CssPropertyMetadata.Generated.cs +++ b/src/WebScene.Css/CssPropertyMetadata.Generated.cs @@ -121,6 +121,7 @@ internal static class CssGeneratedPropertyMetadata "overscroll-behavior-y", "isolation", "will-change", + "text-wrap", ]; internal static readonly string[] SupportedNames = @@ -356,6 +357,7 @@ internal static class CssGeneratedPropertyMetadata "text-indent", "text-overflow", "text-transform", + "text-wrap", "top", "transform", "transform-origin", @@ -501,6 +503,7 @@ internal static bool TryGetKnownId(string name, out int id) "overscroll-behavior-y" => 113, "isolation" => 114, "will-change" => 115, + "text-wrap" => 116, _ => -1, }; return id >= 0; diff --git a/tests/WebPlatformSubset/capabilities.json b/tests/WebPlatformSubset/capabilities.json index 50582cc9..4696769d 100644 --- a/tests/WebPlatformSubset/capabilities.json +++ b/tests/WebPlatformSubset/capabilities.json @@ -448,6 +448,14 @@ "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." + }, + { + "family": "css-text-wrap-mode", + "required": ["text-wrap wrap and nowrap CSSOM", "inherited wrap mode", "retained line-layout mutation", "cascade removal fallback"], + "candidate": ["unchanged Code OSS product pixels", "cross-RID performance qualification"], + "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." } ] } diff --git a/tests/WebPlatformSubset/contracts/css-text-wrap-mode.html b/tests/WebPlatformSubset/contracts/css-text-wrap-mode.html new file mode 100644 index 00000000..503a3715 --- /dev/null +++ b/tests/WebPlatformSubset/contracts/css-text-wrap-mode.html @@ -0,0 +1,25 @@ + + +Text-wrap mode controls retained line wrapping + + + +
alpha beta
+ diff --git a/tests/WebPlatformSubset/webscene-component-profile.json b/tests/WebPlatformSubset/webscene-component-profile.json index 33bc0315..30aa60e9 100644 --- a/tests/WebPlatformSubset/webscene-component-profile.json +++ b/tests/WebPlatformSubset/webscene-component-profile.json @@ -2857,6 +2857,13 @@ "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." + }, + { + "path": "contracts/css-text-wrap-mode.html", + "type": "testharness", + "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." } ], "harnessBlocked": [], diff --git a/tests/WebScene.Css.Tests/CssPropertyCatalogTests.cs b/tests/WebScene.Css.Tests/CssPropertyCatalogTests.cs index f98d1961..ba8c0528 100644 --- a/tests/WebScene.Css.Tests/CssPropertyCatalogTests.cs +++ b/tests/WebScene.Css.Tests/CssPropertyCatalogTests.cs @@ -60,6 +60,7 @@ public void DoesNotMisclassifyNonColorValueProperties(string propertyName) [InlineData("overscrollBehaviorY")] [InlineData("isolation")] [InlineData("willChange")] + [InlineData("textWrap")] public void ExposesSupportedCssomProperties(string name) => Assert.True(CssPropertyCatalog.IsSupported(name)); @@ -128,6 +129,8 @@ public void DoesNotExposeUnknownOrCustomPropertiesAsIdlAttributes(string name) [InlineData("isolation", "group", false)] [InlineData("will-change", "transform, opacity", true)] [InlineData("will-change", "auto, transform", false)] + [InlineData("text-wrap", "nowrap", true)] + [InlineData("text-wrap", "balance", false)] public void ValidatesCssomValuesWithoutFrameworkKnowledge(string name, string value, bool expected) => Assert.Equal(expected, CssPropertyCatalog.IsValidCssomValue(name, value)); }