LibWeb: Allow <svg> to establish a stacking context

83b6bc4 went too far by forbidding SVGSVGElement from establishing a
stacking context. This element type does follow the behavior of CSS
boxes, unlike inner SVG elements like `<rect>`, `<circle>`, etc., which
are not supposed to be aware of concepts like stacking contexts,
overflow clipping, scroll offsets, etc.

This change allows us to delete overrides of `before_paint()` and
`after_paint()` in SVGPaintable and SVGSVGPaintable, because display
list recording code has been rearranged to take care of clipping and
scrolling before recursing into SVGSVGPaintable descendants.

`Screenshot/images/css-transform-box-ref.png` expectation is updated and
fixes a bug where a rectangle at the very bottom of the page was not
clipped correctly.
`Screenshot/images/svg-filters-lb-website-ref.png` has a more subtle
difference, but if you look closely, you’ll see it matches other
browsers more closely now.
This commit is contained in:
Aliaksandr Kalenik 2025-07-12 00:17:38 +02:00 committed by Jelle Raaijmakers
parent 79293a3cbc
commit 910fd426a2
Notes: github-actions[bot] 2025-07-12 09:02:17 +00:00
10 changed files with 87 additions and 60 deletions

View file

@ -174,7 +174,7 @@ bool Node::establishes_stacking_context() const
if (!has_style()) if (!has_style())
return false; return false;
if (is_svg_box() || is_svg_svg_box()) if (is_svg_box())
return false; return false;
// We make a stacking context for the viewport. Painting and hit testing starts from here. // We make a stacking context for the viewport. Painting and hit testing starts from here.

View file

@ -17,26 +17,6 @@ SVGPaintable::SVGPaintable(Layout::SVGBox const& layout_box)
{ {
} }
void SVGPaintable::before_paint(PaintContext& context, PaintPhase phase) const
{
if (!is_visible())
return;
if (!has_css_transform()) {
apply_clip_overflow_rect(context, phase);
}
apply_scroll_offset(context);
}
void SVGPaintable::after_paint(PaintContext& context, PaintPhase phase) const
{
if (!is_visible())
return;
reset_scroll_offset(context);
if (!has_css_transform()) {
clear_clip_overflow_rect(context, phase);
}
}
Layout::SVGBox const& SVGPaintable::layout_box() const Layout::SVGBox const& SVGPaintable::layout_box() const
{ {
return static_cast<Layout::SVGBox const&>(layout_node()); return static_cast<Layout::SVGBox const&>(layout_node());

View file

@ -22,9 +22,6 @@ protected:
SVGPaintable(Layout::SVGBox const&); SVGPaintable(Layout::SVGBox const&);
virtual void before_paint(PaintContext&, PaintPhase) const override;
virtual void after_paint(PaintContext&, PaintPhase) const override;
virtual CSSPixelRect compute_absolute_rect() const override; virtual CSSPixelRect compute_absolute_rect() const override;
}; };

View file

