LibWeb: Support both ::before/::after pseudo elements on button elements

This was mainly a matter of deferring the wrapping of the button's
children until after its internal layout tree has been constructed.
That way we don't lose any pseudo elements spawned along the way.

Fixes #2397.
Fixes #2399.
This commit is contained in:
Andreas Kling 2025-02-03 12:23:15 +01:00 committed by Andreas Kling
parent 0f17ad9ebc
commit 4fa372564d
Notes: github-actions[bot] 2025-02-03 15:00:37 +00:00
7 changed files with 124 additions and 80 deletions

View file

@ -604,13 +604,89 @@ void TreeBuilder::update_layout_tree(DOM::Node& dom_node, TreeBuilder::Context&
}
}
if (should_create_layout_node)
if (should_create_layout_node) {
update_layout_tree_after_children(dom_node, *layout_node, context, element_has_content_visibility_hidden);
wrap_in_button_layout_tree_if_needed(dom_node, *layout_node);
// If we completely finished inserting a block level element into an inline parent, we need to fix up the tree so
// that we can maintain the invariant that all children are either inline or non-inline. We can't do this earlier,
// because the restructuring adds new children after this node that become part of the ancestor stack.
auto* layout_parent = layout_node->parent();
if (layout_parent && layout_parent->display().is_inline_outside() && !display.is_contents()
&& !display.is_inline_outside() && layout_parent->display().is_flow_inside() && !layout_node->is_out_of_flow())
restructure_block_node_in_inline_parent(static_cast<NodeWithStyleAndBoxModelMetrics&>(*layout_node));
}
dom_node.set_needs_layout_tree_update(false);
dom_node.set_child_needs_layout_tree_update(false);
}
void TreeBuilder::wrap_in_button_layout_tree_if_needed(DOM::Node& dom_node, GC::Ref<Layout::Node> layout_node)
{
auto is_button_layout = [&] {
if (dom_node.is_html_button_element())
return true;
if (!dom_node.is_html_input_element())
return false;
// https://html.spec.whatwg.org/multipage/rendering.html#the-input-element-as-a-button
// An input element whose type attribute is in the Submit Button, Reset Button, or Button state, when it generates a CSS box, is expected to depict a button and use button layout
auto const& input_element = static_cast<HTML::HTMLInputElement const&>(dom_node);
if (input_element.is_button())
return true;
return false;
}();
if (!is_button_layout)
return;
auto display = layout_node->display();
// https://html.spec.whatwg.org/multipage/rendering.html#button-layout
// If the computed value of 'inline-size' is 'auto', then the used value is the fit-content inline size.
if (is_button_layout && dom_node.layout_node()->computed_values().width().is_auto()) {
auto& computed_values = as<NodeWithStyle>(*dom_node.layout_node()).mutable_computed_values();
computed_values.set_width(CSS::Size::make_fit_content());
}
// https://html.spec.whatwg.org/multipage/rendering.html#button-layout
// If the element is an input element, or if it is a button element and its computed value for
// 'display' is not 'inline-grid', 'grid', 'inline-flex', or 'flex', then the element's box has
// a child anonymous button content box with the following behaviors:
if (is_button_layout && !display.is_grid_inside() && !display.is_flex_inside()) {
auto& parent = *layout_node;
// If the box does not overflow in the vertical axis, then it is centered vertically.
// FIXME: Only apply alignment when box overflows
auto flex_computed_values = parent.computed_values().clone_inherited_values();
auto& mutable_flex_computed_values = static_cast<CSS::MutableComputedValues&>(*flex_computed_values);
mutable_flex_computed_values.set_display(CSS::Display { CSS::DisplayOutside::Block, CSS::DisplayInside::Flex });
mutable_flex_computed_values.set_justify_content(CSS::JustifyContent::Center);
mutable_flex_computed_values.set_flex_direction(CSS::FlexDirection::Column);
mutable_flex_computed_values.set_height(CSS::Size::make_percentage(CSS::Percentage(100)));
mutable_flex_computed_values.set_min_height(parent.computed_values().min_height());
auto flex_wrapper = parent.heap().template allocate<BlockContainer>(parent.document(), nullptr, move(flex_computed_values));
auto content_box_computed_values = parent.computed_values().clone_inherited_values();
auto content_box_wrapper = parent.heap().template allocate<BlockContainer>(parent.document(), nullptr, move(content_box_computed_values));
content_box_wrapper->set_children_are_inline(parent.children_are_inline());
Vector<GC::Root<Node>> sequence;
for (auto child = parent.first_child(); child; child = child->next_sibling()) {
sequence.append(*child);
}
for (auto& node : sequence) {
parent.remove_child(*node);
content_box_wrapper->append_child(*node);
}
flex_wrapper->append_child(*content_box_wrapper);
parent.append_child(*flex_wrapper);
parent.set_children_are_inline(false);
}
}
void TreeBuilder::update_layout_tree_before_children(DOM::Node& dom_node, GC::Ref<Layout::Node> layout_node, TreeBuilder::Context&, bool element_has_content_visibility_hidden)
{
// Add node for the ::before pseudo-element.
@ -626,7 +702,6 @@ void TreeBuilder::update_layout_tree_after_children(DOM::Node& dom_node, GC::Ref
{
auto& document = dom_node.document();
auto& style_computer = document.style_computer();
auto display = layout_node->display();
if (is<ListItemBox>(*layout_node)) {
auto& element = static_cast<DOM::Element&>(dom_node);
@ -670,66 +745,6 @@ void TreeBuilder::update_layout_tree_after_children(DOM::Node& dom_node, GC::Ref
layout_mask_or_clip_path(clip_path);
}
auto is_button_layout = [&] {
if (dom_node.is_html_button_element())
return true;
if (!dom_node.is_html_input_element())
return false;
// https://html.spec.whatwg.org/multipage/rendering.html#the-input-element-as-a-button
// An input element whose type attribute is in the Submit Button, Reset Button, or Button state, when it generates a CSS box, is expected to depict a button and use button layout
auto const& input_element = static_cast<HTML::HTMLInputElement const&>(dom_node);
if (input_element.is_button())
return true;
return false;
}();
// https://html.spec.whatwg.org/multipage/rendering.html#button-layout
// If the computed value of 'inline-size' is 'auto', then the used value is the fit-content inline size.
if (is_button_layout && dom_node.layout_node()->computed_values().width().is_auto()) {
auto& computed_values = as<NodeWithStyle>(*dom_node.layout_node()).mutable_computed_values();
computed_values.set_width(CSS::Size::make_fit_content());
}
// https://html.spec.whatwg.org/multipage/rendering.html#button-layout
// If the element is an input element, or if it is a button element and its computed value for
// 'display' is not 'inline-grid', 'grid', 'inline-flex', or 'flex', then the element's box has
// a child anonymous button content box with the following behaviors:
if (is_button_layout && !display.is_grid_inside() && !display.is_flex_inside()) {
auto& parent = *dom_node.layout_node();
// If the box does not overflow in the vertical axis, then it is centered vertically.
// FIXME: Only apply alignment when box overflows
auto flex_computed_values = parent.computed_values().clone_inherited_values();
auto& mutable_flex_computed_values = static_cast<CSS::MutableComputedValues&>(*flex_computed_values);
mutable_flex_computed_values.set_display(CSS::Display { CSS::DisplayOutside::Block, CSS::DisplayInside::Flex });
mutable_flex_computed_values.set_justify_content(CSS::JustifyContent::Center);
mutable_flex_computed_values.set_flex_direction(CSS::FlexDirection::Column);
mutable_flex_computed_values.set_height(CSS::Size::make_percentage(CSS::Percentage(100)));
mutable_flex_computed_values.set_min_height(parent.computed_values().min_height());
auto flex_wrapper = parent.heap().template allocate<BlockContainer>(parent.document(), nullptr, move(flex_computed_values));
auto content_box_computed_values = parent.computed_values().clone_inherited_values();
auto content_box_wrapper = parent.heap().template allocate<BlockContainer>(parent.document(), nullptr, move(content_box_computed_values));
content_box_wrapper->set_children_are_inline(parent.children_are_inline());
Vector<GC::Root<Node>> sequence;
for (auto child = parent.first_child(); child; child = child->next_sibling()) {
if (child->is_generated_for_before_pseudo_element())
continue;
sequence.append(*child);
}
for (auto& node : sequence) {
parent.remove_child(*node);
content_box_wrapper->append_child(*node);
}
flex_wrapper->append_child(*content_box_wrapper);
parent.append_child(*flex_wrapper);
parent.set_children_are_inline(false);
}
// Add nodes for the ::after pseudo-element.
if (is<DOM::Element>(dom_node) && layout_node->can_have_children() && !element_has_content_visibility_hidden) {
auto& element = static_cast<DOM::Element&>(dom_node);
@ -737,14 +752,6 @@ void TreeBuilder::update_layout_tree_after_children(DOM::Node& dom_node, GC::Ref
create_pseudo_element_if_needed(element, CSS::Selector::PseudoElement::Type::After, AppendOrPrepend::Append);
pop_parent();
}
// If we completely finished inserting a block level element into an inline parent, we need to fix up the tree so
// that we can maintain the invariant that all children are either inline or non-inline. We can't do this earlier,
// because the restructuring adds new children after this node that become part of the ancestor stack.
auto* layout_parent = layout_node->parent();
if (layout_parent && layout_parent->display().is_inline_outside() && !display.is_contents()
&& !display.is_inline_outside() && layout_parent->display().is_flow_inside() && !layout_node->is_out_of_flow())
restructure_block_node_in_inline_parent(static_cast<NodeWithStyleAndBoxModelMetrics&>(*layout_node));
}
GC::Ptr<Layout::Node> TreeBuilder::build(DOM::Node& dom_node)

View file

@ -31,6 +31,7 @@ private:
void update_layout_tree_before_children(DOM::Node&, GC::Ref<Layout::Node>, Context&, bool element_has_content_visibility_hidden);
void update_layout_tree_after_children(DOM::Node&, GC::Ref<Layout::Node>, Context&, bool element_has_content_visibility_hidden);
void wrap_in_button_layout_tree_if_needed(DOM::Node&, GC::Ref<Layout::Node>);
enum class MustCreateSubtree {
No,
Yes,

View file

@ -4,9 +4,9 @@ Viewport <#document> at (0,0) content-size 800x600 children: not-inline
frag 0 from BlockContainer start: 0, length: 0, rect: [29,29 0x0] baseline: 42
BlockContainer <button> at (29,29) content-size 0x0 positioned inline-block [BFC] children: not-inline
BlockContainer <(anonymous)> at (29,29) content-size 0x0 flex-container(column) [FFC] children: not-inline
BlockContainer <(anonymous)> at (29,29) content-size 0x0 [BFC] children: not-inline
BlockContainer <(anonymous)> at (9,9) content-size 40x40 positioned [BFC] children: inline
TextNode <#text>
BlockContainer <(anonymous)> at (29,29) content-size 0x0 flex-item [BFC] children: not-inline
BlockContainer <(anonymous)> at (9,9) content-size 40x40 positioned [BFC] children: inline
TextNode <#text>
TextNode <#text>
ViewportPaintable (Viewport<#document>) [0,0 800x600]
@ -15,4 +15,4 @@ ViewportPaintable (Viewport<#document>) [0,0 800x600]
PaintableWithLines (BlockContainer<BUTTON>) [8,8 42x42]
PaintableWithLines (BlockContainer(anonymous)) [29,29 0x0]
PaintableWithLines (BlockContainer(anonymous)) [29,29 0x0]
PaintableWithLines (BlockContainer(anonymous)) [9,9 40x40]
PaintableWithLines (BlockContainer(anonymous)) [9,9 40x40]

View file

@ -8,8 +8,8 @@ Viewport <#document> at (0,0) content-size 800x600 children: not-inline
frag 0 from TextNode start: 0, length: 14, rect: [13,10 414.703125x55] baseline: 42.484375
"See more games"
TextNode <#text>
BlockContainer <(anonymous)> at (9,9) content-size 422.703125x57 positioned [BFC] children: inline
TextNode <#text>
BlockContainer <(anonymous)> at (9,9) content-size 422.703125x57 positioned [BFC] children: inline
TextNode <#text>
ViewportPaintable (Viewport<#document>) [0,0 800x600]
PaintableWithLines (BlockContainer<HTML>) [0,0 800x75]
@ -18,4 +18,4 @@ ViewportPaintable (Viewport<#document>) [0,0 800x600]
PaintableWithLines (BlockContainer(anonymous)) [13,10 414.703125x55]
PaintableWithLines (BlockContainer(anonymous)) [13,10 414.703125x55]
TextPaintable (TextNode<#text>)
PaintableWithLines (BlockContainer(anonymous)) [9,9 422.703125x57]
PaintableWithLines (BlockContainer(anonymous)) [9,9 422.703125x57]

View file

@ -3,19 +3,19 @@ Viewport <#document> at (0,0) content-size 800x600 children: not-inline
BlockContainer <body> at (8,8) content-size 784x59 children: inline
frag 0 from BlockContainer start: 0, length: 0, rect: [13,10 414.703125x55] baseline: 44.484375
BlockContainer <button.button_button___eDCW> at (13,10) content-size 414.703125x55 positioned inline-block [BFC] children: not-inline
BlockContainer <(anonymous)> at (9,9) content-size 422.703125x57 positioned [BFC] children: inline
TextNode <#text>
BlockContainer <(anonymous)> at (13,10) content-size 414.703125x55 flex-container(column) [FFC] children: not-inline
BlockContainer <(anonymous)> at (13,10) content-size 414.703125x55 flex-item [BFC] children: inline
frag 0 from TextNode start: 0, length: 14, rect: [13,10 414.703125x55] baseline: 42.484375
"See more games"
BlockContainer <(anonymous)> at (9,9) content-size 422.703125x57 positioned [BFC] children: inline
TextNode <#text>
TextNode <#text>
ViewportPaintable (Viewport<#document>) [0,0 800x600]
PaintableWithLines (BlockContainer<HTML>) [0,0 800x75]
PaintableWithLines (BlockContainer<BODY>) [8,8 784x59]
PaintableWithLines (BlockContainer<BUTTON>.button_button___eDCW) [8,8 424.703125x59]
PaintableWithLines (BlockContainer(anonymous)) [9,9 422.703125x57]
PaintableWithLines (BlockContainer(anonymous)) [13,10 414.703125x55]
PaintableWithLines (BlockContainer(anonymous)) [13,10 414.703125x55]
PaintableWithLines (BlockContainer(anonymous)) [9,9 422.703125x57]
TextPaintable (TextNode<#text>)

View file

@ -0,0 +1,31 @@
Viewport <#document> at (0,0) content-size 800x600 children: not-inline
BlockContainer <html> at (0,0) content-size 800x37 [BFC] children: not-inline
BlockContainer <body> at (8,8) content-size 784x21 children: inline
frag 0 from BlockContainer start: 0, length: 0, rect: [13,10 82x17] baseline: 15.296875
BlockContainer <button> at (13,10) content-size 82x17 inline-block [BFC] children: not-inline
BlockContainer <(anonymous)> at (13,10) content-size 82x17 flex-container(column) [FFC] children: not-inline
BlockContainer <(anonymous)> at (13,10) content-size 82x17 flex-item [BFC] children: inline
frag 0 from TextNode start: 0, length: 3, rect: [40.15625,10 27.640625x17] baseline: 13.296875
"bar"
InlineNode <(anonymous)>
frag 0 from TextNode start: 0, length: 3, rect: [13,10 27.15625x17] baseline: 13.296875
"foo"
TextNode <#text>
TextNode <#text>
InlineNode <(anonymous)>
frag 0 from TextNode start: 0, length: 3, rect: [67.796875,10 27.203125x17] baseline: 13.296875
"baz"
TextNode <#text>
TextNode <#text>
ViewportPaintable (Viewport<#document>) [0,0 800x600]
PaintableWithLines (BlockContainer<HTML>) [0,0 800x37]
PaintableWithLines (BlockContainer<BODY>) [8,8 784x21]
PaintableWithLines (BlockContainer<BUTTON>) [8,8 92x21]
PaintableWithLines (BlockContainer(anonymous)) [13,10 82x17]
PaintableWithLines (BlockContainer(anonymous)) [13,10 82x17]
PaintableWithLines (InlineNode(anonymous))
TextPaintable (TextNode<#text>)
TextPaintable (TextNode<#text>)
PaintableWithLines (InlineNode(anonymous))
TextPaintable (TextNode<#text>)

View file

@ -0,0 +1,5 @@
<!doctype html><style>
*, ::before, ::after { outline: 1px solid black; }
button:before { content: "foo"; }
button:after { content: "baz"; }
</style><body><button>bar</button>