1
Fork 0
mirror of https://github.com/RGBCube/serenity synced 2025-05-31 09:58:11 +00:00

LibWeb: Remove SVG sizing hack and fix viewbox scaling

Previously, if you had an SVG with a viewbox and a definite width
and height, then all SVGGeometryBox boxes within that SVG would
have a width and height set to the size of the parent SVG.

This broke hit testing for SVG paths, and didn't make much sense.
It seems like the SVG sizing hack was patching over the incorrect
logic in viewbox_scaling() and the incorrect path sizing (which was
never reached).

Before this change the view box scaling was:
	element_dimension / viewbox_dimension

Which only seemed to work because of the SVG sizing hack that made
all paths the size of the containing SVG.

After this change SVGGeometryBoxes are (in most cases) sized correctly
based on their bounding boxes, which allows hit testing to function,
and the view box scaling is updated now to:
	  containing_SVG_dimension / viewbox_dimension

Which works with one less hack :^)

This now also handles centering the viewbox within the parent SVG
element and applying any tranforms to the bounding box. This still
a bit ad-hoc, but much more closely matches other browsers now.
This commit is contained in:
MacDue 2023-04-10 12:28:55 +01:00 committed by Andreas Kling
parent 570d71f869
commit 3484db0dc1

View file

@ -2,6 +2,7 @@
* Copyright (c) 2021, Andreas Kling <kling@serenityos.org>
* Copyright (c) 2022, Sam Atkins <atkinssj@serenityos.org>
* Copyright (c) 2022, Tobias Christiansen <tobyase@serenityos.org>
* Copyright (c) 2023, MacDue <macdue@dueutil.tech>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
@ -37,7 +38,8 @@ void SVGFormattingContext::run(Box const& box, LayoutMode, [[maybe_unused]] Avai
auto& svg_svg_element = verify_cast<SVG::SVGSVGElement>(*box.dom_node());
auto root_offset = m_state.get(box).offset;
auto svg_box_state = m_state.get(box);
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())) {
@ -53,48 +55,41 @@ void SVGFormattingContext::run(Box const& box, LayoutMode, [[maybe_unused]] Avai
box.for_each_in_subtree_of_type<SVGBox>([&](SVGBox const& descendant) {
if (is<SVGGeometryBox>(descendant)) {
auto const& geometry_box = static_cast<SVGGeometryBox const&>(descendant);
auto& geometry_box_state = m_state.get_mutable(geometry_box);
auto& dom_node = const_cast<SVGGeometryBox&>(geometry_box).dom_node();
auto& svg_svg_state = m_state.get(static_cast<Box const&>(*svg_svg_element.layout_node()));
if (svg_svg_state.has_definite_width() && svg_svg_state.has_definite_height()) {
geometry_box_state.set_content_offset({ 0, 0 });
geometry_box_state.set_content_width(svg_svg_state.content_width());
geometry_box_state.set_content_height(svg_svg_state.content_height());
return IterationDecision::Continue;
}
// FIXME: Allow for one of {width, height} to not be specified}
if (svg_svg_element.has_attribute(HTML::AttributeNames::width)) {
}
if (svg_svg_element.has_attribute(HTML::AttributeNames::height)) {
}
auto& path = dom_node.get_path();
auto path_bounding_box = path.bounding_box().to_type<CSSPixels>();
// Stroke increases the path's size by stroke_width/2 per side.
CSSPixels stroke_width = geometry_box.dom_node().stroke_width().value_or(0);
path_bounding_box.inflate(stroke_width, stroke_width);
auto transform = dom_node.get_transform();
auto& maybe_view_box = svg_svg_element.view_box();
float viewbox_scale = 1.0f;
CSSPixelPoint offset {};
if (maybe_view_box.has_value()) {
auto view_box = maybe_view_box.value();
CSSPixelPoint viewbox_offset = { view_box.min_x, view_box.min_y };
geometry_box_state.set_content_offset(path_bounding_box.top_left() + viewbox_offset);
// 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("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().value() / view_box.width : 1;
auto scale_height = svg_box_state.has_definite_height() ? svg_box_state.content_height().value() / view_box.height : 1;
viewbox_scale = min(scale_width, scale_height);
geometry_box_state.set_content_width(view_box.width);
geometry_box_state.set_content_height(view_box.height);
// Center the viewbox within the SVG element:
if (svg_box_state.has_definite_width())
offset.translate_by((svg_box_state.content_width() - (view_box.width * viewbox_scale)) / 2, 0);
if (svg_box_state.has_definite_height())
offset.translate_by(0, (svg_box_state.content_height() - (view_box.height * viewbox_scale)) / 2);
return IterationDecision::Continue;
transform = Gfx::AffineTransform {}.scale(viewbox_scale, viewbox_scale).translate({ -view_box.min_x, -view_box.min_y }).multiply(transform);
}
geometry_box_state.set_content_offset(path_bounding_box.top_left());
// Stroke increases the path's size by stroke_width/2 per side.
auto path_bounding_box = transform.map(path.bounding_box()).to_type<CSSPixels>();
CSSPixels stroke_width = geometry_box.dom_node().stroke_width().value_or(0);
path_bounding_box.inflate(stroke_width, stroke_width);
geometry_box_state.set_content_offset(path_bounding_box.top_left() + offset);
geometry_box_state.set_content_width(path_bounding_box.width());
geometry_box_state.set_content_height(path_bounding_box.height());
}