Files
ladybird/Libraries/LibWeb/Painting/PaintableBox.cpp
Timothy Flynn 1931636ac3 LibWeb: Support auto-scrolling contains when the middle mouse is pressed
When the middle mouse button is pressed, we can now scroll the pressed
container while moving the mouse around. We paint an indicator at the
pressed origin (as an overlay), as the scroll speed will depend upon the
distance between the moved mouse and that origin.
2026-04-13 13:01:45 -04:00

1336 lines
59 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* 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;
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)
{
auto& slot = direction == ScrollDirection::Horizontal ? m_horizontal_scrollbar : m_vertical_scrollbar;
if (!slot)
slot = Scrollbar::create(heap(), const_cast<PaintableBox&>(*this), direction);
return *slot;
}
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);
}
}
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 });
}
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,
};
});
auto border_rect = absolute_united_border_box_rect();
auto padding_rect = absolute_united_padding_box_rect();
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);
};
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());
builder.appendff(" {}x{} @ {},{}", border_rect.width(), border_rect.height(), border_rect.x(), border_rect.y());
auto size_text = builder.to_utf16_string();
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())
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
{
// 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 (resizer_contains(adjusted_position, metrics)) {
result.chrome_widget = const_cast<PaintableBox&>(*this).ensure_resize_handle();
return callback(result);
}
for (auto direction : { ScrollDirection::Horizontal, ScrollDirection::Vertical }) {
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;
}
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;
}
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;
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))
};
}
}