ladybird/Libraries/LibWeb/Painting/PaintableBox.cpp

1350 lines
60 KiB
C++
Raw Normal View History

/*
* Copyright (c) 2022-2023, Andreas Kling <andreas@ladybird.org>
* Copyright (c) 2022-2025, Sam Atkins <sam@ladybird.org>
* Copyright (c) 2024-2026, Aliaksandr Kalenik <kalenik.aliaksandr@gmail.com>
* Copyright (c) 2025, Jelle Raaijmakers <jelle@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/GenericShorthands.h>
#include <LibGfx/Font/Font.h>
#include <LibGfx/ImmutableBitmap.h>
#include <LibWeb/CSS/StyleValues/FilterValueListStyleValue.h>
#include <LibWeb/DOM/Document.h>
#include <LibWeb/HTML/HTMLHtmlElement.h>
#include <LibWeb/HTML/Navigable.h>
#include <LibWeb/Layout/InlineNode.h>
#include <LibWeb/Page/EventHandler.h>
#include <LibWeb/Page/MiddleButtonScrollHandler.h>
#include <LibWeb/Page/Page.h>
#include <LibWeb/Painting/BackgroundPainting.h>
#include <LibWeb/Painting/ChromeMetrics.h>
#include <LibWeb/Painting/DisplayListRecorder.h>
#include <LibWeb/Painting/PaintableBox.h>
#include <LibWeb/Painting/ResizeHandle.h>
#include <LibWeb/Painting/SVGPaintable.h>
#include <LibWeb/Painting/SVGSVGPaintable.h>
#include <LibWeb/Painting/Scrollbar.h>
#include <LibWeb/Painting/ShadowPainting.h>
#include <LibWeb/Painting/StackingContext.h>
#include <LibWeb/Painting/TableBordersPainting.h>
#include <LibWeb/Painting/ViewportPaintable.h>
#include <LibWeb/Platform/FontPlugin.h>
#include <LibWeb/SVG/SVGFilterElement.h>
namespace Web::Painting {
GC_DEFINE_ALLOCATOR(PaintableBox);
static bool g_paint_viewport_scrollbars = true;
2025-07-19 23:35:33 -03:00
void set_paint_viewport_scrollbars(bool const enabled)
{
g_paint_viewport_scrollbars = enabled;
}
ResolvedCSSFilter resolve_css_filter(CSS::Filter const& computed_filter, PaintableBox const& paintable_box)
{
auto const& computed_values = paintable_box.computed_values();
auto const& layout_node = paintable_box.layout_node_with_style_and_box_metrics();
ResolvedCSSFilter result;
for (auto const& filter_operation : computed_filter.filters()) {
filter_operation.visit(
[&](CSS::FilterOperation::Blur const& blur) {
auto resolved_radius = blur.resolved_radius();
result.operations.empend(ResolvedCSSFilter::Blur {
.radius = CSSPixels::nearest_value_for(resolved_radius),
});
},
[&](CSS::FilterOperation::DropShadow const& drop_shadow) {
auto to_css_px = [&](NonnullRefPtr<CSS::StyleValue const> const& length) {
return CSS::Length::from_style_value(length, {}).absolute_length_to_px();
};
auto color_context = CSS::ColorResolutionContext::for_layout_node_with_style(layout_node);
auto resolved_color = drop_shadow.color
? drop_shadow.color->to_color(color_context).value_or(computed_values.color())
: computed_values.color();
result.operations.empend(ResolvedCSSFilter::DropShadow {
.offset_x = to_css_px(drop_shadow.offset_x),
.offset_y = to_css_px(drop_shadow.offset_y),
.radius = drop_shadow.radius ? to_css_px(*drop_shadow.radius) : CSSPixels(0),
.color = resolved_color,
});
},
[&](CSS::FilterOperation::Color const& color_operation) {
result.operations.empend(ResolvedCSSFilter::Color {
.operation = color_operation.operation,
.amount = color_operation.resolved_amount(),
});
},
[&](CSS::FilterOperation::HueRotate const& hue_rotate) {
result.operations.empend(ResolvedCSSFilter::HueRotate {
.angle_degrees = hue_rotate.angle_degrees(),
});
},
[&](CSS::URL const& css_url) {
auto& url_string = css_url.url();
if (url_string.is_empty() || !url_string.starts_with('#'))
return;
auto fragment_or_error = url_string.substring_from_byte_offset(1);
if (fragment_or_error.is_error())
return;
auto maybe_filter = paintable_box.document().get_element_by_id(fragment_or_error.value());
if (!maybe_filter)
return;
if (auto* filter_element = as_if<SVG::SVGFilterElement>(*maybe_filter)) {
result.svg_filter = filter_element->gfx_filter(layout_node);
auto bounds = paintable_box.absolute_border_box_rect();
if (bounds.is_empty()) {
if (auto const* svg_ancestor = paintable_box.first_ancestor_of_type<SVGSVGPaintable>())
result.svg_filter_bounds = svg_ancestor->absolute_rect();
}
if (!bounds.is_empty())
result.svg_filter_bounds = bounds;
}
});
}
return result;
}
GC::Ref<PaintableBox> PaintableBox::create(Layout::Box const& layout_box)
{
return layout_box.heap().allocate<PaintableBox>(layout_box);
}
GC::Ref<PaintableBox> PaintableBox::create(Layout::InlineNode const& layout_box)
{
return layout_box.heap().allocate<PaintableBox>(layout_box);
}
PaintableBox::PaintableBox(Layout::Box const& layout_box)
: Paintable(layout_box)
{
}
PaintableBox::PaintableBox(Layout::InlineNode const& layout_box)
: Paintable(layout_box)
{
}
PaintableBox::~PaintableBox()
{
}
void PaintableBox::reset_for_relayout()
{
if (parent())
remove();
while (first_child())
first_child()->remove();
m_containing_block = {};
m_offset = {};
m_content_size = {};
m_box_model = {};
m_overflow_data.clear();
m_override_borders_data.clear();
m_table_cell_coordinates.clear();
m_sticky_insets = nullptr;
m_absolute_rect.clear();
m_absolute_padding_box_rect.clear();
m_absolute_border_box_rect.clear();
m_enclosing_scroll_frame_index = {};
m_own_scroll_frame_index = {};
m_accumulated_visual_context_index = {};
m_accumulated_visual_context_for_descendants_index = {};
m_used_values_for_grid_template_columns = nullptr;
m_used_values_for_grid_template_rows = nullptr;
m_cached_phase_commands = {};
invalidate_stacking_context();
}
void PaintableBox::visit_edges(Cell::Visitor& visitor)
{
Base::visit_edges(visitor);
visitor.visit(m_stacking_context);
visitor.visit(m_horizontal_scrollbar);
visitor.visit(m_vertical_scrollbar);
visitor.visit(m_resize_handle);
}
CSSPixelPoint PaintableBox::scroll_offset() const
{
if (is_viewport_paintable()) {
auto navigable = document().navigable();
VERIFY(navigable);
return navigable->viewport_scroll_offset();
}
auto const& node = layout_node();
if (auto pseudo_element = node.generated_for_pseudo_element(); pseudo_element.has_value())
return node.pseudo_element_generator()->scroll_offset(*pseudo_element);
if (auto const* element = as_if<DOM::Element>(dom_node().ptr()))
return element->scroll_offset({});
return {};
}
PaintableBox::ScrollHandled PaintableBox::set_scroll_offset(CSSPixelPoint offset)
{
auto scrollable_overflow_rect = this->scrollable_overflow_rect();
if (!scrollable_overflow_rect.has_value())
return ScrollHandled::No;
auto padding_rect = absolute_padding_box_rect();
auto max_x_offset = max(scrollable_overflow_rect->width() - padding_rect.width(), 0);
auto max_y_offset = max(scrollable_overflow_rect->height() - padding_rect.height(), 0);
offset.set_x(clamp(offset.x(), 0, max_x_offset));
offset.set_y(clamp(offset.y(), 0, max_y_offset));
// FIXME: If there is horizontal and vertical scroll ignore only part of the new offset
if (offset.y() < 0 || scroll_offset() == offset)
return ScrollHandled::No;
if (is_viewport_paintable()) {
auto navigable = document().navigable();
VERIFY(navigable);
navigable->perform_scroll_of_viewport_scrolling_box(offset);
return ScrollHandled::Yes;
}
document().set_needs_to_refresh_scroll_state(true);
auto& node = layout_node();
if (auto pseudo_element = node.generated_for_pseudo_element(); pseudo_element.has_value()) {
node.pseudo_element_generator()->set_scroll_offset(*pseudo_element, offset);
} else if (auto* element = as_if<DOM::Element>(*dom_node())) {
element->set_scroll_offset({}, offset);
} else {
return ScrollHandled::No;
}
// https://drafts.csswg.org/cssom-view-1/#scrolling-events
// Whenever an element gets scrolled (whether in response to user interaction or by an API),
// the user agent must run these steps:
// 1. Let doc be the elements node document.
auto& document = layout_node().document();
// FIXME: 2. If the element is a snap container, run the steps to update snapchanging targets for the element with
// the elements eventual snap target in the block axis as newBlockTarget and the elements eventual snap
// target in the inline axis as newInlineTarget.
GC::Ptr<DOM::EventTarget> event_target;
if (auto pseudo_element = node.generated_for_pseudo_element(); pseudo_element.has_value())
event_target = node.pseudo_element_generator();
else
event_target = dom_node();
if (!event_target)
return ScrollHandled::Yes;
// 3. If (element, "scroll") is already in docs pending scroll events, abort these steps.
if (document.pending_scroll_events().contains_slow(DOM::Document::PendingScrollEvent { *event_target, HTML::EventNames::scroll }))
return ScrollHandled::Yes;
// 4. Append (element, "scroll") to docs pending scroll events.
document.pending_scroll_events().append({ *event_target, HTML::EventNames::scroll });
set_needs_repaint(InvalidateDisplayList::No);
return ScrollHandled::Yes;
}
PaintableBox::ScrollHandled PaintableBox::scroll_by(int delta_x, int delta_y)
{
return set_scroll_offset(scroll_offset().translated(delta_x, delta_y));
}
void PaintableBox::scroll_into_view(CSSPixelRect rect)
{
auto scrollport = absolute_padding_box_rect();
auto current_offset = scroll_offset();
// Both rect and scrollport are in layout coordinate space (not scroll-adjusted).
auto content_rect = rect.translated(-scrollport.x(), -scrollport.y());
auto new_offset = current_offset;
if (content_rect.right() > current_offset.x() + scrollport.width())
new_offset.set_x(content_rect.right() - scrollport.width());
else if (content_rect.left() < current_offset.x())
new_offset.set_x(content_rect.left());
if (content_rect.bottom() > current_offset.y() + scrollport.height())
new_offset.set_y(content_rect.bottom() - scrollport.height());
else if (content_rect.top() < current_offset.y())
new_offset.set_y(content_rect.top());
set_scroll_offset(new_offset);
}
void PaintableBox::set_offset(CSSPixelPoint offset)
{
m_offset = offset;
}
void PaintableBox::set_content_size(CSSPixelSize size)
{
m_content_size = size;
if (auto layout_box = as_if<Layout::Box>(layout_node()))
layout_box->did_set_content_size();
}
CSSPixelPoint PaintableBox::offset() const
{
return m_offset;
}
CSSPixelRect PaintableBox::compute_absolute_rect() const
{
CSSPixelRect rect { offset(), content_size() };
for (auto const* block = containing_block(); block; block = block->containing_block())
rect.translate_by(block->offset());
return rect;
}
CSSPixelRect PaintableBox::absolute_rect() const
{
if (!m_absolute_rect.has_value())
m_absolute_rect = compute_absolute_rect();
return *m_absolute_rect;
}
CSSPixelRect PaintableBox::absolute_padding_box_rect() const
{
if (!m_absolute_padding_box_rect.has_value()) {
auto absolute_rect = this->absolute_rect();
CSSPixelRect rect;
rect.set_x(absolute_rect.x() - box_model().padding.left);
rect.set_width(content_width() + box_model().padding.left + box_model().padding.right);
rect.set_y(absolute_rect.y() - box_model().padding.top);
rect.set_height(content_height() + box_model().padding.top + box_model().padding.bottom);
m_absolute_padding_box_rect = rect;
}
return *m_absolute_padding_box_rect;
}
Optional<CSSPixelRect> PaintableBox::absolute_resizer_rect(ChromeMetrics const& metrics) const
{
if (!has_resizer())
return {};
auto padding_rect = absolute_padding_box_rect();
CSSPixels x = is_chrome_mirrored() ? padding_rect.x() : padding_rect.right() - metrics.resize_gripper_size;
CSSPixels y = padding_rect.bottom() - metrics.resize_gripper_size;
return CSSPixelRect { x, y, metrics.resize_gripper_size, metrics.resize_gripper_size };
}
CSSPixelRect PaintableBox::absolute_border_box_rect() const
{
if (!m_absolute_border_box_rect.has_value()) {
auto padded_rect = this->absolute_padding_box_rect();
CSSPixelRect rect;
auto use_collapsing_borders_model = override_borders_data().has_value();
// Implement the collapsing border model https://www.w3.org/TR/CSS22/tables.html#collapsing-borders.
auto border_top = use_collapsing_borders_model ? round(box_model().border.top / 2) : box_model().border.top;
auto border_bottom = use_collapsing_borders_model ? round(box_model().border.bottom / 2) : box_model().border.bottom;
auto border_left = use_collapsing_borders_model ? round(box_model().border.left / 2) : box_model().border.left;
auto border_right = use_collapsing_borders_model ? round(box_model().border.right / 2) : box_model().border.right;
rect.set_x(padded_rect.x() - border_left);
rect.set_width(padded_rect.width() + border_left + border_right);
rect.set_y(padded_rect.y() - border_top);
rect.set_height(padded_rect.height() + border_top + border_bottom);
m_absolute_border_box_rect = rect;
}
return *m_absolute_border_box_rect;
}
// https://drafts.csswg.org/css-overflow-4/#overflow-clip-edge
CSSPixelRect PaintableBox::overflow_clip_edge_rect() const
{
// https://drafts.csswg.org/css-overflow-4/#overflow-clip-margin
// Values are defined as follows:
// '<visual-box>'
// Specifies the box edge to use as the overflow clip edge origin, i.e. when the specified offset is zero.
// If omitted, defaults to 'padding-box' on non-replaced elements, or 'content-box' on replaced elements.
// FIXME: We can't parse this yet so it's always omitted for now.
auto overflow_clip_edge = absolute_padding_box_rect();
if (layout_node().is_replaced_box()) {
overflow_clip_edge = absolute_rect();
}
// '<length [0,∞]>'
// The specified offset dictates how much the overflow clip edge is expanded from the specified box edge
// Negative values are invalid. Defaults to zero if omitted.
overflow_clip_edge.inflate(
computed_values().overflow_clip_margin().top().length().absolute_length_to_px(),
computed_values().overflow_clip_margin().right().length().absolute_length_to_px(),
computed_values().overflow_clip_margin().bottom().length().absolute_length_to_px(),
computed_values().overflow_clip_margin().left().length().absolute_length_to_px());
return overflow_clip_edge;
}
template<typename Callable>
static CSSPixelRect united_rect_for_continuation_chain(PaintableBox const& start, Callable get_rect)
{
// Combine the absolute rects of all paintable boxes of all nodes in the continuation chain. Without this, we
// calculate the wrong rect for inline nodes that were split because of block elements.
Optional<CSSPixelRect> result;
// FIXME: instead of walking the continuation chain in the layout tree, also keep track of this chain in the
// painting tree so we can skip visiting the layout nodes altogether.
for (auto const* node = &start.layout_node_with_style_and_box_metrics(); node; node = node->continuation_of_node()) {
for (auto const& paintable : node->paintables()) {
if (!is<PaintableBox>(paintable))
continue;
auto const& paintable_box = static_cast<PaintableBox const&>(paintable);
auto paintable_border_box_rect = get_rect(paintable_box);
if (!result.has_value())
result = paintable_border_box_rect;
else if (!paintable_border_box_rect.is_empty())
result->unite(paintable_border_box_rect);
}
}
return result.value_or({});
}
CSSPixelRect PaintableBox::absolute_united_border_box_rect() const
{
return united_rect_for_continuation_chain(*this, [](auto const& paintable_box) {
return paintable_box.absolute_border_box_rect();
});
}
CSSPixelRect PaintableBox::absolute_united_content_rect() const
{
return united_rect_for_continuation_chain(*this, [](auto const& paintable_box) {
return paintable_box.absolute_rect();
});
}
CSSPixelRect PaintableBox::absolute_united_padding_box_rect() const
{
return united_rect_for_continuation_chain(*this, [](auto const& paintable_box) {
return paintable_box.absolute_padding_box_rect();
});
}
Optional<CSSPixelRect> PaintableBox::get_clip_rect() const
{
auto clip = computed_values().clip();
if (clip.is_rect() && layout_node_with_style_and_box_metrics().is_absolutely_positioned()) {
auto border_box = absolute_border_box_rect();
return clip.to_rect().resolved(border_box);
}
return {};
}
GC::Ptr<Scrollbar> PaintableBox::scrollbar(ScrollDirection direction) const
{
return direction == ScrollDirection::Horizontal ? m_horizontal_scrollbar : m_vertical_scrollbar;
}
GC::Ref<Scrollbar> PaintableBox::ensure_scrollbar(ScrollDirection direction)
2024-06-03 18:29:32 -03:00
{
auto& slot = direction == ScrollDirection::Horizontal ? m_horizontal_scrollbar : m_vertical_scrollbar;
if (!slot)
slot = Scrollbar::create(heap(), const_cast<PaintableBox&>(*this), direction);
return *slot;
2024-06-03 18:29:32 -03:00
}
bool PaintableBox::could_be_scrolled_by_wheel_event(ScrollDirection direction) const
{
bool is_horizontal = direction == ScrollDirection::Horizontal;
Gfx::Orientation orientation = is_horizontal ? Gfx::Orientation::Horizontal : Gfx::Orientation::Vertical;
auto overflow = is_horizontal ? computed_values().overflow_x() : computed_values().overflow_y();
auto scrollable_overflow_rect = this->scrollable_overflow_rect();
if (!scrollable_overflow_rect.has_value())
return false;
CSSPixels scrollable_overflow_size = scrollable_overflow_rect->primary_size_for_orientation(orientation);
CSSPixels scrollport_size = absolute_padding_box_rect().primary_size_for_orientation(orientation);
bool overflow_value_allows_scrolling = overflow == CSS::Overflow::Auto || overflow == CSS::Overflow::Scroll;
if ((is_viewport_paintable() && overflow != CSS::Overflow::Hidden) || overflow_value_allows_scrolling)
return scrollable_overflow_size > scrollport_size;
return false;
}
bool PaintableBox::could_be_scrolled_by_wheel_event() const
{
return could_be_scrolled_by_wheel_event(ScrollDirection::Horizontal) || could_be_scrolled_by_wheel_event(ScrollDirection::Vertical);
}
bool PaintableBox::overflow_property_applies() const
{
// https://drafts.csswg.org/css-overflow-3/#overflow-control
// Overflow properties apply to block containers, flex containers and grid containers.
// FIXME: Ideally we would check whether overflow applies positively rather than listing exceptions. However,
// not all elements that should support overflow are currently identifiable that way.
if (is<SVGPaintable>(*this))
return false;
auto const& display = computed_values().display();
if (layout_node().is_inline_node())
return false;
if (display.is_ruby_inside())
return false;
if (display.is_internal() && !display.is_table_cell() && !display.is_table_caption())
return false;
return true;
}
CSSPixels PaintableBox::available_scrollbar_length(ScrollDirection direction, ChromeMetrics const& metrics) const
{
bool is_horizontal = direction == ScrollDirection::Horizontal;
auto padding_rect = absolute_padding_box_rect();
CSSPixels full_scrollport_length = is_horizontal ? padding_rect.width() : padding_rect.height();
if (has_resizer())
full_scrollport_length -= metrics.resize_gripper_size;
else {
if (is_horizontal && could_be_scrolled_by_wheel_event(ScrollDirection::Vertical))
full_scrollport_length -= metrics.scroll_gutter_thickness;
if (!is_horizontal && could_be_scrolled_by_wheel_event(ScrollDirection::Horizontal))
full_scrollport_length -= metrics.scroll_gutter_thickness;
}
return full_scrollport_length;
}
Optional<CSSPixelRect> PaintableBox::absolute_scrollbar_rect(ScrollDirection direction, bool with_gutter, ChromeMetrics const& metrics) const
{
if (!could_be_scrolled_by_wheel_event(direction))
return {};
if (computed_values().scrollbar_width() == CSS::ScrollbarWidth::None)
return {};
bool is_horizontal = direction == ScrollDirection::Horizontal;
bool adjusting_for_resizer = has_resizer();
CSSPixels rect_thickness = with_gutter
? metrics.scroll_gutter_thickness
: metrics.scroll_thumb_thickness_thin + metrics.scroll_thumb_padding_thin;
CSSPixelRect scrollbar_rect = absolute_padding_box_rect();
if (is_horizontal) {
if (!adjusting_for_resizer && could_be_scrolled_by_wheel_event(ScrollDirection::Vertical)) {
scrollbar_rect.set_width(max(CSSPixels { 0 }, scrollbar_rect.width() - metrics.scroll_gutter_thickness));
if (is_chrome_mirrored())
scrollbar_rect.set_x(scrollbar_rect.x() + metrics.scroll_gutter_thickness);
} else if (adjusting_for_resizer) {
scrollbar_rect.set_width(available_scrollbar_length(ScrollDirection::Horizontal, metrics));
if (is_chrome_mirrored())
scrollbar_rect.set_x(scrollbar_rect.x() + metrics.resize_gripper_size);
}
scrollbar_rect.set_y(max(CSSPixels { 0 }, scrollbar_rect.bottom() - rect_thickness));
scrollbar_rect.set_height(rect_thickness);
} else {
if (adjusting_for_resizer)
scrollbar_rect.set_height(available_scrollbar_length(ScrollDirection::Vertical, metrics));
if (!is_chrome_mirrored())
scrollbar_rect.set_x(max(CSSPixels { 0 }, scrollbar_rect.right() - rect_thickness));
scrollbar_rect.set_width(rect_thickness);
}
return scrollbar_rect;
}
Optional<PaintableBox::ScrollbarData> PaintableBox::compute_scrollbar_data(ScrollDirection direction, ChromeMetrics const& metrics, ScrollStateSnapshot const* scroll_state_snapshot) const
{
bool is_horizontal = direction == ScrollDirection::Horizontal;
auto orientation = is_horizontal ? Gfx::Orientation::Horizontal : Gfx::Orientation::Vertical;
auto overflow = is_horizontal ? computed_values().overflow_x() : computed_values().overflow_y();
if (overflow != CSS::Overflow::Scroll && !could_be_scrolled_by_wheel_event(direction))
return {};
if (!m_own_scroll_frame_index.value())
return {};
CSSPixelRect scrollable_overflow_rect = this->scrollable_overflow_rect().value();
CSSPixels scrollable_overflow_length = scrollable_overflow_rect.primary_size_for_orientation(orientation);
if (scrollable_overflow_length == 0)
return {};
auto const& scrollbar = is_horizontal ? m_horizontal_scrollbar : m_vertical_scrollbar;
bool with_gutter = scrollbar && scrollbar->is_enlarged();
auto scrollbar_rect = absolute_scrollbar_rect(direction, with_gutter, metrics);
if (!scrollbar_rect.has_value())
return {};
CSSPixels thumb_thickness = metrics.scroll_thumb_thickness_thin;
CSSPixels thumb_margin = metrics.scroll_thumb_padding_thin;
if (with_gutter) {
thumb_thickness = metrics.scroll_thumb_thickness;
thumb_margin = CSSPixels { (metrics.scroll_gutter_thickness - metrics.scroll_thumb_thickness) / 2.0 };
}
CSSPixels scrollbar_length = scrollbar_rect->primary_size_for_orientation(orientation);
CSSPixels usable_scrollbar_length = max(CSSPixels { 0 }, scrollbar_length - (2 * thumb_margin));
CSSPixels scrollport_size = absolute_padding_box_rect().primary_size_for_orientation(orientation);
CSSPixels min_thumb_length = min(usable_scrollbar_length, metrics.scroll_thumb_min_length);
CSSPixels thumb_length = max(usable_scrollbar_length * (scrollport_size / scrollable_overflow_length), min_thumb_length);
ScrollbarData scrollbar_data = { .gutter_rect = {}, .thumb_rect = scrollbar_rect.value(), .thumb_travel_to_scroll_ratio = 0 };
scrollbar_data.thumb_rect.set_primary_size_for_orientation(orientation, thumb_length);
scrollbar_data.thumb_rect.set_secondary_size_for_orientation(orientation, thumb_thickness);
scrollbar_data.thumb_rect.translate_primary_offset_for_orientation(orientation, thumb_margin);
if (with_gutter || (!is_horizontal && is_chrome_mirrored()))
scrollbar_data.thumb_rect.translate_secondary_offset_for_orientation(orientation, thumb_margin);
if (with_gutter)
scrollbar_data.gutter_rect = scrollbar_rect.value();
if (scrollable_overflow_length > scrollport_size)
scrollbar_data.thumb_travel_to_scroll_ratio = (usable_scrollbar_length - thumb_length) / (scrollable_overflow_length - scrollport_size);
if (scroll_state_snapshot) {
auto own_offset = scroll_state_snapshot->device_offset_for_index(m_own_scroll_frame_index);
auto device_scroll_offset = is_horizontal ? -own_offset.x() : -own_offset.y();
auto device_pixels_per_css_pixel = static_cast<float>(document().page().client().device_pixels_per_css_pixel());
CSSPixels thumb_offset = CSSPixels::nearest_value_for(device_scroll_offset / device_pixels_per_css_pixel) * scrollbar_data.thumb_travel_to_scroll_ratio;
scrollbar_data.thumb_rect.translate_primary_offset_for_orientation(orientation, thumb_offset);
}
return scrollbar_data;
}
void PaintableBox::paint(DisplayListRecordingContext& context, PaintPhase phase) const
{
if (!is_visible())
return;
auto empty_cells_property_applies = [this]() {
return display().is_internal_table() && computed_values().empty_cells() == CSS::EmptyCells::Hide && !has_children();
};
if (phase == PaintPhase::Background && !empty_cells_property_applies()) {
paint_backdrop_filter(context);
paint_background(context);
paint_box_shadow(context);
}
auto const is_table_with_collapsed_borders = display().is_table_inside() && computed_values().border_collapse() == CSS::BorderCollapse::Collapse;
if (!display().is_table_cell() && !is_table_with_collapsed_borders && phase == PaintPhase::Border) {
paint_border(context);
}
if ((display().is_table_inside() || computed_values().border_collapse() == CSS::BorderCollapse::Collapse) && phase == PaintPhase::TableCollapsedBorder) {
paint_table_borders(context, *this);
}
if (phase == PaintPhase::Outline) {
auto const& outline_data = this->outline_data();
if (outline_data.has_value()) {
auto outline_offset = this->outline_offset();
auto border_radius_data = normalized_border_radii_data(ShrinkRadiiForBorders::No);
auto borders_rect = absolute_border_box_rect();
auto outline_offset_x = outline_offset;
auto outline_offset_y = outline_offset;
// "Both the height and the width of the outside of the shape drawn by the outline should not
// become smaller than twice the computed value of the outline-width property to make sure
// that an outline can be rendered even with large negative values."
// https://www.w3.org/TR/css-ui-4/#outline-offset
// So, if the horizontal outline offset is > half the borders_rect's width then we set it to that.
// (And the same for y)
if ((borders_rect.width() / 2) + outline_offset_x < 0)
outline_offset_x = -borders_rect.width() / 2;
if ((borders_rect.height() / 2) + outline_offset_y < 0)
outline_offset_y = -borders_rect.height() / 2;
border_radius_data.inflate(outline_data->top.width + outline_offset_y, outline_data->right.width + outline_offset_x, outline_data->bottom.width + outline_offset_y, outline_data->left.width + outline_offset_x);
borders_rect.inflate(outline_data->top.width + outline_offset_y, outline_data->right.width + outline_offset_x, outline_data->bottom.width + outline_offset_y, outline_data->left.width + outline_offset_x);
paint_all_borders(context.display_list_recorder(), context.rounded_device_rect(borders_rect), border_radius_data.as_corners(context.device_pixel_converter()), outline_data->to_device_pixels(context));
}
}
if (phase == PaintPhase::Overlay) {
ChromeMetrics const& metrics = context.chrome_metrics();
if ((g_paint_viewport_scrollbars || !is_viewport_paintable())
&& computed_values().scrollbar_width() != CSS::ScrollbarWidth::None) {
auto scrollbar_colors = computed_values().scrollbar_color();
for (auto direction : { ScrollDirection::Vertical, ScrollDirection::Horizontal }) {
auto scrollbar_data = compute_scrollbar_data(direction, metrics);
if (!scrollbar_data.has_value())
continue;
context.display_list_recorder().paint_scrollbar(
m_own_scroll_frame_index,
context.rounded_device_rect(scrollbar_data->gutter_rect).to_type<int>(),
context.rounded_device_rect(scrollbar_data->thumb_rect).to_type<int>(),
scrollbar_data->thumb_travel_to_scroll_ratio.to_double(),
scrollbar_colors.thumb_color,
scrollbar_colors.track_color,
direction == ScrollDirection::Vertical);
}
}
if (auto resizer_rect = absolute_resizer_rect(metrics); resizer_rect.has_value()) {
bool bottom_left_resizer = is_chrome_mirrored();
CSSPixels padding = metrics.resize_gripper_padding;
CSSPixelRect css_rect = resizer_rect.value()
.shrunken(padding, padding)
.translated(bottom_left_resizer ? padding / 2 : -padding / 2, -padding / 2);
Gfx::IntRect rect = context.rounded_device_rect(css_rect).to_type<int>();
Gfx::Color dark { 0, 0, 0, 100 };
Gfx::Color light { 255, 255, 255, 100 };
auto& recorder = context.display_list_recorder();
auto paint_resizer_line = [&](int step, Gfx::Color color) {
Gfx::IntPoint from = { bottom_left_resizer ? rect.left() + step : rect.right() - step, rect.bottom() };
Gfx::IntPoint to = { bottom_left_resizer ? rect.left() : rect.right(), rect.bottom() - step };
recorder.draw_line(from, to, color, 1, Gfx::LineStyle::Solid);
};
for (int step = (rect.width() / 3) - 1; step < rect.width(); step += rect.width() / 3) {
paint_resizer_line(step, light);
paint_resizer_line(step + 1, dark);
}
}
paint_middle_button_scroll_indicator(context);
}
LibWeb: Paint inspector overlays as a separate pass The overlay shown for the node hovered in the inspector is painted as part of the normal tree traversal of all paintables. This works well in most cases, but falls short in specific scenarios: * If the hovered node or one of its ancestors establishes a stacking context and there is another element that establishes a stacking context close by or overlapping it, the overlay and especially the tooltip can become partially hidden behind the second element. Ditto for elements that act as if they established a stacking context. * If the hovered node or one of its ancestors involves clipping, the clip is applied to the overlay and espicially the tooltip. This can cause them to be partially invisible. * Similarly, if the hovered node or one of its ancestors has a defined mask, the mask is applied to the overlay, often making it mostly invisible. * No overlays are shown for SVG nodes because they are painted differently from HTML documents. Some of these problems may be fixable with the current system. But some seem like they fundamentally cannot work fully when the overlays are painted as part of the regular tree traversal. Instead we pull out painting the overlay as a separate pass executed after the tree traversal. This way we ensure that the overlays are always painted last and therefore on top of everything else. This also makes sure that the overlays are unaffected by clips and masks. And since overlay painting is independent from painting the actual elements, it just works as well. However we need to be careful, because we still need to apply some of the steps of the tree traversal to get the correct result. Namely we need to apply scroll offsets and transforms. To do so, we collect all ancestors of the hovered node and apply those as if we were in the normal tree traversal.
2025-09-02 17:57:41 -03:00
}
void PaintableBox::paint_middle_button_scroll_indicator(DisplayListRecordingContext& context) const
{
static constexpr Gfx::Color CIRCLE_COLOR = Gfx::Color { Gfx::Color::White }.with_alpha(220);
static constexpr Gfx::Color ARROW_COLOR = Gfx::Color::DarkGray;
static constexpr auto RADIUS = 16;
static constexpr auto ARROW_SIZE = 6;
static constexpr auto ARROW_OFFSET = 8;
if (!is_viewport_paintable())
return;
auto navigable = document().navigable();
if (!navigable)
return;
auto handler = navigable->event_handler().middle_button_scroll_handler();
if (!handler.has_value())
return;
auto& recorder = context.display_list_recorder();
auto device_origin = context.rounded_device_point(handler->origin()).to_type<int>();
Gfx::IntRect circle { device_origin.x() - RADIUS, device_origin.y() - RADIUS, RADIUS * 2, RADIUS * 2 };
recorder.fill_ellipse(circle, CIRCLE_COLOR);
recorder.draw_ellipse(circle, ARROW_COLOR, 1);
auto paint_arrow = [&](Gfx::FloatPoint p1, Gfx::FloatPoint p2, Gfx::FloatPoint p3) {
Gfx::Path path;
path.move_to(p1);
path.line_to(p2);
path.line_to(p3);
path.close();
recorder.fill_path({ .path = move(path), .paint_style_or_color = ARROW_COLOR });
};
auto x = static_cast<float>(device_origin.x());
auto y = static_cast<float>(device_origin.y());
// FIXME: We could paint a subset of these arrows depending on which direction the container may be scrolled.
paint_arrow({ x, y - ARROW_OFFSET - ARROW_SIZE }, { x - ARROW_SIZE, y - ARROW_OFFSET }, { x + ARROW_SIZE, y - ARROW_OFFSET });
paint_arrow({ x, y + ARROW_OFFSET + ARROW_SIZE }, { x - ARROW_SIZE, y + ARROW_OFFSET }, { x + ARROW_SIZE, y + ARROW_OFFSET });
paint_arrow({ x - ARROW_OFFSET - ARROW_SIZE, y }, { x - ARROW_OFFSET, y - ARROW_SIZE }, { x - ARROW_OFFSET, y + ARROW_SIZE });
paint_arrow({ x + ARROW_OFFSET + ARROW_SIZE, y }, { x + ARROW_OFFSET, y - ARROW_SIZE }, { x + ARROW_OFFSET, y + ARROW_SIZE });
}
LibWeb: Paint inspector overlays as a separate pass The overlay shown for the node hovered in the inspector is painted as part of the normal tree traversal of all paintables. This works well in most cases, but falls short in specific scenarios: * If the hovered node or one of its ancestors establishes a stacking context and there is another element that establishes a stacking context close by or overlapping it, the overlay and especially the tooltip can become partially hidden behind the second element. Ditto for elements that act as if they established a stacking context. * If the hovered node or one of its ancestors involves clipping, the clip is applied to the overlay and espicially the tooltip. This can cause them to be partially invisible. * Similarly, if the hovered node or one of its ancestors has a defined mask, the mask is applied to the overlay, often making it mostly invisible. * No overlays are shown for SVG nodes because they are painted differently from HTML documents. Some of these problems may be fixable with the current system. But some seem like they fundamentally cannot work fully when the overlays are painted as part of the regular tree traversal. Instead we pull out painting the overlay as a separate pass executed after the tree traversal. This way we ensure that the overlays are always painted last and therefore on top of everything else. This also makes sure that the overlays are unaffected by clips and masks. And since overlay painting is independent from painting the actual elements, it just works as well. However we need to be careful, because we still need to apply some of the steps of the tree traversal to get the correct result. Namely we need to apply scroll offsets and transforms. To do so, we collect all ancestors of the hovered node and apply those as if we were in the normal tree traversal.
2025-09-02 17:57:41 -03:00
void PaintableBox::paint_inspector_overlay_internal(DisplayListRecordingContext& context) const
{
auto content_rect = absolute_united_content_rect();
auto margin_rect = united_rect_for_continuation_chain(*this, [](PaintableBox const& box) {
auto margin_box = box.box_model().margin_box();
return CSSPixelRect {
box.absolute_x() - margin_box.left,
box.absolute_y() - margin_box.top,
box.content_width() + margin_box.left + margin_box.right,
box.content_height() + margin_box.top + margin_box.bottom,
};
LibWeb: Paint inspector overlays as a separate pass The overlay shown for the node hovered in the inspector is painted as part of the normal tree traversal of all paintables. This works well in most cases, but falls short in specific scenarios: * If the hovered node or one of its ancestors establishes a stacking context and there is another element that establishes a stacking context close by or overlapping it, the overlay and especially the tooltip can become partially hidden behind the second element. Ditto for elements that act as if they established a stacking context. * If the hovered node or one of its ancestors involves clipping, the clip is applied to the overlay and espicially the tooltip. This can cause them to be partially invisible. * Similarly, if the hovered node or one of its ancestors has a defined mask, the mask is applied to the overlay, often making it mostly invisible. * No overlays are shown for SVG nodes because they are painted differently from HTML documents. Some of these problems may be fixable with the current system. But some seem like they fundamentally cannot work fully when the overlays are painted as part of the regular tree traversal. Instead we pull out painting the overlay as a separate pass executed after the tree traversal. This way we ensure that the overlays are always painted last and therefore on top of everything else. This also makes sure that the overlays are unaffected by clips and masks. And since overlay painting is independent from painting the actual elements, it just works as well. However we need to be careful, because we still need to apply some of the steps of the tree traversal to get the correct result. Namely we need to apply scroll offsets and transforms. To do so, we collect all ancestors of the hovered node and apply those as if we were in the normal tree traversal.
2025-09-02 17:57:41 -03:00
});
auto border_rect = absolute_united_border_box_rect();
auto padding_rect = absolute_united_padding_box_rect();
LibWeb: Paint inspector overlays as a separate pass The overlay shown for the node hovered in the inspector is painted as part of the normal tree traversal of all paintables. This works well in most cases, but falls short in specific scenarios: * If the hovered node or one of its ancestors establishes a stacking context and there is another element that establishes a stacking context close by or overlapping it, the overlay and especially the tooltip can become partially hidden behind the second element. Ditto for elements that act as if they established a stacking context. * If the hovered node or one of its ancestors involves clipping, the clip is applied to the overlay and espicially the tooltip. This can cause them to be partially invisible. * Similarly, if the hovered node or one of its ancestors has a defined mask, the mask is applied to the overlay, often making it mostly invisible. * No overlays are shown for SVG nodes because they are painted differently from HTML documents. Some of these problems may be fixable with the current system. But some seem like they fundamentally cannot work fully when the overlays are painted as part of the regular tree traversal. Instead we pull out painting the overlay as a separate pass executed after the tree traversal. This way we ensure that the overlays are always painted last and therefore on top of everything else. This also makes sure that the overlays are unaffected by clips and masks. And since overlay painting is independent from painting the actual elements, it just works as well. However we need to be careful, because we still need to apply some of the steps of the tree traversal to get the correct result. Namely we need to apply scroll offsets and transforms. To do so, we collect all ancestors of the hovered node and apply those as if we were in the normal tree traversal.
2025-09-02 17:57:41 -03:00
auto paint_inspector_rect = [&](CSSPixelRect const& rect, Color color) {
auto device_rect = context.enclosing_device_rect(rect).to_type<int>();
context.display_list_recorder().fill_rect(device_rect, color.with_alpha(100));
context.display_list_recorder().draw_rect(device_rect, color);
};
LibWeb: Paint inspector overlays as a separate pass The overlay shown for the node hovered in the inspector is painted as part of the normal tree traversal of all paintables. This works well in most cases, but falls short in specific scenarios: * If the hovered node or one of its ancestors establishes a stacking context and there is another element that establishes a stacking context close by or overlapping it, the overlay and especially the tooltip can become partially hidden behind the second element. Ditto for elements that act as if they established a stacking context. * If the hovered node or one of its ancestors involves clipping, the clip is applied to the overlay and espicially the tooltip. This can cause them to be partially invisible. * Similarly, if the hovered node or one of its ancestors has a defined mask, the mask is applied to the overlay, often making it mostly invisible. * No overlays are shown for SVG nodes because they are painted differently from HTML documents. Some of these problems may be fixable with the current system. But some seem like they fundamentally cannot work fully when the overlays are painted as part of the regular tree traversal. Instead we pull out painting the overlay as a separate pass executed after the tree traversal. This way we ensure that the overlays are always painted last and therefore on top of everything else. This also makes sure that the overlays are unaffected by clips and masks. And since overlay painting is independent from painting the actual elements, it just works as well. However we need to be careful, because we still need to apply some of the steps of the tree traversal to get the correct result. Namely we need to apply scroll offsets and transforms. To do so, we collect all ancestors of the hovered node and apply those as if we were in the normal tree traversal.
2025-09-02 17:57:41 -03:00
paint_inspector_rect(margin_rect, Color::Yellow);
paint_inspector_rect(padding_rect, Color::Cyan);
paint_inspector_rect(border_rect, Color::Green);
paint_inspector_rect(content_rect, Color::Magenta);
auto font = Platform::FontPlugin::the().default_font(12);
StringBuilder builder(StringBuilder::Mode::UTF16);
builder.append(debug_description());
LibWeb: Paint inspector overlays as a separate pass The overlay shown for the node hovered in the inspector is painted as part of the normal tree traversal of all paintables. This works well in most cases, but falls short in specific scenarios: * If the hovered node or one of its ancestors establishes a stacking context and there is another element that establishes a stacking context close by or overlapping it, the overlay and especially the tooltip can become partially hidden behind the second element. Ditto for elements that act as if they established a stacking context. * If the hovered node or one of its ancestors involves clipping, the clip is applied to the overlay and espicially the tooltip. This can cause them to be partially invisible. * Similarly, if the hovered node or one of its ancestors has a defined mask, the mask is applied to the overlay, often making it mostly invisible. * No overlays are shown for SVG nodes because they are painted differently from HTML documents. Some of these problems may be fixable with the current system. But some seem like they fundamentally cannot work fully when the overlays are painted as part of the regular tree traversal. Instead we pull out painting the overlay as a separate pass executed after the tree traversal. This way we ensure that the overlays are always painted last and therefore on top of everything else. This also makes sure that the overlays are unaffected by clips and masks. And since overlay painting is independent from painting the actual elements, it just works as well. However we need to be careful, because we still need to apply some of the steps of the tree traversal to get the correct result. Namely we need to apply scroll offsets and transforms. To do so, we collect all ancestors of the hovered node and apply those as if we were in the normal tree traversal.
2025-09-02 17:57:41 -03:00
builder.appendff(" {}x{} @ {},{}", border_rect.width(), border_rect.height(), border_rect.x(), border_rect.y());
auto size_text = builder.to_utf16_string();
LibWeb: Paint inspector overlays as a separate pass The overlay shown for the node hovered in the inspector is painted as part of the normal tree traversal of all paintables. This works well in most cases, but falls short in specific scenarios: * If the hovered node or one of its ancestors establishes a stacking context and there is another element that establishes a stacking context close by or overlapping it, the overlay and especially the tooltip can become partially hidden behind the second element. Ditto for elements that act as if they established a stacking context. * If the hovered node or one of its ancestors involves clipping, the clip is applied to the overlay and espicially the tooltip. This can cause them to be partially invisible. * Similarly, if the hovered node or one of its ancestors has a defined mask, the mask is applied to the overlay, often making it mostly invisible. * No overlays are shown for SVG nodes because they are painted differently from HTML documents. Some of these problems may be fixable with the current system. But some seem like they fundamentally cannot work fully when the overlays are painted as part of the regular tree traversal. Instead we pull out painting the overlay as a separate pass executed after the tree traversal. This way we ensure that the overlays are always painted last and therefore on top of everything else. This also makes sure that the overlays are unaffected by clips and masks. And since overlay painting is independent from painting the actual elements, it just works as well. However we need to be careful, because we still need to apply some of the steps of the tree traversal to get the correct result. Namely we need to apply scroll offsets and transforms. To do so, we collect all ancestors of the hovered node and apply those as if we were in the normal tree traversal.
2025-09-02 17:57:41 -03:00
auto size_text_rect = border_rect;
size_text_rect.set_y(border_rect.y() + border_rect.height());
size_text_rect.set_top(size_text_rect.top());
size_text_rect.set_width(CSSPixels::nearest_value_for(font->width(size_text)) + 4);
size_text_rect.set_height(CSSPixels::nearest_value_for(font->pixel_size()) + 4);
auto size_text_device_rect = context.enclosing_device_rect(size_text_rect).to_type<int>();
context.display_list_recorder().fill_rect(size_text_device_rect, context.palette().color(Gfx::ColorRole::Tooltip));
context.display_list_recorder().draw_rect(size_text_device_rect, context.palette().threed_shadow1());
context.display_list_recorder().draw_text(size_text_device_rect, size_text, font->with_size(font->point_size() * context.device_pixels_per_css_pixel()), Gfx::TextAlignment::Center, context.palette().color(Gfx::ColorRole::TooltipText));
}
void PaintableBox::set_stacking_context(GC::Ref<StackingContext> stacking_context)
{
m_stacking_context = move(stacking_context);
}
void PaintableBox::invalidate_stacking_context()
{
m_stacking_context = nullptr;
}
Optional<int> PaintableBox::effective_z_index() const
{
// https://drafts.csswg.org/css2/#z-index
// Applies to: positioned elements
if (is_positioned())
return computed_values().z_index();
return {};
}
BordersData PaintableBox::remove_element_kind_from_borders_data(PaintableBox::BordersDataWithElementKind borders_data)
{
return {
.top = borders_data.top.border_data,
.right = borders_data.right.border_data,
.bottom = borders_data.bottom.border_data,
.left = borders_data.left.border_data,
};
}
void PaintableBox::paint_border(DisplayListRecordingContext& context) const
{
auto borders_data = m_override_borders_data.has_value() ? remove_element_kind_from_borders_data(m_override_borders_data.value()) : BordersData {
.top = box_model().border.top == 0 ? CSS::BorderData() : computed_values().border_top(),
.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(context.display_list_recorder(), context.rounded_device_rect(absolute_border_box_rect()), normalized_border_radii_data().as_corners(context.device_pixel_converter()), borders_data.to_device_pixels(context));
}
void PaintableBox::paint_backdrop_filter(DisplayListRecordingContext& context) const
{
if (!computed_values().backdrop_filter().has_filters())
return;
auto resolved = resolve_css_filter(computed_values().backdrop_filter(), *this);
auto backdrop_region = context.rounded_device_rect(absolute_border_box_rect());
auto border_radii_data = normalized_border_radii_data();
ScopedCornerRadiusClip corner_clipper { context, backdrop_region, border_radii_data };
if (auto gfx_filter = to_gfx_filter(resolved, context.device_pixels_per_css_pixel()); gfx_filter.has_value())
context.display_list_recorder().apply_backdrop_filter(backdrop_region.to_type<int>(), border_radii_data.as_corners(context.device_pixel_converter()), *gfx_filter);
}
void PaintableBox::paint_background(DisplayListRecordingContext& context) const
{
// If the body's background properties were propagated to the root element, do not re-paint the body's background.
if (layout_node_with_style_and_box_metrics().is_body() && document().html_element()->should_use_body_background_properties())
return;
auto const& computed_values = this->computed_values();
CSSPixelRect background_rect;
Color background_color = computed_values.background_color();
auto const* background_layers = &computed_values.background_layers();
// https://drafts.csswg.org/css-backgrounds/#root-background
auto is_root = layout_node_with_style_and_box_metrics().is_root_element();
if (is_root) {
background_rect = absolute_border_box_rect();
auto& html_element = as<HTML::HTMLHtmlElement>(*layout_node_with_style_and_box_metrics().dom_node());
if (html_element.should_use_body_background_properties()) {
background_layers = document().background_layers();
background_color = document().background_color();
}
} else {
background_rect = absolute_padding_box_rect();
}
// HACK: If the Box has a border, use the bordered_rect to paint the background.
// This way if we have a border-radius there will be no gap between the filling and actual border.
if (computed_values.border_top().width != 0 || computed_values.border_right().width != 0 || computed_values.border_bottom().width != 0 || computed_values.border_left().width != 0)
background_rect = absolute_border_box_rect();
auto border_radii = normalized_border_radii_data();
ResolvedBackground resolved_background;
if (background_layers)
resolved_background = resolve_background_layers(*background_layers, *this, background_color, computed_values.background_color_clip(), background_rect, border_radii);
if (is_root) {
auto canvas_rect = navigable()->viewport_rect();
if (auto overflow_rect = scrollable_overflow_rect(); overflow_rect.has_value())
canvas_rect.unite(overflow_rect.value());
resolved_background.background_rect.unite(canvas_rect);
resolved_background.color_box.rect.unite(canvas_rect);
}
// If the body's background was propagated to the root element, use the body's image-rendering value.
auto image_rendering = computed_values.image_rendering();
if (layout_node().is_root_element()
&& document().html_element()
&& document().html_element()->should_use_body_background_properties()) {
image_rendering = document().background_image_rendering();
}
Painting::paint_background(context, *this, image_rendering, resolved_background, border_radii);
}
void PaintableBox::paint_box_shadow(DisplayListRecordingContext& context) const
{
auto const& box_shadow_layers = computed_values().box_shadow();
if (box_shadow_layers.is_empty())
return;
Vector<Painting::ShadowData> resolved_box_shadow_data;
resolved_box_shadow_data.ensure_capacity(box_shadow_layers.size());
for (auto const& layer : box_shadow_layers)
resolved_box_shadow_data.unchecked_append(ShadowData::from_css(layer, layout_node()));
auto borders_data = BordersData {
.top = computed_values().border_top(),
.right = computed_values().border_right(),
.bottom = computed_values().border_bottom(),
.left = computed_values().border_left(),
};
Painting::paint_box_shadow(context, absolute_border_box_rect(), absolute_padding_box_rect(),
borders_data, normalized_border_radii_data(), resolved_box_shadow_data);
}
BorderRadiiData PaintableBox::normalized_border_radii_data(ShrinkRadiiForBorders shrink) const
{
auto border_radii_data = this->border_radii_data();
if (shrink == ShrinkRadiiForBorders::Yes)
border_radii_data.shrink(computed_values().border_top().width, computed_values().border_right().width, computed_values().border_bottom().width, computed_values().border_left().width);
return border_radii_data;
}
Optional<CSSPixelPoint> PaintableBox::transform_point_to_local(CSSPixelPoint screen_position) const
{
if (!m_accumulated_visual_context_index.value())
LibWeb: Integrate AccumulatedVisualContext with display list Integrate AccumulatedVisualContext with display list recording and playback. This is the main commit of the refactoring that delivers the architectural improvements enabled by AccumulatedVisualContext. Recording changes: Each display list command now stores a single RefPtr<AccumulatedVisualContext> instead of separate scroll_frame_id and ClipFrame. The recorder simply captures the current accumulated context when appending commands. The before_paint()/after_paint() hooks that pushed/popped scroll frame IDs are replaced by directly setting accumulated_visual_context on the recorder before painting each element. Playback changes: The display list player now uses LCA (Lowest Common Ancestor) based traversal to switch between visual contexts efficiently. When transitioning from context A to context B: 1. Find the LCA of A and B in the context tree 2. Pop (restore) states back to the LCA depth 3. Push (save + apply) states from LCA down to B This approach minimizes redundant save/restore operations. For example, when rendering siblings that share a common scroll container, the player keeps that scroll state applied and only switches the divergent parts of their context chains. Key deletions: - Remove translate_by() from all 45 display list commands - commands are now immutable - Remove transform/perspective fields from PushStackingContext - transforms are tracked via AccumulatedVisualContext - Remove push_scroll_frame_id()/pop_scroll_frame_id() from DisplayListRecorder - Remove before_paint()/after_paint() hooks from Paintable - Merge ApplyOpacity, ApplyCompositeAndBlendingOperator, ApplyFilter into single ApplyEffects command Stacking context painting changes: The StackingContext::paint() method is significantly simplified. Instead of building a PushStackingContextParams struct with transform matrices and pushing/popping stacking contexts, it now: 1. Sets the accumulated visual context (which already contains transforms) 2. Applies effects (opacity, blend mode, filters) if needed 3. Applies clip path if needed 4. Paints the content 5. Restores state The visual state management that was interleaved throughout the painting code is now handled uniformly by the context tree.
2026-01-13 16:44:09 -03:00
return screen_position;
auto pixel_ratio = static_cast<float>(document().page().client().device_pixels_per_css_pixel());
auto const& scroll_state = document().paintable()->scroll_state_snapshot();
auto const& visual_context_tree = document().paintable()->visual_context_tree();
auto result = visual_context_tree.transform_point_for_hit_test(m_accumulated_visual_context_index, screen_position.to_type<float>() * pixel_ratio, scroll_state);
if (!result.has_value())
return {};
return (*result / pixel_ratio).to_type<CSSPixels>();
}
Optional<CSSPixelPoint> PaintableBox::transform_point_to_local_for_descendants(CSSPixelPoint screen_position) const
{
if (!m_accumulated_visual_context_for_descendants_index.value())
return screen_position;
auto pixel_ratio = static_cast<float>(document().page().client().device_pixels_per_css_pixel());
auto const& scroll_state = document().paintable()->scroll_state_snapshot();
auto const& visual_context_tree = document().paintable()->visual_context_tree();
auto result = visual_context_tree.transform_point_for_hit_test(m_accumulated_visual_context_for_descendants_index, screen_position.to_type<float>() * pixel_ratio, scroll_state);
if (!result.has_value())
return {};
return (*result / pixel_ratio).to_type<CSSPixels>();
}
CSSPixelRect PaintableBox::transform_rect_to_viewport(CSSPixelRect const& rect) const
{
if (!m_accumulated_visual_context_index.value())
return rect;
auto pixel_ratio = static_cast<float>(document().page().client().device_pixels_per_css_pixel());
auto const& scroll_state = document().paintable()->scroll_state_snapshot();
auto const& visual_context_tree = document().paintable()->visual_context_tree();
auto result = visual_context_tree.transform_rect_to_viewport(m_accumulated_visual_context_index, rect.to_type<float>() * pixel_ratio, scroll_state);
return (result * (1.f / pixel_ratio)).to_type<CSSPixels>();
}
CSSPixelPoint PaintableBox::inverse_transform_point(CSSPixelPoint screen_position) const
{
if (!m_accumulated_visual_context_index.value())
return screen_position;
auto pixel_ratio = static_cast<float>(document().page().client().device_pixels_per_css_pixel());
auto const& visual_context_tree = document().paintable()->visual_context_tree();
auto result = visual_context_tree.inverse_transform_point(m_accumulated_visual_context_index, screen_position.to_type<float>() * pixel_ratio);
return (result / pixel_ratio).to_type<CSSPixels>();
}
CSSPixelPoint PaintableBox::transform_to_local_coordinates(CSSPixelPoint screen_position) const
{
return transform_point_to_local(screen_position).value_or(screen_position);
}
bool PaintableBox::has_resizer() const
{
// https://drafts.csswg.org/css-ui#resize
if (is_viewport_paintable())
return false;
// The effect of the resize property on generated content is undefined.
// Implementations should not apply the resize property to generated content.
if (layout_node().generated_for_pseudo_element().has_value())
return false;
auto axes = physical_resize_axes();
return axes.horizontal || axes.vertical;
}
bool PaintableBox::is_chrome_mirrored() const
{
auto const& writing_mode = computed_values().writing_mode();
return (writing_mode == CSS::WritingMode::HorizontalTb && computed_values().direction() == CSS::Direction::Rtl)
|| writing_mode == CSS::WritingMode::VerticalRl
|| writing_mode == CSS::WritingMode::SidewaysRl;
}
GC::Ptr<ResizeHandle> PaintableBox::resize_handle() const
{
return m_resize_handle;
}
GC::Ref<ResizeHandle> PaintableBox::ensure_resize_handle()
{
if (!m_resize_handle)
m_resize_handle = ResizeHandle::create(heap(), *this);
return *m_resize_handle;
}
bool PaintableBox::handle_mousewheel(Badge<EventHandler>, CSSPixelPoint, unsigned, unsigned, int wheel_delta_x, int wheel_delta_y)
{
// if none of the axes we scrolled with can be accepted by this element, don't handle scroll.
if ((!wheel_delta_x || !could_be_scrolled_by_wheel_event(ScrollDirection::Horizontal)) && (!wheel_delta_y || !could_be_scrolled_by_wheel_event(ScrollDirection::Vertical))) {
return false;
}
auto scroll_handled = scroll_by(wheel_delta_x, wheel_delta_y);
return scroll_handled == ScrollHandled::Yes;
}
TraversalDecision PaintableBox::hit_test_chrome(CSSPixelPoint adjusted_position, Function<TraversalDecision(HitTestResult)> const& callback) const
2024-06-03 18:29:32 -03:00
{
// The vast majority of paintable boxes have no resizer and no scrollable axis. Reject those before constructing
// ChromeMetrics or allocating any Scrollbar.
auto has_resizer = this->has_resizer();
auto can_scroll_horizontally = could_be_scrolled_by_wheel_event(ScrollDirection::Horizontal);
auto can_scroll_vertically = could_be_scrolled_by_wheel_event(ScrollDirection::Vertical);
if (!has_resizer && !can_scroll_horizontally && !can_scroll_vertically)
return TraversalDecision::Continue;
// FIXME: This const_cast is not great, but this method is invoked from overrides of virtual const methods.
HitTestResult result { .paintable = const_cast<PaintableBox&>(*this) };
ChromeMetrics metrics = document().page().chrome_metrics();
if (has_resizer && resizer_contains(adjusted_position, metrics)) {
result.chrome_widget = const_cast<PaintableBox&>(*this).ensure_resize_handle();
return callback(result);
}
auto check_scrollbar = [&](ScrollDirection direction) -> TraversalDecision {
auto scrollbar = const_cast<PaintableBox&>(*this).ensure_scrollbar(direction);
if (scrollbar->contains(adjusted_position, metrics)) {
result.chrome_widget = scrollbar;
return callback(result);
}
return TraversalDecision::Continue;
};
if (can_scroll_horizontally && check_scrollbar(ScrollDirection::Horizontal) == TraversalDecision::Break)
return TraversalDecision::Break;
if (can_scroll_vertically && check_scrollbar(ScrollDirection::Vertical) == TraversalDecision::Break)
return TraversalDecision::Break;
2024-06-03 18:29:32 -03:00
return TraversalDecision::Continue;
}
bool PaintableBox::resizer_contains(CSSPixelPoint adjusted_position, ChromeMetrics const& metrics) const
{
auto handle_rect = absolute_resizer_rect(metrics);
if (!handle_rect.has_value())
return false;
bool bottom_left_resizer = is_chrome_mirrored();
handle_rect->inflate(0, bottom_left_resizer ? 0 : box_model().border.right, box_model().border.bottom, bottom_left_resizer ? box_model().border.left : 0);
return handle_rect->contains(adjusted_position);
}
TraversalDecision PaintableBox::hit_test(CSSPixelPoint position, HitTestType type, Function<TraversalDecision(HitTestResult)> const& callback) const
{
auto const is_visible = computed_values().visibility() == CSS::Visibility::Visible;
Optional<CSSPixelPoint> local_position = transform_point_to_local(position);
// Only hit test chrome (scrollbars, etc.) for visible elements.
if (is_visible && visible_for_hit_testing()) {
if (hit_test_chrome(local_position.value_or(position), callback) == TraversalDecision::Break)
return TraversalDecision::Break;
}
2024-06-03 18:29:32 -03:00
if (is_viewport_paintable()) {
auto& viewport_paintable = const_cast<ViewportPaintable&>(static_cast<ViewportPaintable const&>(*this));
viewport_paintable.build_stacking_context_tree_if_needed();
viewport_paintable.document().update_paint_and_hit_testing_properties_if_needed();
viewport_paintable.refresh_scroll_state();
return stacking_context()->hit_test(position, type, callback);
}
if (stacking_context())
return TraversalDecision::Continue;
if (hit_test_children(position, type, callback) == TraversalDecision::Break)
return TraversalDecision::Break;
// Hidden elements and elements with pointer-events: none shouldn't be hit.
if (!is_visible || !visible_for_hit_testing())
return TraversalDecision::Continue;
LibWeb: Integrate AccumulatedVisualContext with display list Integrate AccumulatedVisualContext with display list recording and playback. This is the main commit of the refactoring that delivers the architectural improvements enabled by AccumulatedVisualContext. Recording changes: Each display list command now stores a single RefPtr<AccumulatedVisualContext> instead of separate scroll_frame_id and ClipFrame. The recorder simply captures the current accumulated context when appending commands. The before_paint()/after_paint() hooks that pushed/popped scroll frame IDs are replaced by directly setting accumulated_visual_context on the recorder before painting each element. Playback changes: The display list player now uses LCA (Lowest Common Ancestor) based traversal to switch between visual contexts efficiently. When transitioning from context A to context B: 1. Find the LCA of A and B in the context tree 2. Pop (restore) states back to the LCA depth 3. Push (save + apply) states from LCA down to B This approach minimizes redundant save/restore operations. For example, when rendering siblings that share a common scroll container, the player keeps that scroll state applied and only switches the divergent parts of their context chains. Key deletions: - Remove translate_by() from all 45 display list commands - commands are now immutable - Remove transform/perspective fields from PushStackingContext - transforms are tracked via AccumulatedVisualContext - Remove push_scroll_frame_id()/pop_scroll_frame_id() from DisplayListRecorder - Remove before_paint()/after_paint() hooks from Paintable - Merge ApplyOpacity, ApplyCompositeAndBlendingOperator, ApplyFilter into single ApplyEffects command Stacking context painting changes: The StackingContext::paint() method is significantly simplified. Instead of building a PushStackingContextParams struct with transform matrices and pushing/popping stacking contexts, it now: 1. Sets the accumulated visual context (which already contains transforms) 2. Applies effects (opacity, blend mode, filters) if needed 3. Applies clip path if needed 4. Paints the content 5. Restores state The visual state management that was interleaved throughout the painting code is now handled uniformly by the context tree.
2026-01-13 16:44:09 -03:00
if (!local_position.has_value())
return TraversalDecision::Continue;
auto border_box_rect = absolute_border_box_rect();
if (!border_box_rect.contains(local_position.value()))
return TraversalDecision::Continue;
if (auto radii = border_radii_data(); radii.has_any_radius()) {
if (!radii.contains(local_position.value(), border_box_rect))
return TraversalDecision::Continue;
}
if (hit_test_continuation(callback) == TraversalDecision::Break)
return TraversalDecision::Break;
return callback(HitTestResult { .paintable = const_cast<PaintableBox&>(*this) });
}
TraversalDecision PaintableBox::hit_test_continuation(Function<TraversalDecision(HitTestResult)> const& callback) const
{
// If we're hit testing the "middle" part of a continuation chain, we are dealing with an anonymous box that is
// linked to a parent inline node. Since our block element children did not match the hit test, but we did, we
// should walk the continuation chain up to the inline parent and return a hit on that instead.
auto continuation_node = layout_node_with_style_and_box_metrics().continuation_of_node();
if (!continuation_node || !layout_node().is_anonymous())
return TraversalDecision::Continue;
while (continuation_node->continuation_of_node())
continuation_node = continuation_node->continuation_of_node();
auto& paintable = *continuation_node->first_paintable();
if (!paintable.visible_for_hit_testing())
return TraversalDecision::Continue;
return callback(HitTestResult { .paintable = paintable });
}
Optional<HitTestResult> PaintableBox::hit_test(CSSPixelPoint position, HitTestType type) const
{
Optional<HitTestResult> result;
(void)PaintableBox::hit_test(position, type, [&](HitTestResult candidate) {
if (!result.has_value()
|| candidate.vertical_distance.value_or(CSSPixels::max_integer_value) < result->vertical_distance.value_or(CSSPixels::max_integer_value)
|| candidate.horizontal_distance.value_or(CSSPixels::max_integer_value) < result->horizontal_distance.value_or(CSSPixels::max_integer_value)) {
result = move(candidate);
}
if (result.has_value() && (type == HitTestType::Exact || (result->vertical_distance == 0 && result->horizontal_distance == 0)))
return TraversalDecision::Break;
return TraversalDecision::Continue;
});
return result;
}
TraversalDecision PaintableBox::hit_test_children(CSSPixelPoint position, HitTestType type, Function<TraversalDecision(HitTestResult)> const& callback) const
{
for (auto const* child = last_child(); child; child = child->previous_sibling()) {
if (child->is_positioned() && child->computed_values().z_index().value_or(0) == 0)
continue;
if (child->has_stacking_context())
continue;
if (child->hit_test(position, type, callback) == TraversalDecision::Break)
return TraversalDecision::Break;
}
return TraversalDecision::Continue;
}
void PaintableBox::set_needs_repaint(InvalidateDisplayList should_invalidate_display_list)
{
if (should_invalidate_display_list == InvalidateDisplayList::Yes) {
invalidate_paint_cache();
// Recurse into anonymous child nodes so we properly invalidate nested contents of e.g. <button>s.
for_each_child_of_type<PaintableBox>([&](auto& child) {
if (child.layout_node().is_anonymous())
child.set_needs_repaint(should_invalidate_display_list);
return IterationDecision::Continue;
});
}
Paintable::set_needs_repaint(should_invalidate_display_list);
}
// https://www.w3.org/TR/css-transforms-1/#reference-box
CSSPixelRect PaintableBox::transform_reference_box() const
{
auto transform_box = computed_values().transform_box();
// For SVG elements without associated CSS layout box, the used value for content-box is fill-box and for
// border-box is stroke-box.
// FIXME: This currently detects any SVG element except the <svg> one. Is that correct?
// And is it correct to use `else` below?
if (is<Painting::SVGPaintable>(*this)) {
switch (transform_box) {
case CSS::TransformBox::ContentBox:
transform_box = CSS::TransformBox::FillBox;
break;
case CSS::TransformBox::BorderBox:
transform_box = CSS::TransformBox::StrokeBox;
break;
default:
break;
}
}
// For elements with associated CSS layout box, the used value for fill-box is content-box and for
// stroke-box and view-box is border-box.
else {
switch (transform_box) {
case CSS::TransformBox::FillBox:
transform_box = CSS::TransformBox::ContentBox;
break;
case CSS::TransformBox::StrokeBox:
case CSS::TransformBox::ViewBox:
transform_box = CSS::TransformBox::BorderBox;
break;
default:
break;
}
}
switch (transform_box) {
case CSS::TransformBox::ContentBox:
// Uses the content box as reference box.
// FIXME: The reference box of a table is the border box of its table wrapper box, not its table box.
return absolute_rect();
case CSS::TransformBox::BorderBox:
// Uses the border box as reference box.
// FIXME: The reference box of a table is the border box of its table wrapper box, not its table box.
return absolute_border_box_rect();
case CSS::TransformBox::FillBox:
// Uses the object bounding box as reference box.
// FIXME: For now we're using the content rect as an approximation.
return absolute_rect();
case CSS::TransformBox::StrokeBox:
// Uses the stroke bounding box as reference box.
// FIXME: For now we're using the border rect as an approximation.
return absolute_border_box_rect();
case CSS::TransformBox::ViewBox:
// Uses the nearest SVG viewport as reference box.
// FIXME: If a viewBox attribute is specified for the SVG viewport creating element:
// - The reference box is positioned at the origin of the coordinate system established by the viewBox attribute.
// - The dimension of the reference box is set to the width and height values of the viewBox attribute.
auto* svg_paintable = first_ancestor_of_type<Painting::SVGSVGPaintable>();
if (!svg_paintable)
return absolute_border_box_rect();
return svg_paintable->absolute_rect();
}
VERIFY_NOT_REACHED();
}
BorderRadiiData PaintableBox::border_radii_data() const
{
auto const& computed_values = this->computed_values();
if (!computed_values.has_noninitial_border_radii())
return {};
CSSPixelRect const border_rect { 0, 0, border_box_width(), border_box_height() };
return normalize_border_radii_data(layout_node(), border_rect, border_rect,
computed_values.border_top_left_radius(), computed_values.border_top_right_radius(),
computed_values.border_bottom_right_radius(), computed_values.border_bottom_left_radius());
}
Optional<BordersData> PaintableBox::outline_data() const
{
auto const& computed_values = this->computed_values();
return borders_data_for_outline(layout_node(), computed_values.outline_color(), computed_values.outline_style(), computed_values.outline_width());
}
CSSPixels PaintableBox::outline_offset() const
{
return computed_values().outline_offset().to_px(layout_node());
}
ScrollFrameIndex PaintableBox::nearest_scroll_frame_index() const
{
if (is_fixed_position())
return {};
auto const* paintable = this->containing_block();
while (paintable) {
if (paintable->own_scroll_frame_index().value())
return paintable->own_scroll_frame_index();
// Sticky elements need to find a scroll container even through fixed-position ancestors,
// because they must reference a scrollport for their sticky offset computation.
if (paintable->is_fixed_position() && !is_sticky_position())
return {};
paintable = paintable->containing_block();
}
return {};
}
PaintableBox const* PaintableBox::nearest_scrollable_ancestor() const
{
auto const* paintable = this->containing_block();
while (paintable) {
if (paintable->could_be_scrolled_by_wheel_event())
return paintable;
if (paintable->is_fixed_position())
return nullptr;
paintable = paintable->containing_block();
}
return nullptr;
}
PaintableBox::PhysicalResizeAxes PaintableBox::physical_resize_axes() const
{
auto const& computed = computed_values();
// https://drafts.csswg.org/css-ui/#resize
if (computed.resize() == CSS::Resize::None)
return {};
// 4.1. ... The resize property applies to elements that are scroll containers. UAs may also apply it,
// regardless of the value of the overflow property, to:
// - Replaced elements representing images or videos, such as img, video, picture, svg, object, or canvas.
// - The <iframe> element.
if (computed.display().is_inline_outside() && computed.display().is_flow_inside())
return {};
bool horizontal_writing_mode = computed.writing_mode() == CSS::WritingMode::HorizontalTb;
return {
.horizontal = computed.overflow_x() != CSS::Overflow::Visible
&& computed.overflow_x() != CSS::Overflow::Clip
&& (computed.resize() == CSS::Resize::Both
|| computed.resize() == CSS::Resize::Horizontal
|| (computed.resize() == CSS::Resize::Inline && horizontal_writing_mode)
|| (computed.resize() == CSS::Resize::Block && !horizontal_writing_mode)),
.vertical = computed.overflow_y() != CSS::Overflow::Visible
&& computed.overflow_y() != CSS::Overflow::Clip
&& (computed.resize() == CSS::Resize::Both
|| computed.resize() == CSS::Resize::Vertical
|| (computed.resize() == CSS::Resize::Inline && !horizontal_writing_mode)
|| (computed.resize() == CSS::Resize::Block && horizontal_writing_mode))
};
}
}