LibWeb: Support <svg> elements with display: block

There are a couple of things that went into this:

- We now calculate the intrinsic width/height and aspect ratio of <svg>
  elements based on the spec algorithm instead of our previous ad-hoc
  guesswork solution.

- Replaced elements with automatic size and intrinsic aspect ratio but
  no intrinsic dimensions are now sized with the stretch-fit width
  formula.

- We take care to assign both used width and used height to <svg>
  elements before running their SVG formatting contexts. This ensures
  that the inside SVG content is laid out with knowledge of its
  viewport geometry.

- We avoid infinite recursion in tentative_height_for_replaced_element()
  by using the already-calculated used width instead of calling the
  function that calculates the used width (since that may call us right
  back again).
This commit is contained in:
Andreas Kling 2023-05-19 18:14:37 +02:00
parent 28d2e26678
commit f0560fd087
Notes: sideshowbarker 2024-07-16 23:52:10 +09:00
11 changed files with 107 additions and 74 deletions

View File

@ -1,7 +1,7 @@
Viewport <#document> at (0,0) content-size 800x600 children: not-inline
BlockContainer <html> at (0,0) content-size 800x37.835937 [BFC] children: not-inline
BlockContainer <body> at (8,8) content-size 784x21.835937 children: inline
line 0 width: 0, height: 21.835937, bottom: 21.835937, baseline: 100
frag 0 from SVGSVGBox start: 0, length: 0, rect: [8,8 0x0]
SVGSVGBox <svg> at (8,8) content-size 0x0 [SVG] children: not-inline
SVGGeometryBox <rect> at (8,8) content-size 100x100 children: not-inline
BlockContainer <html> at (0,0) content-size 800x800 [BFC] children: not-inline
BlockContainer <body> at (8,8) content-size 784x784 children: inline
line 0 width: 784, height: 784, bottom: 784, baseline: 784
frag 0 from SVGSVGBox start: 0, length: 0, rect: [8,8 784x784]
SVGSVGBox <svg> at (8,8) content-size 784x784 [SVG] children: not-inline
SVGGeometryBox <rect> at (8,8) content-size 784x784 children: not-inline

View File

@ -0,0 +1,7 @@
Viewport <#document> at (0,0) content-size 800x600 children: not-inline
BlockContainer <html> at (0,0) content-size 800x170 [BFC] children: not-inline
BlockContainer <body> at (8,8) content-size 784x154 children: not-inline
SVGSVGBox <svg> at (9,9) content-size 100x50 [SVG] children: not-inline
SVGGeometryBox <rect> at (21.5,21.5) content-size 75x25 children: not-inline
SVGSVGBox <svg> at (9,61) content-size 200x100 [SVG] children: not-inline
SVGGeometryBox <rect> at (34,86) content-size 150x50 children: not-inline

View File

@ -0,0 +1,5 @@
Viewport <#document> at (0,0) content-size 800x600 children: not-inline
BlockContainer <html> at (0,0) content-size 800x800 [BFC] children: not-inline
BlockContainer <body> at (8,8) content-size 784x784 children: not-inline
SVGSVGBox <svg> at (8,8) content-size 784x784 [SVG] children: not-inline
SVGGeometryBox <rect> at (8,8) content-size 784x784 children: not-inline

View File

@ -0,0 +1,6 @@
<!doctype html><style>
svg {
border: 1px solid black;
display: block;
}
</style><svg viewBox="0 0 200 100" height="50"><rect x="25" y="25" width="150" height="50"></svg><svg viewBox="0 0 200 100" width="200"><rect x="25" y="25" width="150" height="50"></svg>

View File

@ -0,0 +1,3 @@
<!doctype html><style>
svg { display: block; }
</style><svg viewBox="0 0 10 10"><rect x=0 y=0 width=10 height=10></svg>

View File