@ -24,26 +24,6 @@ SVGSVGPaintable::SVGSVGPaintable(Layout::SVGSVGBox const& layout_box)
{ {
} }
void SVGSVGPaintable::before_paint(PaintContext& context, PaintPhase phase) const
{
if (!is_visible())
return;
if (!has_css_transform()) {
apply_clip_overflow_rect(context, phase);
}
apply_scroll_offset(context);
}
void SVGSVGPaintable::after_paint(PaintContext& context, PaintPhase phase) const
{
if (!is_visible())
return;
reset_scroll_offset(context);
if (!has_css_transform()) {
clear_clip_overflow_rect(context, phase);
}
}
Layout::SVGSVGBox const& SVGSVGPaintable::layout_box() const Layout::SVGSVGBox const& SVGSVGPaintable::layout_box() const
{ {
return static_cast<Layout::SVGSVGBox const&>(layout_node()); return static_cast<Layout::SVGSVGBox const&>(layout_node());
@ -107,10 +87,7 @@ void SVGSVGPaintable::paint_svg_box(PaintContext& context, PaintableBox const& s
} }
if (!skip_painting) { if (!skip_painting) {
svg_box.before_paint(context, PaintPhase::Foreground);
svg_box.paint(context, PaintPhase::Foreground); svg_box.paint(context, PaintPhase::Foreground);
svg_box.after_paint(context, PaintPhase::Foreground);
paint_descendants(context, svg_box, phase); paint_descendants(context, svg_box, phase);
} }

View file

@ -23,9 +23,6 @@ public:
static void paint_svg_box(PaintContext& context, PaintableBox const& svg_box, PaintPhase phase); static void paint_svg_box(PaintContext& context, PaintableBox const& svg_box, PaintPhase phase);
static void paint_descendants(PaintContext& context, PaintableBox const& paintable, PaintPhase phase); static void paint_descendants(PaintContext& context, PaintableBox const& paintable, PaintPhase phase);
virtual void before_paint(PaintContext&, PaintPhase) const override;
virtual void after_paint(PaintContext&, PaintPhase) const override;
protected: protected:
SVGSVGPaintable(Layout::SVGSVGBox const&); SVGSVGPaintable(Layout::SVGSVGBox const&);

View file

@ -108,9 +108,9 @@ void StackingContext::paint_svg(PaintContext& context, PaintableBox const& paint
if (phase != PaintPhase::Foreground) if (phase != PaintPhase::Foreground)
return; return;
paintable.before_paint(context, PaintPhase::Foreground);
paint_node(paintable, context, PaintPhase::Background); paint_node(paintable, context, PaintPhase::Background);
paint_node(paintable, context, PaintPhase::Border); paint_node(paintable, context, PaintPhase::Border);
paintable.before_paint(context, PaintPhase::Foreground);
SVGSVGPaintable::paint_svg_box(context, paintable, phase); SVGSVGPaintable::paint_svg_box(context, paintable, phase);
paintable.after_paint(context, PaintPhase::Foreground); paintable.after_paint(context, PaintPhase::Foreground);
} }
@ -118,14 +118,14 @@ void StackingContext::paint_svg(PaintContext& context, PaintableBox const& paint
void StackingContext::paint_descendants(PaintContext& context, Paintable const& paintable, StackingContextPaintPhase phase) void StackingContext::paint_descendants(PaintContext& context, Paintable const& paintable, StackingContextPaintPhase phase)
{ {
paintable.for_each_child([&context, phase](auto& child) { paintable.for_each_child([&context, phase](auto& child) {
if (child.has_stacking_context())
return IterationDecision::Continue;
if (child.layout_node().is_svg_svg_box()) { if (child.layout_node().is_svg_svg_box()) {
paint_svg(context, static_cast<PaintableBox const&>(child), to_paint_phase(phase)); paint_svg(context, static_cast<PaintableBox const&>(child), to_paint_phase(phase));
return IterationDecision::Continue; return IterationDecision::Continue;
} }
if (child.has_stacking_context())
return IterationDecision::Continue;
// NOTE: Grid specification https://www.w3.org/TR/css-grid-2/#z-order says that grid items should be treated // NOTE: Grid specification https://www.w3.org/TR/css-grid-2/#z-order says that grid items should be treated
// the same way as CSS2 defines for inline-blocks: // the same way as CSS2 defines for inline-blocks:
// "For each one of these, treat the element as if it created a new stacking context, but any positioned // "For each one of these, treat the element as if it created a new stacking context, but any positioned
@ -199,10 +199,7 @@ void StackingContext::paint_descendants(PaintContext& context, Paintable const&
void StackingContext::paint_child(PaintContext& context, StackingContext const& child) void StackingContext::paint_child(PaintContext& context, StackingContext const& child)
{ {
VERIFY(!child.paintable_box().layout_node().is_svg_box()); VERIFY(!child.paintable_box().layout_node().is_svg_box());
VERIFY(!child.paintable_box().layout_node().is_svg_svg_box());
const_cast<StackingContext&>(child).set_last_paint_generation_id(context.paint_generation_id()); const_cast<StackingContext&>(child).set_last_paint_generation_id(context.paint_generation_id());
child.paint(context); child.paint(context);
} }
@ -210,10 +207,27 @@ void StackingContext::paint_internal(PaintContext& context) const
{ {
VERIFY(!paintable_box().layout_node().is_svg_box()); VERIFY(!paintable_box().layout_node().is_svg_box());
if (paintable_box().layout_node().is_svg_svg_box()) { if (paintable_box().layout_node().is_svg_svg_box()) {
paint_svg(context, paintable_box(), PaintPhase::Foreground); auto const& svg_svg_paintable = static_cast<SVGSVGPaintable const&>(paintable_box());
paint_node(svg_svg_paintable, context, PaintPhase::Background);
paint_node(svg_svg_paintable, context, PaintPhase::Border);
svg_svg_paintable.before_paint(context, PaintPhase::Foreground);
SVGSVGPaintable::paint_descendants(context, svg_svg_paintable, PaintPhase::Foreground);
svg_svg_paintable.after_paint(context, PaintPhase::Foreground);
paint_node(svg_svg_paintable, context, PaintPhase::Outline);
if (context.should_paint_overlay()) {
paint_node(svg_svg_paintable, context, PaintPhase::Overlay);
paint_descendants(context, svg_svg_paintable, StackingContextPaintPhase::FocusAndOverlay);
}
return; return;
} }
context.display_list_recorder().push_scroll_frame_id({});
ScopeGuard restore_scroll_frame_id([&] {
context.display_list_recorder().pop_scroll_frame_id();
});
// For a more elaborate description of the algorithm, see CSS 2.1 Appendix E // For a more elaborate description of the algorithm, see CSS 2.1 Appendix E
// Draw the background and borders for the context root (steps 1, 2) // Draw the background and borders for the context root (steps 1, 2)
paint_node(paintable_box(), context, PaintPhase::Background); paint_node(paintable_box(), context, PaintPhase::Background);
@ -357,9 +371,7 @@ void StackingContext::paint(PaintContext& context) const
} }
} }
context.display_list_recorder().push_scroll_frame_id({});
paint_internal(context); paint_internal(context);
context.display_list_recorder().pop_scroll_frame_id();
if (filter.has_value()) { if (filter.has_value()) {
context.display_list_recorder().restore(); context.display_list_recorder().restore();

View file

@ -0,0 +1,32 @@
<!DOCTYPE html>
<style>
#container {
display: flow-root;
position: relative;
width: 200px;
background: #ffffe0;
}
#overlay {
position: absolute;
top: 0;
left: 0;
width: 200px;
height: 40px;
background: crimson;
z-index: 0;
}
#sc {
width: 100px;
height: 100px;
margin-top: 30px;
position: relative;
z-index: 1;
background: lightgreen;
}
</style>
<div id="container">
<div id="overlay"></div>
<div id="sc"></div>
</div>

View file

@ -0,0 +1,32 @@
<!DOCTYPE html>
<link rel="match" href="../../expected/css/svg-element-should-be-allowed-to-establish-stacking-context.html" />
<style>
#container {
position: relative;
width: 200px;
background: #ffffe0;
}
#overlay {
position: absolute;
top: 0;
left: 0;
width: 200px;
height: 40px;
background: crimson;
z-index: 0;
}
svg {
width: 100px;
height: 100px;
margin-top: 30px;
position: relative;
z-index: 1;
background: lightgreen;
}
</style>
<div id="container">
<div id="overlay"></div>
<svg></svg>
</div>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

After

Width:  |  Height:  |  Size: 6.3 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 102 KiB

After

Width:  |  Height:  |  Size: 129 KiB

Before After
Before After