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
22 changes: 22 additions & 0 deletions Libraries/LibWeb/CSS/Invalidation/LanguageInvalidator.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
#include <LibWeb/DOM/CharacterData.h>
#include <LibWeb/DOM/Element.h>
#include <LibWeb/DOM/ShadowRoot.h>
#include <LibWeb/Layout/TextNode.h>
#include <LibWeb/TraversalDecision.h>

namespace Web::CSS::Invalidation {
Expand All @@ -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<Layout::TextNode>(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<Layout::TextSliceNode>(*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;
});
Expand Down
6 changes: 4 additions & 2 deletions Libraries/LibWeb/DOM/Element.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
6 changes: 5 additions & 1 deletion Libraries/LibWeb/DOM/Node.h
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ enum class RootNodeComposed {

#define ENUMERATE_SET_NEEDS_LAYOUT_REASONS(X) \
X(CharacterDataReplaceData) \
X(DefaultPreferredSizeAttributeChange) \
X(EditableStateChange) \
X(FinalizeACrossDocumentNavigation) \
X(GeneratedContentImageFinishedLoading) \
Expand All @@ -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,
Expand All @@ -111,6 +114,7 @@ enum class SetNeedsLayoutReason {
X(HTMLInputElementSrcAttribute) \
X(HTMLObjectElementUpdateLayoutAndChildObjects) \
X(KeyframeEffect) \
X(LanguageChangeUnderCasingTextTransform) \
X(ListItemCounters) \
X(NodeInsertBefore) \
X(NodeInsertBeforeWithDisplayContents) \
Expand Down
7 changes: 7 additions & 0 deletions Libraries/LibWeb/HTML/HTMLInputElement.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 8 additions & 1 deletion Libraries/LibWeb/HTML/HTMLTextAreaElement.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -438,14 +438,21 @@ void HTMLTextAreaElement::children_changed(ChildrenChangedMetadata const& metada
}
}

void HTMLTextAreaElement::form_associated_element_attribute_changed(Utf16FlyString const& name, Optional<Utf16String> const&, Optional<Utf16String> const& value, Optional<Utf16FlyString> const&)
void HTMLTextAreaElement::form_associated_element_attribute_changed(Utf16FlyString const& name, Optional<Utf16String> const& old_value, Optional<Utf16String> const& value, Optional<Utf16FlyString> const&)
{
if (name == HTML::AttributeNames::placeholder) {
if (m_placeholder_text_node)
m_placeholder_text_node->set_data(value.has_value() ? value->utf16_view() : u""sv);
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
Expand Down
13 changes: 9 additions & 4 deletions Libraries/LibWeb/Layout/Node.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -1035,13 +1035,18 @@ void NodeWithStyle::synchronize_table_span_data()
row_span = static_cast<u16>(cell->row_span());
} else if (auto const* column = as_if<HTML::HTMLTableColElement>(*node)) {
column_span = static_cast<u16>(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<u32>().value_or(1);
}
if (auto const* element = as_if<HTML::HTMLElement>(*node))
raw_column_span = element->get_attribute_value(HTML::AttributeNames::span).to_number<u32>().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)
Expand Down
2 changes: 1 addition & 1 deletion Libraries/LibWeb/Layout/Node.h
Original file line number Diff line number Diff line change
Expand Up @@ -424,7 +424,7 @@ class WEB_API NodeWithStyle : public Node {
void clear_image_observers();
void apply_style(NonnullRefPtr<CSS::ComputedValues const>);
void attach_style_resources();
void synchronize_table_span_data();
bool synchronize_table_span_data();

Gfx::Font const& first_available_font() const;
Vector<CSS::BackgroundLayerData> const& background_layers() const { return computed_values().background_layers(); }
Expand Down
12 changes: 6 additions & 6 deletions Libraries/LibWeb/Rust/src/layout/layout_node_arena.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}

Expand Down Expand Up @@ -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::<LayoutNodeArena>() }.set_raw_table_column_span(id, raw_column_span);
});
unsafe { &mut *arena.cast::<LayoutNodeArena>() }.set_raw_table_column_span(id, raw_column_span)
})
}

