mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2025-01-22 17:24:48 -05:00
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:
parent
e75791d9e1
commit
bbc89a383d
Notes:
github-actions[bot]
2024-08-01 10:30:12 +00:00
Author: https://github.com/kalenikaliaksandr Commit: https://github.com/LadybirdBrowser/ladybird/commit/bbc89a383de Pull-request: https://github.com/LadybirdBrowser/ladybird/pull/917 Reviewed-by: https://github.com/awesomekling ✅
8 changed files with 122 additions and 20 deletions
|
@ -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>
|
47
Tests/LibWeb/Ref/scrollable-contains-rotated-boxes.html
Normal file
47
Tests/LibWeb/Ref/scrollable-contains-rotated-boxes.html
Normal 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>
|
|
@ -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*);
|
||||
|
||||
|
|
|
@ -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 {};
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue