LibWeb: Fix overflow clip when "complicated" CSS transform is used

Overflow clipping is currently implemented as:
1. Create clip frame for each box with hidden overflow
2. Calculate clip rect for each clip frame by intersecting padding boxes
   of all boxes with hidden overflow in containing block chain
3. Assign enclosing clip frame (closest clip frame in containing block
   chain) to each PaintableBox
4. Apply clip rect of enclosing clip frame in Paintable::before_paint()

It breaks when any CSS transform other than simple translation is lying
between box with hidden overflow and a clipped box, because clip
rectangle will be applied when transform has already changed.

The fix is implemented by relying on the following rule:
"For elements whose layout is governed by the CSS box model, any value
other than none for the transform also causes the element to establish
a containing block for all descendants."

It means everything nested into a stacking context with CSS transform
can't escape its clip, so it's safe to apply its clip for all children.
This commit is contained in:
Aliaksandr Kalenik 2024-07-31 22:26:43 +03:00 committed by Andreas Kling
parent e75791d9e1
commit bbc89a383d
Notes: github-actions[bot] 2024-08-01 10:30:12 +00:00
8 changed files with 122 additions and 20 deletions

View File

@ -0,0 +1,46 @@
<!DOCTYPE html>
<head>
<link rel="match" href="reference/scrollable-contains-rotated-boxes-ref.html" />
<style>
body {
margin: 0;
padding: 0;
height: 1000px;
}
#scroll-container {
width: 300px;
height: 300px;
overflow-y: scroll;
background-color: #fff;
border: 10px solid blueviolet;
}
.rotated-box {
width: 100px;
height: 100px;
margin: 10px;
border: 5px solid magenta;
background-color: #3498db;
overflow: hidden;
}
.nested-box {
width: 200px;
height: 200px;
background-color: crimson;
}
</style>
</head>
<body>
<div id="scroll-container">
<div class="rotated-box"><div class="nested-box"></div></div>
<div class="rotated-box"><div class="nested-box"></div></div>
<div class="rotated-box"><div class="nested-box"></div></div>
<div class="rotated-box"><div class="nested-box"></div></div>
<div class="rotated-box"><div class="nested-box"></div></div>
<div class="rotated-box"><div class="nested-box"></div></div>
<div class="rotated-box"><div class="nested-box"></div></div>
</div>
</body>
<script>
const scrollContainer = document.getElementById("scroll-container");
scrollContainer.scrollTop = 100;
</script>

View File

@ -0,0 +1,47 @@
<!DOCTYPE html>
<head>
<link rel="match" href="reference/scrollable-contains-rotated-boxes-ref.html" />
<style>
body {
margin: 0;
padding: 0;
height: 1000px;
}
#scroll-container {
width: 300px;
height: 300px;
overflow-y: scroll;
background-color: #fff;
border: 10px solid blueviolet;
}
.rotated-box {
width: 100px;
height: 100px;
margin: 10px;
border: 5px solid magenta;
background-color: #3498db;
transform: rotate(90deg);
overflow: hidden;
}
.nested-box {
width: 200px;
height: 200px;
background-color: crimson;
}
</style>
</head>
<body>
<div id="scroll-container">
<div class="rotated-box"><div class="nested-box"></div></div>
<div class="rotated-box"><div class="nested-box"></div></div>
<div class="rotated-box"><div class="nested-box"></div></div>
<div class="rotated-box"><div class="nested-box"></div></div>
<div class="rotated-box"><div class="nested-box"></div></div>
<div class="rotated-box"><div class="nested-box"></div></div>
<div class="rotated-box"><div class="nested-box"></div></div>
</div>
</body>
<script>
const scrollContainer = document.getElementById("scroll-container");
scrollContainer.scrollTop = 100;
</script>

View File

@ -172,6 +172,8 @@ public:
// https://www.w3.org/TR/CSS22/visuren.html#positioning-scheme
bool is_in_flow() const { return !is_out_of_flow(); }
bool has_css_transform() const { return computed_values().transformations().size() > 0; }
protected:
Node(DOM::Document&, DOM::Node*);

View File

@ -24,16 +24,8 @@ Optional<CSSPixelPoint> ClippableAndScrollable::enclosing_scroll_frame_offset()
Optional<CSSPixelRect> ClippableAndScrollable::clip_rect() const
{
if (m_enclosing_clip_frame) {
auto rect = m_enclosing_clip_frame->rect();
// NOTE: Since the painting command executor applies a CSS transform and the clip rect is calculated
// with this transform taken into account, we need to remove the transform from the clip rect.
// Otherwise, the transform will be applied twice to the clip rect.
// Similarly, for hit-testing, the transform must be removed from the clip rectangle since the position
// includes the transform.
rect.translate_by(-m_combined_css_transform.translation().to_type<CSSPixels>());
return rect;
}
if (m_enclosing_clip_frame)
return m_enclosing_clip_frame->rect();
return {};
}

View File

