ladybird/Libraries/LibWeb/Painting/FieldSetPaintable.cpp
Aliaksandr Kalenik 568b7ce7ea LibWeb: Make Paintable tree ref-counted
The Paintable tree and its supplemental painting data structures were
GC allocated because that was the easiest way to manage it and avoid
leaks introduced by ref cycles. This included the Paintable subclasses
themselves plus StackingContext, ChromeWidget, Scrollbar, ResizeHandle,
and scroll-frame state.

We are now trying to reduce GC allocation churn on layout and painting
updates, so keeping this short-lived rendering tree outside the JS heap
is a better fit. Move Paintable to RefCountedTreeNode, make painting
helpers ref-counted or weakly reference Paintables, and update the
layout and event-handler call sites to use RefPtr/WeakPtr ownership.
2026-05-07 15:03:44 +02:00

142 lines
5 KiB
C++

/*
* Copyright (c) 2024, Kostya Farber <kostya.farber@gmail.com>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibWeb/Layout/FieldSetBox.h>
#include <LibWeb/Layout/LegendBox.h>
#include <LibWeb/Painting/DisplayListRecorder.h>
#include <LibWeb/Painting/FieldSetPaintable.h>
namespace Web::Painting {
NonnullRefPtr<FieldSetPaintable> FieldSetPaintable::create(Layout::FieldSetBox const& layout_box)
{
return adopt_ref(*new 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());
}
// https://html.spec.whatwg.org/multipage/rendering.html#the-fieldset-and-legend-elements
CSSPixels FieldSetPaintable::effective_border_top() const
{
// The space allocated for the element's border on the block-start side is expected to be the element's
// 'border-block-start-width' or the rendered legend's margin box size in the fieldset's block-flow direction,
// whichever is greater.
auto css_border_top = computed_values().border_top().width;
if (auto legend = layout_box().rendered_legend()) {
auto legend_paintable = legend->paintable_box();
auto legend_margin_box_height = legend_paintable->box_model().margin.top
+ legend_paintable->absolute_border_box_rect().height()
+ legend_paintable->box_model().margin.bottom;
return max(css_border_top, legend_margin_box_height);
}
return css_border_top;
}
CSSPixelRect FieldSetPaintable::visual_border_box_rect() const
{
auto css_border_top = computed_values().border_top().width;
auto allocated_border_top = effective_border_top();
// The CSS border is painted centered within the effective border area (which may be larger than the CSS border when
// the legend is taller).
auto visual_border_box_rect = absolute_border_box_rect();
if (allocated_border_top <= css_border_top)
return visual_border_box_rect;
visual_border_box_rect.take_from_top((allocated_border_top - css_border_top) / 2);
return visual_border_box_rect;
}
void FieldSetPaintable::paint_background(DisplayListRecordingContext& context) const
{
auto& recorder = context.display_list_recorder();
recorder.save();
recorder.add_clip_rect(context.rounded_device_rect(visual_border_box_rect()).to_type<int>());
PaintableBox::paint_background(context);
recorder.restore();
}
void FieldSetPaintable::paint(DisplayListRecordingContext& context, PaintPhase phase) const
{
if (!is_visible())
return;
if (phase != PaintPhase::Border) {
PaintableBox::paint(context, phase);
return;
}
auto legend = layout_box().rendered_legend();
if (!legend) {
PaintableBox::paint(context, phase);
return;
}
auto legend_paintable = legend->paintable_box();
auto legend_border_rect = context.rounded_device_rect(legend_paintable->absolute_border_box_rect());
auto top_border_data = computed_values().border_top();
auto top_border = context.enclosing_device_pixels(top_border_data.width).value();
auto device_border_rect = context.rounded_device_rect(visual_border_box_rect());
auto& display_list_recorder = context.display_list_recorder();
auto paint_borders_with_optional_clip = [&](BordersDataDevicePixels borders, Optional<Gfx::IntRect> clip) {
if (clip.has_value()) {
display_list_recorder.save();
display_list_recorder.add_clip_rect(clip.value());
}
auto corners = normalized_border_radii_data().as_corners(context.device_pixel_converter());
paint_all_borders(display_list_recorder, device_border_rect, corners, borders);
if (clip.has_value())
display_list_recorder.restore();
};
BordersData borders_data {
.top = {},
.right = computed_values().border_right(),
.bottom = computed_values().border_bottom(),
.left = computed_values().border_left(),
};
paint_borders_with_optional_clip(borders_data.to_device_pixels(context), {});
// The top border is not expected to be painted behind the border box of the legend.
auto top_border_only = BordersData { .top = top_border_data, .right = {}, .bottom = {}, .left = {} }.to_device_pixels(context);
DevicePixelRect left_segment = {
device_border_rect.x(),
device_border_rect.y(),
legend_border_rect.x() - device_border_rect.x(),
top_border
};
paint_borders_with_optional_clip(top_border_only, left_segment.to_type<int>());
DevicePixelRect right_segment = {
legend_border_rect.right(),
device_border_rect.y(),
device_border_rect.right() - legend_border_rect.right(),
top_border
};
paint_borders_with_optional_clip(top_border_only, right_segment.to_type<int>());
}
}