mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2025-10-19 07:33:20 +00:00

...instead of reaching into DOM tree during hit-testing in order to figure out if an element is inert. This is a part of the effert to make possible running hit-testing solely based on data contained by the paintable tree.
308 lines
11 KiB
C++
308 lines
11 KiB
C++
/*
|
|
* Copyright (c) 2022-2023, Andreas Kling <andreas@ladybird.org>
|
|
* Copyright (c) 2025, Sam Atkins <sam@ladybird.org>
|
|
*
|
|
* SPDX-License-Identifier: BSD-2-Clause
|
|
*/
|
|
|
|
#include <LibWeb/DOM/Document.h>
|
|
#include <LibWeb/Painting/DisplayListRecorder.h>
|
|
#include <LibWeb/Painting/DisplayListRecordingContext.h>
|
|
#include <LibWeb/Painting/Paintable.h>
|
|
#include <LibWeb/Painting/PaintableBox.h>
|
|
#include <LibWeb/Painting/StackingContext.h>
|
|
|
|
namespace Web::Painting {
|
|
|
|
Paintable::Paintable(Layout::Node const& layout_node)
|
|
: m_layout_node(layout_node)
|
|
{
|
|
auto& computed_values = layout_node.computed_values();
|
|
if (layout_node.is_grid_item() && computed_values.z_index().has_value()) {
|
|
// https://www.w3.org/TR/css-grid-2/#z-order
|
|
// grid items with z_index should behave as if position were "relative"
|
|
m_positioned = true;
|
|
} else {
|
|
m_positioned = computed_values.position() != CSS::Positioning::Static;
|
|
}
|
|
|
|
m_fixed_position = computed_values.position() == CSS::Positioning::Fixed;
|
|
m_sticky_position = computed_values.position() == CSS::Positioning::Sticky;
|
|
m_absolutely_positioned = computed_values.position() == CSS::Positioning::Absolute;
|
|
m_floating = layout_node.is_floating();
|
|
m_inline = layout_node.is_inline();
|
|
}
|
|
|
|
Paintable::~Paintable()
|
|
{
|
|
}
|
|
|
|
void Paintable::visit_edges(Cell::Visitor& visitor)
|
|
{
|
|
Base::visit_edges(visitor);
|
|
TreeNode::visit_edges(visitor);
|
|
visitor.visit(m_dom_node);
|
|
visitor.visit(m_layout_node);
|
|
if (m_containing_block.has_value())
|
|
visitor.visit(m_containing_block.value());
|
|
}
|
|
|
|
String Paintable::debug_description() const
|
|
{
|
|
return MUST(String::formatted("{}({})", class_name(), layout_node().debug_description()));
|
|
}
|
|
|
|
void Paintable::resolve_paint_properties()
|
|
{
|
|
m_visible_for_hit_testing = true;
|
|
if (auto dom_node = this->dom_node(); dom_node && dom_node->is_inert()) {
|
|
// https://html.spec.whatwg.org/multipage/interaction.html#inert-subtrees
|
|
// When a node is inert:
|
|
// - Hit-testing must act as if the 'pointer-events' CSS property were set to 'none'.
|
|
m_visible_for_hit_testing = false;
|
|
}
|
|
}
|
|
|
|
bool Paintable::is_visible() const
|
|
{
|
|
auto const& computed_values = this->computed_values();
|
|
return computed_values.visibility() == CSS::Visibility::Visible && computed_values.opacity() != 0;
|
|
}
|
|
|
|
DOM::Document const& Paintable::document() const
|
|
{
|
|
return layout_node().document();
|
|
}
|
|
|
|
DOM::Document& Paintable::document()
|
|
{
|
|
return layout_node().document();
|
|
}
|
|
|
|
CSS::Display Paintable::display() const
|
|
{
|
|
return layout_node().display();
|
|
}
|
|
|
|
PaintableBox* Paintable::containing_block() const
|
|
{
|
|
return m_containing_block.ensure([&] -> GC::Ptr<PaintableBox> {
|
|
auto containing_layout_box = m_layout_node->containing_block();
|
|
if (!containing_layout_box)
|
|
return nullptr;
|
|
return const_cast<PaintableBox*>(containing_layout_box->paintable_box());
|
|
});
|
|
}
|
|
|
|
CSS::ImmutableComputedValues const& Paintable::computed_values() const
|
|
{
|
|
return m_layout_node->computed_values();
|
|
}
|
|
|
|
bool Paintable::visible_for_hit_testing() const
|
|
{
|
|
return m_visible_for_hit_testing && computed_values().pointer_events() != CSS::PointerEvents::None;
|
|
}
|
|
|
|
void Paintable::set_dom_node(GC::Ptr<DOM::Node> dom_node)
|
|
{
|
|
m_dom_node = dom_node;
|
|
}
|
|
|
|
GC::Ptr<DOM::Node> Paintable::dom_node()
|
|
{
|
|
return m_dom_node;
|
|
}
|
|
|
|
GC::Ptr<DOM::Node const> Paintable::dom_node() const
|
|
{
|
|
return m_dom_node;
|
|
}
|
|
|
|
GC::Ptr<HTML::Navigable> Paintable::navigable() const
|
|
{
|
|
return document().navigable();
|
|
}
|
|
|
|
Paintable::DispatchEventOfSameName Paintable::handle_mousedown(Badge<EventHandler>, CSSPixelPoint, unsigned, unsigned)
|
|
{
|
|
return DispatchEventOfSameName::Yes;
|
|
}
|
|
|
|
Paintable::DispatchEventOfSameName Paintable::handle_mouseup(Badge<EventHandler>, CSSPixelPoint, unsigned, unsigned)
|
|
{
|
|
return DispatchEventOfSameName::Yes;
|
|
}
|
|
|
|
Paintable::DispatchEventOfSameName Paintable::handle_mousemove(Badge<EventHandler>, CSSPixelPoint, unsigned, unsigned)
|
|
{
|
|
return DispatchEventOfSameName::Yes;
|
|
}
|
|
|
|
bool Paintable::handle_mousewheel(Badge<EventHandler>, CSSPixelPoint, unsigned, unsigned, int, int)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
TraversalDecision Paintable::hit_test(CSSPixelPoint, HitTestType, Function<TraversalDecision(HitTestResult)> const&) const
|
|
{
|
|
return TraversalDecision::Continue;
|
|
}
|
|
|
|
bool Paintable::has_stacking_context() const
|
|
{
|
|
if (is_paintable_box())
|
|
return static_cast<PaintableBox const&>(*this).stacking_context();
|
|
return false;
|
|
}
|
|
|
|
StackingContext* Paintable::enclosing_stacking_context()
|
|
{
|
|
for (auto* ancestor = parent(); ancestor; ancestor = ancestor->parent()) {
|
|
if (!ancestor->is_paintable_box())
|
|
continue;
|
|
if (auto* stacking_context = static_cast<PaintableBox&>(*ancestor).stacking_context())
|
|
return const_cast<StackingContext*>(stacking_context);
|
|
}
|
|
// We should always reach the viewport's stacking context.
|
|
VERIFY_NOT_REACHED();
|
|
}
|
|
|
|
void Paintable::paint_inspector_overlay(DisplayListRecordingContext& context) const
|
|
{
|
|
Vector<Painting::Paintable const*> self_and_ancestors {};
|
|
for (Paintable const* paintable = this; paintable; paintable = paintable->parent()) {
|
|
self_and_ancestors.append(paintable);
|
|
}
|
|
|
|
for (auto const* paintable : self_and_ancestors.in_reverse()) {
|
|
if (auto const* box = as_if<PaintableBox>(paintable)) {
|
|
box->apply_scroll_offset(context);
|
|
if (box->stacking_context()) {
|
|
auto to_device_pixels_scale = float(context.device_pixels_per_css_pixel());
|
|
auto transform_matrix = box->transform();
|
|
auto transform_origin = box->transform_origin().to_type<float>();
|
|
// We only want the transform here, everything else undesirable for the inspector overlay
|
|
DisplayListRecorder::PushStackingContextParams push_stacking_context_params {
|
|
.opacity = 1.0,
|
|
.compositing_and_blending_operator = Gfx::CompositingAndBlendingOperator::Normal,
|
|
.isolate = false,
|
|
.transform = StackingContextTransform(transform_origin, transform_matrix, to_device_pixels_scale),
|
|
};
|
|
context.display_list_recorder().push_stacking_context(push_stacking_context_params);
|
|
}
|
|
}
|
|
}
|
|
|
|
paint_inspector_overlay_internal(context);
|
|
|
|
for (auto const* paintable : self_and_ancestors) {
|
|
if (auto const* box = as_if<PaintableBox>(paintable)) {
|
|
if (box->stacking_context()) {
|
|
context.display_list_recorder().pop_stacking_context();
|
|
}
|
|
box->reset_scroll_offset(context);
|
|
}
|
|
}
|
|
}
|
|
|
|
void Paintable::set_needs_display(InvalidateDisplayList should_invalidate_display_list)
|
|
{
|
|
auto& document = const_cast<DOM::Document&>(this->document());
|
|
if (should_invalidate_display_list == InvalidateDisplayList::Yes)
|
|
document.invalidate_display_list();
|
|
|
|
auto* containing_block = this->containing_block();
|
|
if (!containing_block)
|
|
return;
|
|
|
|
if (!is<Painting::PaintableWithLines>(*containing_block))
|
|
return;
|
|
for (auto const& fragment : static_cast<Painting::PaintableWithLines const&>(*containing_block).fragments()) {
|
|
document.set_needs_display(fragment.absolute_rect(), InvalidateDisplayList::No);
|
|
};
|
|
}
|
|
|
|
CSSPixelPoint Paintable::box_type_agnostic_position() const
|
|
{
|
|
if (is_paintable_box())
|
|
return static_cast<PaintableBox const*>(this)->absolute_position();
|
|
|
|
VERIFY(is_inline());
|
|
|
|
CSSPixelPoint position;
|
|
if (auto const* block = containing_block(); block && is<Painting::PaintableWithLines>(*block)) {
|
|
auto const& fragments = static_cast<Painting::PaintableWithLines const&>(*block).fragments();
|
|
if (!fragments.is_empty()) {
|
|
position = fragments[0].absolute_rect().location();
|
|
}
|
|
}
|
|
|
|
return position;
|
|
}
|
|
|
|
Painting::BorderRadiiData normalize_border_radii_data(Layout::Node const& node, CSSPixelRect const& rect, CSS::BorderRadiusData const& top_left_radius, CSS::BorderRadiusData const& top_right_radius, CSS::BorderRadiusData const& bottom_right_radius, CSS::BorderRadiusData const& bottom_left_radius)
|
|
{
|
|
Painting::BorderRadiiData radii_px {
|
|
.top_left = {
|
|
top_left_radius.horizontal_radius.to_px(node, rect.width()),
|
|
top_left_radius.vertical_radius.to_px(node, rect.height()) },
|
|
.top_right = { top_right_radius.horizontal_radius.to_px(node, rect.width()), top_right_radius.vertical_radius.to_px(node, rect.height()) },
|
|
.bottom_right = { bottom_right_radius.horizontal_radius.to_px(node, rect.width()), bottom_right_radius.vertical_radius.to_px(node, rect.height()) },
|
|
.bottom_left = { bottom_left_radius.horizontal_radius.to_px(node, rect.width()), bottom_left_radius.vertical_radius.to_px(node, rect.height()) }
|
|
};
|
|
|
|
// Scale overlapping curves according to https://www.w3.org/TR/css-backgrounds-3/#corner-overlap
|
|
// Let f = min(Li/Si), where i ∈ {top, right, bottom, left},
|
|
// Si is the sum of the two corresponding radii of the corners on side i,
|
|
// and Ltop = Lbottom = the width of the box, and Lleft = Lright = the height of the box.
|
|
//
|
|
// NOTE: We iterate twice as a form of iterative refinement. A single scaling pass using
|
|
// fixed-point arithmetic can result in small rounding errors, causing the scaled radii to
|
|
// still slightly overflow the box dimensions. A second pass corrects this remaining error.
|
|
for (int iteration = 0; iteration < 2; ++iteration) {
|
|
auto s_top = radii_px.top_left.horizontal_radius + radii_px.top_right.horizontal_radius;
|
|
auto s_right = radii_px.top_right.vertical_radius + radii_px.bottom_right.vertical_radius;
|
|
auto s_bottom = radii_px.bottom_right.horizontal_radius + radii_px.bottom_left.horizontal_radius;
|
|
auto s_left = radii_px.bottom_left.vertical_radius + radii_px.top_left.vertical_radius;
|
|
|
|
CSSPixelFraction f = 1;
|
|
if (s_top > rect.width())
|
|
f = min(f, rect.width() / s_top);
|
|
if (s_right > rect.height())
|
|
f = min(f, rect.height() / s_right);
|
|
if (s_bottom > rect.width())
|
|
f = min(f, rect.width() / s_bottom);
|
|
if (s_left > rect.height())
|
|
f = min(f, rect.height() / s_left);
|
|
|
|
// If f is 1 or more, the radii fit perfectly and no more scaling is needed
|
|
if (f >= 1)
|
|
break;
|
|
|
|
Painting::BorderRadiusData* corners[] = {
|
|
&radii_px.top_left, &radii_px.top_right, &radii_px.bottom_right, &radii_px.bottom_left
|
|
};
|
|
|
|
for (auto* corner : corners) {
|
|
corner->horizontal_radius *= f;
|
|
corner->vertical_radius *= f;
|
|
}
|
|
}
|
|
|
|
return radii_px;
|
|
}
|
|
|
|
void Paintable::set_needs_paint_only_properties_update(bool needs_update)
|
|
{
|
|
if (needs_update == m_needs_paint_only_properties_update)
|
|
return;
|
|
|
|
m_needs_paint_only_properties_update = needs_update;
|
|
|
|
if (needs_update) {
|
|
document().set_needs_to_resolve_paint_only_properties();
|
|
}
|
|
}
|
|
|
|
}
|