LibWeb: Lay out the fieldset's rendered legend

This commit is contained in:
Kostya Farber 2024-11-23 15:47:32 +00:00 committed by Tim Ledbetter
parent 6218f1a609
commit 81f8866606
Notes: github-actions[bot] 2024-11-29 12:51:25 +00:00
12 changed files with 356 additions and 15 deletions

View file

@ -619,6 +619,7 @@ set(SOURCES
Painting/DisplayList.cpp
Painting/DisplayListPlayerSkia.cpp
Painting/DisplayListRecorder.cpp
Painting/FieldSetPaintable.cpp
Painting/GradientPainting.cpp
Painting/ImagePaintable.cpp
Painting/LabelablePaintable.cpp
@ -833,12 +834,12 @@ compile_ipc(Worker/WebWorkerClient.ipc Worker/WebWorkerClientEndpoint.h)
compile_ipc(Worker/WebWorkerServer.ipc Worker/WebWorkerServerEndpoint.h)
invoke_generator(
"AriaRoles.cpp"
Lagom::GenerateAriaRoles
"${CMAKE_CURRENT_SOURCE_DIR}/ARIA/AriaRoles.json"
"ARIA/AriaRoles.h"
"ARIA/AriaRoles.cpp"
arguments -j "${CMAKE_CURRENT_SOURCE_DIR}/ARIA/AriaRoles.json"
"AriaRoles.cpp"
Lagom::GenerateAriaRoles
"${CMAKE_CURRENT_SOURCE_DIR}/ARIA/AriaRoles.json"
"ARIA/AriaRoles.h"
"ARIA/AriaRoles.cpp"
arguments -j "${CMAKE_CURRENT_SOURCE_DIR}/ARIA/AriaRoles.json"
)
generate_css_implementation()

View file

@ -665,6 +665,7 @@ namespace Web::Painting {
class AudioPaintable;
class ButtonPaintable;
class CheckBoxPaintable;
class FieldSetPaintable;
class LabelablePaintable;
class MediaPaintable;
class Paintable;

View file

@ -12,7 +12,9 @@
#include <LibWeb/Layout/BlockContainer.h>
#include <LibWeb/Layout/BlockFormattingContext.h>
#include <LibWeb/Layout/Box.h>
#include <LibWeb/Layout/FieldSetBox.h>
#include <LibWeb/Layout/InlineFormattingContext.h>
#include <LibWeb/Layout/LegendBox.h>
#include <LibWeb/Layout/LineBuilder.h>
#include <LibWeb/Layout/ListItemBox.h>
#include <LibWeb/Layout/ListItemMarkerBox.h>
@ -73,6 +75,38 @@ void BlockFormattingContext::run(AvailableSpace const& available_space)
return;
}
if (is<FieldSetBox>(root())) {
if (root().children_are_inline())
layout_inline_children(root(), available_space);
else
layout_block_level_children(root(), available_space);
auto const& fieldset_box = verify_cast<FieldSetBox>(root());
if (!(fieldset_box.has_rendered_legend())) {
return;
}
auto const* legend = root().first_child_of_type<LegendBox>();
auto& legend_state = m_state.get_mutable(*legend);
auto& fieldset_state = m_state.get_mutable(root());
// The element is expected to be positioned in the block-flow direction such that
// its border box is centered over the border on the block-start side of the fieldset element.
// FIXME: this should take writing modes into consideration.
auto legend_height = legend_state.border_box_height();
auto new_y = -((legend_height) / 2) - fieldset_state.padding_top;
legend_state.set_content_offset({ legend_state.offset.x(), new_y });
// If the computed value of 'inline-size' is 'auto',
// then the used value is the fit-content inline size.
if (legend->computed_values().width().is_auto()) {
auto width = calculate_fit_content_width(*legend, available_space);
legend_state.set_content_width(width);
}
return;
}
if (root().children_are_inline())
layout_inline_children(root(), available_space);
else

View file

@ -7,6 +7,8 @@
#include <LibWeb/Forward.h>
#include <LibWeb/HTML/HTMLLegendElement.h>
#include <LibWeb/Layout/FieldSetBox.h>
#include <LibWeb/Layout/LegendBox.h>
#include <LibWeb/Painting/FieldSetPaintable.h>
namespace Web::Layout {
@ -22,13 +24,27 @@ FieldSetBox::~FieldSetBox() = default;
bool FieldSetBox::has_rendered_legend() const
{
// https://html.spec.whatwg.org/#rendered-legend
if (this->has_children() && this->first_child()->is_legend_box()) {
auto* first_child = this->first_child();
return first_child->computed_values().float_() == CSS::Float::None
&& first_child->computed_values().position() != CSS::Positioning::Absolute
&& first_child->computed_values().position() != CSS::Positioning::Fixed;
bool has_rendered_legend = false;
if (has_children()) {
for_each_child_of_type<Box>([&](Box const& child) {
if (child.is_anonymous())
return IterationDecision::Continue;
if (!child.is_legend_box())
return IterationDecision::Break;
has_rendered_legend = child.computed_values().float_() == CSS::Float::None
&& child.computed_values().position() != CSS::Positioning::Absolute
&& child.computed_values().position() != CSS::Positioning::Fixed;
return IterationDecision::Break;
});
}
return false;
return has_rendered_legend;
}
GC::Ptr<Painting::Paintable> FieldSetBox::create_paintable() const
{
return Painting::FieldSetPaintable::create(*this);
}
}

View file

@ -8,7 +8,7 @@
#include <LibWeb/Forward.h>
#include <LibWeb/Layout/BlockContainer.h>
#include <LibWeb/Painting/FieldSetPaintable.h>
namespace Web::Layout {
class FieldSetBox final : public BlockContainer {
@ -22,10 +22,10 @@ public:
DOM::Element& dom_node() { return static_cast<DOM::Element&>(*BlockContainer::dom_node()); }
DOM::Element const& dom_node() const { return static_cast<DOM::Element const&>(*BlockContainer::dom_node()); }
void layout_legend() const;
bool has_rendered_legend() const;
virtual GC::Ptr<Painting::Paintable> create_paintable() const override;
private:
bool has_rendered_legend() const;
virtual bool is_fieldset_box() const final
{
return true;

View file

@ -23,6 +23,7 @@
#include <LibWeb/HTML/HTMLLIElement.h>
#include <LibWeb/HTML/HTMLOListElement.h>
#include <LibWeb/HTML/HTMLSlotElement.h>
#include <LibWeb/Layout/FieldSetBox.h>
#include <LibWeb/Layout/ListItemBox.h>
#include <LibWeb/Layout/ListItemMarkerBox.h>
#include <LibWeb/Layout/Node.h>
@ -76,6 +77,9 @@ static Layout::Node& insertion_parent_for_inline_node(Layout::NodeWithStyle& lay
return *layout_parent.last_child();
};
if (is<FieldSetBox>(layout_parent))
return last_child_creating_anonymous_wrapper_if_needed(layout_parent);
if (layout_parent.display().is_inline_outside() && layout_parent.display().is_flow_inside())
return layout_parent;

View file

@ -0,0 +1,106 @@
/*
* Copyright (c) 2024, Kostya Farber <kostya.farber@gmail.com>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibWeb/Layout/LegendBox.h>
#include <LibWeb/Painting/FieldSetPaintable.h>
namespace Web::Painting {
GC_DEFINE_ALLOCATOR(FieldSetPaintable);
GC::Ref<FieldSetPaintable> FieldSetPaintable::create(Layout::FieldSetBox const& layout_box)
{
return layout_box.heap().allocate<FieldSetPaintable>(layout_box);
}
FieldSetPaintable::FieldSetPaintable(Layout::FieldSetBox const& layout_box)
: PaintableBox(layout_box)
{
}
Layout::FieldSetBox& FieldSetPaintable::layout_box()
{
return static_cast<Layout::FieldSetBox&>(layout_node());
}
Layout::FieldSetBox const& FieldSetPaintable::layout_box() const
{
return static_cast<Layout::FieldSetBox const&>(layout_node());
}
void FieldSetPaintable::paint(PaintContext& context, PaintPhase phase) const
{
if (!is_visible())
return;
if (phase != PaintPhase::Border) {
PaintableBox::paint(context, phase);
return;
}
if (!(layout_box().has_rendered_legend())) {
PaintableBox::paint(context, phase);
return;
}
auto& display_list_recorder = context.display_list_recorder();
auto const* legend_box = layout_box().first_child_of_type<Layout::LegendBox>();
auto const* const legend_paintable = legend_box->paintable_box();
auto legend_border_rect = context.rounded_device_rect(legend_paintable->absolute_border_box_rect());
auto fieldset_border_rect = context.rounded_device_rect(absolute_border_box_rect());
BordersData borders_data = BordersData {
.top = CSS::BorderData(),
.right = box_model().border.right == 0 ? CSS::BorderData() : computed_values().border_right(),
.bottom = box_model().border.bottom == 0 ? CSS::BorderData() : computed_values().border_bottom(),
.left = box_model().border.left == 0 ? CSS::BorderData() : computed_values().border_left(),
};
paint_all_borders(display_list_recorder, fieldset_border_rect, normalized_border_radii_data().as_corners(context), borders_data.to_device_pixels(context));
auto top_border_data = box_model().border.top == 0 ? CSS::BorderData() : computed_values().border_top();
auto top_border = context.enclosing_device_pixels(top_border_data.width).value();
// if fieldset has a rendered legend, the top border is not
// expected to be painted behind the border box of the legend
DevicePixelRect left_segment = {
fieldset_border_rect.x(),
fieldset_border_rect.y(),
legend_border_rect.x() - fieldset_border_rect.x(),
top_border
};
DevicePixelRect right_segment = {
legend_border_rect.right(),
fieldset_border_rect.y(),
fieldset_border_rect.right() - legend_border_rect.right(),
top_border
};
BordersData top_border_only = BordersData {
.top = top_border_data,
.right = CSS::BorderData(),
.bottom = CSS::BorderData(),
.left = CSS::BorderData(),
};
display_list_recorder.save();
display_list_recorder.add_clip_rect(left_segment.to_type<int>());
paint_all_borders(display_list_recorder, fieldset_border_rect, normalized_border_radii_data().as_corners(context), top_border_only.to_device_pixels(context));
display_list_recorder.restore();
display_list_recorder.save();
display_list_recorder.add_clip_rect(right_segment.to_type<int>());
paint_all_borders(
display_list_recorder,
fieldset_border_rect,
normalized_border_radii_data().as_corners(context),
top_border_only.to_device_pixels(context));
display_list_recorder.restore();
}
}

View file

@ -0,0 +1,32 @@
/*
* Copyright (c) 2024, Kostya Farber <kostya.farber@gmail.com>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <LibWeb/Forward.h>
#include <LibWeb/Layout/FieldSetBox.h>
#include <LibWeb/Painting/PaintContext.h>
#include <LibWeb/Painting/PaintableBox.h>
namespace Web::Painting {
class FieldSetPaintable final : public PaintableBox {
GC_CELL(FieldSetPaintable, PaintableBox);
GC_DECLARE_ALLOCATOR(FieldSetPaintable);
public:
static GC::Ref<FieldSetPaintable> create(Layout::FieldSetBox const&);
virtual void paint(PaintContext&, PaintPhase) const override;
private:
Layout::FieldSetBox& layout_box();
Layout::FieldSetBox const& layout_box() const;
explicit FieldSetPaintable(Layout::FieldSetBox const&);
};
}

View file

@ -0,0 +1,18 @@
Viewport <#document> at (0,0) content-size 800x600 children: not-inline
BlockContainer <html> at (0,0) content-size 800x600 [BFC] children: not-inline
BlockContainer <body> at (8,8) content-size 784x36.59375 children: not-inline
FieldSetBox <fieldset> at (24,15.59375) content-size 752x17 [BFC] children: not-inline
LegendBox <legend> at (26,1.5) content-size 36.328125x17 children: inline
frag 0 from TextNode start: 0, length: 5, rect: [26,1.5 36.328125x17] baseline: 13.296875
"login"
TextNode <#text>
BlockContainer <(anonymous)> at (8,44.59375) content-size 784x0 children: inline
TextNode <#text>
ViewportPaintable (Viewport<#document>) [0,0 800x600]
PaintableWithLines (BlockContainer<HTML>) [0,0 800x600]
PaintableWithLines (BlockContainer<BODY>) [8,8 784x36.59375] overflow: [8,1.5 784x51.09375]
FieldSetPaintable (FieldSetBox<FIELDSET>) [10,8 780x36.59375] overflow: [12,1.5 776x51.09375]
PaintableWithLines (LegendBox<LEGEND>) [24,1.5 40.328125x17]
TextPaintable (TextNode<#text>)
PaintableWithLines (BlockContainer(anonymous)) [8,44.59375 784x0]

View file

@ -0,0 +1 @@
<fieldset><legend>login</legend></fieldset>

View file

@ -0,0 +1,66 @@
Summary
Harness status: OK
Rerun
Found 56 tests
56 Pass
Details
Result Test Name MessagePass in-body: display
Pass in-body: unicodeBidi
Pass in-body: marginTop
Pass in-body: marginRight
Pass in-body: marginBottom
Pass in-body: marginLeft
Pass in-body: paddingTop
Pass in-body: paddingRight
Pass in-body: paddingBottom
Pass in-body: paddingLeft
Pass in-body: overflow
Pass in-body: height
Pass in-body: box-sizing
Pass in-body: width
Pass rendered-legend: display
Pass rendered-legend: unicodeBidi
Pass rendered-legend: marginTop
Pass rendered-legend: marginRight
Pass rendered-legend: marginBottom
Pass rendered-legend: marginLeft
Pass rendered-legend: paddingTop
Pass rendered-legend: paddingRight
Pass rendered-legend: paddingBottom
Pass rendered-legend: paddingLeft
Pass rendered-legend: overflow
Pass rendered-legend: height
Pass rendered-legend: box-sizing
Pass rendered-legend: width
Pass in-fieldset-second-child: display
Pass in-fieldset-second-child: unicodeBidi
Pass in-fieldset-second-child: marginTop
Pass in-fieldset-second-child: marginRight
Pass in-fieldset-second-child: marginBottom
Pass in-fieldset-second-child: marginLeft
Pass in-fieldset-second-child: paddingTop
Pass in-fieldset-second-child: paddingRight
Pass in-fieldset-second-child: paddingBottom
Pass in-fieldset-second-child: paddingLeft
Pass in-fieldset-second-child: overflow
Pass in-fieldset-second-child: height
Pass in-fieldset-second-child: box-sizing
Pass in-fieldset-second-child: width
Pass in-fieldset-descendant: display
Pass in-fieldset-descendant: unicodeBidi
Pass in-fieldset-descendant: marginTop
Pass in-fieldset-descendant: marginRight
Pass in-fieldset-descendant: marginBottom
Pass in-fieldset-descendant: marginLeft
Pass in-fieldset-descendant: paddingTop
Pass in-fieldset-descendant: paddingRight
Pass in-fieldset-descendant: paddingBottom
Pass in-fieldset-descendant: paddingLeft
Pass in-fieldset-descendant: overflow
Pass in-fieldset-descendant: height
Pass in-fieldset-descendant: box-sizing
Pass in-fieldset-descendant: width

View file

@ -0,0 +1,62 @@
<!doctype html>
<title>The legend element</title>
<script src=../../../../resources/testharness.js></script>
<script src=../../../../resources/testharnessreport.js></script>
<style>
#ref {
display: block;
unicode-bidi: isolate;
padding-left: 2px;
padding-right: 2px;
/* TODO: uncomment this when these properties are widely supported
padding-inline-start: 2px; padding-inline-end: 2px;
*/
}
</style>
<legend id=in-body></legend>
<fieldset>
<legend id=rendered-legend></legend>
<legend id=in-fieldset-second-child></legend>
<div><legend id=in-fieldset-descendant></legend></div>
</fieldset>
<div id=ref></div>
<script>
setup(() => {
self.legends = [].slice.call(document.querySelectorAll('legend'));
self.refStyle = getComputedStyle(document.getElementById('ref'));
self.props = ['display',
'unicodeBidi',
'marginTop',
'marginRight',
'marginBottom',
'marginLeft',
'paddingTop',
'paddingRight',
'paddingBottom',
'paddingLeft',
'overflow',
// Extra tests
'height',
'box-sizing',
];
});
legends.forEach(legend => {
const testStyle = getComputedStyle(legend);
props.forEach(prop => {
test(() => {
assert_equals(testStyle[prop], refStyle[prop]);
}, `${legend.id}: ${prop}`);
});
// Test width separately since it differs outside fieldset vs. in fieldset vs. rendered legend
test(() => {
if (legend.id === 'rendered-legend') {
assert_equals(testStyle.width, '0px');
} else {
assert_not_equals(testStyle.width, '0px');
}
}, `${legend.id}: width`);
});
</script>