Skip to content

Commit

Permalink
LibWeb: Resolve CSS properties that do not affect layout before repaint
Browse files Browse the repository at this point in the history
Recently, we moved the resolution of CSS properties that do not affect
layout to occur within LayoutState::commit(). This decision was a
mistake as it breaks invalidation. With this change, we now re-resolve
all properties that do not affect layout before each repaint.
  • Loading branch information
kalenikaliaksandr committed Feb 2, 2024
1 parent e269526 commit 6d892e6
Show file tree
Hide file tree
Showing 10 changed files with 303 additions and 250 deletions.
16 changes: 16 additions & 0 deletions Tests/LibWeb/Ref/invalidate-css-transform-property.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<!DOCTYPE html>
<link rel="match" href="reference/invalidate-css-transform-property-ref.html" />
<style>
#box {
width: 100px;
height: 100px;
background-color: red;
}
</style>
<div id="box"></div>
<script>
window.onload = function () {
const box = document.getElementById("box");
box.style.transform = "translate(50px, 50px)";
};
</script>
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<!DOCTYPE html>
<style>
#box {
width: 100px;
height: 100px;
background-color: red;
position: relative;
top: 50px;
left: 50px;
}
</style>
<div id="box"></div>
6 changes: 6 additions & 0 deletions Userland/Libraries/LibWeb/DOM/Element.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@
#include <LibWeb/Page/Page.h>
#include <LibWeb/Painting/InlinePaintable.h>
#include <LibWeb/Painting/PaintableBox.h>
#include <LibWeb/Painting/ViewportPaintable.h>
#include <LibWeb/WebIDL/AbstractOperations.h>
#include <LibWeb/WebIDL/DOMException.h>
#include <LibWeb/WebIDL/ExceptionOr.h>
Expand Down Expand Up @@ -909,6 +910,11 @@ JS::NonnullGCPtr<Geometry::DOMRectList> Element::get_client_rects() const
VERIFY(document().navigable());
auto viewport_offset = document().navigable()->viewport_scroll_offset();

if (document().paintable()) {
// NOTE: Make sure CSS transforms are resolved before it is used to calculate the rect position.
const_cast<Painting::ViewportPaintable*>(document().paintable())->resolve_layout_dependent_properties();
}

