| // 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/paint_layer_scrollable_area.h" |
| |
| #include "build/build_config.h" |
| #include "testing/gmock/include/gmock/gmock.h" |
| #include "third_party/blink/public/common/features.h" |
| #include "third_party/blink/renderer/core/animation/scroll_timeline.h" |
| #include "third_party/blink/renderer/core/css/css_numeric_literal_value.h" |
| #include "third_party/blink/renderer/core/frame/local_frame_view.h" |
| #include "third_party/blink/renderer/core/layout/layout_box_model_object.h" |
| #include "third_party/blink/renderer/core/page/scrolling/snap_coordinator.h" |
| #include "third_party/blink/renderer/core/paint/paint_layer.h" |
| #include "third_party/blink/renderer/core/scroll/scroll_types.h" |
| #include "third_party/blink/renderer/core/scroll/scrollbar_theme.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/testing/paint_test_configurations.h" |
| |
| using testing::_; |
| |
| namespace blink { |
| namespace { |
| |
| class ScrollableAreaMockChromeClient : public RenderingTestChromeClient { |
| public: |
| MOCK_METHOD3(MockSetToolTip, void(LocalFrame*, const String&, TextDirection)); |
| void SetToolTip(LocalFrame& frame, |
| const String& tooltip_text, |
| TextDirection dir) override { |
| MockSetToolTip(&frame, tooltip_text, dir); |
| } |
| }; |
| |
| HeapVector<Member<ScrollTimelineOffset>> CreateScrollOffsets( |
| ScrollTimelineOffset* start_scroll_offset = |
| MakeGarbageCollected<ScrollTimelineOffset>( |
| CSSNumericLiteralValue::Create( |
| 10.0, |
| CSSPrimitiveValue::UnitType::kPixels)), |
| ScrollTimelineOffset* end_scroll_offset = |
| MakeGarbageCollected<ScrollTimelineOffset>( |
| CSSNumericLiteralValue::Create( |
| 90.0, |
| CSSPrimitiveValue::UnitType::kPixels))) { |
| HeapVector<Member<ScrollTimelineOffset>> scroll_offsets; |
| scroll_offsets.push_back(start_scroll_offset); |
| scroll_offsets.push_back(end_scroll_offset); |
| return scroll_offsets; |
| } |
| |
| } // namespace |
| |
| class PaintLayerScrollableAreaTest : public RenderingTest, |
| public PaintTestConfigurations { |
| public: |
| PaintLayerScrollableAreaTest() |
| : RenderingTest(MakeGarbageCollected<EmptyLocalFrameClient>()), |
| chrome_client_(MakeGarbageCollected<ScrollableAreaMockChromeClient>()) { |
| } |
| |
| ~PaintLayerScrollableAreaTest() override { |
| testing::Mock::VerifyAndClearExpectations(&GetChromeClient()); |
| } |
| |
| ScrollableAreaMockChromeClient& GetChromeClient() const override { |
| return *chrome_client_; |
| } |
| |
| BackgroundPaintLocation GetBackgroundPaintLocation(const char* element_id) { |
| return To<LayoutBoxModelObject>(GetLayoutObjectByElementId(element_id)) |
| ->GetBackgroundPaintLocation(); |
| } |
| |
| bool IsComposited(const LayoutObject* scroller) { |
| const auto* paint_properties = scroller->FirstFragment().PaintProperties(); |
| return paint_properties && paint_properties->Transform() && |
| paint_properties->Transform()->HasDirectCompositingReasons(); |
| } |
| |
| bool UsesCompositedScrolling(const LayoutObject* scroller) { |
| const auto* paint_properties = scroller->FirstFragment().PaintProperties(); |
| bool composited = |
| paint_properties && paint_properties->ScrollTranslation() && |
| paint_properties->ScrollTranslation()->HasDirectCompositingReasons(); |
| |
| auto* layer = To<LayoutBoxModelObject>(scroller)->Layer(); |
| if (!layer) { |
| DCHECK(!composited); |
| return false; |
| } |
| |
| auto* scrollable_area = layer->GetScrollableArea(); |
| if (!scrollable_area) { |
| DCHECK(!composited); |
| return false; |
| } |
| |
| if (!RuntimeEnabledFeatures::CompositeAfterPaintEnabled()) { |
| DCHECK_EQ(composited, layer->NeedsCompositedScrolling()); |
| DCHECK_EQ(composited, scrollable_area->NeedsCompositedScrolling()); |
| if (composited) |
| DCHECK(layer->GraphicsLayerBacking()); |
| } |
| return composited; |
| } |
| |
| bool GraphicsLayerContentsOpaque(const LayoutObject* scroller) { |
| DCHECK(!RuntimeEnabledFeatures::CompositeAfterPaintEnabled()); |
| return To<LayoutBoxModelObject>(scroller) |
| ->Layer() |
| ->GraphicsLayerBacking() |
| ->CcLayer() |
| .contents_opaque(); |
| } |
| |
| private: |
| void SetUp() override { |
| EnableCompositing(); |
| RenderingTest::SetUp(); |
| } |
| |
| Persistent<ScrollableAreaMockChromeClient> chrome_client_; |
| }; |
| |
| INSTANTIATE_PAINT_TEST_SUITE_P(PaintLayerScrollableAreaTest); |
| |
| TEST_P(PaintLayerScrollableAreaTest, |
| CanPaintBackgroundOntoScrollingContentsLayer) { |
| GetDocument().GetFrame()->GetSettings()->SetPreferCompositingToLCDTextEnabled( |
| true); |
| SetBodyInnerHTML(R"HTML( |
| <style> |
| .scroller { |
| overflow: scroll; |
| will-change: transform; |
| width: 300px; |
| height: 300px; |
| } |
| .spacer { height: 1000px; } |
| #scroller13::-webkit-scrollbar { |
| width: 13px; |
| height: 13px; |
| } |
| </style> |
| <div id='scroller1' class='scroller' style='background: white local;'> |
| <div id='negative-composited-child' style='background-color: red; |
| width: 1px; height: 1px; position: absolute; |
| backface-visibility: hidden; z-index: -1'></div> |
| <div class='spacer'></div> |
| </div> |
| <div id='scroller2' class='scroller' style='background: white content-box; |
| padding: 10px;'> |
| <div class='spacer'></div> |
| </div> |
| <div id='scroller3' class='scroller' |
| style='background: white local content-box; padding: 10px;'> |
| <div class='spacer'></div> |
| </div> |
| <div id='scroller4' class='scroller' |
| style='background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUg), |
| white local;'> |
| <div class='spacer'></div> |
| </div> |
| <div id='scroller5' class='scroller' |
| style='background: |
| url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUg) local, white local;'> |
| <div class='spacer'></div> |
| </div> |
| <div id='scroller6' class='scroller' |
| style='background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUg) |
| local, white padding-box; padding: 10px;'> |
| <div class='spacer'></div> |
| </div> |
| <div id='scroller7' class='scroller' |
| style='background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUg) |
| local, white content-box; padding: 10px;'> |
| <div class='spacer'></div> |
| </div> |
| <div id='scroller8' class='scroller' style='background: white border-box;'> |
| <div class='spacer'></div> |
| </div> |
| <div id='scroller9' class='scroller' style='background: white border-box; |
| border: 10px solid black;'> |
| <div class='spacer'></div> |
| </div> |
| <div id='scroller10' class='scroller' style='background: white border-box; |
| border: 10px solid rgba(0, 0, 0, 0.5);'> |
| <div class='spacer'></div> |
| </div> |
| <div id='scroller11' class='scroller' |
| style='background: white content-box;'> |
| <div class='spacer'></div> |
| </div> |
| <div id='scroller12' class='scroller' style='background: white content-box; |
| padding: 10px;'> |
| <div class='spacer'></div> |
| </div> |
| <div id='scroller13' class='scroller' style='background: white border-box;'> |
| <div class='spacer'></div> |
| </div> |
| <div id='scroller14' class='scroller' style='background: white; |
| border: 1px solid black; outline: 1px solid blue; |
| outline-offset: -1px;'> |
| <div class='spacer'></div> |
| </div> |
| <div id='scroller15' class='scroller' style='background: white; |
| border: 1px solid black; outline: 1px solid blue; |
| outline-offset: -2px;'> |
| <div class='spacer'></div> |
| </div> |
| <div id='css-clip' class='scroller' style='position: absolute; |
| background: white; clip: rect(0px,10px,10px,0px);'> |
| <div class='spacer'></div> |
| </div> |
| <div id='scroller17' class='scroller' |
| style='background: rgba(255, 255, 255, 0.5) border-box; |
| border: 5px solid rgba(0, 0, 0, 0.5);'> |
| <div class='spacer'></div> |
| </div> |
| <div id='scroller18' class='scroller' |
| style='background: white; |
| border: 5px dashed black;'> |
| <div class='spacer'></div> |
| </div> |
| <div id='box-shadow' class='scroller' |
| style='background: white; box-shadow: 10px 10px black'> |
| <div class='spacer'></div> |
| </div> |
| <div id='inset-box-shadow' class='scroller' |
| style='background: white; box-shadow: 10px 10px black inset'> |
| <div class='spacer'></div> |
| </div> |
| )HTML"); |
| |
| // #scroller1 can paint background into scrolling contents layer even with a |
| // negative z-index child. |
| EXPECT_EQ(kBackgroundPaintInScrollingContents, |
| GetBackgroundPaintLocation("scroller1")); |
| |
| // #scroller2 cannot paint background into scrolling contents layer because it |
| // has a content-box clip without local attachment. |
| EXPECT_EQ(kBackgroundPaintInGraphicsLayer, |
| GetBackgroundPaintLocation("scroller2")); |
| |
| // #scroller3 can paint background into scrolling contents layer. |
| EXPECT_EQ(kBackgroundPaintInScrollingContents, |
| GetBackgroundPaintLocation("scroller3")); |
| |
| // #scroller4 cannot paint background into scrolling contents layer because |
| // the background image is not locally attached. |
| EXPECT_EQ(kBackgroundPaintInGraphicsLayer, |
| GetBackgroundPaintLocation("scroller4")); |
| |
| // #scroller5 can paint background into scrolling contents layer because both |
| // the image and color are locally attached. |
| EXPECT_EQ(kBackgroundPaintInScrollingContents, |
| GetBackgroundPaintLocation("scroller5")); |
| |
| // #scroller6 can paint background into scrolling contents layer because the |
| // image is locally attached and even though the color is not, it is filled to |
| // the padding box so it will be drawn the same as a locally attached |
| // background. |
| EXPECT_EQ(kBackgroundPaintInScrollingContents, |
| GetBackgroundPaintLocation("scroller6")); |
| |
| // #scroller7 cannot paint background into scrolling contents layer because |
| // the color is filled to the content box and we have padding so it is not |
| // equivalent to a locally attached background. |
| EXPECT_EQ(kBackgroundPaintInGraphicsLayer, |
| GetBackgroundPaintLocation("scroller7")); |
| |
| // #scroller8 can paint background into scrolling contents layer because its |
| // border-box is equivalent to its padding box since it has no border. |
| EXPECT_EQ(kBackgroundPaintInScrollingContents, |
| GetBackgroundPaintLocation("scroller8")); |
| |
| // #scroller9 can paint background into scrolling contents layer because its |
| // border is opaque so it completely covers the background outside of the |
| // padding-box. |
| EXPECT_EQ(kBackgroundPaintInScrollingContents, |
| GetBackgroundPaintLocation("scroller9")); |
| |
| // #scroller10 paints the background into both layers because its border is |
| // partially transparent so the background must be drawn to the |
| // border-box edges. |
| EXPECT_EQ( |
| kBackgroundPaintInGraphicsLayer | kBackgroundPaintInScrollingContents, |
| GetBackgroundPaintLocation("scroller10")); |
| |
| // #scroller11 can paint background into scrolling contents layer because its |
| // content-box is equivalent to its padding box since it has no padding. |
| EXPECT_EQ(kBackgroundPaintInScrollingContents, |
| GetBackgroundPaintLocation("scroller11")); |
| |
| // #scroller12 cannot paint background into scrolling contents layer because |
| // it has padding so its content-box is not equivalent to its padding-box. |
| EXPECT_EQ(kBackgroundPaintInGraphicsLayer, |
| GetBackgroundPaintLocation("scroller12")); |
| |
| // #scroller13 paints the background into both layers because it has a custom |
| // scrollbar which the background may need to draw under. |
| EXPECT_EQ( |
| kBackgroundPaintInGraphicsLayer | kBackgroundPaintInScrollingContents, |
| GetBackgroundPaintLocation("scroller13")); |
| |
| // #scroller14 can paint background into scrolling contents layer because the |
| // outline is drawn outside the padding box. |
| EXPECT_EQ(kBackgroundPaintInScrollingContents, |
| GetBackgroundPaintLocation("scroller14")); |
| |
| // #scroller15 can paint background into scrolling contents layer because |
| // the outline is drawn into the decoration layer which will not be covered |
| // up. |
| EXPECT_EQ(kBackgroundPaintInScrollingContents, |
| GetBackgroundPaintLocation("scroller15")); |
| |
| // css-clip doesn't affect background paint location. |
| EXPECT_EQ(kBackgroundPaintInScrollingContents, |
| GetBackgroundPaintLocation("css-clip")); |
| |
| // #scroller17 can only be painted once as it is translucent, and it must |
| // be painted in the graphics layer to be under the translucent border. |
| EXPECT_EQ(kBackgroundPaintInGraphicsLayer, |
| GetBackgroundPaintLocation("scroller17")); |
| |
| // #scroller18 can be painted in both layers because the background is a |
| // solid color, it must be because the dashed border reveals the background |
| // underneath it. |
| EXPECT_EQ( |
| kBackgroundPaintInGraphicsLayer | kBackgroundPaintInScrollingContents, |
| GetBackgroundPaintLocation("scroller18")); |
| |
| // Background with normal (non-inset) box shadow can be painted in the |
| // scrolling contents layer. |
| EXPECT_EQ(kBackgroundPaintInScrollingContents, |
| GetBackgroundPaintLocation("box-shadow")); |
| |
| // Background with inset box shadow can only be painted in the main graphics |
| // layer because the shadow can't scroll. |
| EXPECT_EQ(kBackgroundPaintInGraphicsLayer, |
| GetBackgroundPaintLocation("inset-box-shadow")); |
| } |
| |
| TEST_P(PaintLayerScrollableAreaTest, OpaqueContainedLayersPromoted) { |
| SetBodyInnerHTML(R"HTML( |
| <style> |
| #scroller { overflow: scroll; height: 200px; width: 200px; |
| contain: paint; background: white local content-box; |
| border: 10px solid rgba(0, 255, 0, 0.5); } |
| #scrolled { height: 300px; } |
| </style> |
| <div id="scroller"><div id="scrolled"></div></div> |
| )HTML"); |
| |
| auto* scroller = GetLayoutObjectByElementId("scroller"); |
| EXPECT_TRUE(UsesCompositedScrolling(scroller)); |
| if (!RuntimeEnabledFeatures::CompositeAfterPaintEnabled()) |
| EXPECT_TRUE(GraphicsLayerContentsOpaque(scroller)); |
| } |
| |
| TEST_P(PaintLayerScrollableAreaTest, NonStackingContextScrollerPromoted) { |
| SetBodyInnerHTML(R"HTML( |
| <style> |
| #scroller { overflow: scroll; height: 200px; width: 200px; |
| background: white local content-box; |
| border: 10px solid rgba(0, 255, 0, 0.5); } |
| #scrolled { height: 300px; } |
| #positioned { position: relative; } |
| </style> |
| <div id="scroller"> |
| <div id="positioned">Not contained by scroller.</div> |
| <div id="scrolled"></div> |
| </div> |
| )HTML"); |
| |
| EXPECT_TRUE(UsesCompositedScrolling(GetLayoutObjectByElementId("scroller"))); |
| } |
| |
| TEST_P(PaintLayerScrollableAreaTest, TransparentLayersNotPromoted) { |
| SetBodyInnerHTML(R"HTML( |
| <style> |
| #scroller { overflow: scroll; height: 200px; width: 200px; background: |
| rgba(0, 255, 0, 0.5) local content-box; border: 10px solid rgba(0, 255, |
| 0, 0.5); contain: paint; } |
| #scrolled { height: 300px; } |
| </style> |
| <div id="scroller"><div id="scrolled"></div></div> |
| )HTML"); |
| |
| EXPECT_FALSE(UsesCompositedScrolling(GetLayoutObjectByElementId("scroller"))); |
| } |
| |
| TEST_P(PaintLayerScrollableAreaTest, OpaqueLayersDepromotedOnStyleChange) { |
| SetBodyInnerHTML(R"HTML( |
| <style> |
| #scroller { overflow: scroll; height: 200px; width: 200px; background: |
| white local content-box; contain: paint; } |
| #scrolled { height: 300px; } |
| </style> |
| <div id="scroller"><div id="scrolled"></div></div> |
| )HTML"); |
| |
| Element* scroller = GetDocument().getElementById("scroller"); |
| EXPECT_TRUE(UsesCompositedScrolling(scroller->GetLayoutObject())); |
| |
| // Change the background to transparent |
| scroller->setAttribute( |
| html_names::kStyleAttr, |
| "background: rgba(255,255,255,0.5) local content-box;"); |
| UpdateAllLifecyclePhasesForTest(); |
| EXPECT_FALSE(UsesCompositedScrolling(scroller->GetLayoutObject())); |
| } |
| |
| TEST_P(PaintLayerScrollableAreaTest, OpaqueLayersPromotedOnStyleChange) { |
| SetBodyInnerHTML(R"HTML( |
| <style> |
| #scroller { overflow: scroll; height: 200px; width: 200px; background: |
| rgba(255,255,255,0.5) local content-box; contain: paint; } |
| #scrolled { height: 300px; } |
| </style> |
| <div id="scroller"><div id="scrolled"></div></div> |
| )HTML"); |
| |
| Element* scroller = GetDocument().getElementById("scroller"); |
| EXPECT_FALSE(UsesCompositedScrolling(scroller->GetLayoutObject())); |
| |
| // Change the background to opaque |
| scroller->setAttribute(html_names::kStyleAttr, |
| "background: white local content-box;"); |
| UpdateAllLifecyclePhasesForTest(); |
| EXPECT_TRUE(UsesCompositedScrolling(scroller->GetLayoutObject())); |
| if (!RuntimeEnabledFeatures::CompositeAfterPaintEnabled()) |
| EXPECT_TRUE(GraphicsLayerContentsOpaque(scroller->GetLayoutObject())); |
| } |
| |
| // Tests that a transform on the scroller or an ancestor doesn't prevent |
| // promotion. |
| TEST_P(PaintLayerScrollableAreaTest, |
| TransformDoesNotPreventCompositedScrolling) { |
| SetBodyInnerHTML(R"HTML( |
| <style> |
| #scroller { overflow: scroll; height: 200px; width: 200px; background: |
| white local content-box; contain: paint; } |
| #scrolled { height: 300px; } |
| </style> |
| <div id="parent"> |
| <div id="scroller"><div id="scrolled"></div></div> |
| </div> |
| )HTML"); |
| |
| Element* parent = GetDocument().getElementById("parent"); |
| Element* scroller = GetDocument().getElementById("scroller"); |
| EXPECT_TRUE(UsesCompositedScrolling(scroller->GetLayoutObject())); |
| if (!RuntimeEnabledFeatures::CompositeAfterPaintEnabled()) |
| EXPECT_TRUE(GraphicsLayerContentsOpaque(scroller->GetLayoutObject())); |
| |
| // Change the parent to have a transform. |
| parent->setAttribute(html_names::kStyleAttr, "transform: translate(1px, 0);"); |
| UpdateAllLifecyclePhasesForTest(); |
| EXPECT_TRUE(UsesCompositedScrolling(scroller->GetLayoutObject())); |
| |
| // Change the parent to have no transform again. |
| parent->removeAttribute(html_names::kStyleAttr); |
| UpdateAllLifecyclePhasesForTest(); |
| EXPECT_TRUE(UsesCompositedScrolling(scroller->GetLayoutObject())); |
| if (!RuntimeEnabledFeatures::CompositeAfterPaintEnabled()) |
| EXPECT_TRUE(GraphicsLayerContentsOpaque(scroller->GetLayoutObject())); |
| |
| // Apply a transform to the scroller directly. |
| scroller->setAttribute(html_names::kStyleAttr, |
| "transform: translate(1px, 0);"); |
| UpdateAllLifecyclePhasesForTest(); |
| EXPECT_TRUE(UsesCompositedScrolling(scroller->GetLayoutObject())); |
| } |
| |
| TEST_P(PaintLayerScrollableAreaTest, |
| PromoteLayerRegardlessOfSelfAndAncestorOpacity) { |
| SetBodyInnerHTML(R"HTML( |
| <style> |
| #scroller { overflow: scroll; height: 200px; width: 200px; background: |
| white local content-box; contain: paint; } |
| #scrolled { height: 300px; } |
| </style> |
| <div id="parent"> |
| <div id="scroller"><div id="scrolled"></div></div> |
| </div> |
| )HTML"); |
| |
| Element* parent = GetDocument().getElementById("parent"); |
| Element* scroller = GetDocument().getElementById("scroller"); |
| EXPECT_TRUE(UsesCompositedScrolling(scroller->GetLayoutObject())); |
| if (!RuntimeEnabledFeatures::CompositeAfterPaintEnabled()) |
| EXPECT_TRUE(GraphicsLayerContentsOpaque(scroller->GetLayoutObject())); |
| |
| // Change the parent to be partially translucent. |
| parent->setAttribute(html_names::kStyleAttr, "opacity: 0.5;"); |
| UpdateAllLifecyclePhasesForTest(); |
| EXPECT_TRUE(UsesCompositedScrolling(scroller->GetLayoutObject())); |
| |
| // Change the parent to be opaque again. |
| parent->setAttribute(html_names::kStyleAttr, "opacity: 1;"); |
| UpdateAllLifecyclePhasesForTest(); |
| EXPECT_TRUE(UsesCompositedScrolling(scroller->GetLayoutObject())); |
| if (!RuntimeEnabledFeatures::CompositeAfterPaintEnabled()) |
| EXPECT_TRUE(GraphicsLayerContentsOpaque(scroller->GetLayoutObject())); |
| |
| // Make the scroller translucent. |
| scroller->setAttribute(html_names::kStyleAttr, "opacity: 0.5"); |
| UpdateAllLifecyclePhasesForTest(); |
| EXPECT_TRUE(UsesCompositedScrolling(scroller->GetLayoutObject())); |
| } |
| |
| // Test that will-change: transform applied to the scroller will cause the |
| // scrolling contents layer to be promoted. |
| TEST_P(PaintLayerScrollableAreaTest, CompositedScrollOnWillChangeTransform) { |
| SetBodyInnerHTML(R"HTML( |
| <style> |
| #scroller { overflow: scroll; height: 100px; width: 100px; } |
| #scrolled { height: 300px; } |
| </style> |
| <div id="scroller"><div id="scrolled"></div></div> |
| )HTML"); |
| |
| Element* scroller = GetDocument().getElementById("scroller"); |
| EXPECT_FALSE(UsesCompositedScrolling(scroller->GetLayoutObject())); |
| |
| scroller->setAttribute(html_names::kStyleAttr, "will-change: transform"); |
| UpdateAllLifecyclePhasesForTest(); |
| EXPECT_TRUE(UsesCompositedScrolling(scroller->GetLayoutObject())); |
| |
| scroller->setAttribute(html_names::kStyleAttr, ""); |
| UpdateAllLifecyclePhasesForTest(); |
| EXPECT_FALSE(UsesCompositedScrolling(scroller->GetLayoutObject())); |
| } |
| |
| // Test that will-change: transform applied to the scroller will cause the |
| // scrolling contents layer to be promoted. |
| TEST_P(PaintLayerScrollableAreaTest, ScrollLayerOnPointerEvents) { |
| GetDocument().GetFrame()->GetSettings()->SetPreferCompositingToLCDTextEnabled( |
| true); |
| SetBodyInnerHTML(R"HTML( |
| <style> |
| #scroller { overflow: scroll; height: 100px; width: 100px; } |
| #scrolled { height: 300px; } |
| </style> |
| <div id="scroller"><div id="scrolled"></div></div> |
| )HTML"); |
| |
| Element* scroller = GetDocument().getElementById("scroller"); |
| EXPECT_TRUE(UsesCompositedScrolling(scroller->GetLayoutObject())); |
| |
| // pointer-events: none causes the scoller to be invisible for hit testing, |
| // so ScrollsOverflow becomes false on the PaintLayerScrollableArea, and hence |
| // composited scrolling is not present. |
| scroller->setAttribute(html_names::kStyleAttr, "pointer-events: none"); |
| UpdateAllLifecyclePhasesForTest(); |
| EXPECT_FALSE(UsesCompositedScrolling(scroller->GetLayoutObject())); |
| |
| scroller->setAttribute(html_names::kStyleAttr, ""); |
| UpdateAllLifecyclePhasesForTest(); |
| EXPECT_TRUE(UsesCompositedScrolling(scroller->GetLayoutObject())); |
| } |
| |
| // Test that <input> elements don't use composited scrolling even with |
| // "will-change:transform". |
| TEST_P(PaintLayerScrollableAreaTest, InputElementPromotionTest) { |
| SetBodyInnerHTML(R"HTML( |
| <!DOCTYPE html> |
| <style> |
| .composited { will-change: transform; } |
| </style> |
| <input id='input' width=10 style='font-size:40pt;'/> |
| )HTML"); |
| |
| Element* element = GetDocument().getElementById("input"); |
| EXPECT_FALSE(IsComposited(element->GetLayoutObject())); |
| EXPECT_FALSE(UsesCompositedScrolling(element->GetLayoutObject())); |
| |
| element->setAttribute("class", "composited"); |
| UpdateAllLifecyclePhasesForTest(); |
| EXPECT_TRUE(IsComposited(element->GetLayoutObject())); |
| EXPECT_FALSE(UsesCompositedScrolling(element->GetLayoutObject())); |
| } |
| |
| // Test that <select> elements use composited scrolling with |
| // "will-change:transform". |
| TEST_P(PaintLayerScrollableAreaTest, SelectElementPromotionTest) { |
| SetBodyInnerHTML(R"HTML( |
| <!DOCTYPE html> |
| <style> |
| .composited { will-change: transform; } |
| </style> |
| <select id='select' size='2'> |
| <option> value 1</option> |
| <option> value 2</option> |
| <option> value 3</option> |
| <option> value 4</option> |
| </select> |
| )HTML"); |
| |
| Element* element = GetDocument().getElementById("select"); |
| EXPECT_FALSE(IsComposited(element->GetLayoutObject())); |
| EXPECT_FALSE(UsesCompositedScrolling(element->GetLayoutObject())); |
| |
| element->setAttribute("class", "composited"); |
| UpdateAllLifecyclePhasesForTest(); |
| EXPECT_TRUE(IsComposited(element->GetLayoutObject())); |
| #if defined(OS_ANDROID) |
| // <select> implementation is different and not scrollable on Android. |
| EXPECT_FALSE(UsesCompositedScrolling(element->GetLayoutObject())); |
| #else |
| EXPECT_TRUE(UsesCompositedScrolling(element->GetLayoutObject())); |
| #endif |
| } |
| |
| // Ensure OverlayScrollbarColorTheme get updated when page load |
| TEST_P(PaintLayerScrollableAreaTest, OverlayScrollbarColorThemeUpdated) { |
| SetBodyInnerHTML(R"HTML( |
| <style> |
| div { overflow: scroll; } |
| #white { background-color: white; } |
| #black { background-color: black; } |
| </style> |
| <div id="none">a</div> |
| <div id="white">b</div> |
| <div id="black">c</div> |
| )HTML"); |
| |
| PaintLayer* none_layer = GetPaintLayerByElementId("none"); |
| PaintLayer* white_layer = GetPaintLayerByElementId("white"); |
| PaintLayer* black_layer = GetPaintLayerByElementId("black"); |
| |
| ASSERT_TRUE(none_layer); |
| ASSERT_TRUE(white_layer); |
| ASSERT_TRUE(black_layer); |
| |
| ASSERT_EQ(ScrollbarOverlayColorTheme::kScrollbarOverlayColorThemeDark, |
| none_layer->GetScrollableArea()->GetScrollbarOverlayColorTheme()); |
| ASSERT_EQ(ScrollbarOverlayColorTheme::kScrollbarOverlayColorThemeDark, |
| white_layer->GetScrollableArea()->GetScrollbarOverlayColorTheme()); |
| ASSERT_EQ(ScrollbarOverlayColorTheme::kScrollbarOverlayColorThemeLight, |
| black_layer->GetScrollableArea()->GetScrollbarOverlayColorTheme()); |
| } |
| |
| TEST_P(PaintLayerScrollableAreaTest, HideTooltipWhenScrollPositionChanges) { |
| SetBodyInnerHTML(R"HTML( |
| <style> |
| #scroller { width: 100px; height: 100px; overflow: scroll; } |
| #scrolled { height: 300px; } |
| </style> |
| <div id="scroller"><div id="scrolled"></div></div> |
| )HTML"); |
| |
| Element* scroller = GetDocument().getElementById("scroller"); |
| PaintLayerScrollableArea* scrollable_area = |
| To<LayoutBoxModelObject>(scroller->GetLayoutObject()) |
| ->GetScrollableArea(); |
| ASSERT_TRUE(scrollable_area); |
| |
| EXPECT_CALL(GetChromeClient(), |
| MockSetToolTip(GetDocument().GetFrame(), String(), _)) |
| .Times(1); |
| scrollable_area->SetScrollOffset(ScrollOffset(1, 1), |
| mojom::blink::ScrollType::kUser); |
| |
| // Programmatic scrolling should not dismiss the tooltip, so setToolTip |
| // should not be called for this invocation. |
| EXPECT_CALL(GetChromeClient(), |
| MockSetToolTip(GetDocument().GetFrame(), String(), _)) |
| .Times(0); |
| scrollable_area->SetScrollOffset(ScrollOffset(2, 2), |
| mojom::blink::ScrollType::kProgrammatic); |
| } |
| |
| TEST_P(PaintLayerScrollableAreaTest, IncludeOverlayScrollbarsInVisibleWidth) { |
| USE_NON_OVERLAY_SCROLLBARS(); |
| |
| SetBodyInnerHTML(R"HTML( |
| <style> |
| #scroller { overflow: overlay; height: 100px; width: 100px; } |
| #scrolled { width: 100px; height: 200px; } |
| </style> |
| <div id="scroller"><div id="scrolled"></div></div> |
| )HTML"); |
| |
| Element* scroller = GetDocument().getElementById("scroller"); |
| ASSERT_TRUE(scroller); |
| PaintLayerScrollableArea* scrollable_area = |
| To<LayoutBoxModelObject>(scroller->GetLayoutObject()) |
| ->GetScrollableArea(); |
| ASSERT_TRUE(scrollable_area); |
| scrollable_area->SetScrollOffset(ScrollOffset(100, 0), |
| mojom::blink::ScrollType::kClamping); |
| EXPECT_EQ(scrollable_area->GetScrollOffset().Width(), 0); |
| } |
| |
| TEST_P(PaintLayerScrollableAreaTest, ShowAutoScrollbarsForVisibleContent) { |
| USE_NON_OVERLAY_SCROLLBARS(); |
| |
| SetBodyInnerHTML(R"HTML( |
| <style> |
| #outerDiv { |
| width: 15px; |
| height: 100px; |
| overflow-y: auto; |
| overflow-x: hidden; |
| } |
| #innerDiv { |
| height:300px; |
| width: 1px; |
| } |
| </style> |
| <div id='outerDiv'> |
| <div id='innerDiv'></div> |
| </div> |
| )HTML"); |
| |
| Element* outer_div = GetDocument().getElementById("outerDiv"); |
| ASSERT_TRUE(outer_div); |
| outer_div->GetLayoutObject()->SetNeedsLayout("test"); |
| UpdateAllLifecyclePhasesForTest(); |
| PaintLayerScrollableArea* scrollable_area = |
| To<LayoutBoxModelObject>(outer_div->GetLayoutObject()) |
| ->GetScrollableArea(); |
| ASSERT_TRUE(scrollable_area); |
| EXPECT_TRUE(scrollable_area->HasVerticalScrollbar()); |
| } |
| |
| TEST_P(PaintLayerScrollableAreaTest, FloatOverflowInRtlContainer) { |
| USE_NON_OVERLAY_SCROLLBARS(); |
| |
| SetBodyInnerHTML(R"HTML( |
| <!DOCTYPE html> |
| <style> |
| #container { |
| width: 200px; |
| overflow-x: auto; |
| overflow-y: scroll; |
| direction: rtl; |
| } |
| </style> |
| <div id='container'> |
| <div style='float:left'> |
| lorem ipsum |
| </div> |
| </div> |
| )HTML"); |
| |
| Element* container = GetDocument().getElementById("container"); |
| ASSERT_TRUE(container); |
| PaintLayerScrollableArea* scrollable_area = |
| To<LayoutBoxModelObject>(container->GetLayoutObject()) |
| ->GetScrollableArea(); |
| ASSERT_TRUE(scrollable_area); |
| EXPECT_FALSE(scrollable_area->HasHorizontalScrollbar()); |
| } |
| |
| TEST_P(PaintLayerScrollableAreaTest, ScrollOriginInRtlContainer) { |
| SetBodyInnerHTML(R"HTML( |
| <!DOCTYPE html> |
| <style> |
| #container { |
| width: 200px; |
| overflow: auto; |
| direction: rtl; |
| } |
| #content { |
| width: 300px; |
| } |
| </style> |
| <div id='container'> |
| <div id='content'> |
| lorem ipsum |
| <div> |
| </div> |
| )HTML"); |
| |
| Element* container = GetDocument().getElementById("container"); |
| ASSERT_TRUE(container); |
| PaintLayerScrollableArea* scrollable_area = |
| To<LayoutBoxModelObject>(container->GetLayoutObject()) |
| ->GetScrollableArea(); |
| ASSERT_TRUE(scrollable_area); |
| EXPECT_EQ(scrollable_area->ScrollOrigin().X(), 100); |
| } |
| |
| TEST_P(PaintLayerScrollableAreaTest, OverflowHiddenScrollOffsetInvalidation) { |
| SetBodyInnerHTML(R"HTML( |
| <style> |
| #scroller { |
| overflow: hidden; |
| height: 200px; |
| width: 200px; |
| } |
| </style> |
| <div id='scroller'> |
| <div id='forceScroll' style='height: 2000px;'></div> |
| </div> |
| )HTML"); |
| |
| auto* scroller = GetLayoutObjectByElementId("scroller"); |
| auto* scrollable_area = |
| To<LayoutBoxModelObject>(scroller)->GetScrollableArea(); |
| |
| const auto* properties = scroller->FirstFragment().PaintProperties(); |
| |
| // No scroll offset translation is needed when scroll offset is zero. |
| EXPECT_EQ(nullptr, properties->ScrollTranslation()); |
| EXPECT_EQ(FloatSize(0, 0), scrollable_area->GetScrollOffset()); |
| |
| // Going from zero scroll offset to non-zero may require a new paint property |
| // and should invalidate paint and paint properties. |
| scrollable_area->SetScrollOffset(ScrollOffset(0, 1), |
| mojom::blink::ScrollType::kProgrammatic); |
| EXPECT_TRUE(scroller->PaintingLayer()->SelfNeedsRepaint()); |
| EXPECT_TRUE(scroller->NeedsPaintPropertyUpdate()); |
| UpdateAllLifecyclePhasesForTest(); |
| |
| // A scroll offset translation is needed when scroll offset is non-zero. |
| EXPECT_EQ(FloatSize(0, 1), scrollable_area->GetScrollOffset()); |
| EXPECT_NE(nullptr, properties->ScrollTranslation()); |
| |
| // A property update is needed when scroll offset changes. |
| scrollable_area->SetScrollOffset(ScrollOffset(0, 2), |
| mojom::blink::ScrollType::kProgrammatic); |
| EXPECT_TRUE(scroller->NeedsPaintPropertyUpdate()); |
| UpdateAllLifecyclePhasesForTest(); |
| |
| // A scroll offset translation is still needed when scroll offset is non-zero. |
| EXPECT_EQ(FloatSize(0, 2), scrollable_area->GetScrollOffset()); |
| EXPECT_NE(nullptr, properties->ScrollTranslation()); |
| |
| // Going from non-zero scroll offset to zero may require destroying a paint |
| // property and should invalidate paint and paint properties. |
| scrollable_area->SetScrollOffset(ScrollOffset(0, 0), |
| mojom::blink::ScrollType::kProgrammatic); |
| EXPECT_TRUE(scroller->PaintingLayer()->SelfNeedsRepaint()); |
| EXPECT_TRUE(scroller->NeedsPaintPropertyUpdate()); |
| UpdateAllLifecyclePhasesForTest(); |
| |
| // No scroll offset translation is needed when scroll offset is zero. |
| EXPECT_EQ(nullptr, properties->ScrollTranslation()); |
| EXPECT_EQ(FloatSize(0, 0), scrollable_area->GetScrollOffset()); |
| } |
| |
| TEST_P(PaintLayerScrollableAreaTest, ScrollDoesNotInvalidate) { |
| SetBodyInnerHTML(R"HTML( |
| <style> |
| #scroller { |
| overflow: scroll; |
| height: 200px; |
| width: 200px; |
| background: linear-gradient(black, white); |
| } |
| </style> |
| <div id='scroller'> |
| <div id='forceScroll' style='height: 2000px;'></div> |
| </div> |
| )HTML"); |
| |
| auto* scroller = GetLayoutObjectByElementId("scroller"); |
| auto* scrollable_area = |
| To<LayoutBoxModelObject>(scroller)->GetScrollableArea(); |
| |
| const auto* properties = scroller->FirstFragment().PaintProperties(); |
| // Scroll offset translation is needed even when scroll offset is zero. |
| EXPECT_NE(nullptr, properties->ScrollTranslation()); |
| EXPECT_EQ(FloatSize(0, 0), scrollable_area->GetScrollOffset()); |
| |
| // Changing the scroll offset should not require paint invalidation. |
| scrollable_area->SetScrollOffset(ScrollOffset(0, 1), |
| mojom::blink::ScrollType::kProgrammatic); |
| EXPECT_FALSE(scroller->ShouldDoFullPaintInvalidation()); |
| EXPECT_TRUE(scroller->NeedsPaintPropertyUpdate()); |
| UpdateAllLifecyclePhasesForTest(); |
| EXPECT_EQ(FloatSize(0, 1), scrollable_area->GetScrollOffset()); |
| EXPECT_NE(nullptr, properties->ScrollTranslation()); |
| } |
| |
| TEST_P(PaintLayerScrollableAreaTest, |
| ScrollWithStickyNeedsCompositingInputsUpdate) { |
| SetBodyInnerHTML(R"HTML( |
| <style> |
| * { |
| margin: 0; |
| } |
| body { |
| height: 610px; |
| width: 820px; |
| } |
| #sticky { |
| height: 10px; |
| left: 50px; |
| position: sticky; |
| top: 50px; |
| width: 10px; |
| } |
| </style> |
| <div id=sticky></div> |
| )HTML"); |
| |
| auto* scrollable_area = GetLayoutView().GetScrollableArea(); |
| EXPECT_EQ(FloatSize(0, 0), scrollable_area->GetScrollOffset()); |
| |
| // Changing the scroll offset requires a compositing inputs update to rerun |
| // overlap testing. |
| scrollable_area->SetScrollOffset(ScrollOffset(0, 1), |
| mojom::blink::ScrollType::kProgrammatic); |
| if (GetParam() & kCompositeAfterPaint) { |
| // CAP doesn't request compositing inputs update on scroll offset changes. |
| EXPECT_FALSE(scrollable_area->Layer()->NeedsCompositingInputsUpdate()); |
| } else { |
| EXPECT_TRUE(scrollable_area->Layer()->NeedsCompositingInputsUpdate()); |
| } |
| UpdateAllLifecyclePhasesForTest(); |
| EXPECT_EQ(FloatSize(0, 1), scrollable_area->GetScrollOffset()); |
| } |
| |
| TEST_P(PaintLayerScrollableAreaTest, |
| ScrollWithFixedDoesNotNeedCompositingInputsUpdate) { |
| SetBodyInnerHTML(R"HTML( |
| <style> |
| * { |
| margin: 0; |
| } |
| body { |
| height: 610px; |
| width: 820px; |
| } |
| #fixed { |
| height: 10px; |
| left: 50px; |
| position: fixed; |
| top: 50px; |
| width: 10px; |
| } |
| </style> |
| <div id=fixed></div> |
| )HTML"); |
| |
| auto* scrollable_area = GetLayoutView().GetScrollableArea(); |
| EXPECT_EQ(FloatSize(0, 0), scrollable_area->GetScrollOffset()); |
| |
| // Changing the scroll offset should not require compositing inputs update |
| // even though fixed-pos content is present as fixed bounds is already |
| // expanded to include all possible scroll offsets. |
| scrollable_area->SetScrollOffset(ScrollOffset(0, 1), |
| mojom::blink::ScrollType::kProgrammatic); |
| EXPECT_FALSE(scrollable_area->Layer()->NeedsCompositingInputsUpdate()); |
| UpdateAllLifecyclePhasesForTest(); |
| EXPECT_EQ(FloatSize(0, 1), scrollable_area->GetScrollOffset()); |
| } |
| |
| TEST_P(PaintLayerScrollableAreaTest, |
| ScrollWithLocalAttachmentBackgroundInScrollingContents) { |
| SetBodyInnerHTML(R"HTML( |
| <style> |
| #scroller { |
| overflow: scroll; |
| height: 200px; |
| width: 200px; |
| background: linear-gradient(black, white); |
| background-attachment: local; |
| } |
| </style> |
| <div id='scroller'> |
| <div id='forceScroll' style='height: 2000px;'></div> |
| </div> |
| )HTML"); |
| |
| auto* scroller = GetLayoutBoxByElementId("scroller"); |
| auto* scrollable_area = scroller->GetScrollableArea(); |
| EXPECT_EQ(kBackgroundPaintInScrollingContents, |
| scroller->ComputeBackgroundPaintLocationIfComposited()); |
| EXPECT_EQ(kBackgroundPaintInScrollingContents, |
| scroller->GetBackgroundPaintLocation()); |
| EXPECT_TRUE(UsesCompositedScrolling(scroller)); |
| |
| // Programmatically changing the scroll offset. |
| scrollable_area->SetScrollOffset(ScrollOffset(0, 1), |
| mojom::blink::ScrollType::kProgrammatic); |
| // No paint invalidation because it uses composited scrolling. |
| EXPECT_FALSE(scroller->ShouldDoFullPaintInvalidation()); |
| EXPECT_FALSE(scroller->BackgroundNeedsFullPaintInvalidation()); |
| |
| EXPECT_TRUE(scroller->NeedsPaintPropertyUpdate()); |
| UpdateAllLifecyclePhasesForTest(); |
| EXPECT_EQ(FloatSize(0, 1), scrollable_area->GetScrollOffset()); |
| const auto* properties = scroller->FirstFragment().PaintProperties(); |
| EXPECT_NE(nullptr, properties->ScrollTranslation()); |
| } |
| |
| TEST_P(PaintLayerScrollableAreaTest, ScrollWith3DPreserveParent) { |
| SetBodyInnerHTML(R"HTML( |
| <style> |
| #scroller { |
| overflow-y: scroll; |
| height: 200px; |
| width: 200px; |
| background: white; |
| } |
| </style> |
| <div style='transform-style: preserve-3d;'> |
| <div id='scroller'> |
| <div style='height: 2000px;'></div> |
| </div> |
| </div> |
| )HTML"); |
| |
| auto* scroller = GetLayoutBoxByElementId("scroller"); |
| EXPECT_EQ(kBackgroundPaintInGraphicsLayer, |
| scroller->ComputeBackgroundPaintLocationIfComposited()); |
| } |
| |
| TEST_P(PaintLayerScrollableAreaTest, |
| ScrollWithLocalAttachmentBackgroundInMainLayer) { |
| SetBodyInnerHTML(R"HTML( |
| <style> |
| #scroller { |
| overflow: scroll; |
| height: 200px; |
| width: 200px; |
| border: 10px dashed black; |
| background: linear-gradient(black, white) local, yellow; |
| } |
| </style> |
| <div id='scroller'> |
| <div id='forceScroll' style='height: 2000px;'></div> |
| </div> |
| )HTML"); |
| |
| auto* scroller = GetLayoutBoxByElementId("scroller"); |
| auto* scrollable_area = scroller->GetScrollableArea(); |
| EXPECT_EQ( |
| kBackgroundPaintInGraphicsLayer | kBackgroundPaintInScrollingContents, |
| scroller->ComputeBackgroundPaintLocationIfComposited()); |
| EXPECT_EQ( |
| kBackgroundPaintInGraphicsLayer | kBackgroundPaintInScrollingContents, |
| scroller->GetBackgroundPaintLocation()); |
| |
| // Programmatically changing the scroll offset. |
| scrollable_area->SetScrollOffset(ScrollOffset(0, 1), |
| mojom::blink::ScrollType::kProgrammatic); |
| // No invalidation because the background paints into the main layer. |
| EXPECT_TRUE(scroller->ShouldDoFullPaintInvalidation()); |
| EXPECT_TRUE(scroller->BackgroundNeedsFullPaintInvalidation()); |
| EXPECT_TRUE(scroller->NeedsPaintPropertyUpdate()); |
| UpdateAllLifecyclePhasesForTest(); |
| EXPECT_EQ(FloatSize(0, 1), scrollable_area->GetScrollOffset()); |
| const auto* properties = scroller->FirstFragment().PaintProperties(); |
| EXPECT_NE(nullptr, properties->ScrollTranslation()); |
| } |
| |
| TEST_P(PaintLayerScrollableAreaTest, ViewScrollWithFixedAttachmentBackground) { |
| SetBodyInnerHTML(R"HTML( |
| <style> |
| html, #fixed-background { |
| background: linear-gradient(black, white) fixed; |
| } |
| #fixed-background { |
| width: 200px; |
| height: 200px; |
| overflow: scroll; |
| } |
| </style> |
| <div id="fixed-background"> |
| <div style="height: 3000px"></div> |
| </div> |
| <div style="height: 3000px"></div> |
| )HTML"); |
| |
| EXPECT_EQ(kBackgroundPaintInScrollingContents, |
| GetLayoutView().GetBackgroundPaintLocation()); |
| auto* fixed_background_div = GetLayoutBoxByElementId("fixed-background"); |
| EXPECT_EQ(kBackgroundPaintInGraphicsLayer, |
| fixed_background_div->GetBackgroundPaintLocation()); |
| auto* div_scrollable_area = fixed_background_div->GetScrollableArea(); |
| auto* view_scrollable_area = GetLayoutView().GetScrollableArea(); |
| |
| // Programmatically changing the view's scroll offset. Should invalidate all |
| // objects with fixed attachment background. |
| view_scrollable_area->SetScrollOffset( |
| ScrollOffset(0, 1), mojom::blink::ScrollType::kProgrammatic); |
| EXPECT_TRUE(fixed_background_div->ShouldDoFullPaintInvalidation()); |
| EXPECT_TRUE(fixed_background_div->BackgroundNeedsFullPaintInvalidation()); |
| EXPECT_FALSE(fixed_background_div->NeedsPaintPropertyUpdate()); |
| EXPECT_TRUE(GetLayoutView().ShouldDoFullPaintInvalidation()); |
| EXPECT_TRUE(GetLayoutView().BackgroundNeedsFullPaintInvalidation()); |
| EXPECT_TRUE(GetLayoutView().NeedsPaintPropertyUpdate()); |
| UpdateAllLifecyclePhasesForTest(); |
| |
| // Programmatically changing the div's scroll offset. Should invalidate the |
| // scrolled div with fixed attachment background. |
| div_scrollable_area->SetScrollOffset(ScrollOffset(0, 1), |
| mojom::blink::ScrollType::kProgrammatic); |
| EXPECT_TRUE(fixed_background_div->ShouldDoFullPaintInvalidation()); |
| EXPECT_TRUE(fixed_background_div->BackgroundNeedsFullPaintInvalidation()); |
| EXPECT_TRUE(fixed_background_div->NeedsPaintPropertyUpdate()); |
| EXPECT_FALSE(GetLayoutView().ShouldDoFullPaintInvalidation()); |
| EXPECT_FALSE(GetLayoutView().BackgroundNeedsFullPaintInvalidation()); |
| EXPECT_FALSE(GetLayoutView().NeedsPaintPropertyUpdate()); |
| } |
| |
| TEST_P(PaintLayerScrollableAreaTest, |
| ViewScrollWithSolidColorFixedAttachmentBackground) { |
| SetBodyInnerHTML(R"HTML( |
| <style> |
| html, #fixed-background { |
| background: green fixed; |
| } |
| #fixed-background { |
| width: 200px; |
| height: 200px; |
| overflow: scroll; |
| } |
| </style> |
| <div id="fixed-background"> |
| <div style="height: 3000px"></div> |
| </div> |
| <div style="height: 3000px"></div> |
| )HTML"); |
| |
| // Fixed-attachment solid-color background should be treated as default |
| // attachment. |
| EXPECT_EQ(kBackgroundPaintInScrollingContents, |
| GetLayoutView().GetBackgroundPaintLocation()); |
| auto* fixed_background_div = GetLayoutBoxByElementId("fixed-background"); |
| EXPECT_EQ(kBackgroundPaintInScrollingContents, |
| fixed_background_div->ComputeBackgroundPaintLocationIfComposited()); |
| EXPECT_EQ(kBackgroundPaintInScrollingContents, |
| fixed_background_div->GetBackgroundPaintLocation()); |
| auto* div_scrollable_area = fixed_background_div->GetScrollableArea(); |
| auto* view_scrollable_area = GetLayoutView().GetScrollableArea(); |
| |
| // Programmatically changing the view's scroll offset. Should invalidate all |
| // objects with fixed attachment background. |
| view_scrollable_area->SetScrollOffset( |
| ScrollOffset(0, 1), mojom::blink::ScrollType::kProgrammatic); |
| EXPECT_FALSE(fixed_background_div->ShouldDoFullPaintInvalidation()); |
| EXPECT_FALSE(fixed_background_div->BackgroundNeedsFullPaintInvalidation()); |
| EXPECT_FALSE(fixed_background_div->NeedsPaintPropertyUpdate()); |
| EXPECT_FALSE(GetLayoutView().ShouldDoFullPaintInvalidation()); |
| EXPECT_FALSE(GetLayoutView().BackgroundNeedsFullPaintInvalidation()); |
| EXPECT_TRUE(GetLayoutView().NeedsPaintPropertyUpdate()); |
| UpdateAllLifecyclePhasesForTest(); |
| |
| // Programmatically changing the div's scroll offset. Should invalidate the |
| // scrolled div with fixed attachment background. |
| div_scrollable_area->SetScrollOffset(ScrollOffset(0, 1), |
| mojom::blink::ScrollType::kProgrammatic); |
| EXPECT_FALSE(fixed_background_div->ShouldDoFullPaintInvalidation()); |
| EXPECT_FALSE(fixed_background_div->BackgroundNeedsFullPaintInvalidation()); |
| EXPECT_TRUE(fixed_background_div->NeedsPaintPropertyUpdate()); |
| EXPECT_FALSE(GetLayoutView().ShouldDoFullPaintInvalidation()); |
| EXPECT_FALSE(GetLayoutView().BackgroundNeedsFullPaintInvalidation()); |
| EXPECT_FALSE(GetLayoutView().NeedsPaintPropertyUpdate()); |
| } |
| |
| TEST_P(PaintLayerScrollableAreaTest, |
| ViewScrollWithFixedAttachmentBackgroundPreferCompositingToLCDText) { |
| GetDocument().GetFrame()->GetSettings()->SetPreferCompositingToLCDTextEnabled( |
| true); |
| SetBodyInnerHTML(R"HTML( |
| <style> |
| html, #fixed-background { |
| background: linear-gradient(black, white) fixed; |
| } |
| #fixed-background { |
| width: 200px; |
| height: 200px; |
| overflow: scroll; |
| } |
| </style> |
| <div id="fixed-background"> |
| <div style="height: 3000px"></div> |
| </div> |
| <div style="height: 3000px"></div> |
| )HTML"); |
| |
| EXPECT_EQ(kBackgroundPaintInGraphicsLayer, |
| GetLayoutView().GetBackgroundPaintLocation()); |
| auto* fixed_background_div = GetLayoutBoxByElementId("fixed-background"); |
| EXPECT_EQ(kBackgroundPaintInGraphicsLayer, |
| fixed_background_div->GetBackgroundPaintLocation()); |
| auto* div_scrollable_area = fixed_background_div->GetScrollableArea(); |
| auto* view_scrollable_area = GetLayoutView().GetScrollableArea(); |
| |
| // Programmatically changing the view's scroll offset. Should invalidate all |
| // objects with fixed attachment background except the layout view. |
| view_scrollable_area->SetScrollOffset( |
| ScrollOffset(0, 1), mojom::blink::ScrollType::kProgrammatic); |
| EXPECT_TRUE(fixed_background_div->ShouldDoFullPaintInvalidation()); |
| EXPECT_TRUE(fixed_background_div->BackgroundNeedsFullPaintInvalidation()); |
| EXPECT_FALSE(fixed_background_div->NeedsPaintPropertyUpdate()); |
| EXPECT_FALSE(GetLayoutView().ShouldDoFullPaintInvalidation()); |
| EXPECT_FALSE(GetLayoutView().BackgroundNeedsFullPaintInvalidation()); |
| EXPECT_TRUE(GetLayoutView().NeedsPaintPropertyUpdate()); |
| UpdateAllLifecyclePhasesForTest(); |
| |
| // Programmatically changing the div's scroll offset. Should invalidate the |
| // scrolled div with fixed attachment background. |
| div_scrollable_area->SetScrollOffset(ScrollOffset(0, 1), |
| mojom::blink::ScrollType::kProgrammatic); |
| EXPECT_TRUE(fixed_background_div->ShouldDoFullPaintInvalidation()); |
| EXPECT_TRUE(fixed_background_div->BackgroundNeedsFullPaintInvalidation()); |
| EXPECT_TRUE(fixed_background_div->NeedsPaintPropertyUpdate()); |
| EXPECT_FALSE(GetLayoutView().ShouldDoFullPaintInvalidation()); |
| EXPECT_FALSE(GetLayoutView().BackgroundNeedsFullPaintInvalidation()); |
| EXPECT_FALSE(GetLayoutView().NeedsPaintPropertyUpdate()); |
| } |
| |
| TEST_P(PaintLayerScrollableAreaTest, HitTestOverlayScrollbars) { |
| SetBodyInnerHTML(R"HTML( |
| <style> |
| html, body { |
| margin: 0; |
| } |
| #scroller { |
| overflow: scroll; |
| height: 100px; |
| width: 100px; |
| } |
| #scrolled { |
| width: 1000px; |
| height: 1000px; |
| } |
| </style> |
| <div id='scroller'><div id='scrolled'></div></div> |
| )HTML"); |
| |
| auto* scroller = GetLayoutObjectByElementId("scroller"); |
| auto* scrollable_area = |
| To<LayoutBoxModelObject>(scroller)->GetScrollableArea(); |
| |
| scrollable_area->SetScrollbarsHiddenIfOverlay(true); |
| |
| HitTestRequest hit_request(HitTestRequest::kMove | HitTestRequest::kReadOnly); |
| HitTestLocation location(PhysicalOffset(95, 5)); |
| HitTestResult hit_result(hit_request, location); |
| GetDocument().GetLayoutView()->HitTest(location, hit_result); |
| EXPECT_EQ(hit_result.GetScrollbar(), nullptr); |
| location = HitTestLocation(PhysicalOffset(5, 95)); |
| hit_result = HitTestResult(hit_request, location); |
| GetDocument().GetLayoutView()->HitTest(location, hit_result); |
| EXPECT_EQ(hit_result.GetScrollbar(), nullptr); |
| |
| scrollable_area->SetScrollbarsHiddenIfOverlay(false); |
| |
| location = HitTestLocation(PhysicalOffset(95, 5)); |
| hit_result = HitTestResult(hit_request, location); |
| GetDocument().GetLayoutView()->HitTest(location, hit_result); |
| EXPECT_EQ(hit_result.GetScrollbar(), scrollable_area->VerticalScrollbar()); |
| location = HitTestLocation(PhysicalOffset(5, 95)); |
| hit_result = HitTestResult(hit_request, location); |
| GetDocument().GetLayoutView()->HitTest(location, hit_result); |
| EXPECT_EQ(hit_result.GetScrollbar(), scrollable_area->HorizontalScrollbar()); |
| } |
| |
| TEST_P(PaintLayerScrollableAreaTest, CompositedStickyDescendant) { |
| SetBodyInnerHTML(R"HTML( |
| <div id=scroller style="overflow: scroll; width: 500px; height: 300px; |
| will-change: transform"> |
| <div id=sticky style="top: 0px; position: sticky; background: green"> |
| </div> |
| <div style="width: 10px; height: 700px; background: lightblue"></div> |
| </div> |
| )HTML"); |
| auto* scroller = |
| To<LayoutBoxModelObject>(GetLayoutObjectByElementId("scroller")); |
| auto* scrollable_area = scroller->GetScrollableArea(); |
| if (!RuntimeEnabledFeatures::CompositeAfterPaintEnabled()) |
| EXPECT_EQ(kPaintsIntoOwnBacking, scroller->Layer()->GetCompositingState()); |
| auto* sticky = To<LayoutBoxModelObject>(GetLayoutObjectByElementId("sticky")); |
| |
| EXPECT_EQ(&sticky->FirstFragment().LocalBorderBoxProperties().Transform(), |
| sticky->FirstFragment().PaintProperties()->StickyTranslation()); |
| EXPECT_TRUE(sticky->FirstFragment() |
| .PaintProperties() |
| ->StickyTranslation() |
| ->IsIdentity()); |
| |
| scrollable_area->SetScrollOffset(ScrollOffset(0, 50), |
| mojom::blink::ScrollType::kUser); |
| UpdateAllLifecyclePhasesForTest(); |
| |
| EXPECT_EQ(FloatSize(0, 50), sticky->FirstFragment() |
| .PaintProperties() |
| ->StickyTranslation() |
| ->Translation2D()); |
| } |
| |
| TEST_P(PaintLayerScrollableAreaTest, StickyPositionUseCounter) { |
| SetBodyInnerHTML(R"HTML( |
| <div style="overflow: scroll; width: 500px; height: 300px;"> |
| <div id=test></div> |
| <div id=forcescroll style="width: 10px; height: 700px;"></div> |
| </div> |
| )HTML"); |
| EXPECT_FALSE(GetDocument().IsUseCounted(WebFeature::kPositionSticky)); |
| |
| auto* test = GetElementById("test"); |
| test->setAttribute(html_names::kStyleAttr, "position: sticky;"); |
| UpdateAllLifecyclePhasesForTest(); |
| EXPECT_FALSE(GetDocument().IsUseCounted(WebFeature::kPositionSticky)); |
| |
| test->setAttribute(html_names::kStyleAttr, "top: 0; position: sticky;"); |
| UpdateAllLifecyclePhasesForTest(); |
| EXPECT_TRUE(GetDocument().IsUseCounted(WebFeature::kPositionSticky)); |
| } |
| |
| // Delayed scroll offset clamping should not crash. https://crbug.com/842495 |
| TEST_P(PaintLayerScrollableAreaTest, IgnoreDelayedScrollOnDestroyedLayer) { |
| SetBodyInnerHTML(R"HTML( |
| <div id=scroller style="overflow: scroll; width: 200px; height: 200px;"> |
| <div style="height: 1000px;"></div> |
| </div> |
| )HTML"); |
| Element* scroller = GetDocument().getElementById("scroller"); |
| { |
| PaintLayerScrollableArea::DelayScrollOffsetClampScope scope; |
| PaintLayerScrollableArea::DelayScrollOffsetClampScope::SetNeedsClamp( |
| scroller->GetLayoutBox()->GetScrollableArea()); |
| scroller->SetInlineStyleProperty(CSSPropertyID::kDisplay, |
| CSSValueID::kNone); |
| UpdateAllLifecyclePhasesForTest(); |
| } |
| } |
| |
| TEST_P(PaintLayerScrollableAreaTest, ScrollbarMaximum) { |
| SetBodyInnerHTML(R"HTML( |
| <style> |
| #spacer { |
| height: 17.984375px; |
| } |
| #scroller { |
| border-top: 0.328125px solid gray; |
| border-bottom: 0.328125px solid gray; |
| height:149.34375px; |
| width: 100px; |
| overflow-y:auto; |
| } |
| #content { |
| height: 156.578125px; |
| } |
| </style> |
| <div id='spacer'></div> |
| <div id='scroller'> |
| <div id='content'></div> |
| </div> |
| )HTML"); |
| |
| LayoutBox* scroller = GetLayoutBoxByElementId("scroller"); |
| PaintLayerScrollableArea* scrollable_area = scroller->GetScrollableArea(); |
| Scrollbar* scrollbar = scrollable_area->VerticalScrollbar(); |
| |
| scrollable_area->ScrollBy(ScrollOffset(0, 1000), |
| mojom::blink::ScrollType::kProgrammatic); |
| UpdateAllLifecyclePhasesForTest(); |
| EXPECT_EQ(scrollbar->CurrentPos(), scrollbar->Maximum()); |
| } |
| |
| TEST_P(PaintLayerScrollableAreaTest, ScrollingBackgroundVisualRect) { |
| SetBodyInnerHTML(R"HTML( |
| <style> |
| ::-webkit-scrollbar { display: none; } |
| #scroller { |
| width: 100.7px; |
| height: 100.4px; |
| overflow: scroll; |
| border-top: 2.6px solid blue; |
| border-left: 2.4px solid blue; |
| will-change: transform; |
| } |
| #content { |
| width: 50.7px; |
| height: 200.4px; |
| } |
| </style> |
| <div id="scroller"> |
| <div id="content"></div> |
| </div> |
| )HTML"); |
| |
| EXPECT_EQ(IntRect(2, 3, 101, 200), |
| GetLayoutBoxByElementId("scroller") |
| ->GetScrollableArea() |
| ->ScrollingBackgroundVisualRect(PhysicalOffset())); |
| } |
| |
| TEST_P(PaintLayerScrollableAreaTest, RtlScrollOriginSnapping) { |
| SetBodyInnerHTML(R"HTML( |
| <style> |
| #container { |
| direction: rtl; |
| display: flex; |
| } |
| #scroller { |
| width: 100%; |
| height: 100px; |
| overflow: hidden; |
| } |
| #scroller-content { |
| width: 200%; |
| height: 200px; |
| } |
| </style> |
| <div id="container"> |
| <div id="first-child" style="flex:1; display:none"></div> |
| <div style="flex:2.2"> |
| <div id="scroller"> |
| <div id ="scroller-content"></div> |
| </div> |
| </div> |
| </div> |
| )HTML"); |
| |
| // Test that scroll origin is snapped such that maximum scroll offset is |
| // always zero for an rtl block. |
| |
| GetFrame().View()->Resize(795, 600); |
| UpdateAllLifecyclePhasesForTest(); |
| LayoutBox* scroller = GetLayoutBoxByElementId("scroller"); |
| PaintLayerScrollableArea* scrollable_area = scroller->GetScrollableArea(); |
| EXPECT_EQ(scrollable_area->MaximumScrollOffsetInt(), IntSize(0, 100)); |
| |
| Element* first_child = GetElementById("first-child"); |
| first_child->RemoveInlineStyleProperty(CSSPropertyID::kDisplay); |
| UpdateAllLifecyclePhasesForTest(); |
| EXPECT_EQ(scrollable_area->MaximumScrollOffsetInt(), IntSize(0, 100)); |
| } |
| |
| TEST_P(PaintLayerScrollableAreaTest, ShowCustomResizerInTextarea) { |
| GetPage().GetSettings().SetTextAreasAreResizable(true); |
| SetBodyInnerHTML(R"HTML( |
| <!doctype HTML> |
| <style> |
| textarea { |
| width: 200px; |
| height: 100px; |
| } |
| ::-webkit-resizer { |
| background-color: red; |
| } |
| </style> |
| <textarea id="target"></textarea> |
| )HTML"); |
| |
| const auto* paint_layer = GetPaintLayerByElementId("target"); |
| ASSERT_TRUE(paint_layer); |
| |
| EXPECT_NE(paint_layer->GetScrollableArea()->Resizer(), nullptr); |
| } |
| |
| TEST_P(PaintLayerScrollableAreaTest, |
| ApplyPendingHistoryRestoreScrollOffsetTwice) { |
| GetPage().GetSettings().SetTextAreasAreResizable(true); |
| SetBodyInnerHTML(R"HTML( |
| <!doctype HTML> |
| <div id="target" style="overflow: scroll; width: 50px; height: 50px"> |
| <div style="width: 50px; height: 500px"> |
| </div> |
| </div> |
| )HTML"); |
| |
| const auto* paint_layer = GetPaintLayerByElementId("target"); |
| auto* scrollable_area = paint_layer->GetScrollableArea(); |
| |
| HistoryItem::ViewState view_state; |
| view_state.scroll_offset_ = ScrollOffset(0, 100); |
| scrollable_area->SetPendingHistoryRestoreScrollOffset(view_state, true); |
| scrollable_area->ApplyPendingHistoryRestoreScrollOffset(); |
| EXPECT_EQ(ScrollOffset(0, 100), scrollable_area->GetScrollOffset()); |
| |
| scrollable_area->SetScrollOffset(ScrollOffset(0, 50), |
| mojom::blink::ScrollType::kUser); |
| |
| // The second call to ApplyPendingHistoryRestoreScrollOffset should |
| // do nothing, since the history was already restored. |
| scrollable_area->ApplyPendingHistoryRestoreScrollOffset(); |
| EXPECT_EQ(ScrollOffset(0, 50), scrollable_area->GetScrollOffset()); |
| } |
| |
| // Test that a trivial 3D transform results in composited scrolling. |
| TEST_P(PaintLayerScrollableAreaTest, CompositeWithTrivial3D) { |
| SetBodyInnerHTML(R"HTML( |
| <style> |
| #scroller { |
| width: 100px; |
| height: 100px; |
| overflow: scroll; |
| transform: translateZ(0); |
| } |
| #scrolled { |
| width: 200px; |
| height: 200px; |
| } |
| </style> |
| <div id="scroller"> |
| <div id="scrolled"></div> |
| </div> |
| )HTML"); |
| |
| EXPECT_TRUE(UsesCompositedScrolling(GetLayoutObjectByElementId("scroller"))); |
| } |
| |
| class PaintLayerScrollableAreaTestLowEndPlatform |
| : public TestingPlatformSupport { |
| public: |
| bool IsLowEndDevice() override { return true; } |
| }; |
| |
| // Test that a trivial 3D transform results in composited scrolling even on |
| // low-end devices that may not composite trivial 3D transforms. |
| TEST_P(PaintLayerScrollableAreaTest, LowEndCompositeWithTrivial3D) { |
| ScopedTestingPlatformSupport<PaintLayerScrollableAreaTestLowEndPlatform> |
| platform; |
| SetBodyInnerHTML(R"HTML( |
| <style> |
| #scroller { |
| width: 100px; |
| height: 100px; |
| overflow: scroll; |
| transform: translateZ(0); |
| } |
| #scrolled { |
| width: 200px; |
| height: 200px; |
| } |
| </style> |
| <div id="scroller"> |
| <div id="scrolled"></div> |
| </div> |
| )HTML"); |
| |
| EXPECT_TRUE(UsesCompositedScrolling(GetLayoutObjectByElementId("scroller"))); |
| } |
| |
| TEST_P(PaintLayerScrollableAreaTest, SetSnapContainerDataNeedsUpdate) { |
| SetBodyInnerHTML(R"HTML( |
| <style> |
| .scroller { |
| overflow: scroll; |
| height: 200px; |
| width: 200px; |
| } |
| </style> |
| <div id='first_scroller' class='scroller'> |
| <div style='height: 2000px;'></div> |
| </div> |
| <div id='second_scroller' class='scroller'> |
| <div style='height: 2000px;'></div> |
| </div> |
| )HTML"); |
| |
| auto* first_scroller = GetLayoutObjectByElementId("first_scroller"); |
| auto* first_scrollable_area = |
| To<LayoutBoxModelObject>(first_scroller)->GetScrollableArea(); |
| |
| auto* second_scroller = GetLayoutObjectByElementId("second_scroller"); |
| auto* second_scrollable_area = |
| To<LayoutBoxModelObject>(second_scroller)->GetScrollableArea(); |
| |
| EXPECT_EQ(&first_scroller->GetDocument().GetSnapCoordinator(), |
| &second_scroller->GetDocument().GetSnapCoordinator()); |
| |
| auto& snap_coordinator = first_scroller->GetDocument().GetSnapCoordinator(); |
| EXPECT_FALSE(snap_coordinator.AnySnapContainerDataNeedsUpdate()); |
| |
| // SnapCoordinator needs to update all its snap containers if one of them asks |
| // for an update. |
| first_scrollable_area->SetSnapContainerDataNeedsUpdate(true); |
| EXPECT_TRUE(snap_coordinator.AnySnapContainerDataNeedsUpdate()); |
| |
| // SnapCoordinator still needs to update all its snap containers even if one |
| // of them asks not to. |
| second_scrollable_area->SetSnapContainerDataNeedsUpdate(false); |
| EXPECT_TRUE(snap_coordinator.AnySnapContainerDataNeedsUpdate()); |
| |
| first_scrollable_area->SetSnapContainerDataNeedsUpdate(false); |
| EXPECT_TRUE(snap_coordinator.AnySnapContainerDataNeedsUpdate()); |
| |
| snap_coordinator.UpdateAllSnapContainerDataIfNeeded(); |
| EXPECT_FALSE(snap_coordinator.AnySnapContainerDataNeedsUpdate()); |
| } |
| |
| class ScrollTimelineForTest : public ScrollTimeline { |
| public: |
| ScrollTimelineForTest(Document* document, |
| Element* scroll_source, |
| HeapVector<Member<ScrollTimelineOffset>> |
| scroll_offsets = CreateScrollOffsets()) |
| : ScrollTimeline(document, |
| scroll_source, |
| ScrollTimeline::Vertical, |
| std::move(scroll_offsets), |
| 100.0), |
| invalidated_(false) {} |
| void Invalidate() override { |
| ScrollTimeline::Invalidate(); |
| invalidated_ = true; |
| } |
| bool Invalidated() const { return invalidated_; } |
| void ResetInvalidated() { invalidated_ = false; } |
| void Trace(Visitor* visitor) const override { |
| ScrollTimeline::Trace(visitor); |
| } |
| |
| private: |
| bool invalidated_; |
| }; |
| |
| // Verify that scrollable area changes invalidate scroll timeline. |
| TEST_P(PaintLayerScrollableAreaTest, ScrollTimelineInvalidation) { |
| SetBodyInnerHTML(R"HTML( |
| <style> |
| #scroller { overflow: scroll; width: 100px; height: 100px; } |
| #spacer { height: 1000px; } |
| </style> |
| <div id='scroller'> |
| <div id ='spacer'></div> |
| </div> |
| )HTML"); |
| |
| auto* scroller = |
| To<LayoutBoxModelObject>(GetLayoutObjectByElementId("scroller")); |
| PaintLayerScrollableArea* scrollable_area = scroller->GetScrollableArea(); |
| scrollable_area->SetScrollOffset(ScrollOffset(0, 20), |
| mojom::blink::ScrollType::kProgrammatic); |
| Element* scroller_element = GetElementById("scroller"); |
| ScrollTimelineForTest* scroll_timeline = |
| MakeGarbageCollected<ScrollTimelineForTest>(&GetDocument(), |
| scroller_element); |
| scroll_timeline->ResetInvalidated(); |
| // Verify that changing scroll offset invalidates scroll timeline. |
| scrollable_area->SetScrollOffset(ScrollOffset(0, 30), |
| mojom::blink::ScrollType::kProgrammatic); |
| EXPECT_TRUE(scroll_timeline->Invalidated()); |
| scroll_timeline->ResetInvalidated(); |
| |
| // Verify that changing scroller size invalidates scroll timeline. |
| scroller_element->setAttribute(html_names::kStyleAttr, "height:110px;"); |
| UpdateAllLifecyclePhasesForTest(); |
| EXPECT_TRUE(scroll_timeline->Invalidated()); |
| scroll_timeline->ResetInvalidated(); |
| |
| // Verify that changing content area size invalidates scroll timeline. |
| Element* spacer_element = GetElementById("spacer"); |
| spacer_element->setAttribute(html_names::kStyleAttr, "height:900px;"); |
| UpdateAllLifecyclePhasesForTest(); |
| EXPECT_TRUE(scroll_timeline->Invalidated()); |
| scroll_timeline->ResetInvalidated(); |
| } |
| |
| } // namespace blink |