Display lists owned the accumulated visual context tree through a ref-counted pointer. That tied visual-context state to display-list lifetime and made compositor updates treat the two as one unit, even though AVC trees need to become independently updateable compositor state. Make accumulated visual context trees plain versioned values, have each display list store the compatible tree version, and pass the matching tree alongside display-list updates and replay calls. Replay verifies that the provided tree matches the display list before executing it. This prepares the compositor for receiving AVC tree updates separately from display-list updates: it now accepts the tree as a separate update parameter, stores it next to the display list, and uses that stored tree for replay and async-scroll hit testing. Nested display-list resources carry their own tree snapshots for the same version check.
530 lines
26 KiB
C++
530 lines
26 KiB
C++
/*
|
|
* Copyright (c) 2020-2022, Andreas Kling <andreas@ladybird.org>
|
|
* Copyright (c) 2022, Sam Atkins <atkinssj@serenityos.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/QuickSort.h>
|
|
#include <AK/TemporaryChange.h>
|
|
#include <LibGfx/Rect.h>
|
|
#include <LibWeb/DOM/Document.h>
|
|
#include <LibWeb/Layout/ReplacedBox.h>
|
|
#include <LibWeb/Layout/Viewport.h>
|
|
#include <LibWeb/Page/Page.h>
|
|
#include <LibWeb/Painting/DisplayList.h>
|
|
#include <LibWeb/Painting/DisplayListRecorder.h>
|
|
#include <LibWeb/Painting/PaintableBox.h>
|
|
#include <LibWeb/Painting/PaintableWithLines.h>
|
|
#include <LibWeb/Painting/SVGSVGPaintable.h>
|
|
#include <LibWeb/Painting/StackingContext.h>
|
|
#include <LibWeb/Painting/ViewportPaintable.h>
|
|
#include <LibWeb/SVG/SVGMaskElement.h>
|
|
|
|
namespace Web::Painting {
|
|
|
|
static void paint_node(Paintable const& paintable, DisplayListRecordingContext& context, PaintPhase phase)
|
|
{
|
|
TemporaryChange save_nesting_level(context.display_list_recorder().m_save_nesting_level, 0);
|
|
|
|
RefPtr<PaintableBox const> paintable_box = as_if<PaintableBox>(paintable);
|
|
|
|
if (paintable_box) {
|
|
// Text fragments in a PaintableWithLines are content of the block container.
|
|
// They need the descendants' visual context, not the element's own visual context.
|
|
if (is<PaintableWithLines>(paintable) && phase == PaintPhase::Foreground)
|
|
context.display_list_recorder().set_accumulated_visual_context(paintable_box->accumulated_visual_context_for_descendants_index());
|
|
else
|
|
context.display_list_recorder().set_accumulated_visual_context(paintable_box->accumulated_visual_context_index());
|
|
}
|
|
|
|
if (paintable_box && phase == PaintPhase::Background)
|
|
paintable_box->record_async_scrolling_metadata(context);
|
|
|
|
bool const skip_cache = !paintable_box || context.should_show_line_box_borders() || paintable_box->fixed_background_visual_context().has_value();
|
|
if (!skip_cache && paintable_box->has_cached_commands(phase)) {
|
|
context.display_list_recorder().replay_cached_commands(paintable_box->cached_commands(phase));
|
|
} else if (!skip_cache) {
|
|
auto capture = context.display_list_recorder().begin_command_capture();
|
|
paintable.paint(context, phase);
|
|
paintable_box->set_cached_commands(phase, capture.take());
|
|
} else {
|
|
paintable.paint(context, phase);
|
|
}
|
|
|
|
context.display_list_recorder().set_accumulated_visual_context({});
|
|
|
|
VERIFY(context.display_list_recorder().m_save_nesting_level == 0);
|
|
}
|
|
|
|
NonnullRefPtr<StackingContext> StackingContext::create(PaintableBox& paintable, RefPtr<StackingContext> parent, size_t index_in_tree_order)
|
|
{
|
|
auto stacking_context = adopt_ref(*new StackingContext(paintable, parent, index_in_tree_order));
|
|
if (parent)
|
|
parent->m_children.append(stacking_context);
|
|
return stacking_context;
|
|
}
|
|
|
|
StackingContext::StackingContext(PaintableBox& paintable, RefPtr<StackingContext> parent, size_t index_in_tree_order)
|
|
: m_paintable(paintable)
|
|
, m_parent(parent)
|
|
, m_index_in_tree_order(index_in_tree_order)
|
|
{
|
|
VERIFY(!parent || parent.ptr() != this);
|
|
}
|
|
|
|
void StackingContext::sort()
|
|
{
|
|
quick_sort(m_children, [](auto& a, auto& b) {
|
|
auto a_z_index = a->paintable_box().effective_z_index().value_or(0);
|
|
auto b_z_index = b->paintable_box().effective_z_index().value_or(0);
|
|
if (a_z_index == b_z_index)
|
|
return a->m_index_in_tree_order < b->m_index_in_tree_order;
|
|
return a_z_index < b_z_index;
|
|
});
|
|
|
|
for (auto& child : m_children)
|
|
child->sort();
|
|
}
|
|
|
|
void StackingContext::set_last_paint_generation_id(u64 generation_id)
|
|
{
|
|
if (m_last_paint_generation_id.has_value() && m_last_paint_generation_id.value() >= generation_id) {
|
|
dbgln("FIXME: Painting commands are recorded twice for stacking context: {}", paintable_box().layout_node().debug_description());
|
|
}
|
|
m_last_paint_generation_id = generation_id;
|
|
}
|
|
|
|
static PaintPhase to_paint_phase(StackingContext::StackingContextPaintPhase phase)
|
|
{
|
|
// There are not a fully correct mapping since some stacking context phases are combined.
|
|
switch (phase) {
|
|
case StackingContext::StackingContextPaintPhase::Floats:
|
|
case StackingContext::StackingContextPaintPhase::BackgroundAndBordersForInlineLevelAndReplaced:
|
|
case StackingContext::StackingContextPaintPhase::BackgroundAndBorders:
|
|
return PaintPhase::Background;
|
|
case StackingContext::StackingContextPaintPhase::Foreground:
|
|
return PaintPhase::Foreground;
|
|
default:
|
|
VERIFY_NOT_REACHED();
|
|
}
|
|
}
|
|
|
|
static bool establishes_inline_level_painting_context(Paintable const& paintable)
|
|
{
|
|
// CSS 2.2 painting order puts inline-block and inline-table boxes in the inline-level painting step and
|
|
// says to paint each "as if it created a new stacking context", while keeping positioned descendants and
|
|
// actual child stacking contexts in the parent stacking context:
|
|
// https://drafts.csswg.org/css2/#painting-order
|
|
// https://drafts.csswg.org/css2/#elaborate-stacking-contexts
|
|
auto const& layout_node = paintable.layout_node();
|
|
return layout_node.is_inline_block() || layout_node.is_inline_table();
|
|
}
|
|
|
|
static void paint_inline_level_non_positioned_descendant(DisplayListRecordingContext& context, Paintable const& paintable)
|
|
{
|
|
paint_node(paintable, context, PaintPhase::Background);
|
|
paint_node(paintable, context, PaintPhase::Border);
|
|
paint_node(paintable, context, PaintPhase::TableCollapsedBorder);
|
|
StackingContext::paint_descendants(context, paintable, StackingContext::StackingContextPaintPhase::BackgroundAndBorders);
|
|
|
|
// https://drafts.csswg.org/css2/#elaborate-stacking-contexts
|
|
// "For inline-block and inline-table elements: [...] treat the element as if it created a new stacking context,
|
|
// but any positioned descendants and descendants which actually create a new stacking context should be
|
|
// considered part of the parent stacking context, not this new one."
|
|
if (establishes_inline_level_painting_context(paintable))
|
|
StackingContext::paint_descendants(context, paintable, StackingContext::StackingContextPaintPhase::Floats);
|
|
}
|
|
|
|
void StackingContext::paint_node_as_stacking_context(Paintable const& paintable, DisplayListRecordingContext& context)
|
|
{
|
|
if (paintable.is_svg_svg_paintable()) {
|
|
paint_svg(context, static_cast<PaintableBox const&>(paintable), PaintPhase::Foreground);
|
|
return;
|
|
}
|
|
|
|
paint_node(paintable, context, PaintPhase::Background);
|
|
paint_node(paintable, context, PaintPhase::Border);
|
|
paint_descendants(context, paintable, StackingContextPaintPhase::BackgroundAndBorders);
|
|
paint_descendants(context, paintable, StackingContextPaintPhase::Floats);
|
|
paint_descendants(context, paintable, StackingContextPaintPhase::BackgroundAndBordersForInlineLevelAndReplaced);
|
|
paint_node(paintable, context, PaintPhase::Foreground);
|
|
paint_descendants(context, paintable, StackingContextPaintPhase::Foreground);
|
|
paint_node(paintable, context, PaintPhase::Outline);
|
|
paint_node(paintable, context, PaintPhase::Overlay);
|
|
}
|
|
|
|
void StackingContext::paint_svg(DisplayListRecordingContext& context, PaintableBox const& paintable, PaintPhase phase)
|
|
{
|
|
if (phase != PaintPhase::Foreground)
|
|
return;
|
|
|
|
paint_node(paintable, context, PaintPhase::Background);
|
|
paint_node(paintable, context, PaintPhase::Border);
|
|
SVGSVGPaintable::paint_svg_box(context, paintable, phase);
|
|
}
|
|
|
|
void StackingContext::paint_descendants(DisplayListRecordingContext& context, Paintable const& paintable, StackingContextPaintPhase phase)
|
|
{
|
|
paintable.for_each_child([&context, phase](auto& child) {
|
|
if (child.has_stacking_context())
|
|
return IterationDecision::Continue;
|
|
|
|
auto const& z_index = [&] { return child.computed_values().z_index(); };
|
|
|
|
// Positioned descendants at stack level 0 are painted in a separate pass.
|
|
// See `m_positioned_descendants_and_stacking_contexts_with_stack_level_0`.
|
|
if (child.is_positioned() && z_index().value_or(0) == 0)
|
|
return IterationDecision::Continue;
|
|
|
|
if (child.is_svg_svg_paintable()) {
|
|
paint_svg(context, static_cast<PaintableBox const&>(child), to_paint_phase(phase));
|
|
return IterationDecision::Continue;
|
|
}
|
|
|
|
// NOTE: Flex and grid items should be treated the same way as CSS2 defines for inline-blocks:
|
|
// - https://drafts.csswg.org/css-flexbox-1/#painting
|
|
// - https://www.w3.org/TR/css-grid-2/#z-order
|
|
// "For each one of these, treat the element as if it created a new stacking context, but any positioned
|
|
// descendants and descendants which actually create a new stacking context should be considered part of
|
|
// the parent stacking context, not this new one."
|
|
if ((child.layout_node().is_flex_item() || child.layout_node().is_grid_item()) && !z_index().has_value()) {
|
|
// FIXME: This may not be fully correct with respect to the paint phases.
|
|
if (phase == StackingContextPaintPhase::Foreground)
|
|
paint_node_as_stacking_context(child, context);
|
|
return IterationDecision::Continue;
|
|
}
|
|
|
|
// https://drafts.csswg.org/css2/#painting-order
|
|
// All non-positioned floating descendants, in tree order. For each one of these, treat the
|
|
// element as if it created a new stacking context, but any positioned descendants and
|
|
// descendants which actually create a new stacking context should be considered part of the
|
|
// parent stacking context, not this new one.
|
|
if (child.is_floating() && !child.is_positioned() && !z_index().has_value()) {
|
|
if (phase == StackingContextPaintPhase::Floats) {
|
|
paint_node_as_stacking_context(child, context);
|
|
}
|
|
return IterationDecision::Continue;
|
|
}
|
|
|
|
bool child_is_inline_or_replaced = child.is_inline() || is<Layout::ReplacedBox>(child.layout_node());
|
|
bool child_has_inline_level_painting_context = establishes_inline_level_painting_context(child);
|
|
switch (phase) {
|
|
case StackingContextPaintPhase::BackgroundAndBorders:
|
|
if (!child_is_inline_or_replaced && !child.is_floating()) {
|
|
paint_node(child, context, PaintPhase::Background);
|
|
paint_node(child, context, PaintPhase::Border);
|
|
paint_descendants(context, child, phase);
|
|
paint_node(child, context, PaintPhase::TableCollapsedBorder);
|
|
}
|
|
break;
|
|
case StackingContextPaintPhase::Floats:
|
|
if (child.is_floating()) {
|
|
paint_node(child, context, PaintPhase::Background);
|
|
paint_node(child, context, PaintPhase::Border);
|
|
paint_descendants(context, child, StackingContextPaintPhase::BackgroundAndBorders);
|
|
}
|
|
// Atomic inline-level descendants such as inline-blocks and inline tables participate in the parent's
|
|
// inline-level painting step, so their internal floats must not be painted early during the ancestor's
|
|
// float sweep.
|
|
if (!child_has_inline_level_painting_context)
|
|
paint_descendants(context, child, phase);
|
|
break;
|
|
case StackingContextPaintPhase::BackgroundAndBordersForInlineLevelAndReplaced:
|
|
if (child_is_inline_or_replaced) {
|
|
paint_inline_level_non_positioned_descendant(context, child);
|
|
}
|
|
paint_descendants(context, child, phase);
|
|
break;
|
|
case StackingContextPaintPhase::Foreground:
|
|
paint_node(child, context, PaintPhase::Foreground);
|
|
paint_descendants(context, child, phase);
|
|
paint_node(child, context, PaintPhase::Outline);
|
|
paint_node(child, context, PaintPhase::Overlay);
|
|
break;
|
|
}
|
|
|
|
return IterationDecision::Continue;
|
|
});
|
|
}
|
|
|
|
void StackingContext::paint_child(DisplayListRecordingContext& context, StackingContext const& child)
|
|
{
|
|
VERIFY(!child.paintable_box().is_svg_paintable());
|
|
const_cast<StackingContext&>(child).set_last_paint_generation_id(context.paint_generation_id());
|
|
child.paint(context);
|
|
}
|
|
|
|
void StackingContext::paint_internal(DisplayListRecordingContext& context) const
|
|
{
|
|
VERIFY(!paintable_box().is_svg_paintable());
|
|
if (paintable_box().is_svg_svg_paintable()) {
|
|
auto const& svg_svg_paintable = static_cast<SVGSVGPaintable const&>(paintable_box());
|
|
paint_node(svg_svg_paintable, context, PaintPhase::Background);
|
|
paint_node(svg_svg_paintable, context, PaintPhase::Border);
|
|
|
|
SVGSVGPaintable::paint_descendants(context, svg_svg_paintable, PaintPhase::Foreground);
|
|
|
|
paint_node(svg_svg_paintable, context, PaintPhase::Outline);
|
|
if (context.should_paint_overlay()) {
|
|
paint_node(svg_svg_paintable, context, PaintPhase::Overlay);
|
|
}
|
|
return;
|
|
}
|
|
|
|
// For a more elaborate description of the algorithm, see CSS 2.1 Appendix E
|
|
// Draw the background and borders for the context root (steps 1, 2)
|
|
paint_node(paintable_box(), context, PaintPhase::Background);
|
|
paint_node(paintable_box(), context, PaintPhase::Border);
|
|
|
|
// Stacking contexts formed by positioned descendants with negative z-indices (excluding 0) in z-index order
|
|
// (most negative first) then tree order. (step 3)
|
|
// Here, we treat non-positioned stacking contexts as if they were positioned, because CSS 2.0 spec does not
|
|
// account for new properties like `transform` and `opacity` that can create stacking contexts.
|
|
// https://github.com/w3c/csswg-drafts/issues/2717
|
|
for (auto& child : m_children) {
|
|
if (child->paintable_box().effective_z_index().has_value() && child->paintable_box().effective_z_index().value() < 0)
|
|
paint_child(context, *child);
|
|
}
|
|
|
|
// Draw the background and borders for block-level children (step 4)
|
|
paint_descendants(context, paintable_box(), StackingContextPaintPhase::BackgroundAndBorders);
|
|
// Draw the non-positioned floats (step 5)
|
|
paint_descendants(context, paintable_box(), StackingContextPaintPhase::Floats);
|
|
// Draw inline content, replaced content, etc. (steps 6, 7)
|
|
paint_descendants(context, paintable_box(), StackingContextPaintPhase::BackgroundAndBordersForInlineLevelAndReplaced);
|
|
paint_node(paintable_box(), context, PaintPhase::Foreground);
|
|
paint_descendants(context, paintable_box(), StackingContextPaintPhase::Foreground);
|
|
|
|
// Draw positioned descendants with z-index `0` or `auto` in tree order. (step 8)
|
|
// Here, we treat non-positioned stacking contexts as if they were positioned, because CSS 2.0 spec does not
|
|
// account for new properties like `transform` and `opacity` that can create stacking contexts.
|
|
// https://github.com/w3c/csswg-drafts/issues/2717
|
|
for (auto const& weak_paintable : m_positioned_descendants_and_stacking_contexts_with_stack_level_0) {
|
|
auto paintable = weak_paintable.strong_ref();
|
|
if (!paintable)
|
|
continue;
|
|
// At this point, `paintable_box` is a positioned descendant with z-index: auto.
|
|
// FIXME: This is basically duplicating logic found elsewhere in this same function. Find a way to make this more elegant.
|
|
if (auto child = paintable->stacking_context()) {
|
|
paint_child(context, *child);
|
|
} else {
|
|
paint_node_as_stacking_context(*paintable, context);
|
|
}
|
|
};
|
|
|
|
// Stacking contexts formed by positioned descendants with z-indices greater than or equal to 1 in z-index order
|
|
// (smallest first) then tree order. (Step 9)
|
|
// Here, we treat non-positioned stacking contexts as if they were positioned, because CSS 2.0 spec does not
|
|
// account for new properties like `transform` and `opacity` that can create stacking contexts.
|
|
// https://github.com/w3c/csswg-drafts/issues/2717
|
|
for (auto& child : m_children) {
|
|
if (child->paintable_box().effective_z_index().has_value() && child->paintable_box().effective_z_index().value() >= 1)
|
|
paint_child(context, *child);
|
|
}
|
|
|
|
paint_node(paintable_box(), context, PaintPhase::Outline);
|
|
|
|
if (context.should_paint_overlay()) {
|
|
paint_node(paintable_box(), context, PaintPhase::Overlay);
|
|
}
|
|
}
|
|
|
|
void StackingContext::paint(DisplayListRecordingContext& context) const
|
|
{
|
|
// https://drafts.csswg.org/css-transforms-1/#transform-function-lists
|
|
// If a transform function causes the current transformation matrix of an object to be non-invertible, the object
|
|
// and its content do not get displayed.
|
|
if (paintable_box().has_non_invertible_css_transform())
|
|
return;
|
|
|
|
if (paintable_box().computed_values().opacity() == 0.0f)
|
|
return;
|
|
|
|
TemporaryChange save_nesting_level(context.display_list_recorder().m_save_nesting_level, 0);
|
|
ScopeGuard verify_save_and_restore_are_balanced([&] {
|
|
VERIFY(context.display_list_recorder().m_save_nesting_level == 0);
|
|
});
|
|
|
|
auto const& computed_values = paintable_box().computed_values();
|
|
auto mask_image = computed_values.mask_image();
|
|
|
|
if (mask_image) {
|
|
mask_image->resolve_for_size(paintable_box().layout_node_with_style_and_box_metrics(), paintable_box().absolute_padding_box_rect().size());
|
|
}
|
|
|
|
auto effective_context_index = paintable_box().accumulated_visual_context_index();
|
|
context.display_list_recorder().set_accumulated_visual_context(effective_context_index);
|
|
|
|
// For elements with SVG filters, emit a transparent FillRect to trigger filter application.
|
|
// This ensures content-generating filters (feFlood, feImage) work even with empty source.
|
|
if (auto const& bounds = paintable_box().filter().svg_filter_bounds; bounds.has_value()) {
|
|
auto device_rect = context.enclosing_device_rect(*bounds).to_type<int>();
|
|
context.display_list_recorder().fill_rect_transparent(device_rect);
|
|
}
|
|
|
|
// Collect all masks (CSS mask-image, SVG <mask>, SVG <clipPath>).
|
|
Vector<DisplayListRecorder::MaskInfo> masks;
|
|
|
|
if (mask_image) {
|
|
auto visual_context_tree = AccumulatedVisualContextTree::create();
|
|
auto mask_display_list = DisplayList::create(visual_context_tree);
|
|
DisplayListRecorder display_list_recorder(*mask_display_list, visual_context_tree, context.display_list_recorder().resource_storage());
|
|
auto mask_painting_context = context.clone(display_list_recorder);
|
|
auto mask_rect_in_device_pixels = context.enclosing_device_rect(paintable_box().absolute_padding_box_rect());
|
|
mask_image->paint(mask_painting_context, { {}, mask_rect_in_device_pixels.size() }, CSS::ImageRendering::Auto);
|
|
masks.append({ { *mask_display_list, move(visual_context_tree) }, mask_rect_in_device_pixels.to_type<int>(), Gfx::MaskKind::Alpha });
|
|
}
|
|
|
|
if (auto mask_area = paintable_box().get_mask_area(); mask_area.has_value()) {
|
|
if (auto mask_display_list = paintable_box().calculate_mask(context, *mask_area); mask_display_list.has_value()) {
|
|
auto rect = context.enclosing_device_rect(*mask_area).to_type<int>();
|
|
auto kind = paintable_box().get_mask_type().value_or(Gfx::MaskKind::Alpha);
|
|
masks.append({ mask_display_list.release_value(), rect, kind });
|
|
}
|
|
}
|
|
|
|
if (auto clip_area = paintable_box().get_clip_area(); clip_area.has_value()) {
|
|
if (auto clip_display_list = paintable_box().calculate_clip(context, *clip_area); clip_display_list.has_value()) {
|
|
auto rect = context.enclosing_device_rect(*clip_area).to_type<int>();
|
|
masks.append({ clip_display_list.release_value(), rect, Gfx::MaskKind::Alpha });
|
|
}
|
|
}
|
|
|
|
context.display_list_recorder().begin_masks(masks);
|
|
|
|
auto context_before_children = context.display_list_recorder().accumulated_visual_context();
|
|
|
|
paint_internal(context);
|
|
|
|
context.display_list_recorder().set_accumulated_visual_context(context_before_children);
|
|
|
|
context.display_list_recorder().end_masks(masks);
|
|
}
|
|
|
|
TraversalDecision StackingContext::hit_test(CSSPixelPoint position, HitTestType type, Function<TraversalDecision(HitTestResult)> const& callback) const
|
|
{
|
|
// AD-HOC: Elements with non-invertible transforms are not displayed per the spec,
|
|
// so they should not be hit-testable either.
|
|
if (paintable_box().has_non_invertible_css_transform())
|
|
return TraversalDecision::Continue;
|
|
|
|
auto const is_visible = paintable_box().computed_values().visibility() == CSS::Visibility::Visible;
|
|
|
|
// NOTE: Hit testing basically happens in reverse painting order.
|
|
// https://drafts.csswg.org/css2/#z-index
|
|
|
|
// 7. the child stacking contexts with positive stack levels (least positive first).
|
|
// NOTE: Hit testing follows reverse painting order, that's why the conditions here are reversed.
|
|
for (auto const& child : m_children.in_reverse()) {
|
|
if (child->paintable_box().effective_z_index().value_or(0) <= 0)
|
|
break;
|
|
if (child->hit_test(position, type, callback) == TraversalDecision::Break)
|
|
return TraversalDecision::Break;
|
|
}
|
|
|
|
// 6. the child stacking contexts with stack level 0 and the positioned descendants with stack level 0.
|
|
for (auto const& weak_paintable_box : m_positioned_descendants_and_stacking_contexts_with_stack_level_0.in_reverse()) {
|
|
auto paintable_box = weak_paintable_box.strong_ref();
|
|
if (!paintable_box)
|
|
continue;
|
|
if (paintable_box->stacking_context()) {
|
|
if (paintable_box->stacking_context()->hit_test(position, type, callback) == TraversalDecision::Break)
|
|
return TraversalDecision::Break;
|
|
} else {
|
|
if (paintable_box->hit_test(position, type, callback) == TraversalDecision::Break)
|
|
return TraversalDecision::Break;
|
|
}
|
|
}
|
|
|
|
// 5. the in-flow, inline-level, non-positioned descendants, including inline tables and inline blocks.
|
|
if (paintable_box().layout_node().children_are_inline() && is<Layout::BlockContainer>(paintable_box().layout_node())) {
|
|
for (auto paintable = paintable_box().last_child(); paintable; paintable = paintable->previous_sibling()) {
|
|
if (paintable->is_inline() && !paintable->is_absolutely_positioned() && !paintable->has_stacking_context()) {
|
|
if (paintable->hit_test(position, type, callback) == TraversalDecision::Break)
|
|
return TraversalDecision::Break;
|
|
}
|
|
}
|
|
|
|
// Hit test the stacking context root's own fragments if it's a PaintableWithLines.
|
|
if (auto const* paintable_with_lines = as_if<PaintableWithLines>(paintable_box())) {
|
|
auto local_position = paintable_box().transform_point_to_local(position);
|
|
|
|
if (local_position.has_value()) {
|
|
if (paintable_with_lines->hit_test_fragments(position, local_position.value(), type, callback) == TraversalDecision::Break)
|
|
return TraversalDecision::Break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// 4. the non-positioned floats.
|
|
for (auto const& weak_paintable_box : m_non_positioned_floating_descendants.in_reverse()) {
|
|
auto paintable_box = weak_paintable_box.strong_ref();
|
|
if (!paintable_box)
|
|
continue;
|
|
if (paintable_box->hit_test(position, type, callback) == TraversalDecision::Break)
|
|
return TraversalDecision::Break;
|
|
}
|
|
|
|
// 3. the in-flow, non-inline-level, non-positioned descendants.
|
|
if (!paintable_box().layout_node().children_are_inline()) {
|
|
for (auto child = paintable_box().last_child(); child; child = child->previous_sibling()) {
|
|
if (!child->is_paintable_box())
|
|
continue;
|
|
|
|
auto const& paintable_box = as<PaintableBox>(*child);
|
|
if (!paintable_box.is_absolutely_positioned() && !paintable_box.is_floating() && !paintable_box.stacking_context()) {
|
|
if (paintable_box.hit_test(position, type, callback) == TraversalDecision::Break)
|
|
return TraversalDecision::Break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// 2. the child stacking contexts with negative stack levels (most negative first).
|
|
// NB: Hit testing follows reverse painting order, so we visit the least negative stack levels first.
|
|
for (auto const& child : m_children.in_reverse()) {
|
|
// Skip positive/ zero index child stacking contexts, which have already been handled above.
|
|
if (child->paintable_box().effective_z_index().value_or(0) >= 0)
|
|
continue;
|
|
if (child->hit_test(position, type, callback) == TraversalDecision::Break)
|
|
return TraversalDecision::Break;
|
|
}
|
|
|
|
// Hidden elements and elements with pointer-events: none shouldn't be hit.
|
|
if (!is_visible || !paintable_box().visible_for_hit_testing())
|
|
return TraversalDecision::Continue;
|
|
|
|
auto local_position = paintable_box().transform_point_to_local(position);
|
|
|
|
if (local_position.has_value() && paintable_box().absolute_border_box_rect().contains(local_position.value())) {
|
|
if (callback({ .paintable = const_cast<PaintableBox&>(paintable_box()) }) == TraversalDecision::Break)
|
|
return TraversalDecision::Break;
|
|
}
|
|
|
|
return TraversalDecision::Continue;
|
|
}
|
|
|
|
void StackingContext::dump(StringBuilder& builder, int indent) const
|
|
{
|
|
for (int i = 0; i < indent; ++i)
|
|
builder.append(' ');
|
|
CSSPixelRect rect = paintable_box().absolute_rect();
|
|
builder.appendff("SC for {} {} [children: {}] (z-index: ", paintable_box().layout_node().debug_description(), rect, m_children.size());
|
|
|
|
if (paintable_box().effective_z_index().has_value())
|
|
builder.appendff("{}", paintable_box().effective_z_index().value());
|
|
else
|
|
builder.append("auto"sv);
|
|
builder.append(')');
|
|
|
|
if (paintable_box().has_css_transform())
|
|
builder.append(", has_transform"sv);
|
|
|
|
builder.append('\n');
|
|
for (auto& child : m_children)
|
|
child->dump(builder, indent + 1);
|
|
}
|
|
|
|
}
|