LibWeb: Split SVGFormattingContext up into functions

Doing multiple `for_each_in_subtree()` passes was kind of a hack. We
can resolve everything in a single pass with a little more control over
the layout process. This also fixes a few minor issues like the sizing
of nested `<g>` elements.

More work is needed here though as this is still fairly ad-hoc.

Note: This does regress `css-namespace-tag-name-selector.html`,
previously SVG text within `<a>` elements would appear. However, this
was only because `for_each_in_subtree()` would blindly look through the
InlineNodes from the unimplemented `SVGAElement`s.
This commit is contained in:
MacDue 2024-04-07 20:47:13 +01:00 committed by Andreas Kling
commit d7b77d7695
Notes: sideshowbarker 2024-07-17 05:19:06 +09:00
7 changed files with 274 additions and 218 deletions

View file

@ -8,7 +8,7 @@ Viewport <#document> at (0,0) content-size 800x600 children: not-inline
frag 2 from BlockContainer start: 0, length: 0, rect: [319,51 0x108] baseline: 110 frag 2 from BlockContainer start: 0, length: 0, rect: [319,51 0x108] baseline: 110
SVGSVGBox <svg> at (9,9) content-size 300x150 [SVG] children: inline SVGSVGBox <svg> at (9,9) content-size 300x150 [SVG] children: inline
InlineNode <a> InlineNode <a>
SVGTextBox <text> at (29,25.015625) content-size 193.59375x67.578125 children: inline SVGTextBox <text> (not painted) children: inline
TextNode <#text> TextNode <#text>
TextNode <#text> TextNode <#text>
BlockContainer <math> at (319,51) content-size 0x108 children: not-inline BlockContainer <math> at (319,51) content-size 0x108 children: not-inline

View file