Gfx::AffineTransform transform;
for (auto const* containing_block = this->layout_node(); containing_block; containing_block = containing_block->containing_block()) {
Gfx::AffineTransform containing_block_transform;
Expand Down
1 change: 1 addition & 0 deletions Userland/Libraries/LibWeb/HTML/Navigable.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2084,6 +2084,7 @@ void Navigable::paint(Painting::RecordingPainter& recording_painter, PaintConfig

HashMap<Painting::PaintableBox const*, Painting::ViewportPaintable::ScrollFrame> scroll_frames;
if (is_traversable()) {
document->paintable()->resolve_layout_dependent_properties();
document->paintable()->assign_scroll_frame_ids(scroll_frames);
document->paintable()->assign_clip_rectangles();
}
Expand Down
248 changes: 0 additions & 248 deletions Userland/Libraries/LibWeb/Layout/LayoutState.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -197,252 +197,6 @@ static void build_paint_tree(Node& node, Painting::Paintable* parent_paintable =
}
}

static Painting::BorderRadiiData normalized_border_radii_data(Layout::Node const& node, CSSPixelRect const& rect, CSS::BorderRadiusData top_left_radius, CSS::BorderRadiusData top_right_radius, CSS::BorderRadiusData bottom_right_radius, CSS::BorderRadiusData bottom_left_radius)
{
Painting::BorderRadiusData bottom_left_radius_px {};
Painting::BorderRadiusData bottom_right_radius_px {};
Painting::BorderRadiusData top_left_radius_px {};
Painting::BorderRadiusData top_right_radius_px {};

bottom_left_radius_px.horizontal_radius = bottom_left_radius.horizontal_radius.to_px(node, rect.width());
bottom_right_radius_px.horizontal_radius = bottom_right_radius.horizontal_radius.to_px(node, rect.width());
top_left_radius_px.horizontal_radius = top_left_radius.horizontal_radius.to_px(node, rect.width());
top_right_radius_px.horizontal_radius = top_right_radius.horizontal_radius.to_px(node, rect.width());

bottom_left_radius_px.vertical_radius = bottom_left_radius.vertical_radius.to_px(node, rect.height());
bottom_right_radius_px.vertical_radius = bottom_right_radius.vertical_radius.to_px(node, rect.height());
top_left_radius_px.vertical_radius = top_left_radius.vertical_radius.to_px(node, rect.height());
top_right_radius_px.vertical_radius = top_right_radius.vertical_radius.to_px(node, rect.height());

// Scale overlapping curves according to https://www.w3.org/TR/css-backgrounds-3/#corner-overlap
// Let f = min(Li/Si), where i ∈ {top, right, bottom, left},
// Si is the sum of the two corresponding radii of the corners on side i,
// and Ltop = Lbottom = the width of the box, and Lleft = Lright = the height of the box.
auto l_top = rect.width();
auto l_bottom = l_top;
auto l_left = rect.height();
auto l_right = l_left;
auto s_top = (top_left_radius_px.horizontal_radius + top_right_radius_px.horizontal_radius);
auto s_right = (top_right_radius_px.vertical_radius + bottom_right_radius_px.vertical_radius);
auto s_bottom = (bottom_left_radius_px.horizontal_radius + bottom_right_radius_px.horizontal_radius);
auto s_left = (top_left_radius_px.vertical_radius + bottom_left_radius_px.vertical_radius);
CSSPixelFraction f = 1;
f = (s_top != 0) ? min(f, l_top / s_top) : f;
f = (s_right != 0) ? min(f, l_right / s_right) : f;
f = (s_bottom != 0) ? min(f, l_bottom / s_bottom) : f;
f = (s_left != 0) ? min(f, l_left / s_left) : f;

// If f < 1, then all corner radii are reduced by multiplying them by f.
if (f < 1) {
top_left_radius_px.horizontal_radius *= f;
top_left_radius_px.vertical_radius *= f;
top_right_radius_px.horizontal_radius *= f;
top_right_radius_px.vertical_radius *= f;
bottom_right_radius_px.horizontal_radius *= f;
bottom_right_radius_px.vertical_radius *= f;
bottom_left_radius_px.horizontal_radius *= f;
bottom_left_radius_px.vertical_radius *= f;
}

return Painting::BorderRadiiData { top_left_radius_px, top_right_radius_px, bottom_right_radius_px, bottom_left_radius_px };
}

void LayoutState::resolve_layout_dependent_properties()
{
// Resolves layout-dependent properties not handled during layout and stores them in the paint tree.
// Properties resolved include:
// - Border radii
// - Box shadows
// - Text shadows
// - Transforms
// - Transform origins

for (auto& it : used_values_per_layout_node) {
auto& used_values = *it.value;
auto& node = const_cast<NodeWithStyle&>(used_values.node());

auto* paintable = node.paintable();
auto const is_inline_paintable = paintable && paintable->is_inline_paintable();
auto const is_paintable_box = paintable && paintable->is_paintable_box();
auto const is_paintable_with_lines = paintable && paintable->is_paintable_with_lines();

// Border radii
if (is_inline_paintable) {
auto& inline_paintable = static_cast<Painting::InlinePaintable&>(*paintable);
auto& fragments = inline_paintable.fragments();

auto const& top_left_border_radius = inline_paintable.computed_values().border_top_left_radius();
auto const& top_right_border_radius = inline_paintable.computed_values().border_top_right_radius();
auto const& bottom_right_border_radius = inline_paintable.computed_values().border_bottom_right_radius();
auto const& bottom_left_border_radius = inline_paintable.computed_values().border_bottom_left_radius();

auto containing_block_position_in_absolute_coordinates = inline_paintable.containing_block()->paintable_box()->absolute_position();
for (size_t i = 0; i < fragments.size(); ++i) {
auto is_first_fragment = i == 0;
auto is_last_fragment = i == fragments.size() - 1;
auto& fragment = fragments[i];
CSSPixelRect absolute_fragment_rect { containing_block_position_in_absolute_coordinates.translated(fragment.offset()), fragment.size() };
if (is_first_fragment) {
auto extra_start_width = inline_paintable.box_model().padding.left;
absolute_fragment_rect.translate_by(-extra_start_width, 0);
absolute_fragment_rect.set_width(absolute_fragment_rect.width() + extra_start_width);
}
if (is_last_fragment) {
auto extra_end_width = inline_paintable.box_model().padding.right;
absolute_fragment_rect.set_width(absolute_fragment_rect.width() + extra_end_width);
}
auto border_radii_data = normalized_border_radii_data(inline_paintable.layout_node(), absolute_fragment_rect, top_left_border_radius, top_right_border_radius, bottom_right_border_radius, bottom_left_border_radius);
fragment.set_border_radii_data(border_radii_data);
}
}

// Border radii
if (is_paintable_box) {
auto& paintable_box = static_cast<Painting::PaintableBox&>(*paintable);

CSSPixelRect const border_rect { 0, 0, used_values.border_box_width(), used_values.border_box_height() };

auto const& border_top_left_radius = node.computed_values().border_top_left_radius();
auto const& border_top_right_radius = node.computed_values().border_top_right_radius();
auto const& border_bottom_right_radius = node.computed_values().border_bottom_right_radius();
auto const& border_bottom_left_radius = node.computed_values().border_bottom_left_radius();

auto radii_data = normalized_border_radii_data(node, border_rect, border_top_left_radius, border_top_right_radius, border_bottom_right_radius, border_bottom_left_radius);
paintable_box.set_border_radii_data(radii_data);
}

// Box shadows
auto const& box_shadow_data = node.computed_values().box_shadow();
if (!box_shadow_data.is_empty()) {
Vector<Painting::ShadowData> resolved_box_shadow_data;
resolved_box_shadow_data.ensure_capacity(box_shadow_data.size());
for (auto const& layer : box_shadow_data) {
resolved_box_shadow_data.empend(
layer.color,
layer.offset_x.to_px(node),
layer.offset_y.to_px(node),
layer.blur_radius.to_px(node),
layer.spread_distance.to_px(node),
layer.placement == CSS::ShadowPlacement::Outer ? Painting::ShadowPlacement::Outer : Painting::ShadowPlacement::Inner);
}

if (paintable && is<Painting::PaintableBox>(*paintable)) {
auto& paintable_box = static_cast<Painting::PaintableBox&>(*paintable);
paintable_box.set_box_shadow_data(move(resolved_box_shadow_data));
} else if (paintable && is<Painting::InlinePaintable>(*paintable)) {
auto& inline_paintable = static_cast<Painting::InlinePaintable&>(*paintable);
inline_paintable.set_box_shadow_data(move(resolved_box_shadow_data));
} else {
VERIFY_NOT_REACHED();
}
}

// Text shadows
if (is_paintable_with_lines) {
auto const& paintable_with_lines = static_cast<Painting::PaintableWithLines const&>(*paintable);
for (auto const& fragment : paintable_with_lines.fragments()) {
auto const& text_shadow = fragment.m_layout_node->computed_values().text_shadow();
if (!text_shadow.is_empty()) {
Vector<Painting::ShadowData> resolved_shadow_data;
resolved_shadow_data.ensure_capacity(text_shadow.size());
for (auto const& layer : text_shadow) {
resolved_shadow_data.empend(
layer.color,
layer.offset_x.to_px(paintable_with_lines.layout_node()),
layer.offset_y.to_px(paintable_with_lines.layout_node()),
layer.blur_radius.to_px(paintable_with_lines.layout_node()),
layer.spread_distance.to_px(paintable_with_lines.layout_node()),
Painting::ShadowPlacement::Outer);
}
const_cast<Painting::PaintableFragment&>(fragment).set_shadows(move(resolved_shadow_data));
}
}
}

// Transform and transform origin
if (is_paintable_box) {
auto& paintable_box = static_cast<Painting::PaintableBox&>(*paintable);
auto const& transformations = paintable_box.computed_values().transformations();
if (!transformations.is_empty()) {
auto matrix = Gfx::FloatMatrix4x4::identity();
for (auto const& transform : transformations)
matrix = matrix * transform.to_matrix(paintable_box).release_value();
paintable_box.set_transform(matrix);
}

auto const& transform_origin = paintable_box.computed_values().transform_origin();
// https://www.w3.org/TR/css-transforms-1/#transform-box
auto transform_box = paintable_box.computed_values().transform_box();
// For SVG elements without associated CSS layout box, the used value for content-box is fill-box and for
// border-box is stroke-box.
// FIXME: This currently detects any SVG element except the <svg> one. Is that correct?
// And is it correct to use `else` below?
if (is<Painting::SVGPaintable>(paintable_box)) {
switch (transform_box) {
case CSS::TransformBox::ContentBox:
transform_box = CSS::TransformBox::FillBox;
break;
case CSS::TransformBox::BorderBox:
transform_box = CSS::TransformBox::StrokeBox;
break;
default:
break;
}
}
// For elements with associated CSS layout box, the used value for fill-box is content-box and for
// stroke-box and view-box is border-box.
else {
switch (transform_box) {
case CSS::TransformBox::FillBox:
transform_box = CSS::TransformBox::ContentBox;
break;
case CSS::TransformBox::StrokeBox:
case CSS::TransformBox::ViewBox:
transform_box = CSS::TransformBox::BorderBox;
break;
default:
break;
}
}

CSSPixelRect reference_box = [&]() {
switch (transform_box) {
case CSS::TransformBox::ContentBox:
// Uses the content box as reference box.
// FIXME: The reference box of a table is the border box of its table wrapper box, not its table box.
return paintable_box.absolute_rect();
case CSS::TransformBox::BorderBox:
// Uses the border box as reference box.
// FIXME: The reference box of a table is the border box of its table wrapper box, not its table box.
return paintable_box.absolute_border_box_rect();
case CSS::TransformBox::FillBox:
// Uses the object bounding box as reference box.
// FIXME: For now we're using the content rect as an approximation.
return paintable_box.absolute_rect();
case CSS::TransformBox::StrokeBox:
// Uses the stroke bounding box as reference box.
// FIXME: For now we're using the border rect as an approximation.
return paintable_box.absolute_border_box_rect();
case CSS::TransformBox::ViewBox:
// Uses the nearest SVG viewport as reference box.
// FIXME: If a viewBox attribute is specified for the SVG viewport creating element:
// - The reference box is positioned at the origin of the coordinate system established by the viewBox attribute.
// - The dimension of the reference box is set to the width and height values of the viewBox attribute.
auto* svg_paintable = paintable_box.first_ancestor_of_type<Painting::SVGSVGPaintable>();
if (!svg_paintable)
return paintable_box.absolute_border_box_rect();
return svg_paintable->absolute_rect();
}
VERIFY_NOT_REACHED();
}();
auto x = reference_box.left() + transform_origin.x.to_px(node, reference_box.width());
auto y = reference_box.top() + transform_origin.y.to_px(node, reference_box.height());
paintable_box.set_transform_origin({ x, y });
paintable_box.set_transform_origin({ x, y });
}
}
}

void LayoutState::commit(Box& root)
{
// Only the top-level LayoutState should ever be committed.
Expand Down Expand Up @@ -599,8 +353,6 @@ void LayoutState::commit(Box& root)
auto const& box = static_cast<Layout::Box const&>(used_values.node());
measure_scrollable_overflow(box);
}

resolve_layout_dependent_properties();
}

