Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions docs/validation/code-oss-css-text-wrap-802.md
Original file line number Diff line number Diff line change
@@ -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.
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand Down Expand Up @@ -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",
Expand All @@ -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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<std::string_view, 62> native_storage_only_property_catalog{
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -156,3 +156,4 @@ overscroll_behavior_x,
overscroll_behavior_y,
isolation,
will_change,
text_wrap,
Original file line number Diff line number Diff line change
Expand Up @@ -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"},
Expand All @@ -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;
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ inline constexpr auto specified_property_samples = std::to_array<specified_prope
{"overscroll-behavior","contain none"},{"overscroll-behavior-x","auto"},{"overscroll-behavior-y","contain"},
{"isolation","isolate"},
{"will-change","transform, opacity"},
{"text-wrap","nowrap"},
{"width","12px"},{"height","20%"},{"min-width","1rem"},{"min-height","2px"},
{"max-width","none"},{"max-height","fit-content"},
{"left","1px"},{"top","2px"},{"right","3px"},{"bottom","4px"},{"inset","1px 2px 3px 4px"},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -406,6 +406,16 @@ bool apply_text_value(dom_node& node,const std::string& name,const std::string&
node.style.mutable_textual().white_space =
value == "inherit" || value == "unset"
? std::string{} : value;
} else if (name == "text-wrap") {
auto mode = ascii_lower(trim_value(value));
if (mode == "inherit" || mode == "unset") mode.clear();
else if (mode == "initial" || mode == "revert"
|| mode == "revert-layer") mode = "wrap";
if (!mode.empty() && mode != "wrap" && mode != "nowrap") {
decision.classification = "invalid-authoring";
} else {
node.style.mutable_textual().text_wrap = std::move(mode);
}
} else if (name == "word-break") {
node.style.mutable_textual().word_break =
value == "inherit" || value == "unset"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1025,6 +1025,7 @@ std::string resolved_text_transform(const dom_node& node, std::string value)

bool resolved_white_space_wraps(const dom_node& node)
{
if (resolved_text_wrap_mode(node) == "nowrap") return false;
for (auto* current = &node; current != nullptr; current = current->parent) {
if (current->style.textual().white_space.empty()) continue;
return current->style.textual().white_space != "nowrap"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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") {
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,10 +104,11 @@ int main()
static_assert(static_cast<uint16_t>(css_property_id::overscroll_behavior_y) == 152U);
static_assert(static_cast<uint16_t>(css_property_id::isolation) == 153U);
static_assert(static_cast<uint16_t>(css_property_id::will_change) == 154U);
static_assert(native_typed_property_identity_catalog.size() == 229U);
static_assert(static_cast<uint16_t>(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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 = '<div id="text-wrap-grid"></div>';
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<double, std::milli>(
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);
Expand Down Expand Up @@ -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 = `<style>
body { margin:0; }
#parent { text-wrap:nowrap; }
#sample { width:40px; font:14px sans-serif; }
</style><div id="parent"><div id="sample">alpha beta</div></div>`;
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(
Expand Down
Loading
Loading