@ -0,0 +1,24 @@
Viewport <#document> at (0,0) content-size 800x600 children: not-inline
BlockContainer <html> at (0,0) content-size 800x600 [BFC] children: not-inline
BlockContainer <body> at (8,8) content-size 784x100 children: inline
frag 0 from SVGSVGBox start: 0, length: 0, rect: [8,8 100x100] baseline: 100
SVGSVGBox <svg> at (8,8) content-size 100x100 [SVG] children: inline
TextNode <#text>
TextNode <#text>
SVGGraphicsBox <g> at (58,58) content-size 10x10 children: inline
TextNode <#text>
SVGGraphicsBox <g> at (58,58) content-size 10x10 children: inline
TextNode <#text>
SVGGeometryBox <rect> at (58,58) content-size 10x10 children: not-inline
TextNode <#text>
TextNode <#text>
TextNode <#text>
TextNode <#text>
ViewportPaintable (Viewport<#document>) [0,0 800x600]
PaintableWithLines (BlockContainer<HTML>) [0,0 800x600]
PaintableWithLines (BlockContainer<BODY>) [8,8 784x100]
SVGSVGPaintable (SVGSVGBox<svg>) [8,8 100x100]
SVGGraphicsPaintable (SVGGraphicsBox<g>) [58,58 10x10]
SVGGraphicsPaintable (SVGGraphicsBox<g>) [58,58 10x10]
SVGPathPaintable (SVGGeometryBox<rect>) [58,58 10x10]

View file

@ -10,7 +10,7 @@ Viewport <#document> at (0,0) content-size 800x600 children: not-inline
SVGSVGBox <svg> at (8,8) content-size 300x150 [SVG] children: inline SVGSVGBox <svg> at (8,8) content-size 300x150 [SVG] children: inline
TextNode <#text> TextNode <#text>
SVGGraphicsBox <use> at (8,8) content-size 300x150 children: not-inline SVGGraphicsBox <use> at (8,8) content-size 300x150 children: not-inline
SVGGraphicsBox <symbol#braces> at (92.375,26.75) content-size 131.25x112.15625 [BFC] children: inline SVGGraphicsBox <symbol#braces> at (8,8) content-size 300x150 [BFC] children: inline
TextNode <#text> TextNode <#text>
SVGGeometryBox <path> at (92.375,26.75) content-size 131.25x112.15625 children: inline SVGGeometryBox <path> at (92.375,26.75) content-size 131.25x112.15625 children: inline
TextNode <#text> TextNode <#text>
@ -25,5 +25,5 @@ ViewportPaintable (Viewport<#document>) [0,0 800x600]
PaintableWithLines (BlockContainer<DIV>) [8,8 784x150] PaintableWithLines (BlockContainer<DIV>) [8,8 784x150]
SVGSVGPaintable (SVGSVGBox<svg>) [8,8 300x150] SVGSVGPaintable (SVGSVGBox<svg>) [8,8 300x150]
SVGGraphicsPaintable (SVGGraphicsBox<use>) [8,8 300x150] SVGGraphicsPaintable (SVGGraphicsBox<use>) [8,8 300x150]
SVGGraphicsPaintable (SVGGraphicsBox<symbol>#braces) [92.375,26.75 131.25x112.15625] SVGGraphicsPaintable (SVGGraphicsBox<symbol>#braces) [8,8 300x150]
SVGPathPaintable (SVGGeometryBox<path>) [92.375,26.75 131.25x112.15625] SVGPathPaintable (SVGGeometryBox<path>) [92.375,26.75 131.25x112.15625]

View file

@ -4,7 +4,7 @@ Viewport <#document> at (0,0) content-size 800x600 children: not-inline
frag 0 from SVGSVGBox start: 0, length: 0, rect: [9,9 100x100] baseline: 102 frag 0 from SVGSVGBox start: 0, length: 0, rect: [9,9 100x100] baseline: 102
SVGSVGBox <svg#outer> at (9,9) content-size 100x100 [SVG] children: inline SVGSVGBox <svg#outer> at (9,9) content-size 100x100 [SVG] children: inline
TextNode <#text> TextNode <#text>
SVGGraphicsBox <use> at (9,9) content-size 50x50 children: inline SVGGraphicsBox <use> at (9,9) content-size 100x100 children: inline
SVGSVGBox <svg#whee> at (9,9) content-size 100x100 [SVG] children: inline SVGSVGBox <svg#whee> at (9,9) content-size 100x100 [SVG] children: inline
TextNode <#text> TextNode <#text>
SVGGeometryBox <rect> at (9,9) content-size 50x50 children: inline SVGGeometryBox <rect> at (9,9) content-size 50x50 children: inline
@ -16,6 +16,6 @@ ViewportPaintable (Viewport<#document>) [0,0 800x600]
PaintableWithLines (BlockContainer<HTML>) [0,0 800x118] PaintableWithLines (BlockContainer<HTML>) [0,0 800x118]
PaintableWithLines (BlockContainer<BODY>) [8,8 784x102] PaintableWithLines (BlockContainer<BODY>) [8,8 784x102]
SVGSVGPaintable (SVGSVGBox<svg>#outer) [8,8 102x102] SVGSVGPaintable (SVGSVGBox<svg>#outer) [8,8 102x102]
SVGGraphicsPaintable (SVGGraphicsBox<use>) [9,9 50x50] SVGGraphicsPaintable (SVGGraphicsBox<use>) [9,9 100x100]
SVGSVGPaintable (SVGSVGBox<svg>#whee) [9,9 100x100] SVGSVGPaintable (SVGSVGBox<svg>#whee) [9,9 100x100]
SVGPathPaintable (SVGGeometryBox<rect>) [9,9 50x50] SVGPathPaintable (SVGGeometryBox<rect>) [9,9 50x50]

View file

@ -0,0 +1,8 @@
<svg width="100" height="100">
<!-- Both <g> elements should have the same size -->
<g>
<g>
<rect x="50" y="50" height="10" width="10" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 175 B

View file

@ -16,9 +16,6 @@
#include <LibWeb/Layout/SVGFormattingContext.h> #include <LibWeb/Layout/SVGFormattingContext.h>
#include <LibWeb/Layout/SVGGeometryBox.h> #include <LibWeb/Layout/SVGGeometryBox.h>
#include <LibWeb/Layout/SVGMaskBox.h> #include <LibWeb/Layout/SVGMaskBox.h>
#include <LibWeb/Layout/SVGSVGBox.h>
#include <LibWeb/Layout/SVGTextBox.h>
#include <LibWeb/Layout/SVGTextPathBox.h>
#include <LibWeb/SVG/SVGClipPathElement.h> #include <LibWeb/SVG/SVGClipPathElement.h>
#include <LibWeb/SVG/SVGForeignObjectElement.h> #include <LibWeb/SVG/SVGForeignObjectElement.h>
#include <LibWeb/SVG/SVGGElement.h> #include <LibWeb/SVG/SVGGElement.h>
@ -168,37 +165,7 @@ static bool is_container_element(Node const& node)
return false; return false;
} }
enum class TraversalDecision { void SVGFormattingContext::run(Box const& box, LayoutMode, AvailableSpace const& available_space)
Continue,
SkipChildrenAndContinue,
Break,
};
// FIXME: Add TraversalDecision::SkipChildrenAndContinue to TreeNode's implementation.
template<typename Callback>
static TraversalDecision for_each_in_inclusive_subtree(Layout::Node const& node, Callback callback)
{
if (auto decision = callback(node); decision != TraversalDecision::Continue)
return decision;
for (auto* child = node.first_child(); child; child = child->next_sibling()) {
if (for_each_in_inclusive_subtree(*child, callback) == TraversalDecision::Break)
return TraversalDecision::Break;
}
return TraversalDecision::Continue;
}
// FIXME: Add TraversalDecision::SkipChildrenAndContinue to TreeNode's implementation.
template<typename Callback>
static TraversalDecision for_each_in_subtree(Layout::Node const& node, Callback callback)
{
for (auto* child = node.first_child(); child; child = child->next_sibling()) {
if (for_each_in_inclusive_subtree(*child, callback) == TraversalDecision::Break)
return TraversalDecision::Break;
}
return TraversalDecision::Continue;
}
void SVGFormattingContext::run(Box const& box, LayoutMode layout_mode, AvailableSpace const& available_space)
{ {
// NOTE: SVG doesn't have a "formatting context" in the spec, but this is the most // 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. // obvious way to drive SVG layout in our engine at the moment.
@ -223,10 +190,8 @@ void SVGFormattingContext::run(Box const& box, LayoutMode layout_mode, Available
} }
} }
auto viewbox_transform = [&] { m_current_viewbox_transform = m_parent_viewbox_transform;
if (!viewbox.has_value()) if (viewbox.has_value()) {
return m_parent_viewbox_transform;
// FIXME: This should allow just one of width or height to be specified. // 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. // 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()) { if (!svg_box_state.has_definite_width() || !svg_box_state.has_definite_height()) {
@ -241,11 +206,11 @@ void SVGFormattingContext::run(Box const& box, LayoutMode layout_mode, Available
auto viewbox_offset_and_scale = scale_and_align_viewbox_content(preserve_aspect_ratio, *viewbox, { scale_width, scale_height }, svg_box_state); auto viewbox_offset_and_scale = scale_and_align_viewbox_content(preserve_aspect_ratio, *viewbox, { scale_width, scale_height }, svg_box_state);
CSSPixelPoint offset = viewbox_offset_and_scale.offset; CSSPixelPoint offset = viewbox_offset_and_scale.offset;
return Gfx::AffineTransform { m_parent_viewbox_transform }.multiply(Gfx::AffineTransform {} m_current_viewbox_transform = Gfx::AffineTransform { m_current_viewbox_transform }.multiply(Gfx::AffineTransform {}
.translate(offset.to_type<float>()) .translate(offset.to_type<float>())
.scale(viewbox_offset_and_scale.scale_factor_x, viewbox_offset_and_scale.scale_factor_y) .scale(viewbox_offset_and_scale.scale_factor_x, viewbox_offset_and_scale.scale_factor_y)
.translate({ -viewbox->min_x, -viewbox->min_y })); .translate({ -viewbox->min_x, -viewbox->min_y }));
}(); }
if (svg_box_state.has_definite_width() && svg_box_state.has_definite_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. // Scale the box of the viewport based on the parent's viewBox transform.
@ -261,18 +226,6 @@ void SVGFormattingContext::run(Box const& box, LayoutMode layout_mode, Available
svg_box_state.set_has_definite_height(true); svg_box_state.set_has_definite_height(true);
} }
auto root_offset = svg_box_state.offset;
box.for_each_child_of_type<BlockContainer>([&](BlockContainer const& child_box) {
if (is<SVG::SVGForeignObjectElement>(child_box.dom_node())) {
Layout::BlockFormattingContext bfc(m_state, child_box, this);
bfc.run(child_box, LayoutMode::Normal, available_space);
auto& child_state = m_state.get_mutable(child_box);
child_state.set_content_offset(child_state.offset.translated(root_offset));
}
return IterationDecision::Continue;
});
auto viewport_width = [&] { auto viewport_width = [&] {
if (viewbox.has_value()) if (viewbox.has_value())
return CSSPixels::nearest_value_for(viewbox->width); return CSSPixels::nearest_value_for(viewbox->width);
@ -291,165 +244,217 @@ void SVGFormattingContext::run(Box const& box, LayoutMode layout_mode, Available
return CSSPixels {}; return CSSPixels {};
}(); }();
for_each_in_subtree(box, [&](Node const& descendant) { m_available_space = available_space;
if (is<SVGMaskBox>(descendant) || is<SVGClipBox>(descendant)) m_svg_offset = svg_box_state.offset;
return TraversalDecision::SkipChildrenAndContinue; m_viewport_size = { viewport_width, viewport_height };
if (is<SVG::SVGViewport>(descendant.dom_node())) {
// Layout for a nested SVG viewport.
// https://svgwg.org/svg2-draft/coords.html#EstablishingANewSVGViewport.
SVGFormattingContext nested_context(m_state, static_cast<Box const&>(descendant), this, viewbox_transform);
auto& nested_viewport_state = m_state.get_mutable(static_cast<Box const&>(descendant));
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 = descendant.computed_values().x().to_px(descendant, viewport_width); box.for_each_child_of_type<Box>([&](Box const& child) {
auto nested_viewport_y = descendant.computed_values().y().to_px(descendant, viewport_height); layout_svg_element(child);
auto nested_viewport_width = resolve_dimension(descendant, descendant.computed_values().width(), viewport_width);
auto nested_viewport_height = resolve_dimension(descendant, descendant.computed_values().height(), viewport_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(static_cast<Box const&>(descendant), layout_mode, available_space);
return TraversalDecision::SkipChildrenAndContinue;
}
if (is<SVGGraphicsBox>(descendant)) {
auto const& graphics_box = static_cast<SVGGraphicsBox const&>(descendant);
auto& dom_node = const_cast<SVGGraphicsBox&>(graphics_box).dom_node();
auto& graphics_box_state = m_state.get_mutable(graphics_box);
auto svg_transform = dom_node.get_transform();
graphics_box_state.set_computed_svg_transforms(Painting::SVGGraphicsPaintable::ComputedTransforms(viewbox_transform, svg_transform));
auto to_css_pixels_transform = Gfx::AffineTransform {}.multiply(viewbox_transform).multiply(svg_transform);
Gfx::Path path;
if (is<SVGGeometryBox>(descendant)) {
path = static_cast<SVG::SVGGeometryElement&>(dom_node).get_path({ viewport_width, viewport_height });
} else if (is<SVGTextBox>(descendant)) {
auto& text_element = static_cast<SVG::SVGTextPositioningElement&>(dom_node);
auto& font = graphics_box.first_available_font();
auto text_contents = text_element.text_contents();
Utf8View text_utf8 { text_contents };
auto text_width = font.width(text_utf8);
auto text_offset = text_element.get_offset();
// 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();
}
path.move_to(text_offset);
path.text(text_utf8, font);
} else if (is<SVGTextPathBox>(descendant)) {
auto& text_path_element = static_cast<SVG::SVGTextPathElement&>(dom_node);
auto path_or_shape = text_path_element.path_or_shape();
if (!path_or_shape)
return TraversalDecision::Continue;
auto& font = graphics_box.first_available_font();
auto text_contents = text_path_element.text_contents();
Utf8View text_utf8 { text_contents };
auto shape_path = const_cast<SVG::SVGGeometryElement&>(*path_or_shape).get_path({ viewport_width, viewport_height });
path = shape_path.place_text_along(text_utf8, font);
}
auto path_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() * viewbox_transform.x_scale());
path_bounding_box.inflate(stroke_width, stroke_width);
graphics_box_state.set_content_offset(path_bounding_box.top_left());
graphics_box_state.set_content_width(path_bounding_box.width());
graphics_box_state.set_content_height(path_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));
}
return TraversalDecision::Continue;
});
// 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.
box.for_each_in_subtree_of_type<SVGBox>([&](SVGBox const& descendant) {
if (is_container_element(descendant)) {
Gfx::BoundingBox<CSSPixels> bounding_box;
for_each_in_subtree(descendant, [&](Node const& child_of_svg_container) {
if (!is<SVGBox>(child_of_svg_container))
return TraversalDecision::Continue;
// Masks/clips do not change the bounding box of their parents.
if (is<SVGMaskBox>(child_of_svg_container) || is<SVGClipBox>(child_of_svg_container))
return TraversalDecision::SkipChildrenAndContinue;
auto& box_state = m_state.get(static_cast<SVGBox const&>(child_of_svg_container));
bounding_box.add_point(box_state.offset);
bounding_box.add_point(box_state.offset.translated(box_state.content_width(), box_state.content_height()));
return TraversalDecision::Continue;
});
auto& box_state = m_state.get_mutable(descendant);
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);
}
return IterationDecision::Continue;
});
// Lay out masks/clip paths last (as their parent needs to be sized first).
for_each_in_subtree(box, [&](Node const& descendant) {
SVG::SVGUnits content_units {};
if (is<SVGMaskBox>(descendant))
content_units = static_cast<SVGMaskBox const&>(descendant).dom_node().mask_content_units();
else if (is<SVGClipBox>(descendant))
content_units = static_cast<SVGClipBox const&>(descendant).dom_node().clip_path_units();
else
return TraversalDecision::Continue;
// FIXME: Somehow limit <clipPath> contents to: shape elements, <text>, and <use>.
auto& layout_state = m_state.get_mutable(static_cast<Box const&>(descendant));
auto parent_viewbox_transform = viewbox_transform;
if (content_units == SVG::SVGUnits::ObjectBoundingBox) {
auto* parent_node = descendant.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(viewport_width);
layout_state.set_content_height(viewport_height);
}
// Pretend masks/clips are a viewport so we can scale the contents depending on the `contentUnits`.
SVGFormattingContext nested_context(m_state, static_cast<Box const&>(descendant), this, parent_viewbox_transform);
layout_state.set_has_definite_width(true);
layout_state.set_has_definite_height(true);
nested_context.run(static_cast<Box const&>(descendant), layout_mode, available_space);
return TraversalDecision::SkipChildrenAndContinue;
}); });
} }
void SVGFormattingContext::layout_svg_element(Box const& child)
{
if (is<SVG::SVGViewport>(child.dom_node())) {
layout_nested_viewport(child);
} else if (is<SVG::SVGForeignObjectElement>(child.dom_node()) && is<BlockContainer>(child)) {
Layout::BlockFormattingContext bfc(m_state, static_cast<BlockContainer const&>(child), this);
bfc.run(child, LayoutMode::Normal, *m_available_space);
auto& child_state = m_state.get_mutable(child);
child_state.set_content_offset(child_state.offset.translated(m_svg_offset));
} 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, 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(static_cast<Box const&>(viewport), LayoutMode::Normal, *m_available_space);
}
Gfx::Path SVGFormattingContext::compute_path_for_text(SVGTextBox const& text_box)
{
auto& text_element = static_cast<SVG::SVGTextPositioningElement const&>(text_box.dom_node());
auto& font = text_box.first_available_font();
auto text_contents = text_element.text_contents();
Utf8View text_utf8 { text_contents };
auto text_width = font.width(text_utf8);
auto text_offset = text_element.get_offset();
// 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_utf8, font);
return path;
}
Gfx::Path SVGFormattingContext::compute_path_for_text_path(SVGTextPathBox const& text_path_box)
{
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 {};
auto& font = text_path_box.first_available_font();
auto text_contents = text_path_element.text_contents();
Utf8View text_utf8 { text_contents };
auto shape_path = const_cast<SVG::SVGGeometryElement&>(*path_or_shape).get_path(m_viewport_size);
return shape_path.place_text_along(text_utf8, 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 (is<SVGTextBox>(graphics_box)) {
auto& text_box = static_cast<SVGTextBox const&>(graphics_box);
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);
});
} else if (is<SVGTextPathBox>(graphics_box)) {
// FIXME: Support <tspan> in <textPath>.
path = compute_path_for_text_path(static_cast<SVGTextPathBox const&>(graphics_box));
}
auto path_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());
path_bounding_box.inflate(stroke_width, stroke_width);
graphics_box_state.set_content_offset(path_bounding_box.top_left());
graphics_box_state.set_content_width(path_bounding_box.width());
graphics_box_state.set_content_height(path_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));
}
void SVGFormattingContext::layout_graphics_element(SVGGraphicsBox const& graphics_box)
{
auto& graphics_box_state = m_state.get_mutable(graphics_box);
auto svg_transform = const_cast<SVGGraphicsBox&>(graphics_box).dom_node().get_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 {
// 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_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, mask_or_clip, this, parent_viewbox_transform);
layout_state.set_has_definite_width(true);
layout_state.set_has_definite_height(true);
nested_context.run(static_cast<Box const&>(mask_or_clip), LayoutMode::Normal, *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;
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()));
});
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);
}
} }

