mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2026-04-20 10:50:25 +00:00
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.
557 lines
27 KiB
C++
557 lines
27 KiB
C++
/*
|
||
* Copyright (c) 2021-2023, Andreas Kling <andreas@ladybird.org>
|
||
* Copyright (c) 2022, Sam Atkins <atkinssj@serenityos.org>
|
||
* Copyright (c) 2022, Tobias Christiansen <tobyase@serenityos.org>
|
||
* Copyright (c) 2023, MacDue <macdue@dueutil.tech>
|
||
* Copyright (c) 2025, Jelle Raaijmakers <jelle@ladybird.org>
|
||
*
|
||
* SPDX-License-Identifier: BSD-2-Clause
|
||
*/
|
||
|
||
#include <AK/Debug.h>
|
||
#include <LibGfx/BoundingBox.h>
|
||
#include <LibGfx/Font/Font.h>
|
||
#include <LibGfx/Path.h>
|
||
#include <LibGfx/TextLayout.h>
|
||
#include <LibWeb/Layout/BlockFormattingContext.h>
|
||
#include <LibWeb/Layout/SVGClipBox.h>
|
||
#include <LibWeb/Layout/SVGFormattingContext.h>
|
||
#include <LibWeb/Layout/SVGGeometryBox.h>
|
||
#include <LibWeb/Layout/SVGImageBox.h>
|
||
#include <LibWeb/Layout/SVGMaskBox.h>
|
||
#include <LibWeb/Layout/Viewport.h>
|
||
#include <LibWeb/SVG/SVGAElement.h>
|
||
#include <LibWeb/SVG/SVGClipPathElement.h>
|
||
#include <LibWeb/SVG/SVGForeignObjectElement.h>
|
||
#include <LibWeb/SVG/SVGGElement.h>
|
||
#include <LibWeb/SVG/SVGImageElement.h>
|
||
#include <LibWeb/SVG/SVGMaskElement.h>
|
||
#include <LibWeb/SVG/SVGSVGElement.h>
|
||
#include <LibWeb/SVG/SVGSymbolElement.h>
|
||
#include <LibWeb/SVG/SVGUseElement.h>
|
||
|
||
namespace Web::Layout {
|
||
|
||
SVGFormattingContext::SVGFormattingContext(LayoutState& state, LayoutMode layout_mode, Box const& box, FormattingContext* parent, Gfx::AffineTransform parent_viewbox_transform)
|
||
: FormattingContext(Type::SVG, layout_mode, state, box, parent)
|
||
, m_parent_viewbox_transform(parent_viewbox_transform)
|
||
{
|
||
}
|
||
|
||
SVGFormattingContext::~SVGFormattingContext() = default;
|
||
|
||
CSSPixels SVGFormattingContext::automatic_content_width() const
|
||
{
|
||
return 0;
|
||
}
|
||
|
||
CSSPixels SVGFormattingContext::automatic_content_height() const
|
||
{
|
||
return 0;
|
||
}
|
||
|
||
struct ViewBoxTransform {
|
||
CSSPixelPoint offset;
|
||
double scale_factor_x;
|
||
double scale_factor_y;
|
||
};
|
||
|
||
// https://svgwg.org/svg2-draft/coords.html#PreserveAspectRatioAttribute
|
||
static ViewBoxTransform scale_and_align_viewbox_content(SVG::PreserveAspectRatio const& preserve_aspect_ratio,
|
||
SVG::ViewBox const& view_box, Gfx::FloatSize viewbox_scale, auto const& svg_box_state)
|
||
{
|
||
ViewBoxTransform viewbox_transform {};
|
||
|
||
if (preserve_aspect_ratio.align == SVG::PreserveAspectRatio::Align::None) {
|
||
viewbox_transform.scale_factor_x = viewbox_scale.width();
|
||
viewbox_transform.scale_factor_y = viewbox_scale.height();
|
||
viewbox_transform.offset = {};
|
||
return viewbox_transform;
|
||
}
|
||
|
||
switch (preserve_aspect_ratio.meet_or_slice) {
|
||
case SVG::PreserveAspectRatio::MeetOrSlice::Meet:
|
||
// meet (the default) - Scale the graphic such that:
|
||
// - aspect ratio is preserved
|
||
// - the entire ‘viewBox’ is visible within the SVG viewport
|
||
// - the ‘viewBox’ is scaled up as much as possible, while still meeting the other criteria
|
||
viewbox_transform.scale_factor_x = viewbox_transform.scale_factor_y = min(viewbox_scale.width(), viewbox_scale.height());
|
||
break;
|
||
case SVG::PreserveAspectRatio::MeetOrSlice::Slice:
|
||
// slice - Scale the graphic such that:
|
||
// aspect ratio is preserved
|
||
// the entire SVG viewport is covered by the ‘viewBox’
|
||
// the ‘viewBox’ is scaled down as much as possible, while still meeting the other criteria
|
||
viewbox_transform.scale_factor_x = viewbox_transform.scale_factor_y = max(viewbox_scale.width(), viewbox_scale.height());
|
||
break;
|
||
default:
|
||
VERIFY_NOT_REACHED();
|
||
}
|
||
|
||
// Handle X alignment:
|
||
if (svg_box_state.has_definite_width()) {
|
||
switch (preserve_aspect_ratio.align) {
|
||
case SVG::PreserveAspectRatio::Align::xMinYMin:
|
||
case SVG::PreserveAspectRatio::Align::xMinYMid:
|
||
case SVG::PreserveAspectRatio::Align::xMinYMax:
|
||
// Align the <min-x> of the element's ‘viewBox’ with the smallest X value of the SVG viewport.
|
||
viewbox_transform.offset.translate_by(0, 0);
|
||
break;
|
||
case SVG::PreserveAspectRatio::Align::None: {
|
||
// Do not force uniform scaling. Scale the graphic content of the given element non-uniformly
|
||
// if necessary such that the element's bounding box exactly matches the SVG viewport rectangle.
|
||
// FIXME: None is unimplemented (treat as xMidYMid)
|
||
[[fallthrough]];
|
||
}
|
||
case SVG::PreserveAspectRatio::Align::xMidYMin:
|
||
case SVG::PreserveAspectRatio::Align::xMidYMid:
|
||
case SVG::PreserveAspectRatio::Align::xMidYMax:
|
||
// Align the midpoint X value of the element's ‘viewBox’ with the midpoint X value of the SVG viewport.
|
||
viewbox_transform.offset.translate_by((svg_box_state.content_width() - CSSPixels::nearest_value_for(view_box.width * viewbox_transform.scale_factor_x)) / 2, 0);
|
||
break;
|
||
case SVG::PreserveAspectRatio::Align::xMaxYMin:
|
||
case SVG::PreserveAspectRatio::Align::xMaxYMid:
|
||
case SVG::PreserveAspectRatio::Align::xMaxYMax:
|
||
// Align the <min-x>+<width> of the element's ‘viewBox’ with the maximum X value of the SVG viewport.
|
||
viewbox_transform.offset.translate_by((svg_box_state.content_width() - CSSPixels::nearest_value_for(view_box.width * viewbox_transform.scale_factor_x)), 0);
|
||
break;
|
||
default:
|
||
VERIFY_NOT_REACHED();
|
||
}
|
||
}
|
||
|
||
if (svg_box_state.has_definite_width()) {
|
||
switch (preserve_aspect_ratio.align) {
|
||
case SVG::PreserveAspectRatio::Align::xMinYMin:
|
||
case SVG::PreserveAspectRatio::Align::xMidYMin:
|
||
case SVG::PreserveAspectRatio::Align::xMaxYMin:
|
||
// Align the <min-y> of the element's ‘viewBox’ with the smallest Y value of the SVG viewport.
|
||
viewbox_transform.offset.translate_by(0, 0);
|
||
break;
|
||
case SVG::PreserveAspectRatio::Align::None: {
|
||
// Do not force uniform scaling. Scale the graphic content of the given element non-uniformly
|
||
// if necessary such that the element's bounding box exactly matches the SVG viewport rectangle.
|
||
// FIXME: None is unimplemented (treat as xMidYMid)
|
||
[[fallthrough]];
|
||
}
|
||
case SVG::PreserveAspectRatio::Align::xMinYMid:
|
||
case SVG::PreserveAspectRatio::Align::xMidYMid:
|
||
case SVG::PreserveAspectRatio::Align::xMaxYMid:
|
||
// Align the midpoint Y value of the element's ‘viewBox’ with the midpoint Y value of the SVG viewport.
|
||
viewbox_transform.offset.translate_by(0, (svg_box_state.content_height() - CSSPixels::nearest_value_for(view_box.height * viewbox_transform.scale_factor_y)) / 2);
|
||
break;
|
||
case SVG::PreserveAspectRatio::Align::xMinYMax:
|
||
case SVG::PreserveAspectRatio::Align::xMidYMax:
|
||
case SVG::PreserveAspectRatio::Align::xMaxYMax:
|
||
// Align the <min-y>+<height> of the element's ‘viewBox’ with the maximum Y value of the SVG viewport.
|
||
viewbox_transform.offset.translate_by(0, (svg_box_state.content_height() - CSSPixels::nearest_value_for(view_box.height * viewbox_transform.scale_factor_y)));
|
||
break;
|
||
default:
|
||
VERIFY_NOT_REACHED();
|
||
}
|
||
}
|
||
|
||
return viewbox_transform;
|
||
}
|
||
|
||
static bool is_container_element(Node const& node)
|
||
{
|
||
// https://svgwg.org/svg2-draft/struct.html#GroupsOverview
|
||
auto* dom_node = node.dom_node();
|
||
if (!dom_node)
|
||
return false;
|
||
if (is<SVG::SVGAElement>(dom_node))
|
||
return true;
|
||
if (is<SVG::SVGUseElement>(dom_node))
|
||
return true;
|
||
if (is<SVG::SVGSymbolElement>(dom_node))
|
||
return true;
|
||
if (is<SVG::SVGGElement>(dom_node))
|
||
return true;
|
||
if (is<SVG::SVGMaskElement>(dom_node))
|
||
return true;
|
||
return false;
|
||
}
|
||
|
||
void SVGFormattingContext::run(AvailableSpace const& available_space)
|
||
{
|
||
FORMATTING_CONTEXT_TRACE();
|
||
// NOTE: SVG doesn't have a "formatting context" in the spec, but this is the most
|
||
// obvious way to drive SVG layout in our engine at the moment.
|
||
|
||
auto& svg_box_state = m_state.get_mutable(context_box());
|
||
|
||
auto const& document = context_box().document();
|
||
if (document.document_element() == context_box().dom_node() && !document.is_decoded_svg()) {
|
||
// Overwrite the content width/height with the styled node width/height (from <svg width height ...>)
|
||
|
||
// NOTE: If a height had not been provided by the svg element, it was set to the height of the container
|
||
// (see BlockFormattingContext::layout_viewport)
|
||
if (svg_box_state.node().computed_values().width().is_length())
|
||
svg_box_state.set_content_width(svg_box_state.node().computed_values().width().length().to_px(svg_box_state.node()));
|
||
if (svg_box_state.node().computed_values().height().is_length())
|
||
svg_box_state.set_content_height(svg_box_state.node().computed_values().height().length().to_px(svg_box_state.node()));
|
||
// FIXME: In SVG 2, length can also be a percentage. We'll need to support that.
|
||
}
|
||
|
||
// NOTE: We consider all SVG root elements to have definite size in both axes.
|
||
// I'm not sure if this is good or bad, but our viewport transform logic depends on it.
|
||
svg_box_state.set_has_definite_width(true);
|
||
svg_box_state.set_has_definite_height(true);
|
||
|
||
auto* dom_node = context_box().dom_node();
|
||
VERIFY(dom_node);
|
||
auto& svg_graphics_element = as<SVG::SVGGraphicsElement>(*dom_node);
|
||
auto active_view_box = svg_graphics_element.active_view_box();
|
||
// https://svgwg.org/svg2-draft/coords.html#ViewBoxAttribute
|
||
if (active_view_box.has_value()) {
|
||
if (active_view_box->width < 0 || active_view_box->height < 0) {
|
||
// A negative value for <width> or <height> is an error and invalidates the ‘viewBox’ attribute.
|
||
active_view_box = {};
|
||
} else if (active_view_box->width == 0 || active_view_box->height == 0) {
|
||
// A value of zero disables rendering of the element.
|
||
return;
|
||
}
|
||
}
|
||
|
||
m_current_viewbox_transform = m_parent_viewbox_transform;
|
||
if (active_view_box.has_value()) {
|
||
// FIXME: This should allow just one of width or height to be specified.
|
||
// E.g. We should be able to layout <svg width="100%"> where height is unspecified/auto.
|
||
if (!svg_box_state.has_definite_width() || !svg_box_state.has_definite_height()) {
|
||
dbgln_if(LIBWEB_CSS_DEBUG, "FIXME: Attempting to layout indefinitely sized SVG with a viewbox -- this likely won't work!");
|
||
}
|
||
|
||
auto scale_width = svg_box_state.has_definite_width() ? svg_box_state.content_width() / active_view_box->width : 1;
|
||
auto scale_height = svg_box_state.has_definite_height() ? svg_box_state.content_height() / active_view_box->height : 1;
|
||
|
||
// The initial value for preserveAspectRatio is xMidYMid meet.
|
||
SVG::PreserveAspectRatio preserve_aspect_ratio {};
|
||
if (auto* view_box = as_if<SVG::SVGFitToViewBox>(*dom_node)) {
|
||
preserve_aspect_ratio = view_box->preserve_aspect_ratio().value_or(SVG::PreserveAspectRatio {});
|
||
} else if (is<SVG::SVGMaskElement>(*dom_node) || is<SVG::SVGClipPathElement>(*dom_node)) {
|
||
// This allows mask and clipPath elements to be scaled in the x and y directions independently to match the target size.
|
||
preserve_aspect_ratio = SVG::PreserveAspectRatio { SVG::PreserveAspectRatio::Align::None, {} };
|
||
}
|
||
|
||
auto viewbox_offset_and_scale = scale_and_align_viewbox_content(preserve_aspect_ratio, *active_view_box, { scale_width, scale_height }, svg_box_state);
|
||
|
||
CSSPixelPoint offset = viewbox_offset_and_scale.offset;
|
||
m_current_viewbox_transform
|
||
.translate(offset.to_type<float>())
|
||
.scale(viewbox_offset_and_scale.scale_factor_x, viewbox_offset_and_scale.scale_factor_y)
|
||
.translate({ -active_view_box->min_x, -active_view_box->min_y });
|
||
}
|
||
|
||
// NOTE: Calculate viewport dimensions BEFORE scaling the content by m_parent_viewbox_transform.
|
||
// For userSpaceOnUse clips (which have no viewBox), we need the unscaled content dimensions,
|
||
// not the final pixel dimensions. Otherwise, nested clips compound the scale incorrectly.
|
||
auto viewport_width = [&] {
|
||
if (active_view_box.has_value())
|
||
return CSSPixels::nearest_value_for(active_view_box->width);
|
||
if (svg_box_state.has_definite_width())
|
||
return svg_box_state.content_width();
|
||
dbgln_if(LIBWEB_CSS_DEBUG, "FIXME: Failed to resolve width of SVG viewport!");
|
||
return CSSPixels {};
|
||
}();
|
||
|
||
auto viewport_height = [&] {
|
||
if (active_view_box.has_value())
|
||
return CSSPixels::nearest_value_for(active_view_box->height);
|
||
if (svg_box_state.has_definite_height())
|
||
return svg_box_state.content_height();
|
||
dbgln_if(LIBWEB_CSS_DEBUG, "FIXME: Failed to resolve height of SVG viewport!");
|
||
return CSSPixels {};
|
||
}();
|
||
|
||
m_available_space = available_space;
|
||
m_svg_offset = svg_box_state.offset;
|
||
m_viewport_size = { viewport_width, viewport_height };
|
||
|
||
if (svg_box_state.has_definite_width() && svg_box_state.has_definite_height()) {
|
||
// Scale the box of the viewport based on the parent's viewBox transform.
|
||
// The viewBox transform is always just a simple scale + offset.
|
||
// FIXME: Avoid converting SVG box to floats.
|
||
Gfx::FloatRect svg_rect = { svg_box_state.offset.to_type<float>(),
|
||
{ float(svg_box_state.content_width()), float(svg_box_state.content_height()) } };
|
||
svg_rect = m_parent_viewbox_transform.map(svg_rect);
|
||
svg_box_state.set_content_offset(svg_rect.location().to_type<CSSPixels>());
|
||
svg_box_state.set_content_width(CSSPixels(svg_rect.width()));
|
||
svg_box_state.set_content_height(CSSPixels(svg_rect.height()));
|
||
svg_box_state.set_has_definite_width(true);
|
||
svg_box_state.set_has_definite_height(true);
|
||
}
|
||
|
||
context_box().for_each_child_of_type<Box>([&](Box const& child) {
|
||
layout_svg_element(child);
|
||
return IterationDecision::Continue;
|
||
});
|
||
}
|
||
|
||
void SVGFormattingContext::layout_svg_element(Box const& child)
|
||
{
|
||
if (is<SVG::SVGFitToViewBox>(child.dom_node())) {
|
||
layout_nested_viewport(child);
|
||
} else if (is<SVG::SVGForeignObjectElement>(child.dom_node()) && is<BlockContainer>(child)) {
|
||
Layout::BlockFormattingContext bfc(m_state, m_layout_mode, static_cast<BlockContainer const&>(child), this);
|
||
auto& child_state = m_state.get_mutable(child);
|
||
CSSPixelRect rect {
|
||
{
|
||
child.computed_values().x().to_px(child, m_available_space->width.to_px_or_zero()),
|
||
child.computed_values().y().to_px(child, m_available_space->height.to_px_or_zero()),
|
||
},
|
||
{
|
||
child.computed_values().width().to_px(child, m_available_space->width.to_px_or_zero()),
|
||
child.computed_values().height().to_px(child, m_available_space->height.to_px_or_zero()),
|
||
}
|
||
};
|
||
auto transformed_rect = m_current_viewbox_transform.map(rect.to_type<float>()).to_type<CSSPixels>();
|
||
child_state.set_content_offset(transformed_rect.location());
|
||
child_state.set_content_width(transformed_rect.width());
|
||
child_state.set_content_height(transformed_rect.height());
|
||
|
||
bfc.run(AvailableSpace(AvailableSize::make_definite(child_state.content_width()), AvailableSize::make_definite(child_state.content_height())));
|
||
|
||
if (auto* mask_box = child.first_child_of_type<SVGMaskBox>())
|
||
layout_mask_or_clip(*mask_box);
|
||
|
||
if (auto* clip_box = child.first_child_of_type<SVGClipBox>())
|
||
layout_mask_or_clip(*clip_box);
|
||
} else if (is<SVGGraphicsBox>(child)) {
|
||
layout_graphics_element(static_cast<SVGGraphicsBox const&>(child));
|
||
}
|
||
}
|
||
|
||
void SVGFormattingContext::layout_nested_viewport(Box const& viewport)
|
||
{
|
||
// Layout for a nested SVG viewport.
|
||
// https://svgwg.org/svg2-draft/coords.html#EstablishingANewSVGViewport.
|
||
SVGFormattingContext nested_context(m_state, m_layout_mode, viewport, this, m_current_viewbox_transform);
|
||
auto& nested_viewport_state = m_state.get_mutable(viewport);
|
||
auto resolve_dimension = [](auto& node, auto size, auto reference_value) {
|
||
// The value auto for width and height on the ‘svg’ element is treated as 100%.
|
||
// https://svgwg.org/svg2-draft/geometry.html#Sizing
|
||
if (size.is_auto())
|
||
return reference_value;
|
||
return size.to_px(node, reference_value);
|
||
};
|
||
|
||
auto nested_viewport_x = viewport.computed_values().x().to_px(viewport, m_viewport_size.width());
|
||
auto nested_viewport_y = viewport.computed_values().y().to_px(viewport, m_viewport_size.height());
|
||
auto nested_viewport_width = resolve_dimension(viewport, viewport.computed_values().width(), m_viewport_size.width());
|
||
auto nested_viewport_height = resolve_dimension(viewport, viewport.computed_values().height(), m_viewport_size.height());
|
||
nested_viewport_state.set_content_offset({ nested_viewport_x, nested_viewport_y });
|
||
nested_viewport_state.set_content_width(nested_viewport_width);
|
||
nested_viewport_state.set_content_height(nested_viewport_height);
|
||
nested_viewport_state.set_has_definite_width(true);
|
||
nested_viewport_state.set_has_definite_height(true);
|
||
nested_context.run(*m_available_space);
|
||
}
|
||
|
||
Gfx::Path SVGFormattingContext::compute_path_for_text(SVGTextBox const& text_box) const
|
||
{
|
||
auto& text_element = text_box.dom_node();
|
||
// FIXME: Use per-code-point fonts.
|
||
auto& font = text_box.first_available_font();
|
||
auto text_contents = text_element.text_contents();
|
||
auto text_width = font.width(text_contents);
|
||
auto text_offset = m_current_text_position;
|
||
|
||
// https://svgwg.org/svg2-draft/text.html#TextAnchoringProperties
|
||
switch (text_element.text_anchor().value_or(SVG::TextAnchor::Start)) {
|
||
case SVG::TextAnchor::Start:
|
||
// The rendered characters are aligned such that the start of the resulting rendered text is at the initial
|
||
// current text position.
|
||
break;
|
||
case SVG::TextAnchor::Middle: {
|
||
// The rendered characters are shifted such that the geometric middle of the resulting rendered text
|
||
// (determined from the initial and final current text position before applying the text-anchor property)
|
||
// is at the initial current text position.
|
||
text_offset.translate_by(-text_width / 2, 0);
|
||
break;
|
||
}
|
||
case SVG::TextAnchor::End: {
|
||
// The rendered characters are shifted such that the end of the resulting rendered text (final current text
|
||
// position before applying the text-anchor property) is at the initial current text position.
|
||
text_offset.translate_by(-text_width, 0);
|
||
break;
|
||
}
|
||
default:
|
||
VERIFY_NOT_REACHED();
|
||
}
|
||
|
||
Gfx::Path path;
|
||
path.move_to(text_offset);
|
||
path.text(text_contents, font);
|
||
return path;
|
||
}
|
||
|
||
Gfx::Path SVGFormattingContext::compute_path_for_text_path(SVGTextPathBox const& text_path_box) const
|
||
{
|
||
auto& text_path_element = static_cast<SVG::SVGTextPathElement const&>(text_path_box.dom_node());
|
||
auto path_or_shape = text_path_element.path_or_shape();
|
||
if (!path_or_shape)
|
||
return {};
|
||
|
||
// FIXME: Use per-code-point fonts.
|
||
auto& font = text_path_box.first_available_font();
|
||
auto text_contents = text_path_element.text_contents();
|
||
|
||
auto shape_path = const_cast<SVG::SVGGeometryElement&>(*path_or_shape).get_path(m_viewport_size);
|
||
return shape_path.place_text_along(text_contents, font);
|
||
}
|
||
|
||
void SVGFormattingContext::layout_path_like_element(SVGGraphicsBox const& graphics_box)
|
||
{
|
||
auto& graphics_box_state = m_state.get_mutable(graphics_box);
|
||
VERIFY(graphics_box_state.computed_svg_transforms().has_value());
|
||
|
||
auto to_css_pixels_transform = Gfx::AffineTransform {}
|
||
.multiply(m_current_viewbox_transform)
|
||
.multiply(graphics_box_state.computed_svg_transforms()->svg_transform());
|
||
|
||
Gfx::Path path;
|
||
if (is<SVGGeometryBox>(graphics_box)) {
|
||
auto& geometry_box = static_cast<SVGGeometryBox const&>(graphics_box);
|
||
path = const_cast<SVGGeometryBox&>(geometry_box).dom_node().get_path(m_viewport_size);
|
||
} else if (auto* text_box = as_if<SVGTextBox>(graphics_box)) {
|
||
// FIXME: Text offsets must be calculated per character. This only applies the first character's offset.
|
||
auto text_positioning = text_box->dom_node().text_positioning();
|
||
text_positioning.apply_to_text_position(*text_box, m_viewport_size, m_current_text_position, 0u);
|
||
|
||
path = compute_path_for_text(*text_box);
|
||
|
||
// <text> and <tspan> elements can contain more text elements.
|
||
text_box->for_each_child_of_type<SVGGraphicsBox>([&](auto& child) {
|
||
if (is<SVGTextBox>(child) || is<SVGTextPathBox>(child))
|
||
layout_graphics_element(child);
|
||
return IterationDecision::Continue;
|
||
});
|
||
} else if (auto* text_path_box = as_if<SVGTextPathBox>(graphics_box)) {
|
||
// FIXME: Support <tspan> in <textPath>.
|
||
path = compute_path_for_text_path(*text_path_box);
|
||
}
|
||
|
||
auto path_bounding_box = path.bounding_box();
|
||
m_current_text_position = path_bounding_box.bottom_right();
|
||
auto transformed_bounding_box = to_css_pixels_transform.map(path_bounding_box).to_type<CSSPixels>();
|
||
// Stroke increases the path's size by stroke_width/2 per side.
|
||
CSSPixels stroke_width = CSSPixels::nearest_value_for(graphics_box.dom_node().visible_stroke_width() * m_current_viewbox_transform.x_scale());
|
||
transformed_bounding_box.inflate(stroke_width, stroke_width);
|
||
graphics_box_state.set_content_offset(transformed_bounding_box.top_left());
|
||
graphics_box_state.set_content_width(transformed_bounding_box.width());
|
||
graphics_box_state.set_content_height(transformed_bounding_box.height());
|
||
graphics_box_state.set_has_definite_width(true);
|
||
graphics_box_state.set_has_definite_height(true);
|
||
graphics_box_state.set_computed_svg_path(move(path));
|
||
}
|
||
|
||
Gfx::AffineTransform SVGFormattingContext::get_parent_svg_transform(SVGGraphicsBox const& box) const
|
||
{
|
||
// Mask/clip boxes are transform boundaries — the target's transform is applied separately at paint time.
|
||
for (auto* ancestor = box.parent(); ancestor; ancestor = ancestor->parent()) {
|
||
if (is<SVGMaskBox>(*ancestor) || is<SVGClipBox>(*ancestor))
|
||
return {};
|
||
if (auto const* svg_graphics_ancestor = as_if<SVGGraphicsBox>(*ancestor)) {
|
||
auto const& ancestor_state = m_state.get(*svg_graphics_ancestor);
|
||
if (ancestor_state.computed_svg_transforms().has_value())
|
||
return ancestor_state.computed_svg_transforms()->svg_transform();
|
||
}
|
||
}
|
||
return {};
|
||
}
|
||
|
||
void SVGFormattingContext::layout_graphics_element(SVGGraphicsBox const& graphics_box)
|
||
{
|
||
auto& graphics_box_state = m_state.get_mutable(graphics_box);
|
||
auto parent_svg_transform = get_parent_svg_transform(graphics_box);
|
||
auto svg_transform = parent_svg_transform.multiply(const_cast<SVGGraphicsBox&>(graphics_box).dom_node().element_transform());
|
||
graphics_box_state.set_computed_svg_transforms(Painting::SVGGraphicsPaintable::ComputedTransforms(m_current_viewbox_transform, svg_transform));
|
||
|
||
if (is_container_element(graphics_box)) {
|
||
// https://svgwg.org/svg2-draft/struct.html#Groups
|
||
// 5.2. Grouping: the ‘g’ element
|
||
// The ‘g’ element is a container element for grouping together related graphics elements.
|
||
layout_container_element(graphics_box);
|
||
} else if (is<SVGImageBox>(graphics_box)) {
|
||
layout_image_element(static_cast<SVGImageBox const&>(graphics_box));
|
||
} else {
|
||
// Assume this is a path-like element.
|
||
layout_path_like_element(graphics_box);
|
||
}
|
||
|
||
if (auto* mask_box = graphics_box.first_child_of_type<SVGMaskBox>())
|
||
layout_mask_or_clip(*mask_box);
|
||
|
||
if (auto* clip_box = graphics_box.first_child_of_type<SVGClipBox>())
|
||
layout_mask_or_clip(*clip_box);
|
||
}
|
||
|
||
void SVGFormattingContext::layout_image_element(SVGImageBox const& image_box)
|
||
{
|
||
auto& box_state = m_state.get_mutable(image_box);
|
||
// FIXME: Support transforms on SVG image elements.
|
||
auto to_css_pixels_transform = Gfx::AffineTransform {}
|
||
.multiply(m_current_viewbox_transform);
|
||
auto bounding_box = to_css_pixels_transform.map(image_box.dom_node().bounding_box()).to_type<CSSPixels>();
|
||
|
||
box_state.set_content_x(bounding_box.x());
|
||
box_state.set_content_y(bounding_box.y());
|
||
box_state.set_content_width(bounding_box.width());
|
||
box_state.set_content_height(bounding_box.height());
|
||
box_state.set_has_definite_width(true);
|
||
box_state.set_has_definite_height(true);
|
||
}
|
||
|
||
void SVGFormattingContext::layout_mask_or_clip(SVGBox const& mask_or_clip)
|
||
{
|
||
SVG::SVGUnits content_units {};
|
||
if (is<SVGMaskBox>(mask_or_clip))
|
||
content_units = static_cast<SVGMaskBox const&>(mask_or_clip).dom_node().mask_content_units();
|
||
else if (is<SVGClipBox>(mask_or_clip))
|
||
content_units = static_cast<SVGClipBox const&>(mask_or_clip).dom_node().clip_path_units();
|
||
else
|
||
VERIFY_NOT_REACHED();
|
||
// FIXME: Somehow limit <clipPath> contents to: shape elements, <text>, and <use>.
|
||
auto& layout_state = m_state.get_mutable(mask_or_clip);
|
||
auto parent_viewbox_transform = m_current_viewbox_transform;
|
||
if (content_units == SVG::SVGUnits::ObjectBoundingBox) {
|
||
auto* parent_node = mask_or_clip.parent();
|
||
auto& parent_node_state = m_state.get(*parent_node);
|
||
layout_state.set_content_width(parent_node_state.content_width());
|
||
layout_state.set_content_height(parent_node_state.content_height());
|
||
parent_viewbox_transform = Gfx::AffineTransform {}.translate(parent_node_state.offset.to_type<float>());
|
||
} else {
|
||
layout_state.set_content_width(m_viewport_size.width());
|
||
layout_state.set_content_height(m_viewport_size.height());
|
||
}
|
||
// Pretend masks/clips are a viewport so we can scale the contents depending on the `contentUnits`.
|
||
SVGFormattingContext nested_context(m_state, m_layout_mode, mask_or_clip, this, parent_viewbox_transform);
|
||
layout_state.set_has_definite_width(true);
|
||
layout_state.set_has_definite_height(true);
|
||
nested_context.run(*m_available_space);
|
||
}
|
||
|
||
void SVGFormattingContext::layout_container_element(SVGBox const& container)
|
||
{
|
||
auto& box_state = m_state.get_mutable(container);
|
||
Gfx::BoundingBox<CSSPixels> bounding_box;
|
||
container.for_each_child_of_type<Box>([&](Box const& child) {
|
||
// Masks/clips do not change the bounding box of their parents.
|
||
if (is<SVGMaskBox>(child) || is<SVGClipBox>(child))
|
||
return IterationDecision::Continue;
|
||
layout_svg_element(child);
|
||
auto& child_state = m_state.get(child);
|
||
bounding_box.add_point(child_state.offset);
|
||
bounding_box.add_point(child_state.offset.translated(child_state.content_width(), child_state.content_height()));
|
||
return IterationDecision::Continue;
|
||
});
|
||
box_state.set_content_x(bounding_box.x());
|
||
box_state.set_content_y(bounding_box.y());
|
||
box_state.set_content_width(bounding_box.width());
|
||
box_state.set_content_height(bounding_box.height());
|
||
box_state.set_has_definite_width(true);
|
||
box_state.set_has_definite_height(true);
|
||
}
|
||
|
||
}
|