@ -158,13 +158,12 @@ CSSPixelRect PaintableBox::compute_absolute_rect() const
return rect;
}
CSSPixelRect PaintableBox::compute_absolute_padding_rect_with_css_transform_applied() const
CSSPixelRect PaintableBox::compute_absolute_padding_rect_with_scroll_offset_applied() const
{
auto rect = absolute_rect();
auto scroll_offset = this->enclosing_scroll_frame_offset();
if (scroll_offset.has_value())
rect.translate_by(scroll_offset.value());
rect.translate_by(combined_css_transform().translation().to_type<CSSPixels>());
CSSPixelRect padding_rect;
padding_rect.set_x(rect.x() - box_model().padding.left);
@ -233,7 +232,9 @@ void PaintableBox::before_paint(PaintContext& context, [[maybe_unused]] PaintPha
if (!is_visible())
return;
apply_clip_overflow_rect(context, phase);
if (!has_css_transform()) {
apply_clip_overflow_rect(context, phase);
}
apply_scroll_offset(context, phase);
}
@ -243,7 +244,9 @@ void PaintableBox::after_paint(PaintContext& context, [[maybe_unused]] PaintPhas
return;
reset_scroll_offset(context, phase);
clear_clip_overflow_rect(context, phase);
if (!has_css_transform()) {
clear_clip_overflow_rect(context, phase);
}
}
bool PaintableBox::is_scrollable(ScrollDirection direction) const
@ -533,14 +536,12 @@ void PaintableBox::apply_clip_overflow_rect(PaintContext& context, PaintPhase ph
context.display_list_recorder().save();
context.display_list_recorder().add_clip_rect(context.enclosing_device_rect(overflow_clip_rect).to_type<int>());
auto const& border_radii_clips = this->border_radii_clips();
auto const& combined_transform = combined_css_transform();
for (size_t corner_clip_index = 0; corner_clip_index < border_radii_clips.size(); ++corner_clip_index) {
auto const& corner_clip = border_radii_clips[corner_clip_index];
auto corners = corner_clip.radii.as_corners(context);
if (!corners.has_any_radius())
continue;
auto rect = corner_clip.rect.translated(-combined_transform.translation().to_type<CSSPixels>());
context.display_list_recorder().add_rounded_rect_clip(corner_clip.radii.as_corners(context), context.rounded_device_rect(rect).to_type<int>(), CornerClip::Outside);
context.display_list_recorder().add_rounded_rect_clip(corner_clip.radii.as_corners(context), context.rounded_device_rect(corner_clip.rect).to_type<int>(), CornerClip::Outside);
}
}
}

View File

@ -113,6 +113,8 @@ public:
[[nodiscard]] bool has_scrollable_overflow() const { return m_overflow_data->has_scrollable_overflow; }
bool has_css_transform() const { return computed_values().transformations().size() > 0; }
[[nodiscard]] Optional<CSSPixelRect> scrollable_overflow_rect() const
{
if (!m_overflow_data.has_value())
@ -199,7 +201,7 @@ public:
void set_outline_offset(CSSPixels outline_offset) { m_outline_offset = outline_offset; }
CSSPixels outline_offset() const { return m_outline_offset; }
CSSPixelRect compute_absolute_padding_rect_with_css_transform_applied() const;
CSSPixelRect compute_absolute_padding_rect_with_scroll_offset_applied() const;
Optional<CSSPixelRect> get_clip_rect() const;

View File

@ -316,12 +316,18 @@ void StackingContext::paint(PaintContext& context) const
}
}
auto has_css_transform = paintable().is_paintable_box() && paintable_box().has_css_transform();
context.display_list_recorder().save();
if (has_css_transform) {
paintable_box().apply_clip_overflow_rect(context, PaintPhase::Foreground);
}
if (paintable().is_paintable_box() && paintable_box().scroll_frame_id().has_value())
context.display_list_recorder().set_scroll_frame_id(*paintable_box().scroll_frame_id());
context.display_list_recorder().push_stacking_context(push_stacking_context_params);
paint_internal(context);
context.display_list_recorder().pop_stacking_context();
if (has_css_transform)
paintable_box().clear_clip_overflow_rect(context, PaintPhase::Foreground);
context.display_list_recorder().restore();
}

View File

@ -119,6 +119,9 @@ void ViewportPaintable::assign_clip_frames()
}
break;
}
if (block->has_css_transform()) {
break;
}
}
return TraversalDecision::Continue;
});
@ -164,14 +167,17 @@ void ViewportPaintable::refresh_clip_state()
};
clip_frame.clear_border_radii_clips();
if (overflow_x != CSS::Overflow::Visible && overflow_y != CSS::Overflow::Visible) {
auto overflow_clip_rect = paintable_box.compute_absolute_padding_rect_with_css_transform_applied();
auto overflow_clip_rect = paintable_box.compute_absolute_padding_rect_with_scroll_offset_applied();
add_border_radii_clip(overflow_clip_rect, paintable_box.normalized_border_radii_data(ShrinkRadiiForBorders::Yes));
for (auto const* block = &paintable_box.layout_box(); !block->is_viewport(); block = block->containing_block()) {
if (block->has_css_transform()) {
break;
}
auto const& block_paintable_box = *block->paintable_box();
auto block_overflow_x = block_paintable_box.computed_values().overflow_x();
auto block_overflow_y = block_paintable_box.computed_values().overflow_y();
if (block_overflow_x != CSS::Overflow::Visible && block_overflow_y != CSS::Overflow::Visible) {
auto rect = block_paintable_box.compute_absolute_padding_rect_with_css_transform_applied();
auto rect = block_paintable_box.compute_absolute_padding_rect_with_scroll_offset_applied();
overflow_clip_rect.intersect(rect);
add_border_radii_clip(rect, block_paintable_box.normalized_border_radii_data(ShrinkRadiiForBorders::Yes));
}