void LayoutState::UsedValues::set_node(NodeWithStyle& node, UsedValues const* containing_block_used_values)
Expand Down
1 change: 0 additions & 1 deletion Userland/Libraries/LibWeb/Layout/LayoutState.h
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,6 @@ struct LayoutState {

private:
void resolve_relative_positions();
void resolve_layout_dependent_properties();
};

}
12 changes: 12 additions & 0 deletions Userland/Libraries/LibWeb/Painting/Paintable.h
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,18 @@ class Paintable
return TraversalDecision::Continue;
}

template<typename Callback>
TraversalDecision for_each_in_inclusive_subtree(Callback callback)
{
if (auto decision = callback(*this); decision != TraversalDecision::Continue)
return decision;
for (auto* child = first_child(); child; child = child->next_sibling()) {
if (child->for_each_in_inclusive_subtree(callback) == TraversalDecision::Break)
return TraversalDecision::Break;
}
return TraversalDecision::Continue;
}

template<typename Callback>
TraversalDecision for_each_in_inclusive_subtree(Callback callback) const
{
Expand Down
2 changes: 1 addition & 1 deletion Userland/Libraries/LibWeb/Painting/PaintableFragment.h
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
namespace Web::Painting {

class PaintableFragment {
friend struct Layout::LayoutState;
friend class ViewportPaintable;

public:
explicit PaintableFragment(Layout::LineBoxFragment const&);
Expand Down
Loading

0 comments on commit 6d892e6

Please sign in to comment.