ladybird/Libraries/LibWeb/Painting/SVGSVGPaintable.cpp
Andreas Kling 3d3136f60d LibWeb: Paint replaced backgrounds in SVG foreignObject
Paint the background phase for replaced boxes reached through SVG
foreignObject traversal before foreground content. This lets img
background images render correctly when the containing SVG establishes a
stacking context.

Add a ref test for a paint-contained SVG with a foreignObject-hosted img
that uses a transparent SVG source and a CSS background image.
2026-05-30 21:36:47 +02:00

100 lines
3.9 KiB
C++

/*
* Copyright (c) 2018-2022, Andreas Kling <andreas@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibWeb/Layout/ReplacedBox.h>
#include <LibWeb/Painting/DisplayList.h>
#include <LibWeb/Painting/DisplayListRecorder.h>
#include <LibWeb/Painting/SVGSVGPaintable.h>
namespace Web::Painting {
NonnullRefPtr<SVGSVGPaintable> SVGSVGPaintable::create(Layout::SVGSVGBox const& layout_box)
{
return adopt_ref(*new SVGSVGPaintable(layout_box));
}
SVGSVGPaintable::SVGSVGPaintable(Layout::SVGSVGBox const& layout_box)
: PaintableBox(layout_box)
{
}
static void record_foreign_object_descendant_hit_test_items(DisplayListRecordingContext& context, PaintableBox const& paintable)
{
paintable.for_each_child_of_type<PaintableBox>([&](PaintableBox& child) {
child.record_hit_test_items(context, PaintPhase::Background);
record_foreign_object_descendant_hit_test_items(context, child);
child.record_hit_test_items(context, PaintPhase::Foreground);
child.record_hit_test_items(context, PaintPhase::Overlay);
return IterationDecision::Continue;
});
}
void SVGSVGPaintable::paint_svg_box(DisplayListRecordingContext& context, PaintableBox const& svg_box, PaintPhase phase)
{
context.display_list_recorder().set_accumulated_visual_context(svg_box.accumulated_visual_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 = svg_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 masks (SVG <mask>, SVG <clipPath>).
Vector<DisplayListRecorder::MaskInfo> masks;
bool skip_painting = false;
auto mask_area = svg_box.get_mask_area();
if (mask_area.has_value()) {
if (mask_area->is_empty()) {
skip_painting = true;
} else if (auto mask_display_list = svg_box.calculate_mask(context, *mask_area); mask_display_list.has_value()) {
auto rect = context.enclosing_device_rect(*mask_area).to_type<int>();
auto kind = svg_box.get_mask_type().value_or(Gfx::MaskKind::Alpha);
masks.append({ mask_display_list.release_value(), rect, kind });
}
}
auto clip_area = svg_box.get_clip_area();
if (clip_area.has_value()) {
if (clip_area->is_empty()) {
skip_painting = true;
} else if (auto clip_display_list = svg_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);
if (!skip_painting) {
svg_box.record_hit_test_items(context, phase);
if (svg_box.layout_node().is_svg_foreign_object_box())
record_foreign_object_descendant_hit_test_items(context, svg_box);
if (!svg_box.is_svg_paintable()
&& !svg_box.is_svg_svg_paintable()
&& is<Layout::ReplacedBox>(svg_box.layout_node()))
svg_box.paint(context, PaintPhase::Background);
svg_box.paint(context, PaintPhase::Foreground);
paint_descendants(context, svg_box, phase);
}
context.display_list_recorder().end_masks(masks);
}
void SVGSVGPaintable::paint_descendants(DisplayListRecordingContext& context, PaintableBox const& paintable, PaintPhase phase)
{
if (phase != PaintPhase::Foreground)
return;
paintable.for_each_child_of_type<PaintableBox>([&](PaintableBox& child) {
paint_svg_box(context, child, phase);
return IterationDecision::Continue;
});
}
}