#[cfg(test)]
Expand Down
Original file line number Diff line number Diff line change
@@ -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 <html> at [0,0] [0+0+0 800 0+0+0] [0+0+0 32 0+0+0] [BFC] children: not-inline
BlockContainer <body> at [8,8] [8+0+0 784 0+0+8] [8+0+0 16 0+0+8] children: not-inline
BlockContainer <div#host> 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<HTML>) [0,0 800x32]
PaintableWithLines (BlockContainer<BODY>) [8,8 784x16]
PaintableWithLines (BlockContainer<DIV>#host) [8,8 784x16]
PaintableWithLines (BlockContainer(anonymous)) [8,24 784x0]

SC for Viewport<#document> [0,0 800x600] (z-index: auto)
SC for BlockContainer<HTML> [0,0 800x32] (z-index: auto)
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<!DOCTYPE html>
<div id="host" lang="en" style="text-transform: uppercase">istanbul</div>
<script>
document.body.offsetHeight;
document.getElementById("host").setAttribute("lang", "tr");
</script>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
grew: true
3 changes: 3 additions & 0 deletions Tests/LibWeb/Text/expected/table-colspan-change-relayout.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
width before: 100
width after: 150
no-op layout delta: 0
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
grew: true
18 changes: 18 additions & 0 deletions Tests/LibWeb/Text/input/input-size-attribute-relayout.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<!DOCTYPE html>
<script src="include.js"></script>
<style>
input { appearance: none; font-size: 16px; }
</style>
<input id="field" type="text" size="10">
<script>
asyncTest(async (done) => {
document.body.offsetHeight;

const widthBefore = document.getElementById("field").getBoundingClientRect().width;
document.getElementById("field").setAttribute("size", "40");
const widthAfter = document.getElementById("field").getBoundingClientRect().width;

println(`grew: ${widthAfter > widthBefore}`);
done();
});
</script>
33 changes: 33 additions & 0 deletions Tests/LibWeb/Text/input/table-colspan-change-relayout.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<!DOCTYPE html>
<script src="include.js"></script>
<style>
table { border-collapse: collapse; width: 300px; table-layout: fixed; }
td { padding: 0; height: 20px; }
</style>
<table>
<tr>
<td id="first"></td>
<td></td>
<td></td>
</tr>
</table>
<script>
asyncTest(async (done) => {
document.body.offsetHeight;

const widthBefore = document.getElementById("first").getBoundingClientRect().width;
document.getElementById("first").setAttribute("colspan", "2");
const widthAfter = document.getElementById("first").getBoundingClientRect().width;

// A different attribute string with the same effective span must not relayout.
const layoutsBefore = internals.fullLayoutCount();
document.getElementById("first").setAttribute("colspan", "02");
document.body.offsetHeight;
const noopLayoutDelta = internals.fullLayoutCount() - layoutsBefore;

println(`width before: ${widthBefore}`);
println(`width after: ${widthAfter}`);
println(`no-op layout delta: ${noopLayoutDelta}`);
done();
});
</script>
18 changes: 18 additions & 0 deletions Tests/LibWeb/Text/input/textarea-rows-attribute-relayout.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<!DOCTYPE html>
<script src="include.js"></script>
<style>
textarea { appearance: none; font-size: 16px; }
</style>
<textarea id="field" rows="2" cols="20"></textarea>
<script>
asyncTest(async (done) => {
document.body.offsetHeight;

const heightBefore = document.getElementById("field").getBoundingClientRect().height;
document.getElementById("field").setAttribute("rows", "8");
const heightAfter = document.getElementById("field").getBoundingClientRect().height;

println(`grew: ${heightAfter > heightBefore}`);
done();
});
</script>
Loading