blob: a8fb20385e45adfbf584ef893b2c6f5167190deb [file] [log] [blame]
// Copyright 2016 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "third_party/blink/renderer/core/paint/box_paint_invalidator.h"
#include "third_party/blink/renderer/core/frame/local_frame_view.h"
#include "third_party/blink/renderer/core/html/html_frame_owner_element.h"
#include "third_party/blink/renderer/core/html_names.h"
#include "third_party/blink/renderer/core/layout/layout_view.h"
#include "third_party/blink/renderer/core/paint/paint_and_raster_invalidation_test.h"
#include "third_party/blink/renderer/core/paint/paint_controller_paint_test.h"
#include "third_party/blink/renderer/core/paint/paint_invalidator.h"
#include "third_party/blink/renderer/core/paint/paint_layer.h"
#include "third_party/blink/renderer/core/testing/core_unit_test_helper.h"
#include "third_party/blink/renderer/platform/graphics/graphics_layer.h"
#include "third_party/blink/renderer/platform/graphics/paint/raster_invalidation_tracking.h"
#include "third_party/blink/renderer/platform/testing/runtime_enabled_features_test_helpers.h"
namespace blink {
using ::testing::UnorderedElementsAre;
class BoxPaintInvalidatorTest : public PaintAndRasterInvalidationTest {
public:
BoxPaintInvalidatorTest() = default;
protected:
PaintInvalidationReason ComputePaintInvalidationReason(
LayoutBox& box,
const PhysicalOffset& old_paint_offset) {
FragmentData fragment_data;
PaintInvalidatorContext context;
context.old_paint_offset = old_paint_offset;
fragment_data_.SetPaintOffset(box.FirstFragment().PaintOffset());
context.fragment_data = &fragment_data_;
return BoxPaintInvalidator(box, context).ComputePaintInvalidationReason();
}
// This applies when the target is set to meet conditions that we should do
// full paint invalidation instead of incremental invalidation on geometry
// change.
void ExpectFullPaintInvalidationOnGeometryChange(const char* test_title) {
SCOPED_TRACE(test_title);
UpdateAllLifecyclePhasesForTest();
auto& target = *GetDocument().getElementById("target");
auto& box = *target.GetLayoutBox();
auto paint_offset = box.FirstFragment().PaintOffset();
box.SetShouldCheckForPaintInvalidation();
// No geometry change.
EXPECT_EQ(PaintInvalidationReason::kNone,
ComputePaintInvalidationReason(box, paint_offset));
target.setAttribute(
html_names::kStyleAttr,
target.getAttribute(html_names::kStyleAttr) + "; width: 200px");
if (RuntimeEnabledFeatures::CompositeAfterPaintEnabled()) {
GetDocument().View()->UpdateLifecycleToLayoutClean(
DocumentUpdateReason::kTest);
} else {
GetDocument().View()->UpdateLifecycleToCompositingInputsClean(
DocumentUpdateReason::kTest);
}
EXPECT_EQ(PaintInvalidationReason::kGeometry,
ComputePaintInvalidationReason(box, paint_offset));
}
void SetUpHTML() {
SetBodyInnerHTML(R"HTML(
<style>
body {
margin: 0;
height: 0;
}
::-webkit-scrollbar { display: none }
#target {
width: 50px;
height: 100px;
transform-origin: 0 0;
}
.background {
background: blue;
}
.border {
border-width: 20px 10px;
border-style: solid;
border-color: red;
}
</style>
<div id='target' class='border'></div>
)HTML");
}
private:
FragmentData fragment_data_;
};
INSTANTIATE_PAINT_TEST_SUITE_P(BoxPaintInvalidatorTest);
// Paint invalidation for empty content is needed for updating composited layer
// bounds for correct composited hit testing. It won't cause raster invalidation
// (tested in paint_and_raster_invalidation_test.cc).
TEST_P(BoxPaintInvalidatorTest, ComputePaintInvalidationReasonEmptyContent) {
SetUpHTML();
auto& target = *GetDocument().getElementById("target");
auto& box = *target.GetLayoutBox();
// Remove border.
target.setAttribute(html_names::kClassAttr, "");
UpdateAllLifecyclePhasesForTest();
box.SetShouldCheckForPaintInvalidation();
auto paint_offset = box.FirstFragment().PaintOffset();
// No geometry change.
EXPECT_EQ(PaintInvalidationReason::kNone,
ComputePaintInvalidationReason(box, paint_offset));
// Paint offset change.
auto old_paint_offset = paint_offset + PhysicalOffset(10, 20);
EXPECT_EQ(PaintInvalidationReason::kGeometry,
ComputePaintInvalidationReason(box, old_paint_offset));
// Size change.
target.setAttribute(html_names::kStyleAttr, "width: 200px");
GetDocument().View()->UpdateLifecycleToLayoutClean(
DocumentUpdateReason::kTest);
EXPECT_EQ(PaintInvalidationReason::kIncremental,
ComputePaintInvalidationReason(box, paint_offset));
}
TEST_P(BoxPaintInvalidatorTest, ComputePaintInvalidationReasonBasic) {
SetUpHTML();
auto& target = *GetDocument().getElementById("target");
auto& box = *target.GetLayoutBox();
// Remove border.
target.setAttribute(html_names::kClassAttr, "");
target.setAttribute(html_names::kStyleAttr, "background: blue");
UpdateAllLifecyclePhasesForTest();
box.SetShouldCheckForPaintInvalidation();
auto paint_offset = box.FirstFragment().PaintOffset();
EXPECT_EQ(PhysicalOffset(), paint_offset);
// No geometry change.
EXPECT_EQ(PaintInvalidationReason::kNone,
ComputePaintInvalidationReason(box, paint_offset));
// Size change.
target.setAttribute(html_names::kStyleAttr, "background: blue; width: 200px");
GetDocument().View()->UpdateLifecycleToLayoutClean(
DocumentUpdateReason::kTest);
EXPECT_EQ(PaintInvalidationReason::kIncremental,
ComputePaintInvalidationReason(box, paint_offset));
// Add visual overflow.
target.setAttribute(html_names::kStyleAttr,
"background: blue; width: 200px; outline: 5px solid red");
UpdateAllLifecyclePhasesForTest();
// Size change with visual overflow.
target.setAttribute(html_names::kStyleAttr,
"background: blue; width: 100px; outline: 5px solid red");
GetDocument().View()->UpdateLifecycleToLayoutClean(
DocumentUpdateReason::kTest);
EXPECT_EQ(PaintInvalidationReason::kGeometry,
ComputePaintInvalidationReason(box, paint_offset));
// Should use the existing full paint invalidation reason regardless of
// geometry change.
box.SetShouldDoFullPaintInvalidation(PaintInvalidationReason::kStyle);
EXPECT_EQ(PaintInvalidationReason::kStyle,
ComputePaintInvalidationReason(box, paint_offset));
EXPECT_EQ(PaintInvalidationReason::kStyle,
ComputePaintInvalidationReason(box, paint_offset));
}
TEST_P(BoxPaintInvalidatorTest,
InvalidateLineBoxHitTestOnCompositingStyleChange) {
ScopedPaintUnderInvalidationCheckingForTest under_invalidation_checking(true);
SetBodyInnerHTML(R"HTML(
<style>
#target {
width: 100px;
height: 100px;
touch-action: none;
}
</style>
<div id="target" style="will-change: transform;">a<br>b</div>
)HTML");
UpdateAllLifecyclePhasesForTest();
auto& target = *GetDocument().getElementById("target");
target.setAttribute(html_names::kStyleAttr, "");
UpdateAllLifecyclePhasesForTest();
// This test passes if no underinvalidation occurs.
}
TEST_P(BoxPaintInvalidatorTest, ComputePaintInvalidationReasonOtherCases) {
SetUpHTML();
auto& target = *GetDocument().getElementById("target");
// The target initially has border.
ExpectFullPaintInvalidationOnGeometryChange("With border");
// Clear border, set background.
target.setAttribute(html_names::kClassAttr, "background");
target.setAttribute(html_names::kStyleAttr, "border-radius: 5px");
ExpectFullPaintInvalidationOnGeometryChange("With border-radius");
target.setAttribute(html_names::kStyleAttr, "-webkit-mask: url(#)");
ExpectFullPaintInvalidationOnGeometryChange("With mask");
target.setAttribute(html_names::kStyleAttr, "filter: blur(5px)");
ExpectFullPaintInvalidationOnGeometryChange("With filter");
target.setAttribute(html_names::kStyleAttr, "box-shadow: inset 3px 2px");
ExpectFullPaintInvalidationOnGeometryChange("With box-shadow");
target.setAttribute(html_names::kStyleAttr,
"clip-path: circle(50% at 0 50%)");
ExpectFullPaintInvalidationOnGeometryChange("With clip-path");
}
TEST_P(BoxPaintInvalidatorTest, ComputePaintInvalidationReasonOutline) {
SetUpHTML();
auto& target = *GetDocument().getElementById("target");
auto* object = target.GetLayoutObject();
GetDocument().View()->SetTracksRasterInvalidations(true);
target.setAttribute(html_names::kStyleAttr, "outline: 2px solid blue;");
UpdateAllLifecyclePhasesForTest();
EXPECT_THAT(GetRasterInvalidationTracking()->Invalidations(),
UnorderedElementsAre(RasterInvalidationInfo{
object, object->DebugName(), IntRect(0, 0, 72, 142),
PaintInvalidationReason::kStyle}));
GetDocument().View()->SetTracksRasterInvalidations(false);
GetDocument().View()->SetTracksRasterInvalidations(true);
target.setAttribute(html_names::kStyleAttr,
"outline: 2px solid blue; width: 100px;");
UpdateAllLifecyclePhasesForTest();
EXPECT_THAT(GetRasterInvalidationTracking()->Invalidations(),
UnorderedElementsAre(RasterInvalidationInfo{
object, object->DebugName(), IntRect(0, 0, 122, 142),
PaintInvalidationReason::kGeometry}));
GetDocument().View()->SetTracksRasterInvalidations(false);
}
TEST_P(BoxPaintInvalidatorTest, InvalidateHitTestOnCompositingStyleChange) {
ScopedPaintUnderInvalidationCheckingForTest under_invalidation_checking(true);
SetBodyInnerHTML(R"HTML(
<style>
#target {
width: 400px;
height: 300px;
overflow: hidden;
touch-action: none;
}
</style>
<div id="target" style="will-change: transform;"></div>
)HTML");
UpdateAllLifecyclePhasesForTest();
auto& target = *GetDocument().getElementById("target");
target.setAttribute(html_names::kStyleAttr, "");
UpdateAllLifecyclePhasesForTest();
// This test passes if no underinvalidation occurs.
}
TEST_P(BoxPaintInvalidatorTest, InvalidatePaintRectangle) {
SetBodyInnerHTML(R"HTML(
<div id="target" style="width: 200px; height: 200px; background: blue">
</div>
)HTML");
GetDocument().View()->SetTracksRasterInvalidations(true);
auto* target = GetLayoutBoxByElementId("target");
auto* display_item_client = static_cast<DisplayItemClient*>(target);
EXPECT_FALSE(target->HasPartialInvalidationRect());
EXPECT_TRUE(display_item_client->PartialInvalidationVisualRect().IsEmpty());
target->InvalidatePaintRectangle(PhysicalRect(10, 10, 50, 50));
target->InvalidatePaintRectangle(PhysicalRect(30, 30, 60, 60));
EXPECT_TRUE(target->HasPartialInvalidationRect());
EXPECT_TRUE(target->ShouldCheckForPaintInvalidation());
EXPECT_TRUE(display_item_client->IsValid());
UpdateAllLifecyclePhasesExceptPaint();
EXPECT_EQ(IntRect(18, 18, 80, 80),
display_item_client->PartialInvalidationVisualRect());
EXPECT_FALSE(display_item_client->IsValid());
target->InvalidatePaintRectangle(PhysicalRect(30, 30, 50, 80));
UpdateAllLifecyclePhasesExceptPaint();
// PartialInvalidationVisualRect should accumulate until painting.
EXPECT_EQ(IntRect(18, 18, 80, 100),
display_item_client->PartialInvalidationVisualRect());
UpdateAllLifecyclePhasesForTest();
EXPECT_THAT(GetRasterInvalidationTracking()->Invalidations(),
UnorderedElementsAre(RasterInvalidationInfo{
target, target->DebugName(), IntRect(18, 18, 80, 100),
PaintInvalidationReason::kRectangle}));
EXPECT_TRUE(display_item_client->IsValid());
EXPECT_TRUE(display_item_client->PartialInvalidationVisualRect().IsEmpty());
EXPECT_FALSE(target->HasPartialInvalidationRect());
}
} // namespace blink