diff --git a/Libraries/LibWeb/CSS/Invalidation/LanguageInvalidator.cpp b/Libraries/LibWeb/CSS/Invalidation/LanguageInvalidator.cpp index ffee275b4b491..21d644eaafdc6 100644 --- a/Libraries/LibWeb/CSS/Invalidation/LanguageInvalidator.cpp +++ b/Libraries/LibWeb/CSS/Invalidation/LanguageInvalidator.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include namespace Web::CSS::Invalidation { @@ -22,6 +23,27 @@ static void invalidate_descendants_affected_by_language_or_directionality(DOM::E if (!is_directionality_change) element->invalidate_lang_value(); element->set_needs_style_update(true); + return TraversalDecision::Continue; + } + if (is_directionality_change) + return TraversalDecision::Continue; + // Rendered text under a casing text-transform is keyed on the language + // (locale-sensitive casing), which no computed style group reflects, so a style + // recomputation alone never re-renders it. + auto* text_layout_node = as_if(node.unsafe_layout_node()); + if (!text_layout_node || !text_layout_node->parent()) + return TraversalDecision::Continue; + auto text_transform = text_layout_node->parent()->computed_values().text_transform(); + if (!first_is_one_of(text_transform, TextTransform::Uppercase, TextTransform::Lowercase, TextTransform::Capitalize)) + return TraversalDecision::Continue; + if (is(*text_layout_node)) { + // NB: Slice nodes cache data that is calculated at layout tree construction time, + // and locale-sensitive casing can move the first-letter boundary. + if (node.parent()) + node.parent()->set_needs_layout_tree_update(true, DOM::SetNeedsLayoutTreeUpdateReason::LanguageChangeUnderCasingTextTransform); + } else { + text_layout_node->invalidate_text_for_rendering(); + text_layout_node->set_needs_layout_update(DOM::SetNeedsLayoutReason::LanguageChangeUnderCasingTextTransform); } return TraversalDecision::Continue; }); diff --git a/Libraries/LibWeb/DOM/Element.cpp b/Libraries/LibWeb/DOM/Element.cpp index d42d570e4d479..a22ee402cacc5 100644 --- a/Libraries/LibWeb/DOM/Element.cpp +++ b/Libraries/LibWeb/DOM/Element.cpp @@ -1194,8 +1194,10 @@ void Element::run_attribute_change_steps(Utf16FlyString const& local_name, Optio if (old_value != value) { if (local_name.is_one_of(HTML::AttributeNames::colspan, HTML::AttributeNames::rowspan, HTML::AttributeNames::span)) { - if (auto* layout_node = unsafe_layout_node()) - layout_node->synchronize_table_span_data(); + if (auto* layout_node = unsafe_layout_node()) { + if (layout_node->synchronize_table_span_data()) + layout_node->set_needs_layout_update(SetNeedsLayoutReason::TableSpanAttributeChange); + } } if (!document().suppresses_attribute_style_invalidation()) { CSS::Invalidation::invalidate_style_after_attribute_change( diff --git a/Libraries/LibWeb/DOM/Node.h b/Libraries/LibWeb/DOM/Node.h index f75c095c99ded..39f44930449ed 100644 --- a/Libraries/LibWeb/DOM/Node.h +++ b/Libraries/LibWeb/DOM/Node.h @@ -77,6 +77,7 @@ enum class RootNodeComposed { #define ENUMERATE_SET_NEEDS_LAYOUT_REASONS(X) \ X(CharacterDataReplaceData) \ + X(DefaultPreferredSizeAttributeChange) \ X(EditableStateChange) \ X(FinalizeACrossDocumentNavigation) \ X(GeneratedContentImageFinishedLoading) \ @@ -86,13 +87,15 @@ enum class RootNodeComposed { X(HTMLVideoElementNaturalDimensionsChanged) \ X(HTMLVideoElementSetVideoTrack) \ X(KeyframeEffect) \ + X(LanguageChangeUnderCasingTextTransform) \ X(LayoutTreeUpdate) \ X(NavigableSetViewportSize) \ X(SVGGraphicsElementTransformChange) \ X(SVGImageElementFetchTheDocument) \ X(SVGImageFilterFetch) \ X(SVGViewBoxChange) \ - X(StyleChange) + X(StyleChange) \ + X(TableSpanAttributeChange) enum class SetNeedsLayoutReason { #define ENUMERATE_SET_NEEDS_LAYOUT_REASON(e) e, @@ -111,6 +114,7 @@ enum class SetNeedsLayoutReason { X(HTMLInputElementSrcAttribute) \ X(HTMLObjectElementUpdateLayoutAndChildObjects) \ X(KeyframeEffect) \ + X(LanguageChangeUnderCasingTextTransform) \ X(ListItemCounters) \ X(NodeInsertBefore) \ X(NodeInsertBeforeWithDisplayContents) \ diff --git a/Libraries/LibWeb/HTML/HTMLInputElement.cpp b/Libraries/LibWeb/HTML/HTMLInputElement.cpp index 71ab85a55d6de..60440d02e9617 100644 --- a/Libraries/LibWeb/HTML/HTMLInputElement.cpp +++ b/Libraries/LibWeb/HTML/HTMLInputElement.cpp @@ -1646,6 +1646,13 @@ void HTMLInputElement::form_associated_element_attribute_changed(Utf16FlyString m_value = value_sanitization_algorithm(m_value); update_shadow_tree(); } + } else if (name == HTML::AttributeNames::size) { + // size feeds the element's default preferred width, which reaches layout only + // through the replaced-content facts; nothing else schedules a relayout. + if (old_value != value) { + if (auto* layout_node = this->layout_node()) + layout_node->set_needs_layout_update(DOM::SetNeedsLayoutReason::DefaultPreferredSizeAttributeChange); + } } // AD-HOC: A change to any of these attributes can change whether the element satisfies its constraints, and diff --git a/Libraries/LibWeb/HTML/HTMLTextAreaElement.cpp b/Libraries/LibWeb/HTML/HTMLTextAreaElement.cpp index eed7e01d4c05f..0df7caa5b6651 100644 --- a/Libraries/LibWeb/HTML/HTMLTextAreaElement.cpp +++ b/Libraries/LibWeb/HTML/HTMLTextAreaElement.cpp @@ -438,7 +438,7 @@ void HTMLTextAreaElement::children_changed(ChildrenChangedMetadata const& metada } } -void HTMLTextAreaElement::form_associated_element_attribute_changed(Utf16FlyString const& name, Optional const&, Optional const& value, Optional const&) +void HTMLTextAreaElement::form_associated_element_attribute_changed(Utf16FlyString const& name, Optional const& old_value, Optional const& value, Optional const&) { if (name == HTML::AttributeNames::placeholder) { if (m_placeholder_text_node) @@ -446,6 +446,13 @@ void HTMLTextAreaElement::form_associated_element_attribute_changed(Utf16FlyStri update_placeholder_visibility(); } else if (name == HTML::AttributeNames::maxlength) { handle_maxlength_attribute(); + } else if (first_is_one_of(name, HTML::AttributeNames::rows, HTML::AttributeNames::cols)) { + // rows and cols feed the element's default preferred size, which reaches layout + // only through the replaced-content facts; nothing else schedules a relayout. + if (old_value != value) { + if (auto* layout_node = this->layout_node()) + layout_node->set_needs_layout_update(DOM::SetNeedsLayoutReason::DefaultPreferredSizeAttributeChange); + } } // AD-HOC: A change to any of these attributes can change whether the element satisfies its constraints, and diff --git a/Libraries/LibWeb/Layout/Node.cpp b/Libraries/LibWeb/Layout/Node.cpp index 9872abfd034e7..e6caf2b809c22 100644 --- a/Libraries/LibWeb/Layout/Node.cpp +++ b/Libraries/LibWeb/Layout/Node.cpp @@ -1024,7 +1024,7 @@ void NodeWithStyle::publish_style_container_to_node_data() node_data().style = m_computed_values->style_container(); } -void NodeWithStyle::synchronize_table_span_data() +bool NodeWithStyle::synchronize_table_span_data() { u16 column_span = 1; u16 row_span = 1; @@ -1035,13 +1035,18 @@ void NodeWithStyle::synchronize_table_span_data() row_span = static_cast(cell->row_span()); } else if (auto const* column = as_if(*node)) { column_span = static_cast(column->span()); + // The raw span keeps the unclamped attribute value; its only consumer is the + // table formatting context's column handling, so other elements' span + // attributes stay out of the arena map. + raw_column_span = column->get_attribute_value(HTML::AttributeNames::span).to_number().value_or(1); } - if (auto const* element = as_if(*node)) - raw_column_span = element->get_attribute_value(HTML::AttributeNames::span).to_number().value_or(1); } + bool effective_spans_changed = node_data().table_column_span != column_span + || node_data().table_row_span != row_span; node_data().table_column_span = column_span; node_data().table_row_span = row_span; - RustFFI::layout_arena_set_raw_table_column_span(arena_handle(), slot_id(this), raw_column_span); + u32 previous_raw_column_span = RustFFI::layout_arena_set_raw_table_column_span(arena_handle(), slot_id(this), raw_column_span); + return effective_spans_changed || previous_raw_column_span != raw_column_span; } void NodeWithStyle::set_display(CSS::Display display) diff --git a/Libraries/LibWeb/Layout/Node.h b/Libraries/LibWeb/Layout/Node.h index 1b953a9e3cf30..a3456958675d1 100644 --- a/Libraries/LibWeb/Layout/Node.h +++ b/Libraries/LibWeb/Layout/Node.h @@ -424,7 +424,7 @@ class WEB_API NodeWithStyle : public Node { void clear_image_observers(); void apply_style(NonnullRefPtr); void attach_style_resources(); - void synchronize_table_span_data(); + bool synchronize_table_span_data(); Gfx::Font const& first_available_font() const; Vector const& background_layers() const { return computed_values().background_layers(); } diff --git a/Libraries/LibWeb/Rust/src/layout/layout_node_arena.rs b/Libraries/LibWeb/Rust/src/layout/layout_node_arena.rs index cb96b930d725f..98533f5a04802 100644 --- a/Libraries/LibWeb/Rust/src/layout/layout_node_arena.rs +++ b/Libraries/LibWeb/Rust/src/layout/layout_node_arena.rs @@ -706,13 +706,13 @@ impl LayoutNodeArena { .and_then(|slot| slot.facts) } - pub(crate) fn set_raw_table_column_span(&mut self, id: NodeSlotId, value: u32) { + pub(crate) fn set_raw_table_column_span(&mut self, id: NodeSlotId, value: u32) -> u32 { self.assert_owner_thread(); self.data(id); if value == 1 { - self.raw_table_column_spans.remove(&id); + self.raw_table_column_spans.remove(&id).unwrap_or(1) } else { - self.raw_table_column_spans.insert(id, value); + self.raw_table_column_spans.insert(id, value).unwrap_or(1) } } @@ -922,13 +922,13 @@ pub unsafe extern "C" fn layout_arena_set_raw_table_column_span( arena: *mut c_void, id: NodeSlotId, raw_column_span: u32, -) { +) -> u32 { abort_on_panic(|| { assert!(!arena.is_null(), "layout node arena handle is null"); // SAFETY: The C++ wrapper keeps the arena alive for this call and // serializes all access on the document thread. - unsafe { &mut *arena.cast::() }.set_raw_table_column_span(id, raw_column_span); - }); + unsafe { &mut *arena.cast::() }.set_raw_table_column_span(id, raw_column_span) + }) } #[cfg(test)] diff --git a/Tests/LibWeb/Layout/expected/lang-change-casing-text-transform-relayout.txt b/Tests/LibWeb/Layout/expected/lang-change-casing-text-transform-relayout.txt new file mode 100644 index 0000000000000..1fe544976b004 --- /dev/null +++ b/Tests/LibWeb/Layout/expected/lang-change-casing-text-transform-relayout.txt @@ -0,0 +1,19 @@ +Viewport <#document> at [0,0] [0+0+0 800 0+0+0] [0+0+0 600 0+0+0] [BFC] children: not-inline + BlockContainer at [0,0] [0+0+0 800 0+0+0] [0+0+0 32 0+0+0] [BFC] children: not-inline + BlockContainer at [8,8] [8+0+0 784 0+0+8] [8+0+0 16 0+0+8] children: not-inline + BlockContainer at [8,8] [0+0+0 784 0+0+0] [0+0+0 16 0+0+0] children: inline + frag 0 from TextNode start: 0, length: 8, rect: [8,8 86.15625x16] baseline: 12.796875 + "İSTANBUL" + TextNode <#text> (not painted) + BlockContainer <(anonymous)> at [8,24] [0+0+0 784 0+0+0] [0+0+0 0 0+0+0] children: inline + TextNode <#text> (not painted) + TextNode <#text> (not painted) + +ViewportPaintable (Viewport<#document>) [0,0 800x600] + PaintableWithLines (BlockContainer) [0,0 800x32] + PaintableWithLines (BlockContainer) [8,8 784x16] + PaintableWithLines (BlockContainer
#host) [8,8 784x16] + PaintableWithLines (BlockContainer(anonymous)) [8,24 784x0] + +SC for Viewport<#document> [0,0 800x600] (z-index: auto) + SC for BlockContainer [0,0 800x32] (z-index: auto) diff --git a/Tests/LibWeb/Layout/input/lang-change-casing-text-transform-relayout.html b/Tests/LibWeb/Layout/input/lang-change-casing-text-transform-relayout.html new file mode 100644 index 0000000000000..49104c3320203 --- /dev/null +++ b/Tests/LibWeb/Layout/input/lang-change-casing-text-transform-relayout.html @@ -0,0 +1,6 @@ + +
istanbul
+ diff --git a/Tests/LibWeb/Text/expected/input-size-attribute-relayout.txt b/Tests/LibWeb/Text/expected/input-size-attribute-relayout.txt new file mode 100644 index 0000000000000..52a6fc1f2ba62 --- /dev/null +++ b/Tests/LibWeb/Text/expected/input-size-attribute-relayout.txt @@ -0,0 +1 @@ +grew: true diff --git a/Tests/LibWeb/Text/expected/table-colspan-change-relayout.txt b/Tests/LibWeb/Text/expected/table-colspan-change-relayout.txt new file mode 100644 index 0000000000000..09e7d6413ed10 --- /dev/null +++ b/Tests/LibWeb/Text/expected/table-colspan-change-relayout.txt @@ -0,0 +1,3 @@ +width before: 100 +width after: 150 +no-op layout delta: 0 diff --git a/Tests/LibWeb/Text/expected/textarea-rows-attribute-relayout.txt b/Tests/LibWeb/Text/expected/textarea-rows-attribute-relayout.txt new file mode 100644 index 0000000000000..52a6fc1f2ba62 --- /dev/null +++ b/Tests/LibWeb/Text/expected/textarea-rows-attribute-relayout.txt @@ -0,0 +1 @@ +grew: true diff --git a/Tests/LibWeb/Text/input/input-size-attribute-relayout.html b/Tests/LibWeb/Text/input/input-size-attribute-relayout.html new file mode 100644 index 0000000000000..65b768b838631 --- /dev/null +++ b/Tests/LibWeb/Text/input/input-size-attribute-relayout.html @@ -0,0 +1,18 @@ + + + + + diff --git a/Tests/LibWeb/Text/input/table-colspan-change-relayout.html b/Tests/LibWeb/Text/input/table-colspan-change-relayout.html new file mode 100644 index 0000000000000..afeed7cfdfe62 --- /dev/null +++ b/Tests/LibWeb/Text/input/table-colspan-change-relayout.html @@ -0,0 +1,33 @@ + + + + + + + + + +
+ diff --git a/Tests/LibWeb/Text/input/textarea-rows-attribute-relayout.html b/Tests/LibWeb/Text/input/textarea-rows-attribute-relayout.html new file mode 100644 index 0000000000000..9bbf2b0b4a67c --- /dev/null +++ b/Tests/LibWeb/Text/input/textarea-rows-attribute-relayout.html @@ -0,0 +1,18 @@ + + + + +