View file

@ -6,8 +6,12 @@
#pragma once #pragma once
#include <LibGfx/Path.h>
#include <LibWeb/Forward.h> #include <LibWeb/Forward.h>
#include <LibWeb/Layout/FormattingContext.h> #include <LibWeb/Layout/FormattingContext.h>
#include <LibWeb/Layout/SVGSVGBox.h>
#include <LibWeb/Layout/SVGTextBox.h>
#include <LibWeb/Layout/SVGTextPathBox.h>
namespace Web::Layout { namespace Web::Layout {
@ -21,7 +25,22 @@ public:
virtual CSSPixels automatic_content_height() const override; virtual CSSPixels automatic_content_height() const override;
private: private:
void layout_svg_element(Box const&);
void layout_nested_viewport(Box const&);
void layout_container_element(SVGBox const&);
void layout_graphics_element(SVGGraphicsBox const&);
void layout_path_like_element(SVGGraphicsBox const&);
void layout_mask_or_clip(SVGBox const&);
Gfx::Path compute_path_for_text(SVGTextBox const&);
Gfx::Path compute_path_for_text_path(SVGTextPathBox const&);
Gfx::AffineTransform m_parent_viewbox_transform {}; Gfx::AffineTransform m_parent_viewbox_transform {};
Optional<AvailableSpace> m_available_space {};
Gfx::AffineTransform m_current_viewbox_transform {};
CSSPixelSize m_viewport_size {};
CSSPixelPoint m_svg_offset {};
}; };
} }