@ -577,6 +577,9 @@ void BlockFormattingContext::layout_block_level_box(Box const& box, BlockContain
place_block_level_element_in_normal_flow_horizontally(box, available_space);
if (box.is_replaced_box())
compute_height(box, available_space);
if (independent_formatting_context) {
// This box establishes a new formatting context. Pass control to it.
independent_formatting_context->run(box, layout_mode, box_state.available_inner_space_or_constraints_from(available_space));

View File

@ -398,6 +398,9 @@ CSSPixels FormattingContext::tentative_width_for_replaced_element(LayoutState co
// then the used value of 'width' is undefined in CSS 2.2. However, it is suggested that, if the containing block's width does not itself
// depend on the replaced element's width, then the used value of 'width' is calculated from the constraint equation used for block-level,
// non-replaced elements in normal flow.
if (computed_height.is_auto() && computed_width.is_auto() && !box.has_intrinsic_width() && !box.has_intrinsic_height() && box.has_intrinsic_aspect_ratio()) {
return calculate_stretch_fit_width(box, available_space.width, state);
}
// Otherwise, if 'width' has a computed value of 'auto', and the element has an intrinsic width, then that intrinsic width is the used value of 'width'.
if (computed_width.is_auto() && box.has_intrinsic_width())
@ -483,7 +486,7 @@ CSSPixels FormattingContext::tentative_height_for_replaced_element(LayoutState c
//
// (used width) / (intrinsic ratio)
if (computed_height.is_auto() && box.has_intrinsic_aspect_ratio())
return compute_width_for_replaced_element(state, box, available_space) / box.intrinsic_aspect_ratio().value();
return state.get(box).content_width() / box.intrinsic_aspect_ratio().value();
// Otherwise, if 'height' has a computed value of 'auto', and the element has an intrinsic height, then that intrinsic height is the used value of 'height'.
if (computed_height.is_auto() && box.has_intrinsic_height())
@ -1412,12 +1415,12 @@ CSSPixels FormattingContext::containing_block_height_for(Box const& box, LayoutS
}
// https://drafts.csswg.org/css-sizing-3/#stretch-fit-size
CSSPixels FormattingContext::calculate_stretch_fit_width(Box const& box, AvailableSize const& available_width) const
CSSPixels FormattingContext::calculate_stretch_fit_width(Box const& box, AvailableSize const& available_width, LayoutState const& state)
{
// The size a box would take if its outer size filled the available space in the given axis;
// in other words, the stretch fit into the available space, if that is definite.
// Undefined if the available space is indefinite.
auto const& box_state = m_state.get(box);
auto const& box_state = state.get(box);
return available_width.to_px()
- box_state.margin_left
- box_state.margin_right
@ -1427,13 +1430,18 @@ CSSPixels FormattingContext::calculate_stretch_fit_width(Box const& box, Availab
- box_state.border_right;
}
CSSPixels FormattingContext::calculate_stretch_fit_width(Box const& box, AvailableSize const& available_width) const
{
return calculate_stretch_fit_width(box, available_width, m_state);
}
// https://drafts.csswg.org/css-sizing-3/#stretch-fit-size
CSSPixels FormattingContext::calculate_stretch_fit_height(Box const& box, AvailableSize const& available_height) const
CSSPixels FormattingContext::calculate_stretch_fit_height(Box const& box, AvailableSize const& available_height, LayoutState const& state)
{
// The size a box would take if its outer size filled the available space in the given axis;
// in other words, the stretch fit into the available space, if that is definite.
// Undefined if the available space is indefinite.
auto const& box_state = m_state.get(box);
auto const& box_state = state.get(box);
return available_height.to_px()
- box_state.margin_top
- box_state.margin_bottom
@ -1443,6 +1451,11 @@ CSSPixels FormattingContext::calculate_stretch_fit_height(Box const& box, Availa
- box_state.border_bottom;
}
CSSPixels FormattingContext::calculate_stretch_fit_height(Box const& box, AvailableSize const& available_height) const
{
return calculate_stretch_fit_height(box, available_height, m_state);
}
bool FormattingContext::should_treat_width_as_auto(Box const& box, AvailableSpace const& available_space)
{
return box.computed_values().width().is_auto()

View File

@ -79,6 +79,9 @@ public:
[[nodiscard]] CSSPixels calculate_stretch_fit_width(Box const&, AvailableSize const&) const;
[[nodiscard]] CSSPixels calculate_stretch_fit_height(Box const&, AvailableSize const&) const;
[[nodiscard]] static CSSPixels calculate_stretch_fit_width(Box const&, AvailableSize const&, LayoutState const&);
[[nodiscard]] static CSSPixels calculate_stretch_fit_height(Box const&, AvailableSize const&, LayoutState const&);
virtual bool can_determine_size_of_child() const { return false; }
virtual void determine_width_of_child(Box const&, AvailableSpace const&) { }
virtual void determine_height_of_child(Box const&, AvailableSpace const&) { }

View File

@ -113,11 +113,11 @@ void InlineFormattingContext::dimension_box_on_line(Box const& box, LayoutMode l
if (is<ReplacedBox>(box)) {
auto& replaced = verify_cast<ReplacedBox>(box);
if (is<SVGSVGBox>(box))
(void)layout_inside(replaced, layout_mode, box_state.available_inner_space_or_constraints_from(*m_available_space));
box_state.set_content_width(compute_width_for_replaced_element(m_state, replaced, *m_available_space));
box_state.set_content_height(compute_height_for_replaced_element(m_state, replaced, *m_available_space));
if (is<SVGSVGBox>(box))
(void)layout_inside(replaced, layout_mode, box_state.available_inner_space_or_constraints_from(*m_available_space));
return;
}

View File

@ -24,70 +24,60 @@ JS::GCPtr<Painting::Paintable> SVGSVGBox::create_paintable() const
void SVGSVGBox::prepare_for_replaced_layout()
{
if (dom_node().has_attribute(HTML::AttributeNames::width) && dom_node().has_attribute(HTML::AttributeNames::height)) {
Optional<CSSPixels> w;
Optional<CSSPixels> h;
auto parsing_context = CSS::Parser::ParsingContext { document() };
auto width = parse_css_value(parsing_context, dom_node().attribute(Web::HTML::AttributeNames::width), CSS::PropertyID::Width).release_value_but_fixme_should_propagate_errors();
if (!width.is_null() && width->has_length())
w = width->to_length().to_px(*this);
// https://www.w3.org/TR/SVG2/coords.html#SizingSVGInCSS
auto height = parse_css_value(parsing_context, dom_node().attribute((HTML::AttributeNames::height)), CSS::PropertyID::Height).release_value_but_fixme_should_propagate_errors();
if (!height.is_null() && height->has_length())
h = height->to_length().to_px(*this);
if (w.has_value() && h.has_value()) {
set_intrinsic_width(*w);
set_intrinsic_height(*h);
set_intrinsic_aspect_ratio(w->value() / h->value());
return;
}
// The intrinsic dimensions must also be determined from the width and height sizing properties.
// If either width or height are not specified, the used value is the initial value 'auto'.
// 'auto' and percentage lengths must not be used to determine an intrinsic width or intrinsic height.
auto const& computed_width = computed_values().width();
if (computed_width.is_length() && !computed_width.contains_percentage()) {
set_intrinsic_width(computed_width.to_px(*this, 0));
}
Optional<CSSPixelRect> united_rect;
auto add_to_united_rect = [&](CSSPixelRect const& rect) {
if (united_rect.has_value())
united_rect = united_rect->united(rect);
else
united_rect = rect;
};
for_each_in_subtree_of_type<SVGGeometryBox>([&](SVGGeometryBox const& geometry_box) {
auto& dom_node = const_cast<SVG::SVGGeometryElement&>(geometry_box.dom_node());
if (dom_node.has_attribute(HTML::AttributeNames::width) && dom_node.has_attribute(HTML::AttributeNames::height)) {
CSSPixelRect rect;
// FIXME: Allow for relative lengths here
rect.set_width(computed_values().width().to_px(*this, 0));
rect.set_height(computed_values().height().to_px(*this, 0));
add_to_united_rect(rect);
return IterationDecision::Continue;
}
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.
auto stroke_width = geometry_box.dom_node().stroke_width().value_or(0);
path_bounding_box.inflate(stroke_width, stroke_width);
auto& maybe_view_box = this->dom_node().view_box();
if (maybe_view_box.has_value()) {
auto view_box = maybe_view_box.value();
CSSPixelRect rect(view_box.min_x, view_box.min_y, view_box.width, view_box.height);
add_to_united_rect(rect);
return IterationDecision::Continue;
}
add_to_united_rect(path_bounding_box);
return IterationDecision::Continue;
});
if (united_rect.has_value()) {
set_intrinsic_width(united_rect->width());
set_intrinsic_height(united_rect->height());
set_intrinsic_aspect_ratio(united_rect->width().value() / united_rect->height().value());
auto const& computed_height = computed_values().height();
if (computed_height.is_length() && !computed_height.contains_percentage()) {
set_intrinsic_height(computed_height.to_px(*this, 0));
}
set_intrinsic_aspect_ratio(calculate_intrinsic_aspect_ratio());
}
Optional<float> SVGSVGBox::calculate_intrinsic_aspect_ratio() const
{
// https://www.w3.org/TR/SVG2/coords.html#SizingSVGInCSS
// The intrinsic aspect ratio must be calculated using the following algorithm. If the algorithm returns null, then there is no intrinsic aspect ratio.
auto const& computed_width = computed_values().width();
auto const& computed_height = computed_values().height();
// 1. If the width and height sizing properties on the svg element are both absolute values:
if (computed_width.is_length() && !computed_width.contains_percentage() && computed_height.is_length() && !computed_height.contains_percentage()) {
auto width = computed_width.to_px(*this, 0);
auto height = computed_height.to_px(*this, 0);
if (width != 0 && height != 0) {
// 1. return width / height
return width.value() / height.value();
}
return {};
}
// FIXME: 2. If an SVG View is active:
// FIXME: 1. let viewbox be the viewbox defined by the active SVG View
// FIXME: 2. return viewbox.width / viewbox.height
// 3. If the viewBox on the svg element is correctly specified:
if (dom_node().view_box().has_value()) {
// 1. let viewbox be the viewbox defined by the viewBox attribute on the svg element
auto const& viewbox = dom_node().view_box().value();
// 2. return viewbox.width / viewbox.height
return viewbox.width / viewbox.height;
}
// 4. return null
return {};
}
}

View File

@ -19,6 +19,7 @@ public:
virtual ~SVGSVGBox() override = default;
SVG::SVGSVGElement& dom_node() { return verify_cast<SVG::SVGSVGElement>(ReplacedBox::dom_node()); }
SVG::SVGSVGElement const& dom_node() const { return verify_cast<SVG::SVGSVGElement>(ReplacedBox::dom_node()); }
virtual bool can_have_children() const override { return true; }
@ -28,6 +29,8 @@ public:
private:
virtual bool is_svg_svg_box() const final { return true; }
[[nodiscard]] Optional<float> calculate_intrinsic_aspect_ratio() const;
};
template<>