Files
ladybird/Libraries/LibWeb/Painting/SVGMaskable.cpp
Aliaksandr Kalenik e190f3ec23 LibWeb: Compute SVG mask/clip transforms using layout tree hierarchy
No observable behavior change — this is a refactoring of how SVG
transforms are computed for mask and clip content.

Previously, SVGGraphicsElement::get_transform() computed accumulated
transforms by walking the DOM tree. This didn't work correctly for masks
and clips because their DOM structure differs from layout: in the DOM,
mask/clip elements are siblings or ancestors of their targets, but in
the layout tree they become children of the target element. Walking the
DOM tree caused transforms from the target's ancestors to incorrectly
leak into mask/clip content.

The fix walks the layout tree instead of the DOM tree, which means
transform computation must happen during layout (where we have access to
the layout tree structure) rather than on-demand from the DOM element.
This moves the logic to SVGFormattingContext and removes get_transform()
since it can no longer serve its purpose — the DOM element only provides
element_transform() for its own transform now.

During layout, we walk up the layout tree and stop at mask/clip
boundaries, ensuring mask/clip content stays in its own coordinate
space. The target's accumulated transform is applied separately at paint
time.
2026-02-05 09:00:56 +01:00

120 lines
4.7 KiB
C++

/*
* Copyright (c) 2024-2026, Aliaksandr Kalenik <kalenik.aliaksandr@gmail.com>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibWeb/Layout/SVGClipBox.h>
#include <LibWeb/Layout/SVGMaskBox.h>
#include <LibWeb/Painting/SVGClipPaintable.h>
#include <LibWeb/Painting/SVGGraphicsPaintable.h>
#include <LibWeb/Painting/StackingContext.h>
#include <LibWeb/SVG/SVGSVGElement.h>
namespace Web::Painting {
template<typename T>
static T const* first_child_layout_node_of_type(SVG::SVGGraphicsElement const& graphics_element)
{
if (!graphics_element.layout_node())
return nullptr;
return graphics_element.layout_node()->first_child_of_type<T>();
}
static auto get_mask_box(SVG::SVGGraphicsElement const& graphics_element)
{
return first_child_layout_node_of_type<Layout::SVGMaskBox>(graphics_element);
}
static auto get_clip_box(SVG::SVGGraphicsElement const& graphics_element)
{
return first_child_layout_node_of_type<Layout::SVGClipBox>(graphics_element);
}
Optional<CSSPixelRect> SVGMaskable::get_svg_mask_area() const
{
auto const& graphics_element = as<SVG::SVGGraphicsElement const>(*dom_node_of_svg());
if (auto* mask_box = get_mask_box(graphics_element))
return mask_box->dom_node().resolve_masking_area(mask_box->paintable_box()->absolute_border_box_rect());
return {};
}
Optional<CSSPixelRect> SVGMaskable::get_svg_clip_area() const
{
auto const& graphics_element = as<SVG::SVGGraphicsElement const>(*dom_node_of_svg());
if (auto* clip_box = get_clip_box(graphics_element))
return clip_box->paintable_box()->absolute_border_box_rect();
return {};
}
static Gfx::MaskKind mask_type_to_gfx_mask_kind(CSS::MaskType mask_type)
{
switch (mask_type) {
case CSS::MaskType::Alpha:
return Gfx::MaskKind::Alpha;
case CSS::MaskType::Luminance:
return Gfx::MaskKind::Luminance;
default:
VERIFY_NOT_REACHED();
}
}
Optional<Gfx::MaskKind> SVGMaskable::get_svg_mask_type() const
{
auto const& graphics_element = as<SVG::SVGGraphicsElement const>(*dom_node_of_svg());
if (auto* mask_box = get_mask_box(graphics_element))
return mask_type_to_gfx_mask_kind(mask_box->computed_values().mask_type());
return {};
}
static RefPtr<DisplayList> paint_mask_or_clip_to_display_list(
DisplayListRecordingContext& context,
Gfx::AffineTransform const& target_svg_transform,
PaintableBox const& paintable,
CSSPixelRect const& area,
bool is_clip_path)
{
auto mask_rect = context.enclosing_device_rect(area);
auto display_list = DisplayList::create(context.device_pixels_per_css_pixel());
DisplayListRecorder display_list_recorder(*display_list);
display_list_recorder.translate(-mask_rect.location().to_type<int>());
auto paint_context = context.clone(display_list_recorder);
auto const& mask_element = as<SVG::SVGGraphicsElement const>(*paintable.dom_node());
// Layout computes transforms only within the mask/clip subtree, so prepend the target's accumulated transform here.
auto svg_transform = Gfx::AffineTransform { target_svg_transform }.multiply(mask_element.element_transform());
paint_context.set_svg_transform(svg_transform);
paint_context.set_draw_svg_geometry_for_clip_path(is_clip_path);
StackingContext::paint_svg(paint_context, paintable, PaintPhase::Foreground);
return display_list;
}
Gfx::AffineTransform SVGMaskable::target_svg_transform() const
{
// Only SVGGraphicsPaintable carries an SVG transform; other targets (e.g. foreign objects) use identity.
if (auto const* svg_graphics_paintable = as_if<SVGGraphicsPaintable>(*this))
return svg_graphics_paintable->computed_transforms().svg_transform();
return {};
}
RefPtr<DisplayList> SVGMaskable::calculate_svg_mask_display_list(DisplayListRecordingContext& context, CSSPixelRect const& mask_area) const
{
auto const& graphics_element = as<SVG::SVGGraphicsElement const>(*dom_node_of_svg());
auto* mask_box = get_mask_box(graphics_element);
if (!mask_box)
return nullptr;
auto& mask_paintable = static_cast<PaintableBox const&>(*mask_box->first_paintable());
return paint_mask_or_clip_to_display_list(context, target_svg_transform(), mask_paintable, mask_area, false);
}
RefPtr<DisplayList> SVGMaskable::calculate_svg_clip_display_list(DisplayListRecordingContext& context, CSSPixelRect const& clip_area) const
{
auto const& graphics_element = as<SVG::SVGGraphicsElement const>(*dom_node_of_svg());
auto* clip_box = get_clip_box(graphics_element);
if (!clip_box)
return nullptr;
auto& clip_paintable = static_cast<PaintableBox const&>(*clip_box->first_paintable());
return paint_mask_or_clip_to_display_list(context, target_svg_transform(), clip_paintable, clip_area, true);
}
}