/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set ts=8 sts=2 et sw=2 tw=80: */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "APZCTreeManagerTester.h" #include "APZTestCommon.h" #include "InputUtils.h" class APZScrollHandoffTester : public APZCTreeManagerTester { protected: UniquePtr registration; TestAsyncPanZoomController* rootApzc; void CreateScrollHandoffLayerTree1() { const char* treeShape = "x(x)"; LayerIntRect layerVisibleRect[] = {LayerIntRect(0, 0, 100, 100), LayerIntRect(0, 50, 100, 50)}; CreateScrollData(treeShape, layerVisibleRect); SetScrollableFrameMetrics(root, ScrollableLayerGuid::START_SCROLL_ID, CSSRect(0, 0, 200, 200)); SetScrollableFrameMetrics(layers[1], ScrollableLayerGuid::START_SCROLL_ID + 1, CSSRect(0, 0, 100, 100)); SetScrollHandoff(layers[1], root); registration = MakeUnique(LayersId{0}, mcc); UpdateHitTestingTree(); rootApzc = ApzcOf(root); rootApzc->GetFrameMetrics().SetIsRootContent( true); // make root APZC zoomable } void CreateScrollHandoffLayerTree2() { const char* treeShape = "x(x(x))"; LayerIntRect layerVisibleRect[] = {LayerIntRect(0, 0, 100, 100), LayerIntRect(0, 0, 100, 100), LayerIntRect(0, 50, 100, 50)}; CreateScrollData(treeShape, layerVisibleRect); SetScrollableFrameMetrics(root, ScrollableLayerGuid::START_SCROLL_ID, CSSRect(0, 0, 200, 200)); SetScrollableFrameMetrics(layers[1], ScrollableLayerGuid::START_SCROLL_ID + 2, CSSRect(-100, -100, 200, 200)); SetScrollableFrameMetrics(layers[2], ScrollableLayerGuid::START_SCROLL_ID + 1, CSSRect(0, 0, 100, 100)); SetScrollHandoff(layers[1], root); SetScrollHandoff(layers[2], layers[1]); // No ScopedLayerTreeRegistration as that just needs to be done once per // test and this is the second layer tree for a particular test. MOZ_ASSERT(registration); UpdateHitTestingTree(); rootApzc = ApzcOf(root); } void CreateScrollHandoffLayerTree3() { const char* treeShape = "x(x(x)x(x))"; LayerIntRect layerVisibleRect[] = { LayerIntRect(0, 0, 100, 100), // root LayerIntRect(0, 0, 100, 50), // scrolling parent 1 LayerIntRect(0, 0, 100, 50), // scrolling child 1 LayerIntRect(0, 50, 100, 50), // scrolling parent 2 LayerIntRect(0, 50, 100, 50) // scrolling child 2 }; CreateScrollData(treeShape, layerVisibleRect); SetScrollableFrameMetrics(layers[0], ScrollableLayerGuid::START_SCROLL_ID, CSSRect(0, 0, 100, 100)); SetScrollableFrameMetrics(layers[1], ScrollableLayerGuid::START_SCROLL_ID + 1, CSSRect(0, 0, 100, 100)); SetScrollableFrameMetrics(layers[2], ScrollableLayerGuid::START_SCROLL_ID + 2, CSSRect(0, 0, 100, 100)); SetScrollableFrameMetrics(layers[3], ScrollableLayerGuid::START_SCROLL_ID + 3, CSSRect(0, 50, 100, 100)); SetScrollableFrameMetrics(layers[4], ScrollableLayerGuid::START_SCROLL_ID + 4, CSSRect(0, 50, 100, 100)); SetScrollHandoff(layers[1], layers[0]); SetScrollHandoff(layers[3], layers[0]); SetScrollHandoff(layers[2], layers[1]); SetScrollHandoff(layers[4], layers[3]); registration = MakeUnique(LayersId{0}, mcc); UpdateHitTestingTree(); } // Creates a layer tree with a parent layer that is only scrollable // horizontally, and a child layer that is only scrollable vertically. void CreateScrollHandoffLayerTree4() { const char* treeShape = "x(x)"; LayerIntRect layerVisibleRect[] = {LayerIntRect(0, 0, 100, 100), LayerIntRect(0, 0, 100, 100)}; CreateScrollData(treeShape, layerVisibleRect); SetScrollableFrameMetrics(root, ScrollableLayerGuid::START_SCROLL_ID, CSSRect(0, 0, 200, 100)); SetScrollableFrameMetrics(layers[1], ScrollableLayerGuid::START_SCROLL_ID + 1, CSSRect(0, 0, 100, 200)); SetScrollHandoff(layers[1], root); registration = MakeUnique(LayersId{0}, mcc); UpdateHitTestingTree(); rootApzc = ApzcOf(root); } // Creates a layer tree with a parent layer that is not scrollable, and a // child layer that is only scrollable vertically. void CreateScrollHandoffLayerTree5() { const char* treeShape = "x(x)"; LayerIntRect layerVisibleRect[] = { LayerIntRect(0, 0, 100, 100), // scrolling parent LayerIntRect(0, 50, 100, 50) // scrolling child }; CreateScrollData(treeShape, layerVisibleRect); SetScrollableFrameMetrics(root, ScrollableLayerGuid::START_SCROLL_ID, CSSRect(0, 0, 100, 100)); SetScrollableFrameMetrics(layers[1], ScrollableLayerGuid::START_SCROLL_ID + 1, CSSRect(0, 0, 100, 200)); SetScrollHandoff(layers[1], root); registration = MakeUnique(LayersId{0}, mcc); UpdateHitTestingTree(); rootApzc = ApzcOf(root); } // Creates a layer tree with a parent layer that is only scrollable // vertically, and a child layer that is only scrollable horizontally. void CreateScrollHandoffLayerTree6() { const char* treeShape = "x(x)"; LayerIntRect layerVisibleRect[] = {LayerIntRect(0, 0, 100, 100), LayerIntRect(0, 0, 100, 1000)}; CreateScrollData(treeShape, layerVisibleRect); SetScrollableFrameMetrics(root, ScrollableLayerGuid::START_SCROLL_ID, CSSRect(0, 0, 100, 1000)); SetScrollableFrameMetrics(layers[1], ScrollableLayerGuid::START_SCROLL_ID + 1, CSSRect(0, 0, 200, 1000)); SetScrollHandoff(layers[1], root); registration = MakeUnique(LayersId{0}, mcc); UpdateHitTestingTree(); rootApzc = ApzcOf(root); } void TestCrossApzcAxisLock() { SCOPED_GFX_PREF_INT("apz.axis_lock.mode", 1); CreateScrollHandoffLayerTree1(); RefPtr childApzc = ApzcOf(layers[1]); Pan(childApzc, ScreenIntPoint(10, 60), ScreenIntPoint(15, 90), PanOptions::KeepFingerDown | PanOptions::ExactCoordinates); childApzc->AssertAxisLocked(ScrollDirection::eVertical); childApzc->AssertStateIsPanningLockedY(); } }; class APZScrollHandoffTesterMock : public APZScrollHandoffTester { public: APZScrollHandoffTesterMock() { CreateMockHitTester(); } }; class APZCNestedFlingScrollHandoffTester : public APZCTreeManagerTester { protected: void SetUp() { APZCTreeManagerTester::SetUp(); const char* treeShape = "x(x)"; LayerIntRect layerVisibleRect[] = { LayerIntRect(0, 0, 800, 1000), LayerIntRect(0, 0, 100, 100), }; CreateScrollData(treeShape, layerVisibleRect); SetScrollableFrameMetrics(root, ScrollableLayerGuid::START_SCROLL_ID, CSSRect(0, 0, 800, 50000)); SetScrollableFrameMetrics(layers[1], ScrollableLayerGuid::START_SCROLL_ID + 1, CSSRect(0, 0, 800, 100)); SetScrollHandoff(layers[1], root); // Scroll somewhere into the middle of the scroll range, so that we have // lots of space to scroll in both directions. ModifyFrameMetrics(root, [](ScrollMetadata& aSm, FrameMetrics& aMetrics) { aMetrics.SetVisualScrollUpdateType( FrameMetrics::ScrollOffsetUpdateType::eMainThread); aMetrics.SetVisualDestination(CSSPoint(0, 25000)); }); registration = MakeUnique(LayersId{0}, mcc); UpdateHitTestingTree(); subframeApzc = ApzcOf(layers[1]); } void ExecuteDirectionChangingPanGesture( const ScreenIntPoint& aStartPoint, std::initializer_list aXDeltas, std::initializer_list aYDeltas) { APZEventResult result = TouchDown(subframeApzc, aStartPoint, mcc->Time()); // Allowed touch behaviours must be set after sending touch-start. if (result.GetStatus() != nsEventStatus_eConsumeNoDefault) { SetDefaultAllowedTouchBehavior(subframeApzc, result.mInputBlockId); } const TimeDuration kTouchTimeDelta100Hz = TimeDuration::FromMilliseconds(10); ScreenIntPoint currentLocation = aStartPoint; for (int32_t delta : aXDeltas) { mcc->AdvanceBy(kTouchTimeDelta100Hz); if (delta != 0) { currentLocation.x += delta; Unused << TouchMove(subframeApzc, currentLocation, mcc->Time()); } } ExecuteWait(TimeDuration::FromMilliseconds(255)); for (int32_t delta : aYDeltas) { mcc->AdvanceBy(kTouchTimeDelta100Hz); if (delta != 0) { currentLocation.y += delta; Unused << TouchMove(subframeApzc, currentLocation, mcc->Time()); } } Unused << TouchUp(subframeApzc, currentLocation, mcc->Time()); } void ExecuteWait(const TimeDuration& aDuration) { TimeDuration remaining = aDuration; const TimeDuration TIME_BETWEEN_FRAMES = TimeDuration::FromSeconds(1) / int64_t(60); while (remaining.ToMilliseconds() > 0) { mcc->AdvanceBy(TIME_BETWEEN_FRAMES); subframeApzc->AdvanceAnimations(mcc->GetSampleTime()); remaining -= TIME_BETWEEN_FRAMES; } } RefPtr subframeApzc; UniquePtr registration; }; #ifndef MOZ_WIDGET_ANDROID // Currently fails on Android // Here we test that if the processing of a touch block is deferred while we // wait for content to send a prevent-default message, overscroll is still // handed off correctly when the block is processed. TEST_F(APZScrollHandoffTester, DeferredInputEventProcessing) { SCOPED_GFX_PREF_BOOL("apz.allow_immediate_handoff", true); // Set up the APZC tree. CreateScrollHandoffLayerTree1(); RefPtr childApzc = ApzcOf(layers[1]); // Enable touch-listeners so that we can separate the queueing of input // events from them being processed. childApzc->SetWaitForMainThread(); // Queue input events for a pan. uint64_t blockId = 0; Pan(childApzc, 90, 30, PanOptions::NoFling, nullptr, nullptr, &blockId); // Allow the pan to be processed. childApzc->ContentReceivedInputBlock(blockId, false); childApzc->ConfirmTarget(blockId); // Make sure overscroll was handed off correctly. EXPECT_EQ(50, childApzc->GetFrameMetrics().GetVisualScrollOffset().y); EXPECT_EQ(10, rootApzc->GetFrameMetrics().GetVisualScrollOffset().y); } #endif #ifndef MOZ_WIDGET_ANDROID // Currently fails on Android // Here we test that if the layer structure changes in between two input // blocks being queued, and the first block is only processed after the second // one has been queued, overscroll handoff for the first block follows // the original layer structure while overscroll handoff for the second block // follows the new layer structure. TEST_F(APZScrollHandoffTester, LayerStructureChangesWhileEventsArePending) { SCOPED_GFX_PREF_BOOL("apz.allow_immediate_handoff", true); // Set up an initial APZC tree. CreateScrollHandoffLayerTree1(); RefPtr childApzc = ApzcOf(layers[1]); // Enable touch-listeners so that we can separate the queueing of input // events from them being processed. childApzc->SetWaitForMainThread(); // Queue input events for a pan. uint64_t blockId = 0; Pan(childApzc, 90, 30, PanOptions::NoFling, nullptr, nullptr, &blockId); // Modify the APZC tree to insert a new APZC 'middle' into the handoff chain // between the child and the root. CreateScrollHandoffLayerTree2(); WebRenderLayerScrollData* middle = layers[1]; childApzc->SetWaitForMainThread(); TestAsyncPanZoomController* middleApzc = ApzcOf(middle); // Queue input events for another pan. uint64_t secondBlockId = 0; Pan(childApzc, 30, 90, PanOptions::NoFling, nullptr, nullptr, &secondBlockId); // Allow the first pan to be processed. childApzc->ContentReceivedInputBlock(blockId, false); childApzc->ConfirmTarget(blockId); // Make sure things have scrolled according to the handoff chain in // place at the time the touch-start of the first pan was queued. EXPECT_EQ(50, childApzc->GetFrameMetrics().GetVisualScrollOffset().y); EXPECT_EQ(10, rootApzc->GetFrameMetrics().GetVisualScrollOffset().y); EXPECT_EQ(0, middleApzc->GetFrameMetrics().GetVisualScrollOffset().y); // Allow the second pan to be processed. childApzc->ContentReceivedInputBlock(secondBlockId, false); childApzc->ConfirmTarget(secondBlockId); // Make sure things have scrolled according to the handoff chain in // place at the time the touch-start of the second pan was queued. EXPECT_EQ(0, childApzc->GetFrameMetrics().GetVisualScrollOffset().y); EXPECT_EQ(10, rootApzc->GetFrameMetrics().GetVisualScrollOffset().y); EXPECT_EQ(-10, middleApzc->GetFrameMetrics().GetVisualScrollOffset().y); } #endif #ifndef MOZ_WIDGET_ANDROID // Currently fails on Android // Test that putting a second finger down on an APZC while a down-chain APZC // is overscrolled doesn't result in being stuck in overscroll. TEST_F(APZScrollHandoffTesterMock, StuckInOverscroll_Bug1073250) { // Enable overscrolling. SCOPED_GFX_PREF_BOOL("apz.overscroll.enabled", true); SCOPED_GFX_PREF_FLOAT("apz.fling_min_velocity_threshold", 0.0f); CreateScrollHandoffLayerTree1(); TestAsyncPanZoomController* child = ApzcOf(layers[1]); // Pan, causing the parent APZC to overscroll. QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID); Pan(manager, 10, 40, PanOptions::KeepFingerDown); EXPECT_FALSE(child->IsOverscrolled()); EXPECT_TRUE(rootApzc->IsOverscrolled()); // Put a second finger down. MultiTouchInput secondFingerDown = CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_START, mcc->Time()); // Use the same touch identifier for the first touch (0) as Pan(). (A bit // hacky.) secondFingerDown.mTouches.AppendElement( SingleTouchData(0, ScreenIntPoint(10, 40), ScreenSize(0, 0), 0, 0)); secondFingerDown.mTouches.AppendElement( SingleTouchData(1, ScreenIntPoint(30, 20), ScreenSize(0, 0), 0, 0)); manager->ReceiveInputEvent(secondFingerDown); // Release the fingers. MultiTouchInput fingersUp = secondFingerDown; fingersUp.mType = MultiTouchInput::MULTITOUCH_END; manager->ReceiveInputEvent(fingersUp); // Allow any animations to run their course. child->AdvanceAnimationsUntilEnd(); rootApzc->AdvanceAnimationsUntilEnd(); // Make sure nothing is overscrolled. EXPECT_FALSE(child->IsOverscrolled()); EXPECT_FALSE(rootApzc->IsOverscrolled()); } #endif #ifndef MOZ_WIDGET_ANDROID // Currently fails on Android // This is almost exactly like StuckInOverscroll_Bug1073250, except the // APZC receiving the input events for the first touch block is the child // (and thus not the same APZC that overscrolls, which is the parent). TEST_F(APZScrollHandoffTesterMock, StuckInOverscroll_Bug1231228) { // Enable overscrolling. SCOPED_GFX_PREF_BOOL("apz.overscroll.enabled", true); SCOPED_GFX_PREF_FLOAT("apz.fling_min_velocity_threshold", 0.0f); CreateScrollHandoffLayerTree1(); TestAsyncPanZoomController* child = ApzcOf(layers[1]); // Pan, causing the parent APZC to overscroll. QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1); Pan(manager, 60, 90, PanOptions::KeepFingerDown); EXPECT_FALSE(child->IsOverscrolled()); EXPECT_TRUE(rootApzc->IsOverscrolled()); // Put a second finger down. MultiTouchInput secondFingerDown = CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_START, mcc->Time()); // Use the same touch identifier for the first touch (0) as Pan(). (A bit // hacky.) secondFingerDown.mTouches.AppendElement( SingleTouchData(0, ScreenIntPoint(10, 40), ScreenSize(0, 0), 0, 0)); secondFingerDown.mTouches.AppendElement( SingleTouchData(1, ScreenIntPoint(30, 20), ScreenSize(0, 0), 0, 0)); manager->ReceiveInputEvent(secondFingerDown); // Release the fingers. MultiTouchInput fingersUp = secondFingerDown; fingersUp.mType = MultiTouchInput::MULTITOUCH_END; manager->ReceiveInputEvent(fingersUp); // Allow any animations to run their course. child->AdvanceAnimationsUntilEnd(); rootApzc->AdvanceAnimationsUntilEnd(); // Make sure nothing is overscrolled. EXPECT_FALSE(child->IsOverscrolled()); EXPECT_FALSE(rootApzc->IsOverscrolled()); } #endif #ifndef MOZ_WIDGET_ANDROID // Currently fails on Android TEST_F(APZScrollHandoffTester, StuckInOverscroll_Bug1240202a) { // Enable overscrolling. SCOPED_GFX_PREF_BOOL("apz.overscroll.enabled", true); CreateScrollHandoffLayerTree1(); TestAsyncPanZoomController* child = ApzcOf(layers[1]); // Pan, causing the parent APZC to overscroll. Pan(manager, 60, 90, PanOptions::KeepFingerDown); EXPECT_FALSE(child->IsOverscrolled()); EXPECT_TRUE(rootApzc->IsOverscrolled()); // Lift the finger, triggering an overscroll animation // (but don't allow it to run). TouchUp(manager, ScreenIntPoint(10, 90), mcc->Time()); // Put the finger down again, interrupting the animation // and entering the TOUCHING state. TouchDown(manager, ScreenIntPoint(10, 90), mcc->Time()); // Lift the finger once again. TouchUp(manager, ScreenIntPoint(10, 90), mcc->Time()); // Allow any animations to run their course. child->AdvanceAnimationsUntilEnd(); rootApzc->AdvanceAnimationsUntilEnd(); // Make sure nothing is overscrolled. EXPECT_FALSE(child->IsOverscrolled()); EXPECT_FALSE(rootApzc->IsOverscrolled()); } #endif #ifndef MOZ_WIDGET_ANDROID // Currently fails on Android TEST_F(APZScrollHandoffTesterMock, StuckInOverscroll_Bug1240202b) { // Enable overscrolling. SCOPED_GFX_PREF_BOOL("apz.overscroll.enabled", true); CreateScrollHandoffLayerTree1(); TestAsyncPanZoomController* child = ApzcOf(layers[1]); // Pan, causing the parent APZC to overscroll. QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1); Pan(manager, 60, 90, PanOptions::KeepFingerDown); EXPECT_FALSE(child->IsOverscrolled()); EXPECT_TRUE(rootApzc->IsOverscrolled()); // Lift the finger, triggering an overscroll animation // (but don't allow it to run). TouchUp(manager, ScreenIntPoint(10, 90), mcc->Time()); // Put the finger down again, interrupting the animation // and entering the TOUCHING state. QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1); TouchDown(manager, ScreenIntPoint(10, 90), mcc->Time()); // Put a second finger down. Since we're in the TOUCHING state, // the "are we panned into overscroll" check will fail and we // will not ignore the second finger, instead entering the // PINCHING state. MultiTouchInput secondFingerDown(MultiTouchInput::MULTITOUCH_START, 0, mcc->Time(), 0); // Use the same touch identifier for the first touch (0) as TouchDown(). (A // bit hacky.) secondFingerDown.mTouches.AppendElement( SingleTouchData(0, ScreenIntPoint(10, 90), ScreenSize(0, 0), 0, 0)); secondFingerDown.mTouches.AppendElement( SingleTouchData(1, ScreenIntPoint(10, 80), ScreenSize(0, 0), 0, 0)); QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1); QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1); manager->ReceiveInputEvent(secondFingerDown); // Release the fingers. MultiTouchInput fingersUp = secondFingerDown; fingersUp.mType = MultiTouchInput::MULTITOUCH_END; manager->ReceiveInputEvent(fingersUp); // Allow any animations to run their course. child->AdvanceAnimationsUntilEnd(); rootApzc->AdvanceAnimationsUntilEnd(); // Make sure nothing is overscrolled. EXPECT_FALSE(child->IsOverscrolled()); EXPECT_FALSE(rootApzc->IsOverscrolled()); } #endif #ifndef MOZ_WIDGET_ANDROID // Currently fails on Android TEST_F(APZScrollHandoffTester, OpposingConstrainedAxes_Bug1201098) { // Enable overscrolling. SCOPED_GFX_PREF_BOOL("apz.overscroll.enabled", true); CreateScrollHandoffLayerTree4(); RefPtr childApzc = ApzcOf(layers[1]); // Pan, causing the child APZC to overscroll. Pan(childApzc, 50, 60); // Make sure only the child is overscrolled. EXPECT_TRUE(childApzc->IsOverscrolled()); EXPECT_FALSE(rootApzc->IsOverscrolled()); } #endif // Test the behaviour when flinging diagonally and there is room to scroll in // one direction but not the other. In the direction where there is room to // scroll, the fling should continue. In the direction where there is no room // to scroll, the fling should stop without being handed off to the parent. TEST_F(APZScrollHandoffTesterMock, DiagonalFlingNoHandoff) { SCOPED_GFX_PREF_FLOAT("apz.fling_min_velocity_threshold", 0.0f); CreateScrollHandoffLayerTree1(); // Fling up and to the left. The child APZC has room to scroll up, but not // to the left. QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1); Pan(manager, ScreenIntPoint(90, 90), ScreenIntPoint(55, 55)); RefPtr parent = ApzcOf(layers[0]); RefPtr child = ApzcOf(layers[1]); // Advance the child's fling animation once to give any potential handoff // a chance to occur. mcc->AdvanceByMillis(10); child->AdvanceAnimations(mcc->GetSampleTime()); // Assert that the child is still flinging but the parent is not (no handoff // occurred). child->AssertStateIsFling(); parent->AssertStateIsReset(); } // Here we test that if two flings are happening simultaneously, overscroll // is handed off correctly for each. TEST_F(APZScrollHandoffTester, SimultaneousFlings) { SCOPED_GFX_PREF_BOOL("apz.allow_immediate_handoff", true); SCOPED_GFX_PREF_FLOAT("apz.fling_min_velocity_threshold", 0.0f); // Set up an initial APZC tree. CreateScrollHandoffLayerTree3(); RefPtr parent1 = ApzcOf(layers[1]); RefPtr child1 = ApzcOf(layers[2]); RefPtr parent2 = ApzcOf(layers[3]); RefPtr child2 = ApzcOf(layers[4]); // Pan on the lower child. Pan(child2, 45, 5); // Pan on the upper child. Pan(child1, 95, 55); // Check that child1 and child2 are in a FLING state. child1->AssertStateIsFling(); child2->AssertStateIsFling(); // Advance the animations on child1 and child2 until their end. child1->AdvanceAnimationsUntilEnd(); child2->AdvanceAnimationsUntilEnd(); // Check that the flings have been handed off to the parents. child1->AssertStateIsReset(); parent1->AssertStateIsFling(); child2->AssertStateIsReset(); parent2->AssertStateIsFling(); } TEST_F(APZScrollHandoffTester, ImmediateHandoffDisallowed_Pan) { SCOPED_GFX_PREF_BOOL("apz.allow_immediate_handoff", false); CreateScrollHandoffLayerTree1(); RefPtr parentApzc = ApzcOf(layers[0]); RefPtr childApzc = ApzcOf(layers[1]); // Pan on the child, enough to scroll it to its end and have scroll // left to hand off. Since immediate handoff is disallowed, we expect // the leftover scroll not to be handed off. Pan(childApzc, 60, 5); // Verify that the parent has not scrolled. EXPECT_EQ(50, childApzc->GetFrameMetrics().GetVisualScrollOffset().y); EXPECT_EQ(0, parentApzc->GetFrameMetrics().GetVisualScrollOffset().y); // Pan again on the child. This time, since the child was scrolled to // its end when the gesture began, we expect the scroll to be handed off. Pan(childApzc, 60, 50); // Verify that the parent scrolled. EXPECT_EQ(10, parentApzc->GetFrameMetrics().GetVisualScrollOffset().y); } TEST_F(APZScrollHandoffTester, ImmediateHandoffDisallowed_Fling) { SCOPED_GFX_PREF_BOOL("apz.allow_immediate_handoff", false); SCOPED_GFX_PREF_FLOAT("apz.fling_min_velocity_threshold", 0.0f); CreateScrollHandoffLayerTree1(); RefPtr parentApzc = ApzcOf(layers[0]); RefPtr childApzc = ApzcOf(layers[1]); // Pan on the child, enough to get very close to the end, so that the // subsequent fling reaches the end and has leftover velocity to hand off. Pan(childApzc, 60, 2); // Allow the fling to run its course. childApzc->AdvanceAnimationsUntilEnd(); parentApzc->AdvanceAnimationsUntilEnd(); // Verify that the parent has not scrolled. // The first comparison needs to be an ASSERT_NEAR because the fling // computations are such that the final scroll position can be within // COORDINATE_EPSILON of the end rather than right at the end. ASSERT_NEAR(50, childApzc->GetFrameMetrics().GetVisualScrollOffset().y, COORDINATE_EPSILON); EXPECT_EQ(0, parentApzc->GetFrameMetrics().GetVisualScrollOffset().y); // Pan again on the child. This time, since the child was scrolled to // its end when the gesture began, we expect the scroll to be handed off. Pan(childApzc, 60, 40); // Allow the fling to run its course. The fling should also be handed off. childApzc->AdvanceAnimationsUntilEnd(); parentApzc->AdvanceAnimationsUntilEnd(); // Verify that the parent scrolled from the fling. EXPECT_GT(parentApzc->GetFrameMetrics().GetVisualScrollOffset().y, 10); } TEST_F(APZScrollHandoffTester, CrossApzcAxisLock_TouchAction) { TestCrossApzcAxisLock(); } TEST_F(APZScrollHandoffTesterMock, WheelHandoffAfterDirectionReversal) { // Explicitly set the wheel transaction timeout pref because the test relies // on its value. SCOPED_GFX_PREF_INT("mousewheel.transaction.timeout", 1500); // Set up a basic scroll handoff layer tree. CreateScrollHandoffLayerTree1(); rootApzc = ApzcOf(layers[0]); RefPtr childApzc = ApzcOf(layers[1]); FrameMetrics& rootMetrics = rootApzc->GetFrameMetrics(); FrameMetrics& childMetrics = childApzc->GetFrameMetrics(); CSSRect childScrollRange = childMetrics.CalculateScrollRange(); EXPECT_EQ(0, rootMetrics.GetVisualScrollOffset().y); EXPECT_EQ(0, childMetrics.GetVisualScrollOffset().y); ScreenIntPoint cursorLocation(10, 60); // positioned to hit the subframe ScreenPoint upwardDelta(0, -10); ScreenPoint downwardDelta(0, 10); // First wheel upwards. This will have no effect because we're already // scrolled to the top. QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1); Wheel(manager, cursorLocation, upwardDelta, mcc->Time()); EXPECT_EQ(0, rootMetrics.GetVisualScrollOffset().y); EXPECT_EQ(0, childMetrics.GetVisualScrollOffset().y); // Now wheel downwards 6 times. This should scroll the child, and get it // to the bottom of its 50px scroll range. for (size_t i = 0; i < 6; ++i) { mcc->AdvanceByMillis(100); QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1); Wheel(manager, cursorLocation, downwardDelta, mcc->Time()); } EXPECT_EQ(0, rootMetrics.GetVisualScrollOffset().y); EXPECT_EQ(childScrollRange.YMost(), childMetrics.GetVisualScrollOffset().y); // Wheel downwards an additional 16 times, with 100ms increments. // This should be enough to overcome the 1500ms wheel transaction timeout // and start scrolling the root. for (size_t i = 0; i < 16; ++i) { mcc->AdvanceByMillis(100); QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1); Wheel(manager, cursorLocation, downwardDelta, mcc->Time()); } EXPECT_EQ(childScrollRange.YMost(), childMetrics.GetVisualScrollOffset().y); EXPECT_GT(rootMetrics.GetVisualScrollOffset().y, 0); } TEST_F(APZScrollHandoffTesterMock, WheelHandoffNonscrollable) { // Set up a basic scroll layer tree. CreateScrollHandoffLayerTree5(); RefPtr childApzc = ApzcOf(layers[1]); FrameMetrics& childMetrics = childApzc->GetFrameMetrics(); EXPECT_EQ(0, childMetrics.GetVisualScrollOffset().y); ScreenPoint downwardDelta(0, 10); // Positioned to hit the nonscrollable parent frame ScreenIntPoint nonscrollableLocation(40, 10); // Positioned to hit the scrollable subframe ScreenIntPoint scrollableLocation(40, 60); // Start the wheel transaction on a nonscrollable parent frame. QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID); Wheel(manager, nonscrollableLocation, downwardDelta, mcc->Time()); EXPECT_EQ(0, childMetrics.GetVisualScrollOffset().y); // Mouse moves to a scrollable subframe. This should end the transaction. mcc->AdvanceByMillis(100); MouseInput mouseInput(MouseInput::MOUSE_MOVE, MouseInput::ButtonType::PRIMARY_BUTTON, 0, 0, scrollableLocation, mcc->Time(), 0); WidgetMouseEvent mouseEvent = mouseInput.ToWidgetEvent(nullptr); QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1); ((APZInputBridge*)manager.get())->ReceiveInputEvent(mouseEvent); // Wheel downward should scroll the subframe. mcc->AdvanceByMillis(100); QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1); Wheel(manager, scrollableLocation, downwardDelta, mcc->Time()); EXPECT_GT(childMetrics.GetVisualScrollOffset().y, 0); } TEST_F(APZScrollHandoffTesterMock, ChildCloseToEndOfScrollRange) { SCOPED_GFX_PREF_BOOL("apz.overscroll.enabled", true); CreateScrollHandoffLayerTree1(); RefPtr childApzc = ApzcOf(layers[1]); FrameMetrics& rootMetrics = rootApzc->GetFrameMetrics(); FrameMetrics& childMetrics = childApzc->GetFrameMetrics(); // Zoom the page in by 3x. This needs to be reflected in the zoom level // and composition bounds of both APZCs. rootMetrics.SetZoom(CSSToParentLayerScale(3.0)); rootMetrics.SetCompositionBounds(ParentLayerRect(0, 0, 300, 300)); childMetrics.SetZoom(CSSToParentLayerScale(3.0)); childMetrics.SetCompositionBounds(ParentLayerRect(0, 150, 300, 150)); // Scroll the child APZC very close to the end of the scroll range. // The scroll offset is chosen such that in CSS pixels it has 0.01 pixels // room to scroll (less than COORDINATE_EPSILON = 0.02), but in ParentLayer // pixels it has 0.03 pixels room (greater than COORDINATE_EPSILON). childMetrics.SetVisualScrollOffset(CSSPoint(0, 49.99)); EXPECT_FALSE(childApzc->IsOverscrolled()); CSSPoint childBefore = childApzc->GetFrameMetrics().GetVisualScrollOffset(); CSSPoint parentBefore = rootApzc->GetFrameMetrics().GetVisualScrollOffset(); // Synthesize a pan gesture that tries to scroll the child further down. PanGesture(PanGestureInput::PANGESTURE_START, childApzc, ScreenIntPoint(10, 20), ScreenPoint(0, 40), mcc->Time()); mcc->AdvanceByMillis(5); childApzc->AdvanceAnimations(mcc->GetSampleTime()); PanGesture(PanGestureInput::PANGESTURE_END, childApzc, ScreenIntPoint(10, 21), ScreenPoint(0, 0), mcc->Time()); CSSPoint childAfter = childApzc->GetFrameMetrics().GetVisualScrollOffset(); CSSPoint parentAfter = rootApzc->GetFrameMetrics().GetVisualScrollOffset(); bool childScrolled = (childBefore != childAfter); bool parentScrolled = (parentBefore != parentAfter); // Check that either the child or the parent scrolled. // (With the current implementation of comparing quantities to // COORDINATE_EPSILON in CSS units, it will be the parent, but the important // thing is that at least one of the child or parent scroll, i.e. we're not // stuck in a situation where no scroll offset is changing). EXPECT_TRUE(childScrolled || parentScrolled); } TEST_F(APZScrollHandoffTesterMock, ScrollJump_Bug1812227) { // Set the touch start tolerance to 10 pixels. SCOPED_GFX_PREF_FLOAT("apz.touch_start_tolerance", 10 / manager->GetDPI()); CreateScrollHandoffLayerTree6(); RefPtr childApzc = ApzcOf(layers[1]); // Throughout the test, we record the composited vertical scroll position // of the root scroll frame after every event or animation frame. std::vector rootYScrollPositions; auto SampleScrollPosition = [&]() { rootYScrollPositions.push_back( rootApzc->GetFrameMetrics().GetVisualScrollOffset().y); }; // Helper function to perform a light upward flick (finger moves upward // ==> page will scroll downward). auto DoLightUpwardFlick = [&](bool aSimulatePaint = false) { // Don't use Pan() because it decreases the touch start tolerance // to almost zero, and here we want to test a codepath related to // the touch start tolerance. mcc->AdvanceByMillis(16); QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1); TouchDown(manager, {30, 30}, mcc->Time()); SampleScrollPosition(); // If aSimulatePaint=true, simulate a main-thread paint arriving in between // the touch-down (when the input block is created and the cached value // InputBlockState::mTransformToApzc is set) and the first touch-move which // overcomes the touch-tolerance threshold and synthesizes an additional // touch-move event at the threshold. The paint has the effect of resetting // transform to the APZC to zero. The bug occurs if the synthesized // touch-move event incorrectly uses the up-to-date transform to the APZC // rather than the value cached in InputBlockState::mTrasnformToApzc. if (aSimulatePaint) { // For simplicity, simulate a paint with the latest metrics stored on the // APZC. In practice, what would be painted would be from a frame or two // ago, but for reproducing this bug it does not matter. ModifyFrameMetrics(root, [&](ScrollMetadata&, FrameMetrics& aMetrics) { aMetrics = rootApzc->GetFrameMetrics(); }); ModifyFrameMetrics(layers[1], [&](ScrollMetadata&, FrameMetrics& aMetrics) { aMetrics = childApzc->GetFrameMetrics(); }); UpdateHitTestingTree(); } mcc->AdvanceByMillis(16); TouchMove(manager, {30, 10}, mcc->Time()); SampleScrollPosition(); mcc->AdvanceByMillis(16); TouchUp(manager, {30, 10}, mcc->Time()); SampleScrollPosition(); // The root APZC should be flinging. rootApzc->AssertStateIsFling(); }; // Peform one flick. DoLightUpwardFlick(); // Sample the resulting fling partway. Testing shows it goes well past // y=100, so sample it until y=100. while (SampleAnimationsOnce() && rootYScrollPositions.back() < 100) { SampleScrollPosition(); } // Perform a second flick, this time simulating a paint in between // the touch-start and touch-move. DoLightUpwardFlick(true); // Sample the fling until its completion. while (SampleAnimationsOnce()) { SampleScrollPosition(); } // Check that the vertical root scroll position is non-decreasing // throughout the course of the test, i.e. it never jumps back up. for (size_t i = 0; i < (rootYScrollPositions.size() - 1); ++i) { CSSCoord before = rootYScrollPositions[i]; CSSCoord after = rootYScrollPositions[i + 1]; EXPECT_LE(before, after); } } TEST_F(APZCNestedFlingScrollHandoffTester, FlingInOppositeDirection) { RefPtr rootApzc = ApzcOf(root); ParentLayerPoint startRootOffset = rootApzc->GetCurrentAsyncScrollOffset( AsyncTransformConsumer::eForEventHandling); ExecuteDirectionChangingPanGesture( ScreenIntPoint{569, 710}, {-11, -2, -107, -18, -148, -57, -133, -159, -21}, {11, 2, 42, 107, 148}); // Let any animation start and run for a few frames ExecuteWait(TimeDuration::FromMilliseconds(154)); auto vel = subframeApzc->GetVelocityVector(); ParentLayerPoint endRootOffset = rootApzc->GetCurrentAsyncScrollOffset( AsyncTransformConsumer::eForEventHandling); EXPECT_EQ(vel.y, 0.0); rootApzc->AssertStateIsReset(); subframeApzc->AssertStateIsReset(); EXPECT_EQ(startRootOffset.y, endRootOffset.y); }