diff options
Diffstat (limited to 'gfx/layers/apz/test/gtest')
32 files changed, 11736 insertions, 0 deletions
diff --git a/gfx/layers/apz/test/gtest/APZCBasicTester.h b/gfx/layers/apz/test/gtest/APZCBasicTester.h new file mode 100644 index 0000000000..13065f71f5 --- /dev/null +++ b/gfx/layers/apz/test/gtest/APZCBasicTester.h @@ -0,0 +1,112 @@ +/* -*- 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/. */ + +#ifndef mozilla_layers_APZCBasicTester_h +#define mozilla_layers_APZCBasicTester_h + +/** + * Defines a test fixture used for testing a single APZC. + */ + +#include "APZTestCommon.h" + +#include "mozilla/layers/APZSampler.h" +#include "mozilla/layers/APZUpdater.h" + +class APZCBasicTester : public APZCTesterBase { + public: + explicit APZCBasicTester( + AsyncPanZoomController::GestureBehavior aGestureBehavior = + AsyncPanZoomController::DEFAULT_GESTURES) + : mGestureBehavior(aGestureBehavior) {} + + protected: + virtual void SetUp() { + APZCTesterBase::SetUp(); + APZThreadUtils::SetThreadAssertionsEnabled(false); + APZThreadUtils::SetControllerThread(NS_GetCurrentThread()); + + tm = new TestAPZCTreeManager(mcc); + updater = new APZUpdater(tm, false); + sampler = new APZSampler(tm, false); + apzc = + new TestAsyncPanZoomController(LayersId{0}, mcc, tm, mGestureBehavior); + apzc->SetFrameMetrics(TestFrameMetrics()); + apzc->GetScrollMetadata().SetIsLayersIdRoot(true); + // Since we're working with just one APZC, make it the root-content one. + // Tests that want to test the behaviour of a non-root-content APZC + // generally want to do so in a context where it has a root-content + // ancestor, and so would use APZCTreeManagerTester. + // Note that some tests overwrite the initial FrameMetrics; such tests + // still need to take care that the root-content flag is set on the new + // FrameMetrics they set (if they care about root-content behaviours like + // zooming). + apzc->GetFrameMetrics().SetIsRootContent(true); + } + + /** + * Get the APZC's scroll range in CSS pixels. + */ + CSSRect GetScrollRange() const { + const FrameMetrics& metrics = apzc->GetFrameMetrics(); + return CSSRect(metrics.GetScrollableRect().TopLeft(), + metrics.GetScrollableRect().Size() - + metrics.CalculateCompositedSizeInCssPixels()); + } + + virtual void TearDown() { + while (mcc->RunThroughDelayedTasks()) + ; + apzc->Destroy(); + tm->ClearTree(); + tm->ClearContentController(); + + APZCTesterBase::TearDown(); + } + + void MakeApzcWaitForMainThread() { apzc->SetWaitForMainThread(); } + + void MakeApzcZoomable() { + MOZ_ASSERT(apzc->GetFrameMetrics().IsRootContent()); + apzc->UpdateZoomConstraints(ZoomConstraints( + true, true, CSSToParentLayerScale(0.25f), CSSToParentLayerScale(4.0f))); + } + + void MakeApzcUnzoomable() { + apzc->UpdateZoomConstraints(ZoomConstraints(false, false, + CSSToParentLayerScale(1.0f), + CSSToParentLayerScale(1.0f))); + } + + /** + * Sample animations once, 1 ms later than the last sample. + */ + void SampleAnimationOnce() { + const TimeDuration increment = TimeDuration::FromMilliseconds(1); + ParentLayerPoint pointOut; + AsyncTransform viewTransformOut; + mcc->AdvanceBy(increment); + apzc->SampleContentTransformForFrame(&viewTransformOut, pointOut); + } + /** + * Sample animations one frame, 17 ms later than the last sample. + */ + void SampleAnimationOneFrame() { + const TimeDuration increment = TimeDuration::FromMilliseconds(17); + ParentLayerPoint pointOut; + AsyncTransform viewTransformOut; + mcc->AdvanceBy(increment); + apzc->SampleContentTransformForFrame(&viewTransformOut, pointOut); + } + + AsyncPanZoomController::GestureBehavior mGestureBehavior; + RefPtr<TestAPZCTreeManager> tm; + RefPtr<APZSampler> sampler; + RefPtr<APZUpdater> updater; + RefPtr<TestAsyncPanZoomController> apzc; +}; + +#endif // mozilla_layers_APZCBasicTester_h diff --git a/gfx/layers/apz/test/gtest/APZCTreeManagerTester.h b/gfx/layers/apz/test/gtest/APZCTreeManagerTester.h new file mode 100644 index 0000000000..1f402bedc7 --- /dev/null +++ b/gfx/layers/apz/test/gtest/APZCTreeManagerTester.h @@ -0,0 +1,222 @@ +/* -*- 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/. */ + +#ifndef mozilla_layers_APZCTreeManagerTester_h +#define mozilla_layers_APZCTreeManagerTester_h + +/** + * Defines a test fixture used for testing multiple APZCs interacting in + * an APZCTreeManager. + */ + +#include "APZTestAccess.h" +#include "APZTestCommon.h" +#include "gfxPlatform.h" +#include "MockHitTester.h" +#include "apz/src/WRHitTester.h" + +#include "mozilla/layers/APZSampler.h" +#include "mozilla/layers/APZUpdater.h" +#include "mozilla/layers/WebRenderScrollDataWrapper.h" + +class APZCTreeManagerTester : public APZCTesterBase { + protected: + APZCTreeManagerTester() : mHitTester(MakeUnique<WRHitTester>()) {} + + virtual void SetUp() { + APZCTesterBase::SetUp(); + + APZThreadUtils::SetThreadAssertionsEnabled(false); + APZThreadUtils::SetControllerThread(NS_GetCurrentThread()); + + manager = new TestAPZCTreeManager(mcc, std::move(mHitTester)); + updater = new APZUpdater(manager, false); + sampler = new APZSampler(manager, false); + } + + virtual void TearDown() { + while (mcc->RunThroughDelayedTasks()) + ; + manager->ClearTree(); + manager->ClearContentController(); + + APZCTesterBase::TearDown(); + } + + /** + * Sample animations once for all APZCs, 1 ms later than the last sample and + * return whether there is still any active animations or not. + */ + bool SampleAnimationsOnce() { + const TimeDuration increment = TimeDuration::FromMilliseconds(1); + ParentLayerPoint pointOut; + AsyncTransform viewTransformOut; + mcc->AdvanceBy(increment); + + bool activeAnimations = false; + + for (size_t i = 0; i < layers.GetLayerCount(); ++i) { + if (TestAsyncPanZoomController* apzc = ApzcOf(layers[i])) { + activeAnimations |= + apzc->SampleContentTransformForFrame(&viewTransformOut, pointOut); + } + } + + return activeAnimations; + } + + // A convenience function for letting a test modify the frame metrics + // stored on a particular layer. + template <typename Callback> + void ModifyFrameMetrics(WebRenderLayerScrollData* aLayer, + Callback aCallback) { + MOZ_ASSERT(aLayer->GetScrollMetadataCount() == 1); + ScrollMetadata& metadataRef = + APZTestAccess::GetScrollMetadataMut(*aLayer, layers, 0); + aCallback(metadataRef, metadataRef.GetMetrics()); + } + + // A convenience wrapper for manager->UpdateHitTestingTree(). + void UpdateHitTestingTree(uint32_t aPaintSequenceNumber = 0) { + manager->UpdateHitTestingTree(WebRenderScrollDataWrapper{*updater, &layers}, + /* is first paint = */ false, LayersId{0}, + aPaintSequenceNumber); + } + + void CreateScrollData(const char* aTreeShape, + const LayerIntRect* aVisibleRects = nullptr, + const gfx::Matrix4x4* aTransforms = nullptr) { + layers = TestWRScrollData::Create(aTreeShape, *updater, aVisibleRects, + aTransforms); + root = layers[0]; + } + + void CreateMockHitTester() { + mHitTester = MakeUnique<MockHitTester>(); + // Save a pointer in a separate variable, because SetUp() will + // move the value out of mHitTester. + mMockHitTester = static_cast<MockHitTester*>(mHitTester.get()); + } + void QueueMockHitResult(ScrollableLayerGuid::ViewID aScrollId, + gfx::CompositorHitTestInfo aHitInfo = + gfx::CompositorHitTestFlags::eVisibleToHitTest) { + MOZ_ASSERT(mMockHitTester); + mMockHitTester->QueueHitResult(aScrollId, aHitInfo); + } + + RefPtr<TestAPZCTreeManager> manager; + RefPtr<APZSampler> sampler; + RefPtr<APZUpdater> updater; + TestWRScrollData layers; + WebRenderLayerScrollData* root = nullptr; + + UniquePtr<IAPZHitTester> mHitTester; + MockHitTester* mMockHitTester = nullptr; + + protected: + static ScrollMetadata BuildScrollMetadata( + ScrollableLayerGuid::ViewID aScrollId, const CSSRect& aScrollableRect, + const ParentLayerRect& aCompositionBounds) { + ScrollMetadata metadata; + FrameMetrics& metrics = metadata.GetMetrics(); + metrics.SetScrollId(aScrollId); + // By convention in this test file, START_SCROLL_ID is the root, so mark it + // as such. + if (aScrollId == ScrollableLayerGuid::START_SCROLL_ID) { + metadata.SetIsLayersIdRoot(true); + } + metrics.SetCompositionBounds(aCompositionBounds); + metrics.SetScrollableRect(aScrollableRect); + metrics.SetLayoutScrollOffset(CSSPoint(0, 0)); + metadata.SetPageScrollAmount(LayoutDeviceIntSize(50, 100)); + metadata.SetLineScrollAmount(LayoutDeviceIntSize(5, 10)); + return metadata; + } + + void SetScrollMetadata(WebRenderLayerScrollData* aLayer, + const ScrollMetadata& aMetadata) { + MOZ_ASSERT(aLayer->GetScrollMetadataCount() <= 1, + "This function does not support multiple ScrollMetadata on a " + "single layer"); + if (aLayer->GetScrollMetadataCount() == 0) { + // Add new metrics + aLayer->AppendScrollMetadata(layers, aMetadata); + } else { + // Overwrite existing metrics + ModifyFrameMetrics( + aLayer, [&](ScrollMetadata& aSm, FrameMetrics&) { aSm = aMetadata; }); + } + } + + void SetScrollMetadata(WebRenderLayerScrollData* aLayer, + const nsTArray<ScrollMetadata>& aMetadata) { + // The reason for this restriction is that WebRenderLayerScrollData does not + // have an API to *remove* previous metadata. + MOZ_ASSERT(aLayer->GetScrollMetadataCount() == 0, + "This function can only be used on layers which do not yet have " + "scroll metadata"); + for (const ScrollMetadata& metadata : aMetadata) { + aLayer->AppendScrollMetadata(layers, metadata); + } + } + + void SetScrollableFrameMetrics(WebRenderLayerScrollData* aLayer, + ScrollableLayerGuid::ViewID aScrollId, + CSSRect aScrollableRect = CSSRect(-1, -1, -1, + -1)) { + auto localTransform = aLayer->GetTransformTyped() * AsyncTransformMatrix(); + ParentLayerIntRect compositionBounds = RoundedToInt( + localTransform.TransformBounds(LayerRect(aLayer->GetVisibleRect()))); + ScrollMetadata metadata = BuildScrollMetadata( + aScrollId, aScrollableRect, ParentLayerRect(compositionBounds)); + SetScrollMetadata(aLayer, metadata); + } + + bool HasScrollableFrameMetrics(const WebRenderLayerScrollData* aLayer) const { + for (uint32_t i = 0; i < aLayer->GetScrollMetadataCount(); i++) { + if (aLayer->GetScrollMetadata(layers, i).GetMetrics().IsScrollable()) { + return true; + } + } + return false; + } + + void SetScrollHandoff(WebRenderLayerScrollData* aChild, + WebRenderLayerScrollData* aParent) { + ModifyFrameMetrics(aChild, [&](ScrollMetadata& aSm, FrameMetrics&) { + aSm.SetScrollParentId( + aParent->GetScrollMetadata(layers, 0).GetMetrics().GetScrollId()); + }); + } + + TestAsyncPanZoomController* ApzcOf(WebRenderLayerScrollData* aLayer) { + EXPECT_EQ(1u, aLayer->GetScrollMetadataCount()); + return ApzcOf(aLayer, 0); + } + + TestAsyncPanZoomController* ApzcOf(WebRenderLayerScrollData* aLayer, + uint32_t aIndex) { + EXPECT_LT(aIndex, aLayer->GetScrollMetadataCount()); + // Unlike Layer, WebRenderLayerScrollData does not store the associated + // APZCs, so look it up using the tree manager instead. + RefPtr<AsyncPanZoomController> apzc = manager->GetTargetAPZC( + LayersId{0}, + aLayer->GetScrollMetadata(layers, aIndex).GetMetrics().GetScrollId()); + return (TestAsyncPanZoomController*)apzc.get(); + } + + void CreateSimpleScrollingLayer() { + const char* treeShape = "x"; + LayerIntRect layerVisibleRect[] = { + LayerIntRect(0, 0, 200, 200), + }; + CreateScrollData(treeShape, layerVisibleRect); + SetScrollableFrameMetrics(layers[0], ScrollableLayerGuid::START_SCROLL_ID, + CSSRect(0, 0, 500, 500)); + } +}; + +#endif // mozilla_layers_APZCTreeManagerTester_h diff --git a/gfx/layers/apz/test/gtest/APZTestAccess.cpp b/gfx/layers/apz/test/gtest/APZTestAccess.cpp new file mode 100644 index 0000000000..d55d7711f8 --- /dev/null +++ b/gfx/layers/apz/test/gtest/APZTestAccess.cpp @@ -0,0 +1,27 @@ +/* -*- 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 "APZTestAccess.h" +#include "mozilla/layers/WebRenderScrollData.h" + +namespace mozilla { +namespace layers { + +/*static*/ +void APZTestAccess::InitializeForTest(WebRenderLayerScrollData& aLayer, + int32_t aDescendantCount) { + aLayer.InitializeForTest(aDescendantCount); +} + +/*static*/ +ScrollMetadata& APZTestAccess::GetScrollMetadataMut( + WebRenderLayerScrollData& aLayer, WebRenderScrollData& aOwner, + size_t aIndex) { + return aLayer.GetScrollMetadataMut(aOwner, aIndex); +} + +} // namespace layers +} // namespace mozilla diff --git a/gfx/layers/apz/test/gtest/APZTestAccess.h b/gfx/layers/apz/test/gtest/APZTestAccess.h new file mode 100644 index 0000000000..a56fb10a1a --- /dev/null +++ b/gfx/layers/apz/test/gtest/APZTestAccess.h @@ -0,0 +1,36 @@ +/* -*- 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/. */ + +#ifndef mozilla_layers_APZTestAccess_h +#define mozilla_layers_APZTestAccess_h + +#include <cstddef> // for size_t +#include <cstdint> // for int32_t + +namespace mozilla { +namespace layers { + +struct ScrollMetadata; +class WebRenderLayerScrollData; +class WebRenderScrollData; + +// The only purpose of this class is to serve as a single type that can be +// the target of a "friend class" declaration in APZ classes that want to +// give APZ test code access to their private members. +// APZ test code can then access those members via this class. +class APZTestAccess { + public: + static void InitializeForTest(WebRenderLayerScrollData& aLayer, + int32_t aDescendantCount); + static ScrollMetadata& GetScrollMetadataMut(WebRenderLayerScrollData& aLayer, + WebRenderScrollData& aOwner, + size_t aIndex); +}; + +} // namespace layers +} // namespace mozilla + +#endif diff --git a/gfx/layers/apz/test/gtest/APZTestCommon.cpp b/gfx/layers/apz/test/gtest/APZTestCommon.cpp new file mode 100644 index 0000000000..bb47c4e274 --- /dev/null +++ b/gfx/layers/apz/test/gtest/APZTestCommon.cpp @@ -0,0 +1,16 @@ +/* -*- 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 "APZTestCommon.h" + +already_AddRefed<AsyncPanZoomController> TestAPZCTreeManager::NewAPZCInstance( + LayersId aLayersId, GeckoContentController* aController) { + MockContentControllerDelayed* mcc = + static_cast<MockContentControllerDelayed*>(aController); + return MakeRefPtr<TestAsyncPanZoomController>( + aLayersId, mcc, this, AsyncPanZoomController::USE_GESTURE_DETECTOR) + .forget(); +} diff --git a/gfx/layers/apz/test/gtest/APZTestCommon.h b/gfx/layers/apz/test/gtest/APZTestCommon.h new file mode 100644 index 0000000000..414f9c7377 --- /dev/null +++ b/gfx/layers/apz/test/gtest/APZTestCommon.h @@ -0,0 +1,1094 @@ +/* -*- 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/. */ + +#ifndef mozilla_layers_APZTestCommon_h +#define mozilla_layers_APZTestCommon_h + +/** + * Defines a set of mock classes and utility functions/classes for + * writing APZ gtests. + */ + +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +#include "mozilla/Attributes.h" +#include "mozilla/layers/GeckoContentController.h" +#include "mozilla/layers/CompositorBridgeParent.h" +#include "mozilla/layers/DoubleTapToZoom.h" +#include "mozilla/layers/APZThreadUtils.h" +#include "mozilla/layers/MatrixMessage.h" +#include "mozilla/StaticPrefs_layout.h" +#include "mozilla/TypedEnumBits.h" +#include "mozilla/UniquePtr.h" +#include "apz/src/APZCTreeManager.h" +#include "apz/src/AsyncPanZoomController.h" +#include "apz/src/HitTestingTreeNode.h" +#include "base/task.h" +#include "gfxPlatform.h" +#include "TestWRScrollData.h" +#include "UnitTransforms.h" + +using namespace mozilla; +using namespace mozilla::gfx; +using namespace mozilla::layers; +using ::testing::_; +using ::testing::AtLeast; +using ::testing::AtMost; +using ::testing::InSequence; +using ::testing::MockFunction; +using ::testing::NiceMock; +typedef mozilla::layers::GeckoContentController::TapType TapType; + +inline TimeStamp GetStartupTime() { + static TimeStamp sStartupTime = TimeStamp::Now(); + return sStartupTime; +} + +inline uint32_t MillisecondsSinceStartup(TimeStamp aTime) { + return (aTime - GetStartupTime()).ToMilliseconds(); +} + +// Some helper functions for constructing input event objects suitable to be +// passed either to an APZC (which expects an transformed point), or to an APZTM +// (which expects an untransformed point). We handle both cases by setting both +// the transformed and untransformed fields to the same value. +inline SingleTouchData CreateSingleTouchData(int32_t aIdentifier, + const ScreenIntPoint& aPoint) { + SingleTouchData touch(aIdentifier, aPoint, ScreenSize(0, 0), 0, 0); + touch.mLocalScreenPoint = ParentLayerPoint(aPoint.x, aPoint.y); + return touch; +} + +// Convenience wrapper for CreateSingleTouchData() that takes loose coordinates. +inline SingleTouchData CreateSingleTouchData(int32_t aIdentifier, + ScreenIntCoord aX, + ScreenIntCoord aY) { + return CreateSingleTouchData(aIdentifier, ScreenIntPoint(aX, aY)); +} + +inline PinchGestureInput CreatePinchGestureInput( + PinchGestureInput::PinchGestureType aType, const ScreenPoint& aFocus, + float aCurrentSpan, float aPreviousSpan, TimeStamp timestamp) { + ParentLayerPoint localFocus(aFocus.x, aFocus.y); + PinchGestureInput result(aType, PinchGestureInput::UNKNOWN, timestamp, + ExternalPoint(0, 0), aFocus, aCurrentSpan, + aPreviousSpan, 0); + return result; +} + +template <class SetArg, class Storage> +class ScopedGfxSetting { + public: + ScopedGfxSetting(const std::function<SetArg(void)>& aGetPrefFunc, + const std::function<void(SetArg)>& aSetPrefFunc, SetArg aVal) + : mSetPrefFunc(aSetPrefFunc) { + mOldVal = aGetPrefFunc(); + aSetPrefFunc(aVal); + } + + ~ScopedGfxSetting() { mSetPrefFunc(mOldVal); } + + private: + std::function<void(SetArg)> mSetPrefFunc; + Storage mOldVal; +}; + +static inline constexpr auto kDefaultTouchBehavior = + AllowedTouchBehavior::VERTICAL_PAN | AllowedTouchBehavior::HORIZONTAL_PAN | + AllowedTouchBehavior::PINCH_ZOOM | AllowedTouchBehavior::ANIMATING_ZOOM; + +#define FRESH_PREF_VAR_PASTE(id, line) id##line +#define FRESH_PREF_VAR_EXPAND(id, line) FRESH_PREF_VAR_PASTE(id, line) +#define FRESH_PREF_VAR FRESH_PREF_VAR_EXPAND(pref, __LINE__) + +#define SCOPED_GFX_PREF_BOOL(prefName, prefValue) \ + ScopedGfxSetting<bool, bool> FRESH_PREF_VAR( \ + [=]() { return Preferences::GetBool(prefName); }, \ + [=](bool aPrefValue) { Preferences::SetBool(prefName, aPrefValue); }, \ + prefValue) + +#define SCOPED_GFX_PREF_INT(prefName, prefValue) \ + ScopedGfxSetting<int32_t, int32_t> FRESH_PREF_VAR( \ + [=]() { return Preferences::GetInt(prefName); }, \ + [=](int32_t aPrefValue) { Preferences::SetInt(prefName, aPrefValue); }, \ + prefValue) + +#define SCOPED_GFX_PREF_FLOAT(prefName, prefValue) \ + ScopedGfxSetting<float, float> FRESH_PREF_VAR( \ + [=]() { return Preferences::GetFloat(prefName); }, \ + [=](float aPrefValue) { Preferences::SetFloat(prefName, aPrefValue); }, \ + prefValue) + +class MockContentController : public GeckoContentController { + public: + MOCK_METHOD1(NotifyLayerTransforms, void(nsTArray<MatrixMessage>&&)); + MOCK_METHOD1(RequestContentRepaint, void(const RepaintRequest&)); + MOCK_METHOD6(HandleTap, void(TapType, const LayoutDevicePoint&, Modifiers, + const ScrollableLayerGuid&, uint64_t, + const Maybe<DoubleTapToZoomMetrics>&)); + MOCK_METHOD5(NotifyPinchGesture, + void(PinchGestureInput::PinchGestureType, + const ScrollableLayerGuid&, const LayoutDevicePoint&, + LayoutDeviceCoord, Modifiers)); + // Can't use the macros with already_AddRefed :( + void PostDelayedTask(already_AddRefed<Runnable> aTask, int aDelayMs) { + RefPtr<Runnable> task = aTask; + } + bool IsRepaintThread() { return NS_IsMainThread(); } + void DispatchToRepaintThread(already_AddRefed<Runnable> aTask) { + NS_DispatchToMainThread(std::move(aTask)); + } + MOCK_METHOD4(NotifyAPZStateChange, + void(const ScrollableLayerGuid& aGuid, APZStateChange aChange, + int aArg, Maybe<uint64_t> aInputBlockId)); + MOCK_METHOD0(NotifyFlushComplete, void()); + MOCK_METHOD3(NotifyAsyncScrollbarDragInitiated, + void(uint64_t, const ScrollableLayerGuid::ViewID&, + ScrollDirection aDirection)); + MOCK_METHOD1(NotifyAsyncScrollbarDragRejected, + void(const ScrollableLayerGuid::ViewID&)); + MOCK_METHOD1(NotifyAsyncAutoscrollRejected, + void(const ScrollableLayerGuid::ViewID&)); + MOCK_METHOD1(CancelAutoscroll, void(const ScrollableLayerGuid&)); + MOCK_METHOD2(NotifyScaleGestureComplete, + void(const ScrollableLayerGuid&, float aScale)); + MOCK_METHOD4(UpdateOverscrollVelocity, + void(const ScrollableLayerGuid&, float, float, bool)); + MOCK_METHOD4(UpdateOverscrollOffset, + void(const ScrollableLayerGuid&, float, float, bool)); +}; + +class MockContentControllerDelayed : public MockContentController { + public: + MockContentControllerDelayed() + : mTime(SampleTime::FromTest(GetStartupTime())) {} + + const TimeStamp& Time() { return mTime.Time(); } + const SampleTime& GetSampleTime() { return mTime; } + + void AdvanceByMillis(int aMillis) { + AdvanceBy(TimeDuration::FromMilliseconds(aMillis)); + } + + void AdvanceBy(const TimeDuration& aIncrement) { + SampleTime target = mTime + aIncrement; + while (mTaskQueue.Length() > 0 && mTaskQueue[0].second <= target) { + RunNextDelayedTask(); + } + mTime = target; + } + + void PostDelayedTask(already_AddRefed<Runnable> aTask, int aDelayMs) { + RefPtr<Runnable> task = aTask; + SampleTime runAtTime = mTime + TimeDuration::FromMilliseconds(aDelayMs); + int insIndex = mTaskQueue.Length(); + while (insIndex > 0) { + if (mTaskQueue[insIndex - 1].second <= runAtTime) { + break; + } + insIndex--; + } + mTaskQueue.InsertElementAt(insIndex, std::make_pair(task, runAtTime)); + } + + // Run all the tasks in the queue, returning the number of tasks + // run. Note that if a task queues another task while running, that + // new task will not be run. Therefore, there may be still be tasks + // in the queue after this function is called. Only when the return + // value is 0 is the queue guaranteed to be empty. + int RunThroughDelayedTasks() { + nsTArray<std::pair<RefPtr<Runnable>, SampleTime>> runQueue = + std::move(mTaskQueue); + int numTasks = runQueue.Length(); + for (int i = 0; i < numTasks; i++) { + mTime = runQueue[i].second; + runQueue[i].first->Run(); + + // Deleting the task is important in order to release the reference to + // the callee object. + runQueue[i].first = nullptr; + } + return numTasks; + } + + private: + void RunNextDelayedTask() { + std::pair<RefPtr<Runnable>, SampleTime> next = mTaskQueue[0]; + mTaskQueue.RemoveElementAt(0); + mTime = next.second; + next.first->Run(); + // Deleting the task is important in order to release the reference to + // the callee object. + next.first = nullptr; + } + + // The following array is sorted by timestamp (tasks are inserted in order by + // timestamp). + nsTArray<std::pair<RefPtr<Runnable>, SampleTime>> mTaskQueue; + SampleTime mTime; +}; + +class TestAPZCTreeManager : public APZCTreeManager { + public: + explicit TestAPZCTreeManager(MockContentControllerDelayed* aMcc, + UniquePtr<IAPZHitTester> aHitTester = nullptr) + : APZCTreeManager(LayersId{0}, std::move(aHitTester)), mcc(aMcc) { + Init(); + } + + RefPtr<InputQueue> GetInputQueue() const { return mInputQueue; } + + void ClearContentController() { mcc = nullptr; } + + /** + * This function is not currently implemented. + * See bug 1468804 for more information. + **/ + void CancelAnimation() { EXPECT_TRUE(false); } + + bool AdvanceAnimations(const SampleTime& aSampleTime) { + MutexAutoLock lock(mMapLock); + return AdvanceAnimationsInternal(lock, aSampleTime); + } + + APZEventResult ReceiveInputEvent( + InputData& aEvent, + InputBlockCallback&& aCallback = InputBlockCallback()) override { + APZEventResult result = + APZCTreeManager::ReceiveInputEvent(aEvent, std::move(aCallback)); + if (aEvent.mInputType == PANGESTURE_INPUT && + // In the APZCTreeManager::ReceiveInputEvent some type of pan gesture + // events are marked as `mHandledByAPZ = false` (e.g. with Ctrl key + // modifier which causes reflow zoom), in such cases the events will + // never be processed by InputQueue so we shouldn't try to invoke + // AllowsSwipe() here. + aEvent.AsPanGestureInput().mHandledByAPZ && + aEvent.AsPanGestureInput().AllowsSwipe()) { + SetBrowserGestureResponse(result.mInputBlockId, + BrowserGestureResponse::NotConsumed); + } + return result; + } + + protected: + already_AddRefed<AsyncPanZoomController> NewAPZCInstance( + LayersId aLayersId, GeckoContentController* aController) override; + + SampleTime GetFrameTime() override { return mcc->GetSampleTime(); } + + private: + RefPtr<MockContentControllerDelayed> mcc; +}; + +class TestAsyncPanZoomController : public AsyncPanZoomController { + public: + TestAsyncPanZoomController(LayersId aLayersId, + MockContentControllerDelayed* aMcc, + TestAPZCTreeManager* aTreeManager, + GestureBehavior aBehavior = DEFAULT_GESTURES) + : AsyncPanZoomController(aLayersId, aTreeManager, + aTreeManager->GetInputQueue(), aMcc, aBehavior), + mWaitForMainThread(false), + mcc(aMcc) {} + + APZEventResult ReceiveInputEvent( + InputData& aEvent, + const Maybe<nsTArray<uint32_t>>& aTouchBehaviors = Nothing()) { + // This is a function whose signature matches exactly the ReceiveInputEvent + // on APZCTreeManager. This allows us to templates for functions like + // TouchDown, TouchUp, etc so that we can reuse the code for dispatching + // events into both APZC and APZCTM. + APZEventResult result = GetInputQueue()->ReceiveInputEvent( + this, TargetConfirmationFlags{!mWaitForMainThread}, aEvent, + aTouchBehaviors); + + if (aEvent.mInputType == PANGESTURE_INPUT && + aEvent.AsPanGestureInput().AllowsSwipe()) { + GetInputQueue()->SetBrowserGestureResponse( + result.mInputBlockId, BrowserGestureResponse::NotConsumed); + } + return result; + } + + void ContentReceivedInputBlock(uint64_t aInputBlockId, bool aPreventDefault) { + GetInputQueue()->ContentReceivedInputBlock(aInputBlockId, aPreventDefault); + } + + void ConfirmTarget(uint64_t aInputBlockId) { + RefPtr<AsyncPanZoomController> target = this; + GetInputQueue()->SetConfirmedTargetApzc(aInputBlockId, target); + } + + void SetAllowedTouchBehavior(uint64_t aInputBlockId, + const nsTArray<TouchBehaviorFlags>& aBehaviors) { + GetInputQueue()->SetAllowedTouchBehavior(aInputBlockId, aBehaviors); + } + + void SetFrameMetrics(const FrameMetrics& metrics) { + RecursiveMutexAutoLock lock(mRecursiveMutex); + Metrics() = metrics; + } + + void SetScrollMetadata(const ScrollMetadata& aMetadata) { + RecursiveMutexAutoLock lock(mRecursiveMutex); + mScrollMetadata = aMetadata; + } + + FrameMetrics& GetFrameMetrics() { + RecursiveMutexAutoLock lock(mRecursiveMutex); + return mScrollMetadata.GetMetrics(); + } + + ScrollMetadata& GetScrollMetadata() { + RecursiveMutexAutoLock lock(mRecursiveMutex); + return mScrollMetadata; + } + + const FrameMetrics& GetFrameMetrics() const { + RecursiveMutexAutoLock lock(mRecursiveMutex); + return mScrollMetadata.GetMetrics(); + } + + using AsyncPanZoomController::GetOverscrollAmount; + using AsyncPanZoomController::GetVelocityVector; + + void AssertStateIsReset() const { + RecursiveMutexAutoLock lock(mRecursiveMutex); + EXPECT_EQ(NOTHING, mState); + } + + void AssertStateIsFling() const { + RecursiveMutexAutoLock lock(mRecursiveMutex); + EXPECT_EQ(FLING, mState); + } + + void AssertStateIsSmoothScroll() const { + RecursiveMutexAutoLock lock(mRecursiveMutex); + EXPECT_EQ(SMOOTH_SCROLL, mState); + } + + void AssertStateIsSmoothMsdScroll() const { + RecursiveMutexAutoLock lock(mRecursiveMutex); + EXPECT_EQ(SMOOTHMSD_SCROLL, mState); + } + + void AssertStateIsPanningLockedY() { + RecursiveMutexAutoLock lock(mRecursiveMutex); + EXPECT_EQ(PANNING_LOCKED_Y, mState); + } + + void AssertStateIsPanningLockedX() { + RecursiveMutexAutoLock lock(mRecursiveMutex); + EXPECT_EQ(PANNING_LOCKED_X, mState); + } + + void AssertStateIsPanning() { + RecursiveMutexAutoLock lock(mRecursiveMutex); + EXPECT_EQ(PANNING, mState); + } + + void AssertStateIsPanMomentum() { + RecursiveMutexAutoLock lock(mRecursiveMutex); + EXPECT_EQ(PAN_MOMENTUM, mState); + } + + void AssertStateIsWheelScroll() { + RecursiveMutexAutoLock lock(mRecursiveMutex); + EXPECT_EQ(WHEEL_SCROLL, mState); + } + + void SetAxisLocked(ScrollDirections aDirections, bool aLockValue) { + if (aDirections.contains(ScrollDirection::eVertical)) { + mY.SetAxisLocked(aLockValue); + } + if (aDirections.contains(ScrollDirection::eHorizontal)) { + mX.SetAxisLocked(aLockValue); + } + } + + void AssertNotAxisLocked() const { + EXPECT_FALSE(mY.IsAxisLocked()); + EXPECT_FALSE(mX.IsAxisLocked()); + } + + void AssertAxisLocked(ScrollDirection aDirection) const { + switch (aDirection) { + case ScrollDirection::eHorizontal: + EXPECT_TRUE(mY.IsAxisLocked()); + EXPECT_FALSE(mX.IsAxisLocked()); + break; + case ScrollDirection::eVertical: + EXPECT_TRUE(mX.IsAxisLocked()); + EXPECT_FALSE(mY.IsAxisLocked()); + break; + default: + FAIL() << "input direction must be either vertical or horizontal"; + } + } + + void AdvanceAnimationsUntilEnd( + const TimeDuration& aIncrement = TimeDuration::FromMilliseconds(10)) { + while (AdvanceAnimations(mcc->GetSampleTime())) { + mcc->AdvanceBy(aIncrement); + } + } + + bool SampleContentTransformForFrame( + AsyncTransform* aOutTransform, ParentLayerPoint& aScrollOffset, + const TimeDuration& aIncrement = TimeDuration::FromMilliseconds(0)) { + mcc->AdvanceBy(aIncrement); + bool ret = AdvanceAnimations(mcc->GetSampleTime()); + if (aOutTransform) { + *aOutTransform = + GetCurrentAsyncTransform(AsyncPanZoomController::eForEventHandling); + } + aScrollOffset = + GetCurrentAsyncScrollOffset(AsyncPanZoomController::eForEventHandling); + return ret; + } + + CSSPoint GetCompositedScrollOffset() const { + return GetCurrentAsyncScrollOffset( + AsyncPanZoomController::eForCompositing) / + GetFrameMetrics().GetZoom(); + } + + void SetWaitForMainThread() { mWaitForMainThread = true; } + + bool IsOverscrollAnimationRunning() const { + return mState == PanZoomState::OVERSCROLL_ANIMATION; + } + + bool IsWheelScrollAnimationRunning() const { + return mState == PanZoomState::WHEEL_SCROLL; + } + + private: + bool mWaitForMainThread; + MockContentControllerDelayed* mcc; +}; + +class APZCTesterBase : public ::testing::Test { + public: + APZCTesterBase() { mcc = new NiceMock<MockContentControllerDelayed>(); } + + void SetUp() override { + gfxPlatform::GetPlatform(); + // This pref is changed in Pan() without using SCOPED_GFX_PREF + // because the modified value needs to be in place until the touch + // events are processed, which may not happen until the input queue + // is flushed in TearDown(). So, we save and restore its value here. + mTouchStartTolerance = StaticPrefs::apz_touch_start_tolerance(); + } + + void TearDown() override { + Preferences::SetFloat("apz.touch_start_tolerance", mTouchStartTolerance); + } + + enum class PanOptions { + None = 0, + KeepFingerDown = 0x1, + /* + * Do not adjust the touch-start coordinates to overcome the touch-start + * tolerance threshold. If this option is passed, it's up to the caller + * to pass in coordinates that are sufficient to overcome the touch-start + * tolerance *and* cause the desired amount of scrolling. + */ + ExactCoordinates = 0x2, + NoFling = 0x4 + }; + + enum class PinchFlags { + None = 0, + LiftFinger1 = 0x1, + LiftFinger2 = 0x2, + /* + * The bitwise OR result of (LiftFinger1 | LiftFinger2). + * Defined explicitly here because it is used as the default + * argument for PinchWithTouchInput which is defined BEFORE the + * definition of operator| for this class. + */ + LiftBothFingers = 0x3 + }; + + template <class InputReceiver> + APZEventResult Tap(const RefPtr<InputReceiver>& aTarget, + const ScreenIntPoint& aPoint, TimeDuration aTapLength, + nsEventStatus (*aOutEventStatuses)[2] = nullptr); + + template <class InputReceiver> + void TapAndCheckStatus(const RefPtr<InputReceiver>& aTarget, + const ScreenIntPoint& aPoint, TimeDuration aTapLength); + + template <class InputReceiver> + void Pan(const RefPtr<InputReceiver>& aTarget, + const ScreenIntPoint& aTouchStart, const ScreenIntPoint& aTouchEnd, + PanOptions aOptions = PanOptions::None, + nsTArray<uint32_t>* aAllowedTouchBehaviors = nullptr, + nsEventStatus (*aOutEventStatuses)[4] = nullptr, + uint64_t* aOutInputBlockId = nullptr); + + /* + * A version of Pan() that only takes y coordinates rather than (x, y) points + * for the touch start and end points, and uses 10 for the x coordinates. + * This is for convenience, as most tests only need to pan in one direction. + */ + template <class InputReceiver> + void Pan(const RefPtr<InputReceiver>& aTarget, int aTouchStartY, + int aTouchEndY, PanOptions aOptions = PanOptions::None, + nsTArray<uint32_t>* aAllowedTouchBehaviors = nullptr, + nsEventStatus (*aOutEventStatuses)[4] = nullptr, + uint64_t* aOutInputBlockId = nullptr); + + /* + * Dispatches mock touch events to the apzc and checks whether apzc properly + * consumed them and triggered scrolling behavior. + */ + template <class InputReceiver> + void PanAndCheckStatus(const RefPtr<InputReceiver>& aTarget, int aTouchStartY, + int aTouchEndY, bool aExpectConsumed, + nsTArray<uint32_t>* aAllowedTouchBehaviors, + uint64_t* aOutInputBlockId = nullptr); + + template <class InputReceiver> + void DoubleTap(const RefPtr<InputReceiver>& aTarget, + const ScreenIntPoint& aPoint, + nsEventStatus (*aOutEventStatuses)[4] = nullptr, + uint64_t (*aOutInputBlockIds)[2] = nullptr); + + template <class InputReceiver> + void DoubleTapAndCheckStatus(const RefPtr<InputReceiver>& aTarget, + const ScreenIntPoint& aPoint, + uint64_t (*aOutInputBlockIds)[2] = nullptr); + + struct PinchOptions { + nsTArray<uint32_t>* mAllowedTouchBehaviors = nullptr; + nsEventStatus (*mOutEventStatuses)[4] = nullptr; + uint64_t* mOutInputBlockId = nullptr; + PinchFlags mFlags = PinchFlags::LiftBothFingers; + bool mVertical = false; + int* mInputId = nullptr; + Maybe<ScreenIntPoint> mSecondFocus; + TimeDuration mTimeBetweenTouchEvents = TimeDuration::FromMilliseconds(20); + + // Workaround for https://github.com/llvm/llvm-project/issues/36032 + PinchOptions() {} + + // Fluent interface + PinchOptions& AllowedTouchBehaviors( + nsTArray<uint32_t>* aAllowedTouchBehaviors) { + mAllowedTouchBehaviors = aAllowedTouchBehaviors; + return *this; + } + PinchOptions& OutEventStatuses(nsEventStatus (*aOutEventStatuses)[4]) { + mOutEventStatuses = aOutEventStatuses; + return *this; + } + PinchOptions& OutInputBlockId(uint64_t* aOutInputBlockId) { + mOutInputBlockId = aOutInputBlockId; + return *this; + } + PinchOptions& Flags(PinchFlags aFlags) { + mFlags = aFlags; + return *this; + } + PinchOptions& Vertical(bool aVertical) { + mVertical = aVertical; + return *this; + } + PinchOptions& InputId(int& aInputId) { + mInputId = &aInputId; + return *this; + } + PinchOptions& SecondFocus(const ScreenIntPoint& aSecondFocus) { + mSecondFocus = Some(aSecondFocus); + return *this; + } + PinchOptions& TimeBetweenTouchEvents(const TimeDuration& aDuration) { + mTimeBetweenTouchEvents = aDuration; + return *this; + } + }; + + // Pinch with one focus point. Zooms in place with no panning + template <class InputReceiver> + void PinchWithTouchInput(const RefPtr<InputReceiver>& aTarget, + const ScreenIntPoint& aFocus, float aScale, + PinchOptions aOptions = PinchOptions()); + + template <class InputReceiver> + void PinchWithTouchInputAndCheckStatus( + const RefPtr<InputReceiver>& aTarget, const ScreenIntPoint& aFocus, + float aScale, int& inputId, bool aShouldTriggerPinch, + nsTArray<uint32_t>* aAllowedTouchBehaviors); + + template <class InputReceiver> + void PinchWithPinchInput(const RefPtr<InputReceiver>& aTarget, + const ScreenIntPoint& aFocus, + const ScreenIntPoint& aSecondFocus, float aScale, + nsEventStatus (*aOutEventStatuses)[3] = nullptr); + + template <class InputReceiver> + void PinchWithPinchInputAndCheckStatus(const RefPtr<InputReceiver>& aTarget, + const ScreenIntPoint& aFocus, + float aScale, + bool aShouldTriggerPinch); + + protected: + RefPtr<MockContentControllerDelayed> mcc; + + private: + float mTouchStartTolerance; +}; + +MOZ_MAKE_ENUM_CLASS_BITWISE_OPERATORS(APZCTesterBase::PanOptions) +MOZ_MAKE_ENUM_CLASS_BITWISE_OPERATORS(APZCTesterBase::PinchFlags) + +template <class InputReceiver> +APZEventResult APZCTesterBase::Tap(const RefPtr<InputReceiver>& aTarget, + const ScreenIntPoint& aPoint, + TimeDuration aTapLength, + nsEventStatus (*aOutEventStatuses)[2]) { + APZEventResult touchDownResult = TouchDown(aTarget, aPoint, mcc->Time()); + if (aOutEventStatuses) { + (*aOutEventStatuses)[0] = touchDownResult.GetStatus(); + } + mcc->AdvanceBy(aTapLength); + + // If touch-action is enabled then simulate the allowed touch behaviour + // notification that the main thread is supposed to deliver. + if (touchDownResult.GetStatus() != nsEventStatus_eConsumeNoDefault) { + SetDefaultAllowedTouchBehavior(aTarget, touchDownResult.mInputBlockId); + } + + APZEventResult touchUpResult = TouchUp(aTarget, aPoint, mcc->Time()); + if (aOutEventStatuses) { + (*aOutEventStatuses)[1] = touchUpResult.GetStatus(); + } + return touchDownResult; +} + +template <class InputReceiver> +void APZCTesterBase::TapAndCheckStatus(const RefPtr<InputReceiver>& aTarget, + const ScreenIntPoint& aPoint, + TimeDuration aTapLength) { + nsEventStatus statuses[2]; + Tap(aTarget, aPoint, aTapLength, &statuses); + EXPECT_EQ(nsEventStatus_eConsumeDoDefault, statuses[0]); + EXPECT_EQ(nsEventStatus_eConsumeDoDefault, statuses[1]); +} + +template <class InputReceiver> +void APZCTesterBase::Pan(const RefPtr<InputReceiver>& aTarget, + const ScreenIntPoint& aTouchStart, + const ScreenIntPoint& aTouchEnd, PanOptions aOptions, + nsTArray<uint32_t>* aAllowedTouchBehaviors, + nsEventStatus (*aOutEventStatuses)[4], + uint64_t* aOutInputBlockId) { + // Reduce the move tolerance to a tiny value. + // We can't use a scoped pref because this value might be read at some later + // time when the events are actually processed, rather than when we deliver + // them. + const float touchStartTolerance = 0.1f; + const float panThreshold = touchStartTolerance * aTarget->GetDPI(); + Preferences::SetFloat("apz.touch_start_tolerance", touchStartTolerance); + Preferences::SetFloat("apz.touch_move_tolerance", 0.0f); + int overcomeTouchToleranceX = 0; + int overcomeTouchToleranceY = 0; + if (!(aOptions & PanOptions::ExactCoordinates)) { + // Have the direction of the adjustment to overcome the touch tolerance + // match the direction of the entire gesture, otherwise we run into + // trouble such as accidentally activating the axis lock. + if (aTouchStart.x != aTouchEnd.x && aTouchStart.y != aTouchEnd.y) { + // Tests that need to avoid rounding error here can arrange for + // panThreshold to be 10 (by setting the DPI to 100), which makes sure + // that these are the legs in a Pythagorean triple where panThreshold is + // the hypotenuse. Watch out for changes of APZCPinchTester::mDPI. + overcomeTouchToleranceX = panThreshold / 10 * 6; + overcomeTouchToleranceY = panThreshold / 10 * 8; + } else if (aTouchStart.x != aTouchEnd.x) { + overcomeTouchToleranceX = panThreshold; + } else if (aTouchStart.y != aTouchEnd.y) { + overcomeTouchToleranceY = panThreshold; + } + } + + const TimeDuration TIME_BETWEEN_TOUCH_EVENT = + TimeDuration::FromMilliseconds(20); + + // Even if the caller doesn't care about the block id, we need it to set the + // allowed touch behaviour below, so make sure aOutInputBlockId is non-null. + uint64_t blockId; + if (!aOutInputBlockId) { + aOutInputBlockId = &blockId; + } + + // Make sure the move is large enough to not be handled as a tap + APZEventResult result = + TouchDown(aTarget, + ScreenIntPoint(aTouchStart.x + overcomeTouchToleranceX, + aTouchStart.y + overcomeTouchToleranceY), + mcc->Time()); + if (aOutInputBlockId) { + *aOutInputBlockId = result.mInputBlockId; + } + if (aOutEventStatuses) { + (*aOutEventStatuses)[0] = result.GetStatus(); + } + + mcc->AdvanceBy(TIME_BETWEEN_TOUCH_EVENT); + + // Allowed touch behaviours must be set after sending touch-start. + if (result.GetStatus() != nsEventStatus_eConsumeNoDefault) { + if (aAllowedTouchBehaviors) { + EXPECT_EQ(1UL, aAllowedTouchBehaviors->Length()); + aTarget->SetAllowedTouchBehavior(*aOutInputBlockId, + *aAllowedTouchBehaviors); + } else { + SetDefaultAllowedTouchBehavior(aTarget, *aOutInputBlockId); + } + } + + result = TouchMove(aTarget, aTouchStart, mcc->Time()); + if (aOutEventStatuses) { + (*aOutEventStatuses)[1] = result.GetStatus(); + } + + mcc->AdvanceBy(TIME_BETWEEN_TOUCH_EVENT); + + const int numSteps = 3; + auto stepVector = (aTouchEnd - aTouchStart) / numSteps; + for (int k = 1; k < numSteps; k++) { + auto stepPoint = aTouchStart + stepVector * k; + Unused << TouchMove(aTarget, stepPoint, mcc->Time()); + + mcc->AdvanceBy(TIME_BETWEEN_TOUCH_EVENT); + } + + result = TouchMove(aTarget, aTouchEnd, mcc->Time()); + if (aOutEventStatuses) { + (*aOutEventStatuses)[2] = result.GetStatus(); + } + + mcc->AdvanceBy(TIME_BETWEEN_TOUCH_EVENT); + + if (!(aOptions & PanOptions::KeepFingerDown)) { + result = TouchUp(aTarget, aTouchEnd, mcc->Time()); + } else { + result.SetStatusAsIgnore(); + } + if (aOutEventStatuses) { + (*aOutEventStatuses)[3] = result.GetStatus(); + } + + if ((aOptions & PanOptions::NoFling)) { + aTarget->CancelAnimation(); + } + + // Don't increment the time here. Animations started on touch-up, such as + // flings, are affected by elapsed time, and we want to be able to sample + // them immediately after they start, without time having elapsed. +} + +template <class InputReceiver> +void APZCTesterBase::Pan(const RefPtr<InputReceiver>& aTarget, int aTouchStartY, + int aTouchEndY, PanOptions aOptions, + nsTArray<uint32_t>* aAllowedTouchBehaviors, + nsEventStatus (*aOutEventStatuses)[4], + uint64_t* aOutInputBlockId) { + Pan(aTarget, ScreenIntPoint(10, aTouchStartY), ScreenIntPoint(10, aTouchEndY), + aOptions, aAllowedTouchBehaviors, aOutEventStatuses, aOutInputBlockId); +} + +template <class InputReceiver> +void APZCTesterBase::PanAndCheckStatus( + const RefPtr<InputReceiver>& aTarget, int aTouchStartY, int aTouchEndY, + bool aExpectConsumed, nsTArray<uint32_t>* aAllowedTouchBehaviors, + uint64_t* aOutInputBlockId) { + nsEventStatus statuses[4]; // down, move, move, up + Pan(aTarget, aTouchStartY, aTouchEndY, PanOptions::None, + aAllowedTouchBehaviors, &statuses, aOutInputBlockId); + + EXPECT_EQ(nsEventStatus_eConsumeDoDefault, statuses[0]); + + nsEventStatus touchMoveStatus; + if (aExpectConsumed) { + touchMoveStatus = nsEventStatus_eConsumeDoDefault; + } else { + touchMoveStatus = nsEventStatus_eIgnore; + } + EXPECT_EQ(touchMoveStatus, statuses[1]); + EXPECT_EQ(touchMoveStatus, statuses[2]); +} + +template <class InputReceiver> +void APZCTesterBase::DoubleTap(const RefPtr<InputReceiver>& aTarget, + const ScreenIntPoint& aPoint, + nsEventStatus (*aOutEventStatuses)[4], + uint64_t (*aOutInputBlockIds)[2]) { + APZEventResult result = TouchDown(aTarget, aPoint, mcc->Time()); + if (aOutEventStatuses) { + (*aOutEventStatuses)[0] = result.GetStatus(); + } + if (aOutInputBlockIds) { + (*aOutInputBlockIds)[0] = result.mInputBlockId; + } + mcc->AdvanceByMillis(10); + + // If touch-action is enabled then simulate the allowed touch behaviour + // notification that the main thread is supposed to deliver. + if (result.GetStatus() != nsEventStatus_eConsumeNoDefault) { + SetDefaultAllowedTouchBehavior(aTarget, result.mInputBlockId); + } + + result = TouchUp(aTarget, aPoint, mcc->Time()); + if (aOutEventStatuses) { + (*aOutEventStatuses)[1] = result.GetStatus(); + } + mcc->AdvanceByMillis(10); + result = TouchDown(aTarget, aPoint, mcc->Time()); + if (aOutEventStatuses) { + (*aOutEventStatuses)[2] = result.GetStatus(); + } + if (aOutInputBlockIds) { + (*aOutInputBlockIds)[1] = result.mInputBlockId; + } + mcc->AdvanceByMillis(10); + + if (result.GetStatus() != nsEventStatus_eConsumeNoDefault) { + SetDefaultAllowedTouchBehavior(aTarget, result.mInputBlockId); + } + + result = TouchUp(aTarget, aPoint, mcc->Time()); + if (aOutEventStatuses) { + (*aOutEventStatuses)[3] = result.GetStatus(); + } +} + +template <class InputReceiver> +void APZCTesterBase::DoubleTapAndCheckStatus( + const RefPtr<InputReceiver>& aTarget, const ScreenIntPoint& aPoint, + uint64_t (*aOutInputBlockIds)[2]) { + nsEventStatus statuses[4]; + DoubleTap(aTarget, aPoint, &statuses, aOutInputBlockIds); + EXPECT_EQ(nsEventStatus_eConsumeDoDefault, statuses[0]); + EXPECT_EQ(nsEventStatus_eConsumeDoDefault, statuses[1]); + EXPECT_EQ(nsEventStatus_eConsumeDoDefault, statuses[2]); + EXPECT_EQ(nsEventStatus_eConsumeDoDefault, statuses[3]); +} + +template <class InputReceiver> +void APZCTesterBase::PinchWithTouchInput(const RefPtr<InputReceiver>& aTarget, + const ScreenIntPoint& aFocus, + float aScale, PinchOptions aOptions) { + // Having pinch coordinates in float type may cause problems with + // high-precision scale values since SingleTouchData accepts integer value. + // But for trivial tests it should be ok. + const float pinchLength = 100.0; + const float pinchLengthScaled = pinchLength * aScale; + + const float pinchLengthX = aOptions.mVertical ? 0 : pinchLength; + const float pinchLengthScaledX = aOptions.mVertical ? 0 : pinchLengthScaled; + const float pinchLengthY = aOptions.mVertical ? pinchLength : 0; + const float pinchLengthScaledY = aOptions.mVertical ? pinchLengthScaled : 0; + + // Even if the caller doesn't care about the block id, we need it to set the + // allowed touch behaviour below, so make sure aOutInputBlockId is non-null. + uint64_t blockId; + if (!aOptions.mOutInputBlockId) { + aOptions.mOutInputBlockId = &blockId; + } + + int inputId = aOptions.mInputId ? *aOptions.mInputId : 0; + + // If a second focus point is not specified in the pinch options, use the + // same focus point throughout the gesture. + ScreenIntPoint secondFocus = + aOptions.mSecondFocus.isSome() ? *aOptions.mSecondFocus : aFocus; + + MultiTouchInput mtiStart = + MultiTouchInput(MultiTouchInput::MULTITOUCH_START, 0, mcc->Time(), 0); + mtiStart.mTouches.AppendElement(CreateSingleTouchData(inputId, aFocus)); + mtiStart.mTouches.AppendElement(CreateSingleTouchData(inputId + 1, aFocus)); + APZEventResult result; + result = aTarget->ReceiveInputEvent(mtiStart); + if (aOptions.mOutInputBlockId) { + *aOptions.mOutInputBlockId = result.mInputBlockId; + } + if (aOptions.mOutEventStatuses) { + (*aOptions.mOutEventStatuses)[0] = result.GetStatus(); + } + + if (aOptions.mAllowedTouchBehaviors) { + EXPECT_EQ(2UL, aOptions.mAllowedTouchBehaviors->Length()); + aTarget->SetAllowedTouchBehavior(*aOptions.mOutInputBlockId, + *aOptions.mAllowedTouchBehaviors); + } else { + SetDefaultAllowedTouchBehavior(aTarget, *aOptions.mOutInputBlockId, 2); + } + + mcc->AdvanceBy(aOptions.mTimeBetweenTouchEvents); + + ScreenIntPoint pinchStartPoint1(aFocus.x - int32_t(pinchLengthX), + aFocus.y - int32_t(pinchLengthY)); + ScreenIntPoint pinchStartPoint2(aFocus.x + int32_t(pinchLengthX), + aFocus.y + int32_t(pinchLengthY)); + + MultiTouchInput mtiMove1 = + MultiTouchInput(MultiTouchInput::MULTITOUCH_MOVE, 0, mcc->Time(), 0); + mtiMove1.mTouches.AppendElement( + CreateSingleTouchData(inputId, pinchStartPoint1)); + mtiMove1.mTouches.AppendElement( + CreateSingleTouchData(inputId + 1, pinchStartPoint2)); + result = aTarget->ReceiveInputEvent(mtiMove1); + if (aOptions.mOutEventStatuses) { + (*aOptions.mOutEventStatuses)[1] = result.GetStatus(); + } + + mcc->AdvanceBy(aOptions.mTimeBetweenTouchEvents); + + // Pinch instantly but move in steps. + const int numSteps = 3; + auto stepVector = (secondFocus - aFocus) / numSteps; + for (int k = 1; k < numSteps; k++) { + ScreenIntPoint stepFocus = aFocus + stepVector * k; + ScreenIntPoint stepPoint1(stepFocus.x - int32_t(pinchLengthScaledX), + stepFocus.y - int32_t(pinchLengthScaledY)); + ScreenIntPoint stepPoint2(stepFocus.x + int32_t(pinchLengthScaledX), + stepFocus.y + int32_t(pinchLengthScaledY)); + MultiTouchInput mtiMoveStep = + MultiTouchInput(MultiTouchInput::MULTITOUCH_MOVE, 0, mcc->Time(), 0); + mtiMoveStep.mTouches.AppendElement( + CreateSingleTouchData(inputId, stepPoint1)); + mtiMoveStep.mTouches.AppendElement( + CreateSingleTouchData(inputId + 1, stepPoint2)); + Unused << aTarget->ReceiveInputEvent(mtiMoveStep); + + mcc->AdvanceBy(aOptions.mTimeBetweenTouchEvents); + } + + ScreenIntPoint pinchEndPoint1(secondFocus.x - int32_t(pinchLengthScaledX), + secondFocus.y - int32_t(pinchLengthScaledY)); + ScreenIntPoint pinchEndPoint2(secondFocus.x + int32_t(pinchLengthScaledX), + secondFocus.y + int32_t(pinchLengthScaledY)); + + MultiTouchInput mtiMove2 = + MultiTouchInput(MultiTouchInput::MULTITOUCH_MOVE, 0, mcc->Time(), 0); + mtiMove2.mTouches.AppendElement( + CreateSingleTouchData(inputId, pinchEndPoint1)); + mtiMove2.mTouches.AppendElement( + CreateSingleTouchData(inputId + 1, pinchEndPoint2)); + result = aTarget->ReceiveInputEvent(mtiMove2); + if (aOptions.mOutEventStatuses) { + (*aOptions.mOutEventStatuses)[2] = result.GetStatus(); + } + + if (aOptions.mFlags & (PinchFlags::LiftFinger1 | PinchFlags::LiftFinger2)) { + mcc->AdvanceBy(aOptions.mTimeBetweenTouchEvents); + + MultiTouchInput mtiEnd = + MultiTouchInput(MultiTouchInput::MULTITOUCH_END, 0, mcc->Time(), 0); + if (aOptions.mFlags & PinchFlags::LiftFinger1) { + mtiEnd.mTouches.AppendElement( + CreateSingleTouchData(inputId, pinchEndPoint1)); + } + if (aOptions.mFlags & PinchFlags::LiftFinger2) { + mtiEnd.mTouches.AppendElement( + CreateSingleTouchData(inputId + 1, pinchEndPoint2)); + } + result = aTarget->ReceiveInputEvent(mtiEnd); + if (aOptions.mOutEventStatuses) { + (*aOptions.mOutEventStatuses)[3] = result.GetStatus(); + } + } + + inputId += 2; + + if (aOptions.mInputId) { + *aOptions.mInputId = inputId; + } +} + +template <class InputReceiver> +void APZCTesterBase::PinchWithTouchInputAndCheckStatus( + const RefPtr<InputReceiver>& aTarget, const ScreenIntPoint& aFocus, + float aScale, int& inputId, bool aShouldTriggerPinch, + nsTArray<uint32_t>* aAllowedTouchBehaviors) { + nsEventStatus statuses[4]; // down, move, move, up + PinchWithTouchInput(aTarget, aFocus, aScale, + PinchOptions() + .AllowedTouchBehaviors(aAllowedTouchBehaviors) + .OutEventStatuses(&statuses) + .InputId(inputId)); + + nsEventStatus expectedMoveStatus = aShouldTriggerPinch + ? nsEventStatus_eConsumeDoDefault + : nsEventStatus_eIgnore; + EXPECT_EQ(nsEventStatus_eConsumeDoDefault, statuses[0]); + EXPECT_EQ(expectedMoveStatus, statuses[1]); + EXPECT_EQ(expectedMoveStatus, statuses[2]); +} + +template <class InputReceiver> +void APZCTesterBase::PinchWithPinchInput( + const RefPtr<InputReceiver>& aTarget, const ScreenIntPoint& aFocus, + const ScreenIntPoint& aSecondFocus, float aScale, + nsEventStatus (*aOutEventStatuses)[3]) { + const TimeDuration TIME_BETWEEN_PINCH_INPUT = + TimeDuration::FromMilliseconds(50); + + auto event = CreatePinchGestureInput(PinchGestureInput::PINCHGESTURE_START, + aFocus, 10.0, 10.0, mcc->Time()); + APZEventResult actual = aTarget->ReceiveInputEvent(event); + if (aOutEventStatuses) { + (*aOutEventStatuses)[0] = actual.GetStatus(); + } + mcc->AdvanceBy(TIME_BETWEEN_PINCH_INPUT); + + event = + CreatePinchGestureInput(PinchGestureInput::PINCHGESTURE_SCALE, + aSecondFocus, 10.0 * aScale, 10.0, mcc->Time()); + actual = aTarget->ReceiveInputEvent(event); + if (aOutEventStatuses) { + (*aOutEventStatuses)[1] = actual.GetStatus(); + } + mcc->AdvanceBy(TIME_BETWEEN_PINCH_INPUT); + + event = + CreatePinchGestureInput(PinchGestureInput::PINCHGESTURE_END, aSecondFocus, + 10.0 * aScale, 10.0 * aScale, mcc->Time()); + actual = aTarget->ReceiveInputEvent(event); + if (aOutEventStatuses) { + (*aOutEventStatuses)[2] = actual.GetStatus(); + } +} + +template <class InputReceiver> +void APZCTesterBase::PinchWithPinchInputAndCheckStatus( + const RefPtr<InputReceiver>& aTarget, const ScreenIntPoint& aFocus, + float aScale, bool aShouldTriggerPinch) { + nsEventStatus statuses[3]; // scalebegin, scale, scaleend + PinchWithPinchInput(aTarget, aFocus, aFocus, aScale, &statuses); + + nsEventStatus expectedStatus = aShouldTriggerPinch + ? nsEventStatus_eConsumeDoDefault + : nsEventStatus_eIgnore; + EXPECT_EQ(expectedStatus, statuses[0]); + EXPECT_EQ(expectedStatus, statuses[1]); +} + +inline FrameMetrics TestFrameMetrics() { + FrameMetrics fm; + + fm.SetDisplayPort(CSSRect(0, 0, 10, 10)); + fm.SetCompositionBounds(ParentLayerRect(0, 0, 10, 10)); + fm.SetScrollableRect(CSSRect(0, 0, 100, 100)); + + return fm; +} + +#endif // mozilla_layers_APZTestCommon_h diff --git a/gfx/layers/apz/test/gtest/InputUtils.h b/gfx/layers/apz/test/gtest/InputUtils.h new file mode 100644 index 0000000000..16caec84f9 --- /dev/null +++ b/gfx/layers/apz/test/gtest/InputUtils.h @@ -0,0 +1,151 @@ +/* -*- 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/. */ + +#ifndef mozilla_layers_InputUtils_h +#define mozilla_layers_InputUtils_h + +/** + * Defines a set of utility functions for generating input events + * to an APZC/APZCTM during APZ gtests. + */ + +#include "APZTestCommon.h" + +/* The InputReceiver template parameter used in the helper functions below needs + * to be a class that implements functions with the signatures: + * APZEventResult ReceiveInputEvent(const InputData& aEvent); + * void SetAllowedTouchBehavior(uint64_t aInputBlockId, + * const nsTArray<uint32_t>& aBehaviours); + * The classes that currently implement these are APZCTreeManager and + * TestAsyncPanZoomController. Using this template allows us to test individual + * APZC instances in isolation and also an entire APZ tree, while using the same + * code to dispatch input events. + */ + +template <class InputReceiver> +void SetDefaultAllowedTouchBehavior(const RefPtr<InputReceiver>& aTarget, + uint64_t aInputBlockId, + int touchPoints = 1) { + nsTArray<uint32_t> defaultBehaviors; + // use the default value where everything is allowed + for (int i = 0; i < touchPoints; i++) { + defaultBehaviors.AppendElement( + mozilla::layers::AllowedTouchBehavior::HORIZONTAL_PAN | + mozilla::layers::AllowedTouchBehavior::VERTICAL_PAN | + mozilla::layers::AllowedTouchBehavior::PINCH_ZOOM | + mozilla::layers::AllowedTouchBehavior::ANIMATING_ZOOM); + } + aTarget->SetAllowedTouchBehavior(aInputBlockId, defaultBehaviors); +} + +inline MultiTouchInput CreateMultiTouchInput( + MultiTouchInput::MultiTouchType aType, TimeStamp aTime) { + return MultiTouchInput(aType, MillisecondsSinceStartup(aTime), aTime, 0); +} + +template <class InputReceiver> +APZEventResult TouchDown(const RefPtr<InputReceiver>& aTarget, + const ScreenIntPoint& aPoint, TimeStamp aTime) { + MultiTouchInput mti = + CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_START, aTime); + mti.mTouches.AppendElement(CreateSingleTouchData(0, aPoint)); + return aTarget->ReceiveInputEvent(mti); +} + +template <class InputReceiver> +APZEventResult TouchMove(const RefPtr<InputReceiver>& aTarget, + const ScreenIntPoint& aPoint, TimeStamp aTime) { + MultiTouchInput mti = + CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_MOVE, aTime); + mti.mTouches.AppendElement(CreateSingleTouchData(0, aPoint)); + return aTarget->ReceiveInputEvent(mti); +} + +template <class InputReceiver> +APZEventResult TouchUp(const RefPtr<InputReceiver>& aTarget, + const ScreenIntPoint& aPoint, TimeStamp aTime) { + MultiTouchInput mti = + CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_END, aTime); + mti.mTouches.AppendElement(CreateSingleTouchData(0, aPoint)); + return aTarget->ReceiveInputEvent(mti); +} + +template <class InputReceiver> +APZEventResult Wheel(const RefPtr<InputReceiver>& aTarget, + const ScreenIntPoint& aPoint, const ScreenPoint& aDelta, + TimeStamp aTime) { + ScrollWheelInput input(aTime, 0, ScrollWheelInput::SCROLLMODE_INSTANT, + ScrollWheelInput::SCROLLDELTA_PIXEL, aPoint, aDelta.x, + aDelta.y, false, WheelDeltaAdjustmentStrategy::eNone); + return aTarget->ReceiveInputEvent(input); +} + +// Tests that use this function should set general.smoothScroll=true, otherwise +// the smooth scroll animation code will set the animation duration to 0. +template <class InputReceiver> +APZEventResult SmoothWheel(const RefPtr<InputReceiver>& aTarget, + const ScreenIntPoint& aPoint, + const ScreenPoint& aDelta, TimeStamp aTime) { + ScrollWheelInput input(aTime, 0, ScrollWheelInput::SCROLLMODE_SMOOTH, + ScrollWheelInput::SCROLLDELTA_LINE, aPoint, aDelta.x, + aDelta.y, false, WheelDeltaAdjustmentStrategy::eNone); + return aTarget->ReceiveInputEvent(input); +} + +template <class InputReceiver> +APZEventResult MouseDown(const RefPtr<InputReceiver>& aTarget, + const ScreenIntPoint& aPoint, TimeStamp aTime) { + MouseInput input(MouseInput::MOUSE_DOWN, + MouseInput::ButtonType::PRIMARY_BUTTON, 0, 0, aPoint, aTime, + 0); + return aTarget->ReceiveInputEvent(input); +} + +template <class InputReceiver> +APZEventResult MouseMove(const RefPtr<InputReceiver>& aTarget, + const ScreenIntPoint& aPoint, TimeStamp aTime) { + MouseInput input(MouseInput::MOUSE_MOVE, + MouseInput::ButtonType::PRIMARY_BUTTON, 0, 0, aPoint, aTime, + 0); + return aTarget->ReceiveInputEvent(input); +} + +template <class InputReceiver> +APZEventResult MouseUp(const RefPtr<InputReceiver>& aTarget, + const ScreenIntPoint& aPoint, TimeStamp aTime) { + MouseInput input(MouseInput::MOUSE_UP, MouseInput::ButtonType::PRIMARY_BUTTON, + 0, 0, aPoint, aTime, 0); + return aTarget->ReceiveInputEvent(input); +} + +template <class InputReceiver> +APZEventResult PanGesture(PanGestureInput::PanGestureType aType, + const RefPtr<InputReceiver>& aTarget, + const ScreenIntPoint& aPoint, + const ScreenPoint& aDelta, TimeStamp aTime, + Modifiers aModifiers = MODIFIER_NONE, + bool aSimulateMomentum = false) { + PanGestureInput input(aType, aTime, aPoint, aDelta, aModifiers); + input.mSimulateMomentum = aSimulateMomentum; + if constexpr (std::is_same_v<InputReceiver, TestAsyncPanZoomController>) { + // In the case of TestAsyncPanZoomController we know for sure that the + // event will be handled by APZ so set it explicitly. + input.mHandledByAPZ = true; + } + return aTarget->ReceiveInputEvent(input); +} + +template <class InputReceiver> +APZEventResult PanGestureWithModifiers(PanGestureInput::PanGestureType aType, + Modifiers aModifiers, + const RefPtr<InputReceiver>& aTarget, + const ScreenIntPoint& aPoint, + const ScreenPoint& aDelta, + TimeStamp aTime) { + return PanGesture(aType, aTarget, aPoint, aDelta, aTime, aModifiers); +} + +#endif // mozilla_layers_InputUtils_h diff --git a/gfx/layers/apz/test/gtest/MockHitTester.cpp b/gfx/layers/apz/test/gtest/MockHitTester.cpp new file mode 100644 index 0000000000..b445b02056 --- /dev/null +++ b/gfx/layers/apz/test/gtest/MockHitTester.cpp @@ -0,0 +1,76 @@ +/* -*- 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 "MockHitTester.h" +#include "TreeTraversal.h" +#include "apz/src/APZCTreeManager.h" +#include "apz/src/AsyncPanZoomController.h" +#include "mozilla/gfx/CompositorHitTestInfo.h" +#include "mozilla/layers/ScrollableLayerGuid.h" + +namespace mozilla::layers { + +IAPZHitTester::HitTestResult MockHitTester::GetAPZCAtPoint( + const ScreenPoint& aHitTestPoint, + const RecursiveMutexAutoLock& aProofOfTreeLock) { + MOZ_ASSERT(!mQueuedResults.empty()); + HitTestResult result = std::move(mQueuedResults.front()); + mQueuedResults.pop(); + return result; +} + +void MockHitTester::QueueHitResult(ScrollableLayerGuid::ViewID aScrollId, + gfx::CompositorHitTestInfo aHitInfo) { + LayersId layersId = GetRootLayersId(); // currently this is all the tests use + RefPtr<HitTestingTreeNode> node = + GetTargetNode(ScrollableLayerGuid(layersId, 0, aScrollId), + ScrollableLayerGuid::EqualsIgnoringPresShell); + MOZ_ASSERT(node); + AsyncPanZoomController* apzc = node->GetApzc(); + MOZ_ASSERT(apzc); + HitTestResult result; + result.mTargetApzc = apzc; + result.mHitResult = aHitInfo; + result.mLayersId = layersId; + mQueuedResults.push(std::move(result)); +} + +void MockHitTester::QueueScrollbarThumbHitResult( + ScrollableLayerGuid::ViewID aScrollId, ScrollDirection aDirection) { + RecursiveMutexAutoLock lock(GetTreeLock()); + LayersId layersId = GetRootLayersId(); // currently this is all the tests use + // First find the scrolalble node, to get the APZC. + RefPtr<HitTestingTreeNode> scrollableNode = + GetTargetNode(ScrollableLayerGuid(layersId, 0, aScrollId), + ScrollableLayerGuid::EqualsIgnoringPresShell); + MOZ_ASSERT(scrollableNode); + AsyncPanZoomController* apzc = scrollableNode->GetApzc(); + MOZ_ASSERT(apzc); + + // Now find the scroll thumb node. + RefPtr<HitTestingTreeNode> scrollThumbNode = + BreadthFirstSearch<ReverseIterator>( + GetRootNode(), [&](HitTestingTreeNode* aNode) { + return aNode->GetLayersId() == layersId && + aNode->IsScrollThumbNode() && + aNode->GetScrollbarDirection() == aDirection && + aNode->GetScrollTargetId() == aScrollId; + }); + MOZ_ASSERT(scrollThumbNode); + + HitTestResult result; + result.mTargetApzc = apzc; + result.mHitResult = {gfx::CompositorHitTestFlags::eVisibleToHitTest, + gfx::CompositorHitTestFlags::eScrollbarThumb}; + if (aDirection == ScrollDirection::eVertical) { + result.mHitResult += gfx::CompositorHitTestFlags::eScrollbarVertical; + } + InitializeHitTestingTreeNodeAutoLock(result.mScrollbarNode, lock, + scrollThumbNode); + mQueuedResults.push(std::move(result)); +} + +} // namespace mozilla::layers diff --git a/gfx/layers/apz/test/gtest/MockHitTester.h b/gfx/layers/apz/test/gtest/MockHitTester.h new file mode 100644 index 0000000000..9f01ae8a2c --- /dev/null +++ b/gfx/layers/apz/test/gtest/MockHitTester.h @@ -0,0 +1,43 @@ +/* -*- 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/. */ + +#ifndef mozilla_layers_MockHitTester_h +#define mozilla_layers_MockHitTester_h + +#include "apz/src/IAPZHitTester.h" +#include "mozilla/gfx/CompositorHitTestInfo.h" +#include "mozilla/layers/LayersTypes.h" + +#include <queue> + +namespace mozilla::layers { + +// IAPZHitTester implementation for APZ gtests. +// This does not actually perform hit-testing, it just allows +// the test code to specify the expected hit test results. +class MockHitTester final : public IAPZHitTester { + public: + HitTestResult GetAPZCAtPoint( + const ScreenPoint& aHitTestPoint, + const RecursiveMutexAutoLock& aProofOfTreeLock) override; + + // Queue a hit test result whose target APZC is the APZC + // with scroll id |aScrollId|, and the provided hit test flags. + void QueueHitResult(ScrollableLayerGuid::ViewID aScrollId, + gfx::CompositorHitTestInfo aHitInfo); + + // Queue a hit test result whose target is the scrollbar of the APZC + // with scroll id |aScrollId| in the direction specified by |aDirection|. + void QueueScrollbarThumbHitResult(ScrollableLayerGuid::ViewID aScrollId, + ScrollDirection aDirection); + + private: + std::queue<HitTestResult> mQueuedResults; +}; + +} // namespace mozilla::layers + +#endif // define mozilla_layers_MockHitTester_h diff --git a/gfx/layers/apz/test/gtest/TestAxisLock.cpp b/gfx/layers/apz/test/gtest/TestAxisLock.cpp new file mode 100644 index 0000000000..d1d13a9d52 --- /dev/null +++ b/gfx/layers/apz/test/gtest/TestAxisLock.cpp @@ -0,0 +1,645 @@ +/* -*- 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" +#include "gtest/gtest.h" + +#include <cmath> + +class APZCAxisLockCompatTester : public APZCTreeManagerTester, + public testing::WithParamInterface<int> { + public: + APZCAxisLockCompatTester() : oldAxisLockMode(0) { CreateMockHitTester(); } + + int oldAxisLockMode; + + UniquePtr<ScopedLayerTreeRegistration> registration; + + RefPtr<TestAsyncPanZoomController> apzc; + + void SetUp() { + APZCTreeManagerTester::SetUp(); + + oldAxisLockMode = Preferences::GetInt("apz.axis_lock.mode"); + + Preferences::SetInt("apz.axis_lock.mode", GetParam()); + } + + void TearDown() { + APZCTreeManagerTester::TearDown(); + + Preferences::SetInt("apz.axis_lock.mode", oldAxisLockMode); + } + + static std::string PrintFromParam(const testing::TestParamInfo<int>& info) { + switch (info.param) { + case 0: + return "FREE"; + case 1: + return "STANDARD"; + case 2: + return "STICKY"; + case 3: + return "DOMINANT_AXIS"; + default: + return "UNKNOWN"; + } + } +}; + +class APZCAxisLockTester : public APZCTreeManagerTester { + public: + APZCAxisLockTester() { CreateMockHitTester(); } + + UniquePtr<ScopedLayerTreeRegistration> registration; + + RefPtr<TestAsyncPanZoomController> apzc; + + void SetupBasicTest() { + const char* treeShape = "x"; + LayerIntRect layerVisibleRect[] = { + LayerIntRect(0, 0, 100, 100), + }; + CreateScrollData(treeShape, layerVisibleRect); + SetScrollableFrameMetrics(root, ScrollableLayerGuid::START_SCROLL_ID, + CSSRect(0, 0, 500, 500)); + + registration = MakeUnique<ScopedLayerTreeRegistration>(LayersId{0}, mcc); + + UpdateHitTestingTree(); + } + + void BreakStickyAxisLockTestGesture(const ScrollDirections& aDirections) { + float panX = 0; + float panY = 0; + + if (aDirections.contains(ScrollDirection::eVertical)) { + panY = 30; + } + if (aDirections.contains(ScrollDirection::eHorizontal)) { + panX = 30; + } + + // Kick off the gesture that may lock onto an axis + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID); + PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 50), + ScreenPoint(panX, panY), mcc->Time()); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID); + PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 50), + ScreenPoint(panX, panY), mcc->Time()); + } + + void BreakStickyAxisLockTest(const ScrollDirections& aDirections) { + // Create the gesture for the test. + BreakStickyAxisLockTestGesture(aDirections); + + // Based on the scroll direction(s) ensure the state is what we expect. + if (aDirections == ScrollDirection::eVertical) { + apzc->AssertStateIsPanningLockedY(); + apzc->AssertAxisLocked(ScrollDirection::eVertical); + EXPECT_GT(apzc->GetVelocityVector().y, 0); + EXPECT_EQ(apzc->GetVelocityVector().x, 0); + } else if (aDirections == ScrollDirection::eHorizontal) { + apzc->AssertStateIsPanningLockedX(); + apzc->AssertAxisLocked(ScrollDirection::eHorizontal); + EXPECT_GT(apzc->GetVelocityVector().x, 0); + EXPECT_EQ(apzc->GetVelocityVector().y, 0); + } else { + apzc->AssertStateIsPanning(); + apzc->AssertNotAxisLocked(); + EXPECT_GT(apzc->GetVelocityVector().x, 0); + EXPECT_GT(apzc->GetVelocityVector().y, 0); + } + + // Cleanup for next test. + apzc->AdvanceAnimationsUntilEnd(); + } +}; + +TEST_F(APZCAxisLockTester, BasicDominantAxisUse) { + SCOPED_GFX_PREF_INT("apz.axis_lock.mode", 1); + SCOPED_GFX_PREF_FLOAT("apz.axis_lock.lock_angle", M_PI / 4.0f); + + SetupBasicTest(); + + apzc = ApzcOf(root); + + // Kick off the initial gesture that triggers the momentum scroll. + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID); + PanGesture(PanGestureInput::PANGESTURE_START, manager, ScreenIntPoint(50, 50), + ScreenIntPoint(1, 2), mcc->Time()); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID); + PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 50), + ScreenPoint(15, 30), mcc->Time()); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID); + PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 50), + ScreenPoint(15, 30), mcc->Time()); + + // Should be in a PANNING_LOCKED_Y state with no horizontal velocity. + apzc->AssertStateIsPanningLockedY(); + apzc->AssertAxisLocked(ScrollDirection::eVertical); + EXPECT_GT(apzc->GetVelocityVector().y, 0); + EXPECT_EQ(apzc->GetVelocityVector().x, 0); + + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID); + PanGesture(PanGestureInput::PANGESTURE_END, manager, ScreenIntPoint(50, 50), + ScreenPoint(0, 0), mcc->Time()); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + + // Ensure that we have not panned on the horizontal axis. + ParentLayerPoint panEndOffset = apzc->GetCurrentAsyncScrollOffset( + AsyncTransformConsumer::eForEventHandling); + EXPECT_EQ(panEndOffset.x, 0); + + // The lock onto the Y axis extends into momentum scroll. + apzc->AssertAxisLocked(ScrollDirection::eVertical); + + // Start the momentum scroll. + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID); + PanGesture(PanGestureInput::PANGESTURE_MOMENTUMSTART, manager, + ScreenIntPoint(50, 50), ScreenPoint(30, 90), mcc->Time()); + mcc->AdvanceByMillis(10); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID); + PanGesture(PanGestureInput::PANGESTURE_MOMENTUMPAN, manager, + ScreenIntPoint(50, 50), ScreenPoint(10, 30), mcc->Time()); + mcc->AdvanceByMillis(10); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID); + PanGesture(PanGestureInput::PANGESTURE_MOMENTUMPAN, manager, + ScreenIntPoint(50, 50), ScreenPoint(10, 30), mcc->Time()); + mcc->AdvanceByMillis(10); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + + // In momentum locking mode, we should still be locked onto the Y axis. + apzc->AssertStateIsPanMomentum(); + apzc->AssertAxisLocked(ScrollDirection::eVertical); + EXPECT_GT(apzc->GetVelocityVector().y, 0); + EXPECT_EQ(apzc->GetVelocityVector().x, 0); + + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID); + PanGesture(PanGestureInput::PANGESTURE_MOMENTUMEND, manager, + ScreenIntPoint(50, 50), ScreenPoint(0, 0), mcc->Time()); + + // After momentum scroll end, ensure we are no longer locked onto an axis. + apzc->AssertNotAxisLocked(); + + // Wait until the end of the animation and ensure the final state is + // reasonable. + apzc->AdvanceAnimationsUntilEnd(); + ParentLayerPoint finalOffset = apzc->GetCurrentAsyncScrollOffset( + AsyncTransformConsumer::eForEventHandling); + + // Ensure we have scrolled some amount on the Y axis in momentum scroll. + EXPECT_GT(finalOffset.y, panEndOffset.y); + EXPECT_EQ(finalOffset.x, 0.0f); +} + +TEST_F(APZCAxisLockTester, NewGestureBreaksMomentumAxisLock) { + SCOPED_GFX_PREF_INT("apz.axis_lock.mode", 1); + SCOPED_GFX_PREF_FLOAT("apz.axis_lock.lock_angle", M_PI / 4.0f); + + SetupBasicTest(); + + apzc = ApzcOf(root); + + // Kick off the initial gesture that triggers the momentum scroll. + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID); + PanGesture(PanGestureInput::PANGESTURE_START, manager, ScreenIntPoint(50, 50), + ScreenIntPoint(2, 1), mcc->Time()); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID); + PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 50), + ScreenPoint(30, 15), mcc->Time()); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID); + PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 50), + ScreenPoint(30, 15), mcc->Time()); + + // Should be in a PANNING_LOCKED_X state with no vertical velocity. + apzc->AssertStateIsPanningLockedX(); + apzc->AssertAxisLocked(ScrollDirection::eHorizontal); + EXPECT_GT(apzc->GetVelocityVector().x, 0); + EXPECT_EQ(apzc->GetVelocityVector().y, 0); + + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID); + PanGesture(PanGestureInput::PANGESTURE_END, manager, ScreenIntPoint(50, 50), + ScreenPoint(0, 0), mcc->Time()); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + + // Double check that we have not panned on the vertical axis. + ParentLayerPoint panEndOffset = apzc->GetCurrentAsyncScrollOffset( + AsyncTransformConsumer::eForEventHandling); + EXPECT_EQ(panEndOffset.y, 0); + + // Ensure that the axis locks extends into momentum scroll. + apzc->AssertAxisLocked(ScrollDirection::eHorizontal); + + // Start the momentum scroll. + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID); + PanGesture(PanGestureInput::PANGESTURE_MOMENTUMSTART, manager, + ScreenIntPoint(50, 50), ScreenPoint(80, 40), mcc->Time()); + mcc->AdvanceByMillis(10); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID); + PanGesture(PanGestureInput::PANGESTURE_MOMENTUMPAN, manager, + ScreenIntPoint(50, 50), ScreenPoint(20, 10), mcc->Time()); + mcc->AdvanceByMillis(10); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID); + PanGesture(PanGestureInput::PANGESTURE_MOMENTUMPAN, manager, + ScreenIntPoint(50, 50), ScreenPoint(20, 10), mcc->Time()); + mcc->AdvanceByMillis(10); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + + // In momentum locking mode, we should still be locked onto the X axis. + apzc->AssertStateIsPanMomentum(); + apzc->AssertAxisLocked(ScrollDirection::eHorizontal); + EXPECT_GT(apzc->GetVelocityVector().x, 0); + EXPECT_EQ(apzc->GetVelocityVector().y, 0); + + ParentLayerPoint beforeBreakOffset = apzc->GetCurrentAsyncScrollOffset( + AsyncTransformConsumer::eForEventHandling); + EXPECT_EQ(beforeBreakOffset.y, 0); + // Ensure we have scrolled some amount on the X axis in momentum scroll. + EXPECT_GT(beforeBreakOffset.x, panEndOffset.x); + + // Kick off the gesture that breaks the lock onto the X axis. + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID); + PanGesture(PanGestureInput::PANGESTURE_START, manager, ScreenIntPoint(50, 50), + ScreenIntPoint(1, 2), mcc->Time()); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + + ParentLayerPoint afterBreakOffset = apzc->GetCurrentAsyncScrollOffset( + AsyncTransformConsumer::eForEventHandling); + + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID); + PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 50), + ScreenPoint(15, 30), mcc->Time()); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID); + PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 50), + ScreenPoint(15, 30), mcc->Time()); + + // The lock onto the X axis should be broken and we now should be locked + // onto the Y axis. + apzc->AssertStateIsPanningLockedY(); + apzc->AssertAxisLocked(ScrollDirection::eVertical); + EXPECT_GT(apzc->GetVelocityVector().y, 0); + EXPECT_EQ(apzc->GetVelocityVector().x, 0); + + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID); + PanGesture(PanGestureInput::PANGESTURE_END, manager, ScreenIntPoint(50, 50), + ScreenPoint(0, 0), mcc->Time()); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + + // The lock onto the Y axis extends into momentum scroll. + apzc->AssertAxisLocked(ScrollDirection::eVertical); + + // Wait until the end of the animation and ensure the final state is + // reasonable. + apzc->AdvanceAnimationsUntilEnd(); + ParentLayerPoint finalOffset = apzc->GetCurrentAsyncScrollOffset( + AsyncTransformConsumer::eForEventHandling); + + EXPECT_GT(finalOffset.y, 0); + // Ensure that we did not scroll on the X axis after the vertical scroll + // started. + EXPECT_EQ(finalOffset.x, afterBreakOffset.x); +} + +TEST_F(APZCAxisLockTester, BreakStickyAxisLock) { + SCOPED_GFX_PREF_INT("apz.axis_lock.mode", 2); + SCOPED_GFX_PREF_FLOAT("apz.axis_lock.lock_angle", M_PI / 6.0f); + SCOPED_GFX_PREF_FLOAT("apz.axis_lock.breakout_angle", M_PI / 6.0f); + + SetupBasicTest(); + + apzc = ApzcOf(root); + + // Start a gesture to get us locked onto the Y axis. + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID); + PanGesture(PanGestureInput::PANGESTURE_START, manager, ScreenIntPoint(50, 50), + ScreenIntPoint(0, 2), mcc->Time()); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + + // Ensure that we have locked onto the Y axis. + apzc->AssertStateIsPanningLockedY(); + + // Test switch to locking onto the X axis. + BreakStickyAxisLockTest(ScrollDirection::eHorizontal); + + // Test switch back to locking onto the Y axis. + BreakStickyAxisLockTest(ScrollDirection::eVertical); + + // Test breaking all axis locks from a Y axis lock. + BreakStickyAxisLockTest(ScrollDirections(ScrollDirection::eHorizontal, + ScrollDirection::eVertical)); + + // We should be in a panning state. + apzc->AssertStateIsPanning(); + apzc->AssertNotAxisLocked(); + + // Lock back to the X axis. + BreakStickyAxisLockTestGesture(ScrollDirection::eHorizontal); + + // End the gesture. + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID); + PanGesture(PanGestureInput::PANGESTURE_END, manager, ScreenIntPoint(50, 50), + ScreenPoint(0, 0), mcc->Time()); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + + // Start a gesture to get us locked onto the X axis. + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID); + PanGesture(PanGestureInput::PANGESTURE_START, manager, ScreenIntPoint(50, 50), + ScreenIntPoint(2, 0), mcc->Time()); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + + // Ensure that we have locked onto the X axis. + apzc->AssertStateIsPanningLockedX(); + + // Test breaking all axis locks from a X axis lock. + BreakStickyAxisLockTest(ScrollDirections(ScrollDirection::eHorizontal, + ScrollDirection::eVertical)); + + // We should be in a panning state. + apzc->AssertStateIsPanning(); + apzc->AssertNotAxisLocked(); + + // Test switch back to locking onto the Y axis. + BreakStickyAxisLockTest(ScrollDirection::eVertical); +} + +TEST_F(APZCAxisLockTester, BreakAxisLockByLockAngle) { + SCOPED_GFX_PREF_INT("apz.axis_lock.mode", 2); + SCOPED_GFX_PREF_FLOAT("apz.axis_lock.lock_angle", M_PI / 4.0f); + SCOPED_GFX_PREF_FLOAT("apz.axis_lock.breakout_angle", M_PI / 8.0f); + + SetupBasicTest(); + + apzc = ApzcOf(root); + + // Start a gesture to get us locked onto the Y axis. + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID); + PanGesture(PanGestureInput::PANGESTURE_START, manager, ScreenIntPoint(50, 50), + ScreenIntPoint(1, 10), mcc->Time()); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + + // Ensure that we have locked onto the Y axis. + apzc->AssertStateIsPanningLockedY(); + + // Stay within 45 degrees from the X axis, and more than 22.5 degrees from + // the Y axis. This should break the Y lock and lock us to the X axis. + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID); + PanGesture(PanGestureInput::PANGESTURE_PAN, manager, ScreenIntPoint(50, 50), + ScreenIntPoint(12, 10), mcc->Time()); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + + // Ensure that we have locked onto the X axis. + apzc->AssertStateIsPanningLockedX(); + + // End the gesture. + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID); + PanGesture(PanGestureInput::PANGESTURE_END, manager, ScreenIntPoint(50, 50), + ScreenPoint(0, 0), mcc->Time()); + apzc->AdvanceAnimations(mcc->GetSampleTime()); +} + +TEST_F(APZCAxisLockTester, TestDominantAxisScrolling) { + SCOPED_GFX_PREF_INT("apz.axis_lock.mode", 3); + + int panY; + int panX; + + SetupBasicTest(); + + apzc = ApzcOf(root); + + ParentLayerPoint lastOffset = apzc->GetCurrentAsyncScrollOffset( + AsyncPanZoomController::eForEventHandling); + + // In dominant axis mode, test pan gesture events with varying gesture + // angles and ensure that we only pan on one axis. + for (panX = 0, panY = 50; panY >= 0; panY -= 10, panX += 5) { + // Gesture that should be locked onto one axis + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID); + PanGesture(PanGestureInput::PANGESTURE_START, manager, + ScreenIntPoint(50, 50), ScreenIntPoint(panX, panY), mcc->Time()); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID); + PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 50), + ScreenPoint(static_cast<float>(panX), static_cast<float>(panY)), + mcc->Time()); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID); + PanGesture(PanGestureInput::PANGESTURE_END, manager, ScreenIntPoint(50, 50), + ScreenPoint(0, 0), mcc->Time()); + apzc->AdvanceAnimationsUntilEnd(); + + ParentLayerPoint scrollOffset = apzc->GetCurrentAsyncScrollOffset( + AsyncPanZoomController::eForEventHandling); + + if (panX > panY) { + // If we're closer to the X axis ensure that we moved on the horizontal + // axis and there was no movement on the vertical axis. + EXPECT_GT(scrollOffset.x, lastOffset.x); + EXPECT_EQ(scrollOffset.y, lastOffset.y); + } else { + // If we're closer to the Y axis ensure that we moved on the vertical + // axis and there was no movement on the horizontal axis. + EXPECT_GT(scrollOffset.y, lastOffset.y); + EXPECT_EQ(scrollOffset.x, lastOffset.x); + } + + lastOffset = scrollOffset; + } +} + +TEST_F(APZCAxisLockTester, TestCanScrollWithAxisLock) { + SCOPED_GFX_PREF_INT("apz.axis_lock.mode", 2); + + SetupBasicTest(); + + apzc = ApzcOf(root); + + // The axis locks do not impact CanScroll() + apzc->SetAxisLocked(ScrollDirection::eHorizontal, true); + EXPECT_EQ(apzc->CanScroll(ParentLayerPoint(10, 0)), true); + + apzc->SetAxisLocked(ScrollDirection::eHorizontal, false); + apzc->SetAxisLocked(ScrollDirection::eVertical, true); + EXPECT_EQ(apzc->CanScroll(ParentLayerPoint(0, 10)), true); +} + +TEST_F(APZCAxisLockTester, TestScrollHandoffAxisLockConflict) { + SCOPED_GFX_PREF_INT("apz.axis_lock.mode", 2); + + // Create two scrollable frames. One parent frame with one child. + 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, 500, 500)); + SetScrollableFrameMetrics(layers[1], ScrollableLayerGuid::START_SCROLL_ID + 1, + CSSRect(0, 0, 500, 500)); + SetScrollHandoff(layers[1], root); + + registration = MakeUnique<ScopedLayerTreeRegistration>(LayersId{0}, mcc); + + UpdateHitTestingTree(); + + RefPtr<TestAsyncPanZoomController> rootApzc = ApzcOf(root); + apzc = ApzcOf(layers[1]); + + // Create a gesture on the y-axis that should lock the x axis. + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1); + PanGesture(PanGestureInput::PANGESTURE_START, manager, ScreenIntPoint(50, 50), + ScreenIntPoint(0, 2), mcc->Time()); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1); + PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 50), + ScreenPoint(0, 15), mcc->Time()); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1); + PanGesture(PanGestureInput::PANGESTURE_END, manager, ScreenIntPoint(50, 50), + ScreenPoint(0, 0), mcc->Time()); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimationsUntilEnd(); + + // We are locked onto the y-axis. + apzc->AssertAxisLocked(ScrollDirection::eVertical); + + // There should be movement in the child. + ParentLayerPoint childCurrentOffset = apzc->GetCurrentAsyncScrollOffset( + AsyncTransformConsumer::eForEventHandling); + EXPECT_GT(childCurrentOffset.y, 0); + EXPECT_EQ(childCurrentOffset.x, 0); + + // There should be no movement in the parent. + ParentLayerPoint parentCurrentOffset = rootApzc->GetCurrentAsyncScrollOffset( + AsyncTransformConsumer::eForEventHandling); + EXPECT_EQ(parentCurrentOffset.y, 0); + EXPECT_EQ(parentCurrentOffset.x, 0); + + // Create a gesture on the x-axis, that should be directed + // at the child, even if the x-axis is locked. + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1); + PanGesture(PanGestureInput::PANGESTURE_START, manager, ScreenIntPoint(50, 50), + ScreenIntPoint(2, 0), mcc->Time()); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1); + PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 50), + ScreenPoint(15, 0), mcc->Time()); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1); + PanGesture(PanGestureInput::PANGESTURE_END, manager, ScreenIntPoint(50, 50), + ScreenPoint(0, 0), mcc->Time()); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimationsUntilEnd(); + + // We broke the y-axis lock and are now locked onto the x-axis. + apzc->AssertAxisLocked(ScrollDirection::eHorizontal); + + // There should be some movement in the child on the x-axis. + ParentLayerPoint childFinalOffset = apzc->GetCurrentAsyncScrollOffset( + AsyncTransformConsumer::eForEventHandling); + EXPECT_GT(childFinalOffset.x, 0); + + // There should still be no movement in the parent. + ParentLayerPoint parentFinalOffset = rootApzc->GetCurrentAsyncScrollOffset( + AsyncTransformConsumer::eForEventHandling); + EXPECT_EQ(parentFinalOffset.y, 0); + EXPECT_EQ(parentFinalOffset.x, 0); +} + +// The delta from the initial pan gesture should be reflected in the +// current offset for all axis locking modes. +TEST_P(APZCAxisLockCompatTester, TestPanGestureStart) { + const char* treeShape = "x"; + LayerIntRect layerVisibleRect[] = { + LayerIntRect(0, 0, 100, 100), + }; + CreateScrollData(treeShape, layerVisibleRect); + SetScrollableFrameMetrics(root, ScrollableLayerGuid::START_SCROLL_ID, + CSSRect(0, 0, 500, 500)); + + registration = MakeUnique<ScopedLayerTreeRegistration>(LayersId{0}, mcc); + + UpdateHitTestingTree(); + + apzc = ApzcOf(root); + + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID); + PanGesture(PanGestureInput::PANGESTURE_START, manager, ScreenIntPoint(50, 50), + ScreenIntPoint(0, 10), mcc->Time()); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimationsUntilEnd(); + ParentLayerPoint currentOffset = apzc->GetCurrentAsyncScrollOffset( + AsyncTransformConsumer::eForEventHandling); + + EXPECT_EQ(currentOffset.x, 0); + EXPECT_EQ(currentOffset.y, 10); +} + +// All APZCAxisLockCompatTester tests should be run for each apz.axis_lock.mode. +// If another mode is added, the value should be added to this list. +INSTANTIATE_TEST_SUITE_P(APZCAxisLockCompat, APZCAxisLockCompatTester, + testing::Values(0, 1, 2, 3), + APZCAxisLockCompatTester::PrintFromParam); diff --git a/gfx/layers/apz/test/gtest/TestBasic.cpp b/gfx/layers/apz/test/gtest/TestBasic.cpp new file mode 100644 index 0000000000..8ac879ae5d --- /dev/null +++ b/gfx/layers/apz/test/gtest/TestBasic.cpp @@ -0,0 +1,791 @@ +/* -*- 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 "APZCBasicTester.h" +#include "APZTestCommon.h" + +#include "InputUtils.h" + +static ScrollGenerationCounter sGenerationCounter; + +TEST_F(APZCBasicTester, Overzoom) { + // the visible area of the document in CSS pixels is x=10 y=0 w=100 h=100 + FrameMetrics fm; + fm.SetCompositionBounds(ParentLayerRect(0, 0, 100, 100)); + fm.SetScrollableRect(CSSRect(0, 0, 125, 150)); + fm.SetVisualScrollOffset(CSSPoint(10, 0)); + fm.SetZoom(CSSToParentLayerScale(1.0)); + fm.SetIsRootContent(true); + apzc->SetFrameMetrics(fm); + + MakeApzcZoomable(); + + EXPECT_CALL(*mcc, RequestContentRepaint(_)).Times(1); + + PinchWithPinchInputAndCheckStatus(apzc, ScreenIntPoint(50, 50), 0.5, true); + + fm = apzc->GetFrameMetrics(); + EXPECT_EQ(0.8f, fm.GetZoom().scale); + // bug 936721 - PGO builds introduce rounding error so + // use a fuzzy match instead + EXPECT_LT(std::abs(fm.GetVisualScrollOffset().x), 1e-5); + EXPECT_LT(std::abs(fm.GetVisualScrollOffset().y), 1e-5); +} + +TEST_F(APZCBasicTester, ZoomLimits) { + SCOPED_GFX_PREF_FLOAT("apz.min_zoom", 0.9f); + SCOPED_GFX_PREF_FLOAT("apz.max_zoom", 2.0f); + + // the visible area of the document in CSS pixels is x=10 y=0 w=100 h=100 + FrameMetrics fm; + fm.SetCompositionBounds(ParentLayerRect(0, 0, 100, 100)); + fm.SetScrollableRect(CSSRect(0, 0, 125, 150)); + fm.SetZoom(CSSToParentLayerScale(1.0)); + fm.SetIsRootContent(true); + apzc->SetFrameMetrics(fm); + + MakeApzcZoomable(); + + // This should take the zoom scale to 0.8, but we've capped it at 0.9. + PinchWithPinchInputAndCheckStatus(apzc, ScreenIntPoint(50, 50), 0.5, true); + + fm = apzc->GetFrameMetrics(); + EXPECT_EQ(0.9f, fm.GetZoom().scale); + + // This should take the zoom scale to 2.7, but we've capped it at 2. + PinchWithPinchInputAndCheckStatus(apzc, ScreenIntPoint(50, 50), 3, true); + + fm = apzc->GetFrameMetrics(); + EXPECT_EQ(2.0f, fm.GetZoom().scale); +} + +TEST_F(APZCBasicTester, SimpleTransform) { + ParentLayerPoint pointOut; + AsyncTransform viewTransformOut; + apzc->SampleContentTransformForFrame(&viewTransformOut, pointOut); + + EXPECT_EQ(ParentLayerPoint(), pointOut); + EXPECT_EQ(AsyncTransform(), viewTransformOut); +} + +TEST_F(APZCBasicTester, ComplexTransform) { + // This test assumes there is a page that gets rendered to + // two layers. In CSS pixels, the first layer is 50x50 and + // the second layer is 25x50. The widget scale factor is 3.0 + // and the presShell resolution is 2.0. Therefore, these layers + // end up being 300x300 and 150x300 in layer pixels. + // + // The second (child) layer has an additional CSS transform that + // stretches it by 2.0 on the x-axis. Therefore, after applying + // CSS transforms, the two layers are the same size in screen + // pixels. + // + // The screen itself is 24x24 in screen pixels (therefore 4x4 in + // CSS pixels). The displayport is 1 extra CSS pixel on all + // sides. + + RefPtr<TestAsyncPanZoomController> childApzc = + new TestAsyncPanZoomController(LayersId{0}, mcc, tm); + + const char* treeShape = "x(x)"; + // LayerID 0 1 + LayerIntRect layerVisibleRect[] = { + LayerIntRect(0, 0, 300, 300), + LayerIntRect(0, 0, 150, 300), + }; + Matrix4x4 transforms[] = { + Matrix4x4(), + Matrix4x4(), + }; + transforms[0].PostScale( + 0.5f, 0.5f, + 1.0f); // this results from the 2.0 resolution on the root layer + transforms[1].PostScale( + 2.0f, 1.0f, + 1.0f); // this is the 2.0 x-axis CSS transform on the child layer + + auto layers = TestWRScrollData::Create(treeShape, *updater, layerVisibleRect, + transforms); + + ScrollMetadata metadata; + FrameMetrics& metrics = metadata.GetMetrics(); + metrics.SetCompositionBounds(ParentLayerRect(0, 0, 24, 24)); + metrics.SetDisplayPort(CSSRect(-1, -1, 6, 6)); + metrics.SetVisualScrollOffset(CSSPoint(10, 10)); + metrics.SetLayoutViewport(CSSRect(10, 10, 8, 8)); + metrics.SetScrollableRect(CSSRect(0, 0, 50, 50)); + metrics.SetCumulativeResolution(LayoutDeviceToLayerScale(2)); + metrics.SetPresShellResolution(2.0f); + metrics.SetZoom(CSSToParentLayerScale(6)); + metrics.SetDevPixelsPerCSSPixel(CSSToLayoutDeviceScale(3)); + metrics.SetScrollId(ScrollableLayerGuid::START_SCROLL_ID); + + ScrollMetadata childMetadata = metadata; + FrameMetrics& childMetrics = childMetadata.GetMetrics(); + childMetrics.SetScrollId(ScrollableLayerGuid::START_SCROLL_ID + 1); + + layers[0]->AppendScrollMetadata(layers, metadata); + layers[1]->AppendScrollMetadata(layers, childMetadata); + + ParentLayerPoint pointOut; + AsyncTransform viewTransformOut; + + // Both the parent and child layer should behave exactly the same here, + // because the CSS transform on the child layer does not affect the + // SampleContentTransformForFrame code + + // initial transform + apzc->SetFrameMetrics(metrics); + apzc->NotifyLayersUpdated(metadata, true, true); + apzc->SampleContentTransformForFrame(&viewTransformOut, pointOut); + EXPECT_EQ(AsyncTransform(LayerToParentLayerScale(1), ParentLayerPoint()), + viewTransformOut); + EXPECT_EQ(ParentLayerPoint(60, 60), pointOut); + + childApzc->SetFrameMetrics(childMetrics); + childApzc->NotifyLayersUpdated(childMetadata, true, true); + childApzc->SampleContentTransformForFrame(&viewTransformOut, pointOut); + EXPECT_EQ(AsyncTransform(LayerToParentLayerScale(1), ParentLayerPoint()), + viewTransformOut); + EXPECT_EQ(ParentLayerPoint(60, 60), pointOut); + + // do an async scroll by 5 pixels and check the transform + metrics.ScrollBy(CSSPoint(5, 0)); + apzc->SetFrameMetrics(metrics); + apzc->SampleContentTransformForFrame(&viewTransformOut, pointOut); + EXPECT_EQ( + AsyncTransform(LayerToParentLayerScale(1), ParentLayerPoint(-30, 0)), + viewTransformOut); + EXPECT_EQ(ParentLayerPoint(90, 60), pointOut); + + childMetrics.ScrollBy(CSSPoint(5, 0)); + childApzc->SetFrameMetrics(childMetrics); + childApzc->SampleContentTransformForFrame(&viewTransformOut, pointOut); + EXPECT_EQ( + AsyncTransform(LayerToParentLayerScale(1), ParentLayerPoint(-30, 0)), + viewTransformOut); + EXPECT_EQ(ParentLayerPoint(90, 60), pointOut); + + // do an async zoom of 1.5x and check the transform + metrics.ZoomBy(1.5f); + apzc->SetFrameMetrics(metrics); + apzc->SampleContentTransformForFrame(&viewTransformOut, pointOut); + EXPECT_EQ( + AsyncTransform(LayerToParentLayerScale(1.5), ParentLayerPoint(-45, 0)), + viewTransformOut); + EXPECT_EQ(ParentLayerPoint(135, 90), pointOut); + + childMetrics.ZoomBy(1.5f); + childApzc->SetFrameMetrics(childMetrics); + childApzc->SampleContentTransformForFrame(&viewTransformOut, pointOut); + EXPECT_EQ( + AsyncTransform(LayerToParentLayerScale(1.5), ParentLayerPoint(-45, 0)), + viewTransformOut); + EXPECT_EQ(ParentLayerPoint(135, 90), pointOut); + + childApzc->Destroy(); +} + +TEST_F(APZCBasicTester, Fling) { + SCOPED_GFX_PREF_FLOAT("apz.fling_min_velocity_threshold", 0.0f); + int touchStart = 50; + int touchEnd = 10; + ParentLayerPoint pointOut; + AsyncTransform viewTransformOut; + + // Fling down. Each step scroll further down + Pan(apzc, touchStart, touchEnd); + ParentLayerPoint lastPoint; + for (int i = 1; i < 50; i += 1) { + apzc->SampleContentTransformForFrame(&viewTransformOut, pointOut, + TimeDuration::FromMilliseconds(1)); + EXPECT_GT(pointOut.y, lastPoint.y); + lastPoint = pointOut; + } +} + +#ifndef MOZ_WIDGET_ANDROID // Maybe fails on Android +TEST_F(APZCBasicTester, ResumeInterruptedTouchDrag_Bug1592435) { + // Start a touch-drag and scroll some amount, not lifting the finger. + SCOPED_GFX_PREF_FLOAT("apz.touch_start_tolerance", 1.0f / 1000.0f); + ScreenIntPoint touchPos(10, 50); + uint64_t touchBlock = TouchDown(apzc, touchPos, mcc->Time()).mInputBlockId; + SetDefaultAllowedTouchBehavior(apzc, touchBlock); + for (int i = 0; i < 20; ++i) { + touchPos.y -= 1; + mcc->AdvanceByMillis(1); + TouchMove(apzc, touchPos, mcc->Time()); + } + + // Take note of the scroll offset before the interruption. + CSSPoint scrollOffsetBeforeInterruption = + apzc->GetFrameMetrics().GetVisualScrollOffset(); + + // Have the main thread interrupt the touch-drag by sending + // a main thread scroll update to a nearby location. + CSSPoint mainThreadOffset = scrollOffsetBeforeInterruption; + mainThreadOffset.y -= 5; + ScrollMetadata metadata = apzc->GetScrollMetadata(); + metadata.GetMetrics().SetLayoutScrollOffset(mainThreadOffset); + nsTArray<ScrollPositionUpdate> scrollUpdates; + scrollUpdates.AppendElement(ScrollPositionUpdate::NewScroll( + ScrollOrigin::Other, CSSPoint::ToAppUnits(mainThreadOffset))); + metadata.SetScrollUpdates(scrollUpdates); + metadata.GetMetrics().SetScrollGeneration( + scrollUpdates.LastElement().GetGeneration()); + apzc->NotifyLayersUpdated(metadata, false, true); + + // Continue and finish the touch-drag gesture. + for (int i = 0; i < 20; ++i) { + touchPos.y -= 1; + mcc->AdvanceByMillis(1); + TouchMove(apzc, touchPos, mcc->Time()); + } + + // Check that the portion of the touch-drag that occurred after + // the interruption caused additional scrolling. + CSSPoint finalScrollOffset = apzc->GetFrameMetrics().GetVisualScrollOffset(); + EXPECT_GT(finalScrollOffset.y, scrollOffsetBeforeInterruption.y); + + // Now do the same thing, but for a visual scroll update. + scrollOffsetBeforeInterruption = + apzc->GetFrameMetrics().GetVisualScrollOffset(); + mainThreadOffset = scrollOffsetBeforeInterruption; + mainThreadOffset.y -= 5; + metadata = apzc->GetScrollMetadata(); + metadata.GetMetrics().SetVisualDestination(mainThreadOffset); + metadata.GetMetrics().SetScrollGeneration( + sGenerationCounter.NewMainThreadGeneration()); + metadata.GetMetrics().SetVisualScrollUpdateType(FrameMetrics::eMainThread); + scrollUpdates.Clear(); + metadata.SetScrollUpdates(scrollUpdates); + apzc->NotifyLayersUpdated(metadata, false, true); + for (int i = 0; i < 20; ++i) { + touchPos.y -= 1; + mcc->AdvanceByMillis(1); + TouchMove(apzc, touchPos, mcc->Time()); + } + finalScrollOffset = apzc->GetFrameMetrics().GetVisualScrollOffset(); + EXPECT_GT(finalScrollOffset.y, scrollOffsetBeforeInterruption.y); + + // Clean up by ending the touch gesture. + mcc->AdvanceByMillis(1); + TouchUp(apzc, touchPos, mcc->Time()); +} +#endif + +TEST_F(APZCBasicTester, RelativeScrollOffset) { + // Set up initial conditions: zoomed in, layout offset at (100, 100), + // visual offset at (120, 120); the relative offset is therefore (20, 20). + ScrollMetadata metadata; + FrameMetrics& metrics = metadata.GetMetrics(); + metrics.SetScrollableRect(CSSRect(0, 0, 1000, 1000)); + metrics.SetLayoutViewport(CSSRect(100, 100, 100, 100)); + metrics.SetZoom(CSSToParentLayerScale(2.0)); + metrics.SetCompositionBounds(ParentLayerRect(0, 0, 100, 100)); + metrics.SetVisualScrollOffset(CSSPoint(120, 120)); + metrics.SetIsRootContent(true); + apzc->SetFrameMetrics(metrics); + + // Scroll the layout viewport to (200, 200). + ScrollMetadata mainThreadMetadata = metadata; + FrameMetrics& mainThreadMetrics = mainThreadMetadata.GetMetrics(); + mainThreadMetrics.SetLayoutScrollOffset(CSSPoint(200, 200)); + nsTArray<ScrollPositionUpdate> scrollUpdates; + scrollUpdates.AppendElement(ScrollPositionUpdate::NewScroll( + ScrollOrigin::Other, CSSPoint::ToAppUnits(CSSPoint(200, 200)))); + mainThreadMetadata.SetScrollUpdates(scrollUpdates); + mainThreadMetrics.SetScrollGeneration( + scrollUpdates.LastElement().GetGeneration()); + apzc->NotifyLayersUpdated(mainThreadMetadata, /*isFirstPaint=*/false, + /*thisLayerTreeUpdated=*/true); + + // Check that the relative offset has been preserved. + metrics = apzc->GetFrameMetrics(); + EXPECT_EQ(metrics.GetLayoutScrollOffset(), CSSPoint(200, 200)); + EXPECT_EQ(metrics.GetVisualScrollOffset(), CSSPoint(220, 220)); +} + +TEST_F(APZCBasicTester, MultipleSmoothScrollsSmooth) { + SCOPED_GFX_PREF_BOOL("general.smoothScroll", true); + // We want to test that if we send multiple smooth scroll requests that we + // still smoothly animate, ie that we get non-zero change every frame while + // the animation is running. + + ScrollMetadata metadata; + FrameMetrics& metrics = metadata.GetMetrics(); + metrics.SetScrollableRect(CSSRect(0, 0, 100, 10000)); + metrics.SetLayoutViewport(CSSRect(0, 0, 100, 100)); + metrics.SetZoom(CSSToParentLayerScale(1.0)); + metrics.SetCompositionBounds(ParentLayerRect(0, 0, 100, 100)); + metrics.SetVisualScrollOffset(CSSPoint(0, 0)); + metrics.SetIsRootContent(true); + apzc->SetFrameMetrics(metrics); + + // Structure of this test. + // -send a pure relative smooth scroll request via NotifyLayersUpdated + // -advance animations a few times, check that scroll offset is increasing + // after the first few advances + // -send a pure relative smooth scroll request via NotifyLayersUpdated + // -advance animations a few times, check that scroll offset is increasing + // -send a pure relative smooth scroll request via NotifyLayersUpdated + // -advance animations a few times, check that scroll offset is increasing + + ScrollMetadata metadata2 = metadata; + nsTArray<ScrollPositionUpdate> scrollUpdates2; + scrollUpdates2.AppendElement(ScrollPositionUpdate::NewPureRelativeScroll( + ScrollOrigin::Other, ScrollMode::Smooth, + CSSPoint::ToAppUnits(CSSPoint(0, 200)))); + metadata2.SetScrollUpdates(scrollUpdates2); + metadata2.GetMetrics().SetScrollGeneration( + scrollUpdates2.LastElement().GetGeneration()); + apzc->NotifyLayersUpdated(metadata2, /*isFirstPaint=*/false, + /*thisLayerTreeUpdated=*/true); + + // Get the animation going + for (uint32_t i = 0; i < 3; i++) { + SampleAnimationOneFrame(); + } + + float offset = + apzc->GetCurrentAsyncScrollOffset(AsyncPanZoomController::eForCompositing) + .y; + ASSERT_GT(offset, 0); + float lastOffset = offset; + + for (uint32_t i = 0; i < 2; i++) { + for (uint32_t j = 0; j < 3; j++) { + SampleAnimationOneFrame(); + offset = apzc->GetCurrentAsyncScrollOffset( + AsyncPanZoomController::eForCompositing) + .y; + ASSERT_GT(offset, lastOffset); + lastOffset = offset; + } + + ScrollMetadata metadata3 = metadata; + nsTArray<ScrollPositionUpdate> scrollUpdates3; + scrollUpdates3.AppendElement(ScrollPositionUpdate::NewPureRelativeScroll( + ScrollOrigin::Other, ScrollMode::Smooth, + CSSPoint::ToAppUnits(CSSPoint(0, 200)))); + metadata3.SetScrollUpdates(scrollUpdates3); + metadata3.GetMetrics().SetScrollGeneration( + scrollUpdates3.LastElement().GetGeneration()); + apzc->NotifyLayersUpdated(metadata3, /*isFirstPaint=*/false, + /*thisLayerTreeUpdated=*/true); + } + + for (uint32_t j = 0; j < 7; j++) { + SampleAnimationOneFrame(); + offset = apzc->GetCurrentAsyncScrollOffset( + AsyncPanZoomController::eForCompositing) + .y; + ASSERT_GT(offset, lastOffset); + lastOffset = offset; + } +} + +class APZCSmoothScrollTester : public APZCBasicTester { + public: + // Test that a smooth scroll animation correctly handles its destination + // being updated by a relative scroll delta from the main thread (a "content + // shift"). + void TestContentShift() { + // Set up scroll frame. Starting scroll position is (0, 0). + ScrollMetadata metadata; + FrameMetrics& metrics = metadata.GetMetrics(); + metrics.SetScrollableRect(CSSRect(0, 0, 100, 10000)); + metrics.SetLayoutViewport(CSSRect(0, 0, 100, 100)); + metrics.SetZoom(CSSToParentLayerScale(1.0)); + metrics.SetCompositionBounds(ParentLayerRect(0, 0, 100, 100)); + metrics.SetVisualScrollOffset(CSSPoint(0, 0)); + metrics.SetIsRootContent(true); + apzc->SetFrameMetrics(metrics); + + // Start smooth scroll via main-thread request. + nsTArray<ScrollPositionUpdate> scrollUpdates; + scrollUpdates.AppendElement(ScrollPositionUpdate::NewPureRelativeScroll( + ScrollOrigin::Other, ScrollMode::Smooth, + CSSPoint::ToAppUnits(CSSPoint(0, 1000)))); + metadata.SetScrollUpdates(scrollUpdates); + metrics.SetScrollGeneration(scrollUpdates.LastElement().GetGeneration()); + apzc->NotifyLayersUpdated(metadata, false, true); + + // Sample the smooth scroll animation until we get past y=500. + apzc->AssertStateIsSmoothScroll(); + float y = 0; + while (y < 500) { + SampleAnimationOneFrame(); + y = apzc->GetFrameMetrics().GetVisualScrollOffset().y; + } + + // Send a relative scroll of y = -400. + scrollUpdates.Clear(); + scrollUpdates.AppendElement(ScrollPositionUpdate::NewRelativeScroll( + CSSPoint::ToAppUnits(CSSPoint(0, 500)), + CSSPoint::ToAppUnits(CSSPoint(0, 100)))); + metadata.SetScrollUpdates(scrollUpdates); + metrics.SetScrollGeneration(scrollUpdates.LastElement().GetGeneration()); + apzc->NotifyLayersUpdated(metadata, false, false); + + // Verify the relative scroll was applied but didn't cancel the animation. + float y2 = apzc->GetFrameMetrics().GetVisualScrollOffset().y; + ASSERT_EQ(y2, y - 400); + apzc->AssertStateIsSmoothScroll(); + + // Sample the animation again and check that it respected the relative + // scroll. + SampleAnimationOneFrame(); + float y3 = apzc->GetFrameMetrics().GetVisualScrollOffset().y; + ASSERT_GT(y3, y2); + ASSERT_LT(y3, 500); + + // Continue animation until done and check that it ended up at a correctly + // adjusted destination. + apzc->AdvanceAnimationsUntilEnd(); + float y4 = apzc->GetFrameMetrics().GetVisualScrollOffset().y; + ASSERT_EQ(y4, 600); // 1000 (initial destination) - 400 (relative scroll) + } + + // Test that a smooth scroll animation correctly handles a content + // shift, followed by an UpdateDelta due to a new input event. + void TestContentShiftThenUpdateDelta() { + // Set up scroll frame. Starting position is (0, 0). + ScrollMetadata metadata; + FrameMetrics& metrics = metadata.GetMetrics(); + metrics.SetScrollableRect(CSSRect(0, 0, 1000, 10000)); + metrics.SetLayoutViewport(CSSRect(0, 0, 1000, 1000)); + metrics.SetZoom(CSSToParentLayerScale(1.0)); + metrics.SetCompositionBounds(ParentLayerRect(0, 0, 1000, 1000)); + metrics.SetVisualScrollOffset(CSSPoint(0, 0)); + metrics.SetIsRootContent(true); + // Set the line scroll amount to 100 pixels. Note that SmoothWheel() takes + // a delta denominated in lines. + metadata.SetLineScrollAmount({100, 100}); + // The page scroll amount also needs to be set, otherwise the wheel handling + // code will get confused by things like the "don't scroll more than one + // page" check. + metadata.SetPageScrollAmount({1000, 1000}); + apzc->SetScrollMetadata(metadata); + + // Send a wheel event to trigger smooth scrolling by 5 lines (= 500 pixels). + SmoothWheel(apzc, ScreenIntPoint(50, 50), ScreenPoint(0, 5), mcc->Time()); + apzc->AssertStateIsWheelScroll(); + + // Sample the wheel scroll animation until we get past y=200. + float y = 0; + while (y < 200) { + SampleAnimationOneFrame(); + y = apzc->GetFrameMetrics().GetVisualScrollOffset().y; + } + + // Apply a content shift of y=100. + nsTArray<ScrollPositionUpdate> scrollUpdates; + scrollUpdates.AppendElement(ScrollPositionUpdate::NewRelativeScroll( + CSSPoint::ToAppUnits(CSSPoint(0, 200)), + CSSPoint::ToAppUnits(CSSPoint(0, 300)))); + metadata.SetScrollUpdates(scrollUpdates); + metrics.SetScrollGeneration(scrollUpdates.LastElement().GetGeneration()); + apzc->NotifyLayersUpdated(metadata, false, true); + + // Check that the content shift was applied but didn't cancel the animation. + // At this point, the animation's internal state should be targeting a + // destination of y=600. + float y2 = apzc->GetFrameMetrics().GetVisualScrollOffset().y; + ASSERT_EQ(y2, y + 100); + apzc->AssertStateIsWheelScroll(); + + // Sample the animation until we get past y=400. + while (y < 400) { + SampleAnimationOneFrame(); + y = apzc->GetFrameMetrics().GetVisualScrollOffset().y; + } + + // Send another wheel event to trigger smooth scrolling by another 5 lines + // (=500 pixels). This should update the animation to target a destination + // of y=1100. + SmoothWheel(apzc, ScreenIntPoint(50, 50), ScreenPoint(0, 5), mcc->Time()); + + // Continue the animation until done and check that it ended up at y=1100. + apzc->AdvanceAnimationsUntilEnd(); + float yEnd = apzc->GetFrameMetrics().GetVisualScrollOffset().y; + ASSERT_EQ(yEnd, 1100); + } + + // Test that a content shift does not cause a smooth scroll animation to + // overshoot its (updated) destination. + void TestContentShiftDoesNotCauseOvershoot() { + // Follow the same steps as in TestContentShiftThenUpdateDelta(), + // except use a content shift of y=1000. + ScrollMetadata metadata; + FrameMetrics& metrics = metadata.GetMetrics(); + metrics.SetScrollableRect(CSSRect(0, 0, 1000, 10000)); + metrics.SetLayoutViewport(CSSRect(0, 0, 1000, 1000)); + metrics.SetZoom(CSSToParentLayerScale(1.0)); + metrics.SetCompositionBounds(ParentLayerRect(0, 0, 1000, 1000)); + metrics.SetVisualScrollOffset(CSSPoint(0, 0)); + metrics.SetIsRootContent(true); + metadata.SetLineScrollAmount({100, 100}); + metadata.SetPageScrollAmount({1000, 1000}); + apzc->SetScrollMetadata(metadata); + + // First wheel event, smooth scroll destination is y=500. + SmoothWheel(apzc, ScreenIntPoint(50, 50), ScreenPoint(0, 5), mcc->Time()); + apzc->AssertStateIsWheelScroll(); + + // Sample until we get past y=200. + float y = 0; + while (y < 200) { + SampleAnimationOneFrame(); + y = apzc->GetFrameMetrics().GetVisualScrollOffset().y; + } + + // Apply a content shift of y=1000. The current scroll position is now + // y>1200, and the updated destination is y=1500. + nsTArray<ScrollPositionUpdate> scrollUpdates; + scrollUpdates.AppendElement(ScrollPositionUpdate::NewRelativeScroll( + CSSPoint::ToAppUnits(CSSPoint(0, 200)), + CSSPoint::ToAppUnits(CSSPoint(0, 1200)))); + metadata.SetScrollUpdates(scrollUpdates); + metrics.SetScrollGeneration(scrollUpdates.LastElement().GetGeneration()); + apzc->NotifyLayersUpdated(metadata, false, true); + float y2 = apzc->GetFrameMetrics().GetVisualScrollOffset().y; + ASSERT_EQ(y2, y + 1000); + apzc->AssertStateIsWheelScroll(); + + // Sample until we get past y=1300. + while (y < 1300) { + SampleAnimationOneFrame(); + y = apzc->GetFrameMetrics().GetVisualScrollOffset().y; + } + + // Second wheel event, destination is now y=2000. + // MSD physics has a bug where the UpdateDelta() causes the content shift + // to be applied in duplicate on the next sample, causing the scroll + // position to be y>2000! + SmoothWheel(apzc, ScreenIntPoint(50, 50), ScreenPoint(0, 5), mcc->Time()); + + // Check that the scroll position remains <= 2000 until the end of the + // animation. + while (apzc->IsWheelScrollAnimationRunning()) { + SampleAnimationOneFrame(); + ASSERT_LE(apzc->GetFrameMetrics().GetVisualScrollOffset().y, 2000); + } + ASSERT_EQ(2000, apzc->GetFrameMetrics().GetVisualScrollOffset().y); + } +}; + +TEST_F(APZCSmoothScrollTester, ContentShiftBezier) { + SCOPED_GFX_PREF_BOOL("general.smoothScroll", true); + SCOPED_GFX_PREF_BOOL("general.smoothScroll.msdPhysics.enabled", false); + TestContentShift(); +} + +TEST_F(APZCSmoothScrollTester, ContentShiftMsd) { + SCOPED_GFX_PREF_BOOL("general.smoothScroll", true); + SCOPED_GFX_PREF_BOOL("general.smoothScroll.msdPhysics.enabled", true); + TestContentShift(); +} + +TEST_F(APZCSmoothScrollTester, ContentShiftThenUpdateDeltaBezier) { + SCOPED_GFX_PREF_BOOL("general.smoothScroll", true); + SCOPED_GFX_PREF_BOOL("general.smoothScroll.msdPhysics.enabled", false); + TestContentShiftThenUpdateDelta(); +} + +TEST_F(APZCSmoothScrollTester, ContentShiftThenUpdateDeltaMsd) { + SCOPED_GFX_PREF_BOOL("general.smoothScroll", true); + SCOPED_GFX_PREF_BOOL("general.smoothScroll.msdPhysics.enabled", true); + TestContentShiftThenUpdateDelta(); +} + +TEST_F(APZCSmoothScrollTester, ContentShiftDoesNotCauseOvershootBezier) { + SCOPED_GFX_PREF_BOOL("general.smoothScroll", true); + SCOPED_GFX_PREF_BOOL("general.smoothScroll.msdPhysics.enabled", false); + TestContentShiftDoesNotCauseOvershoot(); +} + +TEST_F(APZCSmoothScrollTester, ContentShiftDoesNotCauseOvershootMsd) { + SCOPED_GFX_PREF_BOOL("general.smoothScroll", true); + SCOPED_GFX_PREF_BOOL("general.smoothScroll.msdPhysics.enabled", true); + TestContentShiftDoesNotCauseOvershoot(); +} + +TEST_F(APZCBasicTester, ZoomAndScrollableRectChangeAfterZoomChange) { + // We want to check that a small scrollable rect change (which causes us to + // reclamp our scroll position, including in the sampled state) does not move + // the scroll offset in the sample state based the zoom in the apzc, only + // based on the zoom in the sampled state. + + // First we zoom in to the right hand side. Then start zooming out, then send + // a scrollable rect change and check that it doesn't change the sampled state + // scroll offset. + + ScrollMetadata metadata; + FrameMetrics& metrics = metadata.GetMetrics(); + metrics.SetCompositionBounds(ParentLayerRect(0, 0, 100, 100)); + metrics.SetScrollableRect(CSSRect(0, 0, 100, 1000)); + metrics.SetLayoutViewport(CSSRect(0, 0, 100, 100)); + metrics.SetVisualScrollOffset(CSSPoint(0, 0)); + metrics.SetZoom(CSSToParentLayerScale(1.0)); + metrics.SetIsRootContent(true); + apzc->SetFrameMetrics(metrics); + + MakeApzcZoomable(); + + // Zoom to right side. + ZoomTarget zoomTarget{CSSRect(75, 25, 25, 25)}; + apzc->ZoomToRect(zoomTarget, 0); + + // Run the animation to completion, should take 250ms/16.67ms = 15 frames, but + // do extra to make sure. + for (uint32_t i = 0; i < 30; i++) { + SampleAnimationOneFrame(); + } + + EXPECT_FALSE(apzc->IsAsyncZooming()); + + // Zoom out. + ZoomTarget zoomTarget2{CSSRect(0, 0, 100, 100)}; + apzc->ZoomToRect(zoomTarget2, 0); + + // Run the animation a few times to get it going. + for (uint32_t i = 0; i < 2; i++) { + SampleAnimationOneFrame(); + } + + // Check that it is decreasing in scale. + float prevScale = + apzc->GetCurrentPinchZoomScale(AsyncPanZoomController::eForCompositing) + .scale; + for (uint32_t i = 0; i < 2; i++) { + SampleAnimationOneFrame(); + float scale = + apzc->GetCurrentPinchZoomScale(AsyncPanZoomController::eForCompositing) + .scale; + ASSERT_GT(prevScale, scale); + prevScale = scale; + } + + float offset = + apzc->GetCurrentAsyncScrollOffset(AsyncPanZoomController::eForCompositing) + .x; + + // Change the scrollable rect slightly to trigger a reclamp. + ScrollMetadata metadata2 = metadata; + metadata2.GetMetrics().SetScrollableRect(CSSRect(0, 0, 100, 1000.2)); + apzc->NotifyLayersUpdated(metadata2, /*isFirstPaint=*/false, + /*thisLayerTreeUpdated=*/true); + + float newOffset = + apzc->GetCurrentAsyncScrollOffset(AsyncPanZoomController::eForCompositing) + .x; + + ASSERT_EQ(newOffset, offset); +} + +TEST_F(APZCBasicTester, ZoomToRectAndCompositionBoundsChange) { + // We want to check that content sending a composition bounds change (due to + // addition of scrollbars) during a zoom animation does not cause us to take + // the out of date content resolution. + + ScrollMetadata metadata; + FrameMetrics& metrics = metadata.GetMetrics(); + metrics.SetCompositionBounds(ParentLayerRect(0, 0, 100, 100)); + metrics.SetCompositionBoundsWidthIgnoringScrollbars(ParentLayerCoord{100}); + metrics.SetScrollableRect(CSSRect(0, 0, 100, 1000)); + metrics.SetLayoutViewport(CSSRect(0, 0, 100, 100)); + metrics.SetVisualScrollOffset(CSSPoint(0, 0)); + metrics.SetZoom(CSSToParentLayerScale(1.0)); + metrics.SetIsRootContent(true); + apzc->SetFrameMetrics(metrics); + + MakeApzcZoomable(); + + // Start a zoom to a rect. + ZoomTarget zoomTarget{CSSRect(25, 25, 25, 25)}; + apzc->ZoomToRect(zoomTarget, 0); + + // Run the animation a few times to get it going. + // Check that it is increasing in scale. + float prevScale = + apzc->GetCurrentPinchZoomScale(AsyncPanZoomController::eForCompositing) + .scale; + for (uint32_t i = 0; i < 3; i++) { + SampleAnimationOneFrame(); + float scale = + apzc->GetCurrentPinchZoomScale(AsyncPanZoomController::eForCompositing) + .scale; + ASSERT_GE(scale, prevScale); + prevScale = scale; + } + + EXPECT_TRUE(apzc->IsAsyncZooming()); + + // Simulate the appearance of a scrollbar by reducing the width of + // the composition bounds, while keeping + // mCompositionBoundsWidthIgnoringScrollbars unchanged. + ScrollMetadata metadata2 = metadata; + metadata2.GetMetrics().SetCompositionBounds(ParentLayerRect(0, 0, 90, 100)); + apzc->NotifyLayersUpdated(metadata2, /*isFirstPaint=*/false, + /*thisLayerTreeUpdated=*/true); + + float scale = + apzc->GetCurrentPinchZoomScale(AsyncPanZoomController::eForCompositing) + .scale; + + ASSERT_EQ(scale, prevScale); + + // Run the rest of the animation to completion, should take 250ms/16.67ms = 15 + // frames total, but do extra to make sure. + for (uint32_t i = 0; i < 30; i++) { + SampleAnimationOneFrame(); + scale = + apzc->GetCurrentPinchZoomScale(AsyncPanZoomController::eForCompositing) + .scale; + ASSERT_GE(scale, prevScale); + prevScale = scale; + } + + EXPECT_FALSE(apzc->IsAsyncZooming()); +} + +TEST_F(APZCBasicTester, StartTolerance) { + SCOPED_GFX_PREF_FLOAT("apz.touch_start_tolerance", 10 / tm->GetDPI()); + + FrameMetrics fm; + fm.SetCompositionBounds(ParentLayerRect(0, 0, 100, 100)); + fm.SetScrollableRect(CSSRect(0, 0, 100, 300)); + fm.SetVisualScrollOffset(CSSPoint(0, 50)); + fm.SetIsRootContent(true); + apzc->SetFrameMetrics(fm); + + uint64_t touchBlock = TouchDown(apzc, {50, 50}, mcc->Time()).mInputBlockId; + SetDefaultAllowedTouchBehavior(apzc, touchBlock); + + CSSPoint initialScrollOffset = + apzc->GetFrameMetrics().GetVisualScrollOffset(); + + mcc->AdvanceByMillis(1); + TouchMove(apzc, {50, 70}, mcc->Time()); + + // Expect 10 pixels of scrolling: the distance from (50,50) to (50,70) + // minus the 10-pixel touch start tolerance. + ASSERT_EQ(initialScrollOffset.y - 10, + apzc->GetFrameMetrics().GetVisualScrollOffset().y); + + mcc->AdvanceByMillis(1); + TouchMove(apzc, {50, 90}, mcc->Time()); + + // Expect 30 pixels of scrolling: the distance from (50,50) to (50,90) + // minus the 10-pixel touch start tolerance. + ASSERT_EQ(initialScrollOffset.y - 30, + apzc->GetFrameMetrics().GetVisualScrollOffset().y); + + // Clean up by ending the touch gesture. + mcc->AdvanceByMillis(1); + TouchUp(apzc, {50, 90}, mcc->Time()); +} diff --git a/gfx/layers/apz/test/gtest/TestEventRegions.cpp b/gfx/layers/apz/test/gtest/TestEventRegions.cpp new file mode 100644 index 0000000000..f0f262eb82 --- /dev/null +++ b/gfx/layers/apz/test/gtest/TestEventRegions.cpp @@ -0,0 +1,201 @@ +/* -*- 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" +#include "mozilla/layers/LayersTypes.h" + +class APZEventRegionsTester : public APZCTreeManagerTester { + protected: + UniquePtr<ScopedLayerTreeRegistration> registration; + TestAsyncPanZoomController* rootApzc; + + void CreateEventRegionsLayerTree1() { + const char* treeShape = "x(xx)"; + LayerIntRect layerVisibleRects[] = { + LayerIntRect(0, 0, 200, 200), // root + LayerIntRect(0, 0, 100, 200), // left half + LayerIntRect(0, 100, 200, 100), // bottom half + }; + CreateScrollData(treeShape, layerVisibleRects); + SetScrollableFrameMetrics(root, ScrollableLayerGuid::START_SCROLL_ID); + SetScrollableFrameMetrics(layers[1], + ScrollableLayerGuid::START_SCROLL_ID + 1); + SetScrollableFrameMetrics(layers[2], + ScrollableLayerGuid::START_SCROLL_ID + 2); + SetScrollHandoff(layers[1], root); + SetScrollHandoff(layers[2], root); + + registration = MakeUnique<ScopedLayerTreeRegistration>(LayersId{0}, mcc); + UpdateHitTestingTree(); + rootApzc = ApzcOf(root); + } + + void CreateEventRegionsLayerTree2() { + const char* treeShape = "x(x)"; + LayerIntRect layerVisibleRects[] = { + LayerIntRect(0, 0, 100, 500), + LayerIntRect(0, 150, 100, 100), + }; + CreateScrollData(treeShape, layerVisibleRects); + SetScrollableFrameMetrics(root, ScrollableLayerGuid::START_SCROLL_ID); + + registration = MakeUnique<ScopedLayerTreeRegistration>(LayersId{0}, mcc); + UpdateHitTestingTree(); + rootApzc = ApzcOf(root); + } + + void CreateBug1117712LayerTree() { + const char* treeShape = "x(x(x)x)"; + // LayerID 0 1 2 3 + // 0 is the root + // 1 is a container layer whose sole purpose to make a non-empty ancestor + // transform for 2, so that 2's screen-to-apzc and apzc-to-gecko + // transforms are different from 3's. + // 2 is a small layer that is the actual target + // 3 is a big layer obscuring 2 with a dispatch-to-content region + LayerIntRect layerVisibleRects[] = { + LayerIntRect(0, 0, 100, 100), + LayerIntRect(0, 0, 0, 0), + LayerIntRect(0, 0, 10, 10), + LayerIntRect(0, 0, 100, 100), + }; + Matrix4x4 layerTransforms[] = { + Matrix4x4(), + Matrix4x4::Translation(50, 0, 0), + Matrix4x4(), + Matrix4x4(), + }; + CreateScrollData(treeShape, layerVisibleRects, layerTransforms); + + SetScrollableFrameMetrics(layers[2], ScrollableLayerGuid::START_SCROLL_ID, + CSSRect(0, 0, 10, 10)); + SetScrollableFrameMetrics(layers[3], + ScrollableLayerGuid::START_SCROLL_ID + 1, + CSSRect(0, 0, 100, 100)); + SetScrollHandoff(layers[3], layers[2]); + + registration = MakeUnique<ScopedLayerTreeRegistration>(LayersId{0}, mcc); + UpdateHitTestingTree(); + } +}; + +class APZEventRegionsTesterMock : public APZEventRegionsTester { + public: + APZEventRegionsTesterMock() { CreateMockHitTester(); } +}; + +TEST_F(APZEventRegionsTesterMock, HitRegionImmediateResponse) { + CreateEventRegionsLayerTree1(); + + TestAsyncPanZoomController* root = ApzcOf(layers[0]); + TestAsyncPanZoomController* left = ApzcOf(layers[1]); + TestAsyncPanZoomController* bottom = ApzcOf(layers[2]); + + MockFunction<void(std::string checkPointName)> check; + { + InSequence s; + EXPECT_CALL(*mcc, + HandleTap(TapType::eSingleTap, _, _, left->GetGuid(), _, _)) + .Times(1); + EXPECT_CALL(check, Call("Tapped on left")); + EXPECT_CALL(*mcc, + HandleTap(TapType::eSingleTap, _, _, bottom->GetGuid(), _, _)) + .Times(1); + EXPECT_CALL(check, Call("Tapped on bottom")); + EXPECT_CALL(*mcc, + HandleTap(TapType::eSingleTap, _, _, root->GetGuid(), _, _)) + .Times(1); + EXPECT_CALL(check, Call("Tapped on root")); + EXPECT_CALL(check, Call("Tap pending on d-t-c region")); + EXPECT_CALL(*mcc, + HandleTap(TapType::eSingleTap, _, _, bottom->GetGuid(), _, _)) + .Times(1); + EXPECT_CALL(check, Call("Tapped on bottom again")); + EXPECT_CALL(*mcc, + HandleTap(TapType::eSingleTap, _, _, left->GetGuid(), _, _)) + .Times(1); + EXPECT_CALL(check, Call("Tapped on left this time")); + } + + TimeDuration tapDuration = TimeDuration::FromMilliseconds(100); + + // Tap in the exposed hit regions of each of the layers once and ensure + // the clicks are dispatched right away + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1); + Tap(manager, ScreenIntPoint(10, 10), tapDuration); + mcc->RunThroughDelayedTasks(); // this runs the tap event + check.Call("Tapped on left"); + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 2); + Tap(manager, ScreenIntPoint(110, 110), tapDuration); + mcc->RunThroughDelayedTasks(); // this runs the tap event + check.Call("Tapped on bottom"); + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID); + Tap(manager, ScreenIntPoint(110, 10), tapDuration); + mcc->RunThroughDelayedTasks(); // this runs the tap event + check.Call("Tapped on root"); + + // Now tap on the dispatch-to-content region where the layers overlap + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 2, + {CompositorHitTestFlags::eVisibleToHitTest, + CompositorHitTestFlags::eIrregularArea}); + Tap(manager, ScreenIntPoint(10, 110), tapDuration); + mcc->RunThroughDelayedTasks(); // this runs the main-thread timeout + check.Call("Tap pending on d-t-c region"); + mcc->RunThroughDelayedTasks(); // this runs the tap event + check.Call("Tapped on bottom again"); + + // Now let's do that again, but simulate a main-thread response + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 2, + {CompositorHitTestFlags::eVisibleToHitTest, + CompositorHitTestFlags::eIrregularArea}); + APZEventResult result = + Tap(manager, ScreenIntPoint(10, 110), tapDuration, nullptr); + nsTArray<ScrollableLayerGuid> targets; + targets.AppendElement(left->GetGuid()); + manager->SetTargetAPZC(result.mInputBlockId, targets); + while (mcc->RunThroughDelayedTasks()) + ; // this runs the tap event + check.Call("Tapped on left this time"); +} + +TEST_F(APZEventRegionsTesterMock, HitRegionAccumulatesChildren) { + CreateEventRegionsLayerTree2(); + + // Tap in the area of the child layer that's not directly included in the + // parent layer's hit region. Verify that it comes out of the APZC's + // content controller, which indicates the input events got routed correctly + // to the APZC. + EXPECT_CALL(*mcc, + HandleTap(TapType::eSingleTap, _, _, rootApzc->GetGuid(), _, _)) + .Times(1); + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID); + Tap(manager, ScreenIntPoint(10, 160), TimeDuration::FromMilliseconds(100)); +} + +TEST_F(APZEventRegionsTesterMock, Bug1117712) { + CreateBug1117712LayerTree(); + + TestAsyncPanZoomController* apzc2 = ApzcOf(layers[2]); + + // These touch events should hit the dispatch-to-content region of layers[3] + // and so get queued with that APZC as the tentative target. + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1, + {CompositorHitTestFlags::eVisibleToHitTest, + CompositorHitTestFlags::eIrregularArea}); + APZEventResult result = Tap(manager, ScreenIntPoint(55, 5), + TimeDuration::FromMilliseconds(100), nullptr); + // But now we tell the APZ that really it hit layers[2], and expect the tap + // to be delivered at the correct coordinates. + EXPECT_CALL(*mcc, HandleTap(TapType::eSingleTap, LayoutDevicePoint(55, 5), 0, + apzc2->GetGuid(), _, _)) + .Times(1); + + nsTArray<ScrollableLayerGuid> targets; + targets.AppendElement(apzc2->GetGuid()); + manager->SetTargetAPZC(result.mInputBlockId, targets); +} diff --git a/gfx/layers/apz/test/gtest/TestEventResult.cpp b/gfx/layers/apz/test/gtest/TestEventResult.cpp new file mode 100644 index 0000000000..7e1d77bd84 --- /dev/null +++ b/gfx/layers/apz/test/gtest/TestEventResult.cpp @@ -0,0 +1,476 @@ +/* -*- 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" +#include "mozilla/EventForwards.h" +#include "mozilla/layers/LayersTypes.h" +#include <tuple> + +class APZEventResultTester : public APZCTreeManagerTester { + protected: + UniquePtr<ScopedLayerTreeRegistration> registration; + + void UpdateOverscrollBehavior(OverscrollBehavior aX, OverscrollBehavior aY) { + ModifyFrameMetrics(root, [aX, aY](ScrollMetadata& sm, FrameMetrics& _) { + OverscrollBehaviorInfo overscroll; + overscroll.mBehaviorX = aX; + overscroll.mBehaviorY = aY; + sm.SetOverscrollBehavior(overscroll); + }); + UpdateHitTestingTree(); + } + + void SetScrollOffsetOnMainThread(const CSSPoint& aPoint) { + RefPtr<TestAsyncPanZoomController> apzc = ApzcOf(root); + + ScrollMetadata metadata = apzc->GetScrollMetadata(); + metadata.GetMetrics().SetLayoutScrollOffset(aPoint); + nsTArray<ScrollPositionUpdate> scrollUpdates; + scrollUpdates.AppendElement(ScrollPositionUpdate::NewScroll( + ScrollOrigin::Other, CSSPoint::ToAppUnits(aPoint))); + metadata.SetScrollUpdates(scrollUpdates); + metadata.GetMetrics().SetScrollGeneration( + scrollUpdates.LastElement().GetGeneration()); + apzc->NotifyLayersUpdated(metadata, /*aIsFirstPaint=*/false, + /*aThisLayerTreeUpdated=*/true); + } + + void CreateScrollableRootLayer() { + const char* treeShape = "x"; + LayerIntRect layerVisibleRects[] = { + LayerIntRect(0, 0, 100, 100), + }; + CreateScrollData(treeShape, layerVisibleRects); + SetScrollableFrameMetrics(root, ScrollableLayerGuid::START_SCROLL_ID, + CSSRect(0, 0, 200, 200)); + ModifyFrameMetrics(root, [](ScrollMetadata& sm, FrameMetrics& metrics) { + metrics.SetIsRootContent(true); + }); + registration = MakeUnique<ScopedLayerTreeRegistration>(LayersId{0}, mcc); + UpdateHitTestingTree(); + } + + enum class PreventDefaultFlag { No, Yes }; + std::tuple<APZEventResult, APZHandledResult> TapDispatchToContent( + const ScreenIntPoint& aPoint, PreventDefaultFlag aPreventDefaultFlag) { + APZEventResult result = + Tap(manager, aPoint, TimeDuration::FromMilliseconds(100)); + + APZHandledResult delayedAnswer{APZHandledPlace::Invalid, SideBits::eNone, + ScrollDirections()}; + manager->AddInputBlockCallback( + result.mInputBlockId, + {result.GetStatus(), [&](uint64_t id, const APZHandledResult& answer) { + EXPECT_EQ(id, result.mInputBlockId); + delayedAnswer = answer; + }}); + manager->SetAllowedTouchBehavior(result.mInputBlockId, + {AllowedTouchBehavior::VERTICAL_PAN}); + manager->SetTargetAPZC(result.mInputBlockId, {result.mTargetGuid}); + manager->ContentReceivedInputBlock( + result.mInputBlockId, aPreventDefaultFlag == PreventDefaultFlag::Yes); + return {result, delayedAnswer}; + } + + void OverscrollDirectionsWithEventHandlerTest( + PreventDefaultFlag aPreventDefaultFlag) { + UpdateHitTestingTree(); + + APZHandledPlace expectedPlace = + aPreventDefaultFlag == PreventDefaultFlag::No + ? APZHandledPlace::HandledByRoot + : APZHandledPlace::HandledByContent; + { + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID, + {CompositorHitTestFlags::eVisibleToHitTest, + CompositorHitTestFlags::eIrregularArea}); + auto [result, delayedHandledResult] = + TapDispatchToContent(ScreenIntPoint(50, 50), aPreventDefaultFlag); + EXPECT_EQ(result.GetHandledResult(), Nothing()); + EXPECT_EQ( + delayedHandledResult, + (APZHandledResult{expectedPlace, SideBits::eBottom | SideBits::eRight, + EitherScrollDirection})); + } + + // overscroll-behavior: contain, contain. + UpdateOverscrollBehavior(OverscrollBehavior::Contain, + OverscrollBehavior::Contain); + { + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID, + {CompositorHitTestFlags::eVisibleToHitTest, + CompositorHitTestFlags::eIrregularArea}); + auto [result, delayedHandledResult] = + TapDispatchToContent(ScreenIntPoint(50, 50), aPreventDefaultFlag); + EXPECT_EQ(result.GetHandledResult(), Nothing()); + EXPECT_EQ( + delayedHandledResult, + (APZHandledResult{expectedPlace, SideBits::eBottom | SideBits::eRight, + ScrollDirections()})); + } + + // overscroll-behavior: none, none. + UpdateOverscrollBehavior(OverscrollBehavior::None, + OverscrollBehavior::None); + { + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID, + {CompositorHitTestFlags::eVisibleToHitTest, + CompositorHitTestFlags::eIrregularArea}); + auto [result, delayedHandledResult] = + TapDispatchToContent(ScreenIntPoint(50, 50), aPreventDefaultFlag); + EXPECT_EQ(result.GetHandledResult(), Nothing()); + EXPECT_EQ( + delayedHandledResult, + (APZHandledResult{expectedPlace, SideBits::eBottom | SideBits::eRight, + ScrollDirections()})); + } + + // overscroll-behavior: auto, none. + UpdateOverscrollBehavior(OverscrollBehavior::Auto, + OverscrollBehavior::None); + { + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID, + {CompositorHitTestFlags::eVisibleToHitTest, + CompositorHitTestFlags::eIrregularArea}); + auto [result, delayedHandledResult] = + TapDispatchToContent(ScreenIntPoint(50, 50), aPreventDefaultFlag); + EXPECT_EQ(result.GetHandledResult(), Nothing()); + EXPECT_EQ( + delayedHandledResult, + (APZHandledResult{expectedPlace, SideBits::eBottom | SideBits::eRight, + HorizontalScrollDirection})); + } + + // overscroll-behavior: none, auto. + UpdateOverscrollBehavior(OverscrollBehavior::None, + OverscrollBehavior::Auto); + { + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID, + {CompositorHitTestFlags::eVisibleToHitTest, + CompositorHitTestFlags::eIrregularArea}); + auto [result, delayedHandledResult] = + TapDispatchToContent(ScreenIntPoint(50, 50), aPreventDefaultFlag); + EXPECT_EQ(result.GetHandledResult(), Nothing()); + EXPECT_EQ( + delayedHandledResult, + (APZHandledResult{expectedPlace, SideBits::eBottom | SideBits::eRight, + VerticalScrollDirection})); + } + } + + void ScrollableDirectionsWithEventHandlerTest( + PreventDefaultFlag aPreventDefaultFlag) { + UpdateHitTestingTree(); + + APZHandledPlace expectedPlace = + aPreventDefaultFlag == PreventDefaultFlag::No + ? APZHandledPlace::HandledByRoot + : APZHandledPlace::HandledByContent; + { + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID, + {CompositorHitTestFlags::eVisibleToHitTest, + CompositorHitTestFlags::eIrregularArea}); + auto [result, delayedHandledResult] = + TapDispatchToContent(ScreenIntPoint(50, 50), aPreventDefaultFlag); + EXPECT_EQ(result.GetHandledResult(), Nothing()); + EXPECT_EQ( + delayedHandledResult, + (APZHandledResult{expectedPlace, SideBits::eBottom | SideBits::eRight, + EitherScrollDirection})); + } + + // scroll down a bit. + SetScrollOffsetOnMainThread(CSSPoint(0, 10)); + { + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID, + {CompositorHitTestFlags::eVisibleToHitTest, + CompositorHitTestFlags::eIrregularArea}); + auto [result, delayedHandledResult] = + TapDispatchToContent(ScreenIntPoint(50, 50), aPreventDefaultFlag); + EXPECT_EQ(result.GetHandledResult(), Nothing()); + EXPECT_EQ(delayedHandledResult, + (APZHandledResult{ + expectedPlace, + SideBits::eTop | SideBits::eBottom | SideBits::eRight, + EitherScrollDirection})); + } + + // scroll to the bottom edge + SetScrollOffsetOnMainThread(CSSPoint(0, 100)); + { + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID, + {CompositorHitTestFlags::eVisibleToHitTest, + CompositorHitTestFlags::eIrregularArea}); + auto [result, delayedHandledResult] = + TapDispatchToContent(ScreenIntPoint(50, 50), aPreventDefaultFlag); + EXPECT_EQ(result.GetHandledResult(), Nothing()); + EXPECT_EQ( + delayedHandledResult, + (APZHandledResult{expectedPlace, SideBits::eRight | SideBits::eTop, + EitherScrollDirection})); + } + + // scroll to right a bit. + SetScrollOffsetOnMainThread(CSSPoint(10, 100)); + { + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID, + {CompositorHitTestFlags::eVisibleToHitTest, + CompositorHitTestFlags::eIrregularArea}); + auto [result, delayedHandledResult] = + TapDispatchToContent(ScreenIntPoint(50, 50), aPreventDefaultFlag); + EXPECT_EQ(result.GetHandledResult(), Nothing()); + EXPECT_EQ( + delayedHandledResult, + (APZHandledResult{expectedPlace, + SideBits::eLeft | SideBits::eRight | SideBits::eTop, + EitherScrollDirection})); + } + + // scroll to the right edge. + SetScrollOffsetOnMainThread(CSSPoint(100, 100)); + { + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID, + {CompositorHitTestFlags::eVisibleToHitTest, + CompositorHitTestFlags::eIrregularArea}); + auto [result, delayedHandledResult] = + TapDispatchToContent(ScreenIntPoint(50, 50), aPreventDefaultFlag); + EXPECT_EQ(result.GetHandledResult(), Nothing()); + EXPECT_EQ( + delayedHandledResult, + (APZHandledResult{expectedPlace, SideBits::eTop | SideBits::eLeft, + EitherScrollDirection})); + } + } +}; + +TEST_F(APZEventResultTester, OverscrollDirections) { + CreateScrollableRootLayer(); + + TimeDuration tapDuration = TimeDuration::FromMilliseconds(100); + + // The default value of overscroll-behavior is auto. + APZEventResult result = Tap(manager, ScreenIntPoint(50, 50), tapDuration); + EXPECT_EQ(result.GetHandledResult()->mOverscrollDirections, + EitherScrollDirection); + + // overscroll-behavior: contain, contain. + UpdateOverscrollBehavior(OverscrollBehavior::Contain, + OverscrollBehavior::Contain); + result = Tap(manager, ScreenIntPoint(50, 50), tapDuration); + EXPECT_EQ(result.GetHandledResult()->mOverscrollDirections, + ScrollDirections()); + + // overscroll-behavior: none, none. + UpdateOverscrollBehavior(OverscrollBehavior::None, OverscrollBehavior::None); + result = Tap(manager, ScreenIntPoint(50, 50), tapDuration); + EXPECT_EQ(result.GetHandledResult()->mOverscrollDirections, + ScrollDirections()); + + // overscroll-behavior: auto, none. + UpdateOverscrollBehavior(OverscrollBehavior::Auto, OverscrollBehavior::None); + result = Tap(manager, ScreenIntPoint(50, 50), tapDuration); + EXPECT_EQ(result.GetHandledResult()->mOverscrollDirections, + HorizontalScrollDirection); + + // overscroll-behavior: none, auto. + UpdateOverscrollBehavior(OverscrollBehavior::None, OverscrollBehavior::Auto); + result = Tap(manager, ScreenIntPoint(50, 50), tapDuration); + EXPECT_EQ(result.GetHandledResult()->mOverscrollDirections, + VerticalScrollDirection); +} + +TEST_F(APZEventResultTester, ScrollableDirections) { + CreateScrollableRootLayer(); + + TimeDuration tapDuration = TimeDuration::FromMilliseconds(100); + + APZEventResult result = Tap(manager, ScreenIntPoint(50, 50), tapDuration); + // scrollable to down/right. + EXPECT_EQ(result.GetHandledResult()->mScrollableDirections, + SideBits::eBottom | SideBits::eRight); + + // scroll down a bit. + SetScrollOffsetOnMainThread(CSSPoint(0, 10)); + result = Tap(manager, ScreenIntPoint(50, 50), tapDuration); + // also scrollable toward top. + EXPECT_EQ(result.GetHandledResult()->mScrollableDirections, + SideBits::eTop | SideBits::eBottom | SideBits::eRight); + + // scroll to the bottom edge + SetScrollOffsetOnMainThread(CSSPoint(0, 100)); + result = Tap(manager, ScreenIntPoint(50, 50), tapDuration); + EXPECT_EQ(result.GetHandledResult()->mScrollableDirections, + SideBits::eRight | SideBits::eTop); + + // scroll to right a bit. + SetScrollOffsetOnMainThread(CSSPoint(10, 100)); + result = Tap(manager, ScreenIntPoint(50, 50), tapDuration); + EXPECT_EQ(result.GetHandledResult()->mScrollableDirections, + SideBits::eLeft | SideBits::eRight | SideBits::eTop); + + // scroll to the right edge. + SetScrollOffsetOnMainThread(CSSPoint(100, 100)); + result = Tap(manager, ScreenIntPoint(50, 50), tapDuration); + EXPECT_EQ(result.GetHandledResult()->mScrollableDirections, + SideBits::eLeft | SideBits::eTop); +} + +class APZEventResultTesterMock : public APZEventResultTester { + public: + APZEventResultTesterMock() { CreateMockHitTester(); } +}; + +TEST_F(APZEventResultTesterMock, OverscrollDirectionsWithEventHandler) { + CreateScrollableRootLayer(); + + OverscrollDirectionsWithEventHandlerTest(PreventDefaultFlag::No); +} + +TEST_F(APZEventResultTesterMock, + OverscrollDirectionsWithPreventDefaultEventHandler) { + CreateScrollableRootLayer(); + + OverscrollDirectionsWithEventHandlerTest(PreventDefaultFlag::Yes); +} + +TEST_F(APZEventResultTesterMock, ScrollableDirectionsWithEventHandler) { + CreateScrollableRootLayer(); + + ScrollableDirectionsWithEventHandlerTest(PreventDefaultFlag::No); +} + +TEST_F(APZEventResultTesterMock, + ScrollableDirectionsWithPreventDefaultEventHandler) { + CreateScrollableRootLayer(); + + ScrollableDirectionsWithEventHandlerTest(PreventDefaultFlag::Yes); +} + +// Test that APZEventResult::GetHandledResult() is correctly +// populated. +TEST_F(APZEventResultTesterMock, HandledByRootApzcFlag) { + // Create simple layer tree containing a dispatch-to-content region + // that covers part but not all of its area. + const char* treeShape = "x"; + LayerIntRect layerVisibleRects[] = { + LayerIntRect(0, 0, 100, 100), + }; + CreateScrollData(treeShape, layerVisibleRects); + SetScrollableFrameMetrics(root, ScrollableLayerGuid::START_SCROLL_ID, + CSSRect(0, 0, 100, 200)); + ModifyFrameMetrics(root, [](ScrollMetadata& sm, FrameMetrics& metrics) { + metrics.SetIsRootContent(true); + }); + // away from the scrolling container layer. + registration = MakeUnique<ScopedLayerTreeRegistration>(LayersId{0}, mcc); + UpdateHitTestingTree(); + + // Tap the top half and check that we report that the event was + // handled by the root APZC. + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID); + APZEventResult result = + TouchDown(manager, ScreenIntPoint(50, 25), mcc->Time()); + TouchUp(manager, ScreenIntPoint(50, 25), mcc->Time()); + EXPECT_EQ(result.GetHandledResult(), + Some(APZHandledResult{APZHandledPlace::HandledByRoot, + SideBits::eBottom, EitherScrollDirection})); + + // Tap the bottom half and check that we report that we're not + // sure whether the event was handled by the root APZC. + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID, + {CompositorHitTestFlags::eVisibleToHitTest, + CompositorHitTestFlags::eIrregularArea}); + result = TouchDown(manager, ScreenIntPoint(50, 75), mcc->Time()); + TouchUp(manager, ScreenIntPoint(50, 75), mcc->Time()); + EXPECT_EQ(result.GetHandledResult(), Nothing()); + + // Register an input block callback that will tell us the + // delayed answer. + APZHandledResult delayedAnswer{APZHandledPlace::Invalid, SideBits::eNone, + ScrollDirections()}; + manager->AddInputBlockCallback( + result.mInputBlockId, + {result.GetStatus(), [&](uint64_t id, const APZHandledResult& answer) { + EXPECT_EQ(id, result.mInputBlockId); + delayedAnswer = answer; + }}); + + // Send APZ the relevant notifications to allow it to process the + // input block. + manager->SetAllowedTouchBehavior(result.mInputBlockId, + {AllowedTouchBehavior::VERTICAL_PAN}); + manager->SetTargetAPZC(result.mInputBlockId, {result.mTargetGuid}); + manager->ContentReceivedInputBlock(result.mInputBlockId, + /*aPreventDefault=*/false); + + // Check that we received the delayed answer and it is what we expect. + EXPECT_EQ(delayedAnswer, + (APZHandledResult{APZHandledPlace::HandledByRoot, SideBits::eBottom, + EitherScrollDirection})); + + // Now repeat the tap on the bottom half, but simulate a prevent-default. + // This time, we expect a delayed answer of `HandledByContent`. + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID, + {CompositorHitTestFlags::eVisibleToHitTest, + CompositorHitTestFlags::eIrregularArea}); + result = TouchDown(manager, ScreenIntPoint(50, 75), mcc->Time()); + TouchUp(manager, ScreenIntPoint(50, 75), mcc->Time()); + EXPECT_EQ(result.GetHandledResult(), Nothing()); + manager->AddInputBlockCallback( + result.mInputBlockId, + {result.GetStatus(), [&](uint64_t id, const APZHandledResult& answer) { + EXPECT_EQ(id, result.mInputBlockId); + delayedAnswer = answer; + }}); + manager->SetAllowedTouchBehavior(result.mInputBlockId, + {AllowedTouchBehavior::VERTICAL_PAN}); + manager->SetTargetAPZC(result.mInputBlockId, {result.mTargetGuid}); + manager->ContentReceivedInputBlock(result.mInputBlockId, + /*aPreventDefault=*/true); + EXPECT_EQ(delayedAnswer, + (APZHandledResult{APZHandledPlace::HandledByContent, + SideBits::eBottom, EitherScrollDirection})); + + // Shrink the scrollable area, now it's no longer scrollable. + ModifyFrameMetrics(root, [](ScrollMetadata& sm, FrameMetrics& metrics) { + metrics.SetScrollableRect(CSSRect(0, 0, 100, 100)); + }); + UpdateHitTestingTree(); + // Now repeat the tap on the bottom half with an event handler. + // This time, we expect a delayed answer of `Unhandled`. + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID, + {CompositorHitTestFlags::eVisibleToHitTest, + CompositorHitTestFlags::eIrregularArea}); + result = TouchDown(manager, ScreenIntPoint(50, 75), mcc->Time()); + TouchUp(manager, ScreenIntPoint(50, 75), mcc->Time()); + EXPECT_EQ(result.GetHandledResult(), Nothing()); + manager->AddInputBlockCallback( + result.mInputBlockId, + {result.GetStatus(), [&](uint64_t id, const APZHandledResult& answer) { + EXPECT_EQ(id, result.mInputBlockId); + delayedAnswer = answer; + }}); + manager->SetAllowedTouchBehavior(result.mInputBlockId, + {AllowedTouchBehavior::VERTICAL_PAN}); + manager->SetTargetAPZC(result.mInputBlockId, {result.mTargetGuid}); + manager->ContentReceivedInputBlock(result.mInputBlockId, + /*aPreventDefault=*/false); + EXPECT_EQ(delayedAnswer, + (APZHandledResult{APZHandledPlace::Unhandled, SideBits::eNone, + EitherScrollDirection})); + + // Repeat the tap on the bottom half, with no event handler. + // Make sure we get an eager answer of `Unhandled`. + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID); + result = TouchDown(manager, ScreenIntPoint(50, 75), mcc->Time()); + TouchUp(manager, ScreenIntPoint(50, 75), mcc->Time()); + EXPECT_EQ(result.GetStatus(), nsEventStatus_eIgnore); + EXPECT_EQ(result.GetHandledResult(), + Some(APZHandledResult{APZHandledPlace::Unhandled, SideBits::eNone, + EitherScrollDirection})); +} diff --git a/gfx/layers/apz/test/gtest/TestFlingAcceleration.cpp b/gfx/layers/apz/test/gtest/TestFlingAcceleration.cpp new file mode 100644 index 0000000000..08f7d3d2ac --- /dev/null +++ b/gfx/layers/apz/test/gtest/TestFlingAcceleration.cpp @@ -0,0 +1,252 @@ +/* -*- 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 <initializer_list> +#include "APZCTreeManagerTester.h" +#include "APZTestCommon.h" +#include "InputUtils.h" + +class APZCFlingAccelerationTester : public APZCTreeManagerTester { + protected: + void SetUp() { + APZCTreeManagerTester::SetUp(); + const char* treeShape = "x"; + LayerIntRect layerVisibleRect[] = { + LayerIntRect(0, 0, 800, 1000), + }; + CreateScrollData(treeShape, layerVisibleRect); + SetScrollableFrameMetrics(root, ScrollableLayerGuid::START_SCROLL_ID, + CSSRect(0, 0, 800, 50000)); + // 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<ScopedLayerTreeRegistration>(LayersId{0}, mcc); + UpdateHitTestingTree(); + + apzc = ApzcOf(root); + } + + void ExecutePanGesture100Hz(const ScreenIntPoint& aStartPoint, + std::initializer_list<int32_t> aYDeltas) { + APZEventResult result = TouchDown(apzc, aStartPoint, mcc->Time()); + + // Allowed touch behaviours must be set after sending touch-start. + if (result.GetStatus() != nsEventStatus_eConsumeNoDefault) { + SetDefaultAllowedTouchBehavior(apzc, result.mInputBlockId); + } + + const TimeDuration kTouchTimeDelta100Hz = + TimeDuration::FromMilliseconds(10); + + ScreenIntPoint currentLocation = aStartPoint; + for (int32_t delta : aYDeltas) { + mcc->AdvanceBy(kTouchTimeDelta100Hz); + if (delta != 0) { + currentLocation.y += delta; + Unused << TouchMove(apzc, currentLocation, mcc->Time()); + } + } + + Unused << TouchUp(apzc, 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); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + remaining -= TIME_BETWEEN_FRAMES; + } + } + + RefPtr<TestAsyncPanZoomController> apzc; + UniquePtr<ScopedLayerTreeRegistration> registration; +}; + +enum class UpOrDown : uint8_t { Up, Down }; + +// This is a macro so that the assertions print useful line numbers. +#define CHECK_VELOCITY(aUpOrDown, aLowerBound, aUpperBound) \ + do { \ + auto vel = apzc->GetVelocityVector(); \ + if (UpOrDown::aUpOrDown == UpOrDown::Up) { \ + EXPECT_LT(vel.y, 0.0); \ + } else { \ + EXPECT_GT(vel.y, 0.0); \ + } \ + EXPECT_GE(vel.Length(), aLowerBound); \ + EXPECT_LE(vel.Length(), aUpperBound); \ + } while (0) + +// These tests have the following pattern: Two flings are executed, with a bit +// of wait time in between. The deltas in each pan gesture have been captured +// from a real phone, from touch events triggered by real fingers. +// We check the velocity at the end to detect whether the fling was accelerated +// or not. As an additional safety precaution, we also check the velocities for +// the first fling, so that changes in behavior are easier to analyze. +// One added challenge of this test is the fact that it has to work with on +// multiple platforms, and we use different velocity estimation strategies and +// different fling physics depending on the platform. +// The upper and lower bounds for the velocities were chosen in such a way that +// the test passes on all platforms. At the time of writing, we usually end up +// with higher velocities on Android than on Desktop, so the observed velocities +// on Android became the upper bounds and the observed velocities on Desktop +// becaume the lower bounds, each rounded out to a multiple of 0.1. + +TEST_F(APZCFlingAccelerationTester, TwoNormalFlingsShouldAccelerate) { + ExecutePanGesture100Hz(ScreenIntPoint{665, 1244}, + {0, 0, -21, -44, -52, -55, -53, -49, -46, -47}); + CHECK_VELOCITY(Down, 4.5, 6.8); + + ExecuteWait(TimeDuration::FromMilliseconds(375)); + CHECK_VELOCITY(Down, 2.2, 5.1); + + ExecutePanGesture100Hz(ScreenIntPoint{623, 1211}, + {-6, -51, -55, 0, -53, -57, -60, -60, -56}); + CHECK_VELOCITY(Down, 9.0, 14.0); +} + +TEST_F(APZCFlingAccelerationTester, TwoFastFlingsShouldAccelerate) { + ExecutePanGesture100Hz(ScreenIntPoint{764, 714}, + {9, 30, 49, 60, 64, 64, 62, 59, 51}); + CHECK_VELOCITY(Up, 5.0, 7.5); + + ExecuteWait(TimeDuration::FromMilliseconds(447)); + CHECK_VELOCITY(Up, 2.3, 5.2); + + ExecutePanGesture100Hz(ScreenIntPoint{743, 739}, + {7, 0, 38, 66, 75, 146, 0, 119}); + CHECK_VELOCITY(Up, 13.0, 20.0); +} + +TEST_F(APZCFlingAccelerationTester, + FlingsInOppositeDirectionShouldNotAccelerate) { + ExecutePanGesture100Hz(ScreenIntPoint{728, 1381}, + {0, 0, 0, -12, -24, -32, -43, -46, 0}); + CHECK_VELOCITY(Down, 2.9, 5.3); + + ExecuteWait(TimeDuration::FromMilliseconds(153)); + CHECK_VELOCITY(Down, 2.1, 4.8); + + ExecutePanGesture100Hz(ScreenIntPoint{698, 1059}, + {0, 0, 14, 61, 41, 0, 45, 35}); + CHECK_VELOCITY(Up, 3.2, 4.3); +} + +TEST_F(APZCFlingAccelerationTester, + ShouldNotAccelerateWhenPreviousFlingHasSlowedDown) { + ExecutePanGesture100Hz(ScreenIntPoint{748, 1046}, + {0, 9, 15, 23, 31, 30, 0, 34, 31, 29, 28, 24, 24, 11}); + CHECK_VELOCITY(Up, 2.2, 3.0); + ExecuteWait(TimeDuration::FromMilliseconds(498)); + CHECK_VELOCITY(Up, 0.5, 1.0); + ExecutePanGesture100Hz(ScreenIntPoint{745, 1056}, + {0, 10, 17, 29, 29, 33, 33, 0, 31, 27, 13}); + CHECK_VELOCITY(Up, 1.8, 2.7); +} + +TEST_F(APZCFlingAccelerationTester, ShouldNotAccelerateWhenPausedAtStartOfPan) { + ExecutePanGesture100Hz( + ScreenIntPoint{711, 1468}, + {0, 0, 0, 0, -8, 0, -18, -32, -50, -57, -66, -68, -63, -60}); + CHECK_VELOCITY(Down, 6.2, 8.6); + + ExecuteWait(TimeDuration::FromMilliseconds(285)); + CHECK_VELOCITY(Down, 3.4, 7.4); + + ExecutePanGesture100Hz( + ScreenIntPoint{658, 1352}, + {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, -8, -18, -34, -53, -70, -75, -75, -64}); + CHECK_VELOCITY(Down, 6.7, 9.1); +} + +TEST_F(APZCFlingAccelerationTester, ShouldNotAccelerateWhenPausedDuringPan) { + ExecutePanGesture100Hz( + ScreenIntPoint{732, 1423}, + {0, 0, 0, -5, 0, -15, -41, -71, -90, -93, -85, -64, -44}); + CHECK_VELOCITY(Down, 7.5, 10.1); + + ExecuteWait(TimeDuration::FromMilliseconds(204)); + CHECK_VELOCITY(Down, 4.8, 9.4); + + ExecutePanGesture100Hz( + ScreenIntPoint{651, 1372}, + {0, 0, 0, -6, 0, -16, -26, -41, -49, -65, -66, -61, -50, -35, -24, + -17, -11, -8, -6, -5, -4, -3, -2, -2, -2, -2, -2, -2, -2, -2, + -3, -4, -5, -7, -9, -10, -10, -12, -18, -25, -23, -28, -30, -24}); + CHECK_VELOCITY(Down, 2.5, 3.4); +} + +TEST_F(APZCFlingAccelerationTester, + ShouldNotAccelerateWhenOppositeDirectionDuringPan) { + ExecutePanGesture100Hz(ScreenIntPoint{663, 1371}, + {0, 0, 0, -5, -18, -31, -49, -56, -61, -54, -55}); + CHECK_VELOCITY(Down, 5.4, 7.1); + + ExecuteWait(TimeDuration::FromMilliseconds(255)); + CHECK_VELOCITY(Down, 3.1, 6.0); + + ExecutePanGesture100Hz( + ScreenIntPoint{726, 930}, + {0, 0, 0, 0, 30, 0, 19, 24, 32, 30, 37, 33, + 33, 32, 25, 23, 23, 18, 13, 9, 5, 3, 1, 0, + -7, -19, -38, -53, -68, -79, -85, -73, -64, -54}); + CHECK_VELOCITY(Down, 7.0, 10.0); +} + +TEST_F(APZCFlingAccelerationTester, + ShouldAccelerateAfterLongWaitIfVelocityStillHigh) { + // Reduce friction with the "Desktop" fling physics a little, so that it + // behaves more similarly to the Android fling physics, and has enough + // velocity after the wait time to allow for acceleration. + SCOPED_GFX_PREF_FLOAT("apz.fling_friction", 0.0012); + + ExecutePanGesture100Hz(ScreenIntPoint{739, 1424}, + {0, 0, -5, -10, -20, 0, -110, -86, 0, -102, -105}); + CHECK_VELOCITY(Down, 6.3, 9.4); + + ExecuteWait(TimeDuration::FromMilliseconds(1117)); + CHECK_VELOCITY(Down, 1.6, 3.3); + + ExecutePanGesture100Hz(ScreenIntPoint{726, 1380}, + {0, -8, 0, -30, -60, -87, -104, -111}); + CHECK_VELOCITY(Down, 13.0, 23.0); +} + +TEST_F(APZCFlingAccelerationTester, ShouldNotAccelerateAfterCanceledWithTap) { + // First, build up a lot of speed. + ExecutePanGesture100Hz(ScreenIntPoint{569, 710}, + {11, 2, 107, 18, 148, 57, 133, 159, 21}); + ExecuteWait(TimeDuration::FromMilliseconds(154)); + ExecutePanGesture100Hz(ScreenIntPoint{581, 650}, + {12, 68, 0, 162, 78, 140, 167}); + ExecuteWait(TimeDuration::FromMilliseconds(123)); + ExecutePanGesture100Hz(ScreenIntPoint{568, 723}, {11, 0, 79, 91, 131, 171}); + ExecuteWait(TimeDuration::FromMilliseconds(123)); + ExecutePanGesture100Hz(ScreenIntPoint{598, 678}, + {8, 55, 22, 87, 117, 220, 54}); + ExecuteWait(TimeDuration::FromMilliseconds(134)); + ExecutePanGesture100Hz(ScreenIntPoint{585, 854}, {45, 137, 107, 102, 79}); + ExecuteWait(TimeDuration::FromMilliseconds(246)); + + // Then, interrupt with a tap. + ExecutePanGesture100Hz(ScreenIntPoint{566, 812}, {0, 0, 0, 0}); + ExecuteWait(TimeDuration::FromMilliseconds(869)); + + // Then do a regular fling. + ExecutePanGesture100Hz(ScreenIntPoint{599, 819}, + {0, 0, 8, 35, 8, 38, 29, 37}); + + CHECK_VELOCITY(Up, 2.8, 4.2); +} diff --git a/gfx/layers/apz/test/gtest/TestGestureDetector.cpp b/gfx/layers/apz/test/gtest/TestGestureDetector.cpp new file mode 100644 index 0000000000..f244ca4dc7 --- /dev/null +++ b/gfx/layers/apz/test/gtest/TestGestureDetector.cpp @@ -0,0 +1,845 @@ +/* -*- 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 "gtest/gtest.h" +#include "gmock/gmock.h" + +#include "APZCBasicTester.h" +#include "APZTestCommon.h" +#include "InputUtils.h" +#include "mozilla/StaticPrefs_apz.h" + +// Note: There are additional tests that test gesture detection behaviour +// with multiple APZCs in TestTreeManager.cpp. + +class APZCGestureDetectorTester : public APZCBasicTester { + public: + APZCGestureDetectorTester() + : APZCBasicTester(AsyncPanZoomController::USE_GESTURE_DETECTOR) {} + + protected: + FrameMetrics GetPinchableFrameMetrics() { + FrameMetrics fm; + fm.SetCompositionBounds(ParentLayerRect(200, 200, 100, 200)); + fm.SetScrollableRect(CSSRect(0, 0, 980, 1000)); + fm.SetVisualScrollOffset(CSSPoint(300, 300)); + fm.SetZoom(CSSToParentLayerScale(2.0)); + // APZC only allows zooming on the root scrollable frame. + fm.SetIsRootContent(true); + // the visible area of the document in CSS pixels is x=300 y=300 w=50 h=100 + return fm; + } +}; + +#ifndef MOZ_WIDGET_ANDROID // Currently fails on Android +TEST_F(APZCGestureDetectorTester, Pan_After_Pinch) { + SCOPED_GFX_PREF_INT("apz.axis_lock.mode", 2); + SCOPED_GFX_PREF_FLOAT("apz.axis_lock.lock_angle", M_PI / 6.0f); + SCOPED_GFX_PREF_FLOAT("apz.axis_lock.breakout_angle", M_PI / 8.0f); + + FrameMetrics originalMetrics = GetPinchableFrameMetrics(); + apzc->SetFrameMetrics(originalMetrics); + + MakeApzcZoomable(); + + // Test parameters + float zoomAmount = 1.25; + float pinchLength = 100.0; + float pinchLengthScaled = pinchLength * zoomAmount; + int focusX = 250; + int focusY = 300; + int panDistance = 20; + const TimeDuration TIME_BETWEEN_TOUCH_EVENT = + TimeDuration::FromMilliseconds(50); + + int firstFingerId = 0; + int secondFingerId = firstFingerId + 1; + + // Put fingers down + MultiTouchInput mti = + MultiTouchInput(MultiTouchInput::MULTITOUCH_START, 0, mcc->Time(), 0); + mti.mTouches.AppendElement( + CreateSingleTouchData(firstFingerId, focusX, focusY)); + mti.mTouches.AppendElement( + CreateSingleTouchData(secondFingerId, focusX, focusY)); + apzc->ReceiveInputEvent(mti, Some(nsTArray<uint32_t>{kDefaultTouchBehavior})); + mcc->AdvanceBy(TIME_BETWEEN_TOUCH_EVENT); + + // Spread fingers out to enter the pinch state + mti = MultiTouchInput(MultiTouchInput::MULTITOUCH_MOVE, 0, mcc->Time(), 0); + mti.mTouches.AppendElement(CreateSingleTouchData( + firstFingerId, static_cast<int32_t>(focusX - pinchLength), focusY)); + mti.mTouches.AppendElement(CreateSingleTouchData( + secondFingerId, static_cast<int32_t>(focusX + pinchLength), focusY)); + apzc->ReceiveInputEvent(mti); + mcc->AdvanceBy(TIME_BETWEEN_TOUCH_EVENT); + + // Do the actual pinch of 1.25x + mti = MultiTouchInput(MultiTouchInput::MULTITOUCH_MOVE, 0, mcc->Time(), 0); + mti.mTouches.AppendElement(CreateSingleTouchData( + firstFingerId, static_cast<int32_t>(focusX - pinchLengthScaled), focusY)); + mti.mTouches.AppendElement(CreateSingleTouchData( + secondFingerId, static_cast<int32_t>(focusX + pinchLengthScaled), + focusY)); + apzc->ReceiveInputEvent(mti); + mcc->AdvanceBy(TIME_BETWEEN_TOUCH_EVENT); + + // Verify that the zoom changed, just to make sure our code above did what it + // was supposed to. + FrameMetrics zoomedMetrics = apzc->GetFrameMetrics(); + float newZoom = zoomedMetrics.GetZoom().scale; + EXPECT_EQ(originalMetrics.GetZoom().scale * zoomAmount, newZoom); + + // Now we lift one finger... + mti = MultiTouchInput(MultiTouchInput::MULTITOUCH_END, 0, mcc->Time(), 0); + mti.mTouches.AppendElement(CreateSingleTouchData( + secondFingerId, static_cast<int32_t>(focusX + pinchLengthScaled), + focusY)); + apzc->ReceiveInputEvent(mti); + mcc->AdvanceBy(TIME_BETWEEN_TOUCH_EVENT); + + // ... and pan with the remaining finger. This pan just breaks through the + // distance threshold. + focusY += StaticPrefs::apz_touch_start_tolerance() * tm->GetDPI(); + mti = MultiTouchInput(MultiTouchInput::MULTITOUCH_MOVE, 0, mcc->Time(), 0); + mti.mTouches.AppendElement(CreateSingleTouchData( + firstFingerId, static_cast<int32_t>(focusX - pinchLengthScaled), focusY)); + apzc->ReceiveInputEvent(mti); + mcc->AdvanceBy(TIME_BETWEEN_TOUCH_EVENT); + + // This one does an actual pan of 20 pixels + focusY += panDistance; + mti = MultiTouchInput(MultiTouchInput::MULTITOUCH_MOVE, 0, mcc->Time(), 0); + mti.mTouches.AppendElement(CreateSingleTouchData( + firstFingerId, static_cast<int32_t>(focusX - pinchLengthScaled), focusY)); + apzc->ReceiveInputEvent(mti); + mcc->AdvanceBy(TIME_BETWEEN_TOUCH_EVENT); + + // Lift the remaining finger + mti = MultiTouchInput(MultiTouchInput::MULTITOUCH_END, 0, mcc->Time(), 0); + mti.mTouches.AppendElement(CreateSingleTouchData( + firstFingerId, static_cast<int32_t>(focusX - pinchLengthScaled), focusY)); + apzc->ReceiveInputEvent(mti); + mcc->AdvanceBy(TIME_BETWEEN_TOUCH_EVENT); + + // Verify that we scrolled + FrameMetrics finalMetrics = apzc->GetFrameMetrics(); + EXPECT_EQ(zoomedMetrics.GetVisualScrollOffset().y - (panDistance / newZoom), + finalMetrics.GetVisualScrollOffset().y); + + // Clear out any remaining fling animation and pending tasks + apzc->AdvanceAnimationsUntilEnd(); + while (mcc->RunThroughDelayedTasks()) + ; + apzc->AssertStateIsReset(); +} +#endif + +TEST_F(APZCGestureDetectorTester, Pan_With_Tap) { + SCOPED_GFX_PREF_FLOAT("apz.touch_start_tolerance", 0.1); + + FrameMetrics originalMetrics = GetPinchableFrameMetrics(); + apzc->SetFrameMetrics(originalMetrics); + + // Making the APZC zoomable isn't really needed for the correct operation of + // this test, but it could help catch regressions where we accidentally enter + // a pinch state. + MakeApzcZoomable(); + + // Test parameters + int touchX = 250; + int touchY = 300; + int panDistance = 20; + + int firstFingerId = 0; + int secondFingerId = firstFingerId + 1; + + const float panThreshold = + StaticPrefs::apz_touch_start_tolerance() * tm->GetDPI(); + + // Put finger down + MultiTouchInput mti = + CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_START, mcc->Time()); + mti.mTouches.AppendElement( + CreateSingleTouchData(firstFingerId, touchX, touchY)); + apzc->ReceiveInputEvent(mti, Some(nsTArray<uint32_t>{kDefaultTouchBehavior})); + + // Start a pan, break through the threshold + touchY += panThreshold; + mti = CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_MOVE, mcc->Time()); + mti.mTouches.AppendElement( + CreateSingleTouchData(firstFingerId, touchX, touchY)); + apzc->ReceiveInputEvent(mti); + + // Do an actual pan for a bit + touchY += panDistance; + mti = CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_MOVE, mcc->Time()); + mti.mTouches.AppendElement( + CreateSingleTouchData(firstFingerId, touchX, touchY)); + apzc->ReceiveInputEvent(mti); + + // Put a second finger down + mti = CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_START, mcc->Time()); + mti.mTouches.AppendElement( + CreateSingleTouchData(firstFingerId, touchX, touchY)); + mti.mTouches.AppendElement( + CreateSingleTouchData(secondFingerId, touchX + 10, touchY)); + apzc->ReceiveInputEvent(mti, Some(nsTArray<uint32_t>{kDefaultTouchBehavior})); + + // Lift the second finger + mti = CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_END, mcc->Time()); + mti.mTouches.AppendElement( + CreateSingleTouchData(secondFingerId, touchX + 10, touchY)); + apzc->ReceiveInputEvent(mti); + + // Bust through the threshold again + touchY += panThreshold; + mti = CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_MOVE, mcc->Time()); + mti.mTouches.AppendElement( + CreateSingleTouchData(firstFingerId, touchX, touchY)); + apzc->ReceiveInputEvent(mti); + + // Do some more actual panning + touchY += panDistance; + mti = CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_MOVE, mcc->Time()); + mti.mTouches.AppendElement( + CreateSingleTouchData(firstFingerId, touchX, touchY)); + apzc->ReceiveInputEvent(mti); + + // Lift the first finger + mti = CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_END, mcc->Time()); + mti.mTouches.AppendElement( + CreateSingleTouchData(firstFingerId, touchX, touchY)); + apzc->ReceiveInputEvent(mti); + + // Verify that we scrolled + FrameMetrics finalMetrics = apzc->GetFrameMetrics(); + float zoom = finalMetrics.GetZoom().scale; + EXPECT_EQ( + originalMetrics.GetVisualScrollOffset().y - (panDistance * 2 / zoom), + finalMetrics.GetVisualScrollOffset().y); + + // Clear out any remaining fling animation and pending tasks + apzc->AdvanceAnimationsUntilEnd(); + while (mcc->RunThroughDelayedTasks()) + ; + apzc->AssertStateIsReset(); +} + +TEST_F(APZCGestureDetectorTester, SecondTapIsFar_Bug1586496) { + // Test that we receive two single-tap events when two tap gestures are + // close in time but far in distance. + EXPECT_CALL(*mcc, HandleTap(TapType::eSingleTap, _, 0, apzc->GetGuid(), _, _)) + .Times(2); + + TimeDuration brief = + TimeDuration::FromMilliseconds(StaticPrefs::apz_max_tap_time() / 10.0); + + ScreenIntPoint point(10, 10); + Tap(apzc, point, brief); + + mcc->AdvanceBy(brief); + + point.x += static_cast<int32_t>(apzc->GetSecondTapTolerance() * 2); + point.y += static_cast<int32_t>(apzc->GetSecondTapTolerance() * 2); + + Tap(apzc, point, brief); +} + +class APZCFlingStopTester : public APZCGestureDetectorTester { + protected: + // Start a fling, and then tap while the fling is ongoing. When + // aSlow is false, the tap will happen while the fling is at a + // high velocity, and we check that the tap doesn't trigger sending a tap + // to content. If aSlow is true, the tap will happen while the fling + // is at a slow velocity, and we check that the tap does trigger sending + // a tap to content. See bug 1022956. + void DoFlingStopTest(bool aSlow) { + int touchStart = 50; + int touchEnd = 10; + + // Start the fling down. + Pan(apzc, touchStart, touchEnd); + // The touchstart from the pan will leave some cancelled tasks in the queue, + // clear them out + + // If we want to tap while the fling is fast, let the fling advance for 10ms + // only. If we want the fling to slow down more, advance to 2000ms. These + // numbers may need adjusting if our friction and threshold values change, + // but they should be deterministic at least. + int timeDelta = aSlow ? 2000 : 10; + int tapCallsExpected = aSlow ? 2 : 1; + + // Advance the fling animation by timeDelta milliseconds. + ParentLayerPoint pointOut; + AsyncTransform viewTransformOut; + apzc->SampleContentTransformForFrame( + &viewTransformOut, pointOut, TimeDuration::FromMilliseconds(timeDelta)); + + // Deliver a tap to abort the fling. Ensure that we get a SingleTap + // call out of it if and only if the fling is slow. + EXPECT_CALL(*mcc, + HandleTap(TapType::eSingleTap, _, 0, apzc->GetGuid(), _, _)) + .Times(tapCallsExpected); + Tap(apzc, ScreenIntPoint(10, 10), 0); + while (mcc->RunThroughDelayedTasks()) + ; + + // Deliver another tap, to make sure that taps are flowing properly once + // the fling is aborted. + Tap(apzc, ScreenIntPoint(100, 100), 0); + while (mcc->RunThroughDelayedTasks()) + ; + + // Verify that we didn't advance any further after the fling was aborted, in + // either case. + ParentLayerPoint finalPointOut; + apzc->SampleContentTransformForFrame(&viewTransformOut, finalPointOut); + EXPECT_EQ(pointOut.x, finalPointOut.x); + EXPECT_EQ(pointOut.y, finalPointOut.y); + + apzc->AssertStateIsReset(); + } + + void DoFlingStopWithSlowListener(bool aPreventDefault) { + MakeApzcWaitForMainThread(); + + int touchStart = 50; + int touchEnd = 10; + uint64_t blockId = 0; + + // Start the fling down. + Pan(apzc, touchStart, touchEnd, PanOptions::None, nullptr, nullptr, + &blockId); + apzc->ConfirmTarget(blockId); + apzc->ContentReceivedInputBlock(blockId, false); + + // Sample the fling a couple of times to ensure it's going. + ParentLayerPoint point, finalPoint; + AsyncTransform viewTransform; + apzc->SampleContentTransformForFrame(&viewTransform, point, + TimeDuration::FromMilliseconds(10)); + apzc->SampleContentTransformForFrame(&viewTransform, finalPoint, + TimeDuration::FromMilliseconds(10)); + EXPECT_GT(finalPoint.y, point.y); + + // Now we put our finger down to stop the fling + blockId = + TouchDown(apzc, ScreenIntPoint(10, 10), mcc->Time()).mInputBlockId; + + // Re-sample to make sure it hasn't moved + apzc->SampleContentTransformForFrame(&viewTransform, point, + TimeDuration::FromMilliseconds(10)); + EXPECT_EQ(finalPoint.x, point.x); + EXPECT_EQ(finalPoint.y, point.y); + + // respond to the touchdown that stopped the fling. + // even if we do a prevent-default on it, the animation should remain + // stopped. + apzc->ContentReceivedInputBlock(blockId, aPreventDefault); + + // Verify the page hasn't moved + apzc->SampleContentTransformForFrame(&viewTransform, point, + TimeDuration::FromMilliseconds(70)); + EXPECT_EQ(finalPoint.x, point.x); + EXPECT_EQ(finalPoint.y, point.y); + + // clean up + TouchUp(apzc, ScreenIntPoint(10, 10), mcc->Time()); + + apzc->AssertStateIsReset(); + } +}; + +TEST_F(APZCFlingStopTester, FlingStop) { + SCOPED_GFX_PREF_FLOAT("apz.fling_min_velocity_threshold", 0.0f); + DoFlingStopTest(false); +} + +TEST_F(APZCFlingStopTester, FlingStopTap) { + SCOPED_GFX_PREF_FLOAT("apz.fling_min_velocity_threshold", 0.0f); + DoFlingStopTest(true); +} + +TEST_F(APZCFlingStopTester, FlingStopSlowListener) { + SCOPED_GFX_PREF_FLOAT("apz.fling_min_velocity_threshold", 0.0f); + DoFlingStopWithSlowListener(false); +} + +TEST_F(APZCFlingStopTester, FlingStopPreventDefault) { + SCOPED_GFX_PREF_FLOAT("apz.fling_min_velocity_threshold", 0.0f); + DoFlingStopWithSlowListener(true); +} + +TEST_F(APZCGestureDetectorTester, ShortPress) { + MakeApzcUnzoomable(); + + MockFunction<void(std::string checkPointName)> check; + { + InSequence s; + // This verifies that the single tap notification is sent after the + // touchup is fully processed. The ordering here is important. + EXPECT_CALL(check, Call("pre-tap")); + EXPECT_CALL(check, Call("post-tap")); + EXPECT_CALL(*mcc, HandleTap(TapType::eSingleTap, LayoutDevicePoint(10, 10), + 0, apzc->GetGuid(), _, _)) + .Times(1); + } + + check.Call("pre-tap"); + TapAndCheckStatus(apzc, ScreenIntPoint(10, 10), + TimeDuration::FromMilliseconds(100)); + check.Call("post-tap"); + + apzc->AssertStateIsReset(); +} + +TEST_F(APZCGestureDetectorTester, MediumPress) { + MakeApzcUnzoomable(); + + MockFunction<void(std::string checkPointName)> check; + { + InSequence s; + // This verifies that the single tap notification is sent after the + // touchup is fully processed. The ordering here is important. + EXPECT_CALL(check, Call("pre-tap")); + EXPECT_CALL(check, Call("post-tap")); + EXPECT_CALL(*mcc, HandleTap(TapType::eSingleTap, LayoutDevicePoint(10, 10), + 0, apzc->GetGuid(), _, _)) + .Times(1); + } + + check.Call("pre-tap"); + TapAndCheckStatus(apzc, ScreenIntPoint(10, 10), + TimeDuration::FromMilliseconds(400)); + check.Call("post-tap"); + + apzc->AssertStateIsReset(); +} + +class APZCLongPressTester : public APZCGestureDetectorTester { + protected: + void DoLongPressTest(uint32_t aBehavior) { + MakeApzcUnzoomable(); + + APZEventResult result = + TouchDown(apzc, ScreenIntPoint(10, 10), mcc->Time()); + EXPECT_EQ(nsEventStatus_eConsumeDoDefault, result.GetStatus()); + uint64_t blockId = result.mInputBlockId; + + if (result.GetStatus() != nsEventStatus_eConsumeNoDefault) { + // SetAllowedTouchBehavior() must be called after sending touch-start. + nsTArray<uint32_t> allowedTouchBehaviors; + allowedTouchBehaviors.AppendElement(aBehavior); + apzc->SetAllowedTouchBehavior(blockId, allowedTouchBehaviors); + } + // Have content "respond" to the touchstart + apzc->ContentReceivedInputBlock(blockId, false); + + MockFunction<void(std::string checkPointName)> check; + + { + InSequence s; + + EXPECT_CALL(check, Call("preHandleLongTap")); + blockId++; + EXPECT_CALL(*mcc, HandleTap(TapType::eLongTap, LayoutDevicePoint(10, 10), + 0, apzc->GetGuid(), blockId, _)) + .Times(1); + EXPECT_CALL(check, Call("postHandleLongTap")); + + EXPECT_CALL(check, Call("preHandleLongTapUp")); + EXPECT_CALL(*mcc, + HandleTap(TapType::eLongTapUp, LayoutDevicePoint(10, 10), 0, + apzc->GetGuid(), _, _)) + .Times(1); + EXPECT_CALL(check, Call("postHandleLongTapUp")); + } + + // Manually invoke the longpress while the touch is currently down. + check.Call("preHandleLongTap"); + mcc->RunThroughDelayedTasks(); + check.Call("postHandleLongTap"); + + // Dispatching the longpress event starts a new touch block, which + // needs a new content response and also has a pending timeout task + // in the queue. Deal with those here. We do the content response first + // with preventDefault=false, and then we run the timeout task which + // "loses the race" and does nothing. + apzc->ContentReceivedInputBlock(blockId, false); + mcc->AdvanceByMillis(1000); + + // Finally, simulate lifting the finger. Since the long-press wasn't + // prevent-defaulted, we should get a long-tap-up event. + check.Call("preHandleLongTapUp"); + result = TouchUp(apzc, ScreenIntPoint(10, 10), mcc->Time()); + mcc->RunThroughDelayedTasks(); + EXPECT_EQ(nsEventStatus_eConsumeDoDefault, result.GetStatus()); + check.Call("postHandleLongTapUp"); + + apzc->AssertStateIsReset(); + } + + void DoLongPressPreventDefaultTest(uint32_t aBehavior) { + MakeApzcUnzoomable(); + + EXPECT_CALL(*mcc, RequestContentRepaint(_)).Times(0); + + int touchX = 10, touchStartY = 50, touchEndY = 10; + + APZEventResult result = + TouchDown(apzc, ScreenIntPoint(touchX, touchStartY), mcc->Time()); + EXPECT_EQ(nsEventStatus_eConsumeDoDefault, result.GetStatus()); + uint64_t blockId = result.mInputBlockId; + + if (result.GetStatus() != nsEventStatus_eConsumeNoDefault) { + // SetAllowedTouchBehavior() must be called after sending touch-start. + nsTArray<uint32_t> allowedTouchBehaviors; + allowedTouchBehaviors.AppendElement(aBehavior); + apzc->SetAllowedTouchBehavior(blockId, allowedTouchBehaviors); + } + // Have content "respond" to the touchstart + apzc->ContentReceivedInputBlock(blockId, false); + + MockFunction<void(std::string checkPointName)> check; + + { + InSequence s; + + EXPECT_CALL(check, Call("preHandleLongTap")); + blockId++; + EXPECT_CALL(*mcc, HandleTap(TapType::eLongTap, + LayoutDevicePoint(touchX, touchStartY), 0, + apzc->GetGuid(), blockId, _)) + .Times(1); + EXPECT_CALL(check, Call("postHandleLongTap")); + } + + // Manually invoke the longpress while the touch is currently down. + check.Call("preHandleLongTap"); + mcc->RunThroughDelayedTasks(); + check.Call("postHandleLongTap"); + + // There should be a TimeoutContentResponse task in the queue still, + // waiting for the response from the longtap event dispatched above. + // Send the signal that content has handled the long-tap, and then run + // the timeout task (it will be a no-op because the content "wins" the + // race. This takes the place of the "contextmenu" event. + apzc->ContentReceivedInputBlock(blockId, true); + mcc->AdvanceByMillis(1000); + + MultiTouchInput mti = + CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_MOVE, mcc->Time()); + mti.mTouches.AppendElement(CreateSingleTouchData(0, touchX, touchEndY)); + result = apzc->ReceiveInputEvent(mti); + EXPECT_EQ(nsEventStatus_eConsumeDoDefault, result.GetStatus()); + + EXPECT_CALL(*mcc, HandleTap(TapType::eSingleTap, + LayoutDevicePoint(touchX, touchEndY), 0, + apzc->GetGuid(), _, _)) + .Times(0); + result = TouchUp(apzc, ScreenIntPoint(touchX, touchEndY), mcc->Time()); + EXPECT_EQ(nsEventStatus_eConsumeDoDefault, result.GetStatus()); + + ParentLayerPoint pointOut; + AsyncTransform viewTransformOut; + apzc->SampleContentTransformForFrame(&viewTransformOut, pointOut); + + EXPECT_EQ(ParentLayerPoint(), pointOut); + EXPECT_EQ(AsyncTransform(), viewTransformOut); + + apzc->AssertStateIsReset(); + } +}; + +TEST_F(APZCLongPressTester, LongPress) { + DoLongPressTest(kDefaultTouchBehavior); +} + +TEST_F(APZCLongPressTester, LongPressPreventDefault) { + DoLongPressPreventDefaultTest(kDefaultTouchBehavior); +} + +TEST_F(APZCGestureDetectorTester, DoubleTap) { + MakeApzcWaitForMainThread(); + MakeApzcZoomable(); + + EXPECT_CALL(*mcc, HandleTap(TapType::eSingleTap, LayoutDevicePoint(10, 10), 0, + apzc->GetGuid(), _, _)) + .Times(0); + EXPECT_CALL(*mcc, HandleTap(TapType::eDoubleTap, LayoutDevicePoint(10, 10), 0, + apzc->GetGuid(), _, _)) + .Times(1); + + uint64_t blockIds[2]; + DoubleTapAndCheckStatus(apzc, ScreenIntPoint(10, 10), &blockIds); + + // responses to the two touchstarts + apzc->ContentReceivedInputBlock(blockIds[0], false); + apzc->ContentReceivedInputBlock(blockIds[1], false); + + apzc->AssertStateIsReset(); +} + +TEST_F(APZCGestureDetectorTester, DoubleTapNotZoomable) { + MakeApzcWaitForMainThread(); + MakeApzcUnzoomable(); + + EXPECT_CALL(*mcc, HandleTap(TapType::eSingleTap, LayoutDevicePoint(10, 10), 0, + apzc->GetGuid(), _, _)) + .Times(1); + EXPECT_CALL(*mcc, HandleTap(TapType::eSecondTap, LayoutDevicePoint(10, 10), 0, + apzc->GetGuid(), _, _)) + .Times(1); + EXPECT_CALL(*mcc, HandleTap(TapType::eDoubleTap, LayoutDevicePoint(10, 10), 0, + apzc->GetGuid(), _, _)) + .Times(0); + + uint64_t blockIds[2]; + DoubleTapAndCheckStatus(apzc, ScreenIntPoint(10, 10), &blockIds); + + // responses to the two touchstarts + apzc->ContentReceivedInputBlock(blockIds[0], false); + apzc->ContentReceivedInputBlock(blockIds[1], false); + + apzc->AssertStateIsReset(); +} + +TEST_F(APZCGestureDetectorTester, DoubleTapPreventDefaultFirstOnly) { + MakeApzcWaitForMainThread(); + MakeApzcZoomable(); + + EXPECT_CALL(*mcc, HandleTap(TapType::eSingleTap, LayoutDevicePoint(10, 10), 0, + apzc->GetGuid(), _, _)) + .Times(1); + EXPECT_CALL(*mcc, HandleTap(TapType::eDoubleTap, LayoutDevicePoint(10, 10), 0, + apzc->GetGuid(), _, _)) + .Times(0); + + uint64_t blockIds[2]; + DoubleTapAndCheckStatus(apzc, ScreenIntPoint(10, 10), &blockIds); + + // responses to the two touchstarts + apzc->ContentReceivedInputBlock(blockIds[0], true); + apzc->ContentReceivedInputBlock(blockIds[1], false); + + apzc->AssertStateIsReset(); +} + +TEST_F(APZCGestureDetectorTester, DoubleTapPreventDefaultBoth) { + MakeApzcWaitForMainThread(); + MakeApzcZoomable(); + + EXPECT_CALL(*mcc, HandleTap(TapType::eSingleTap, LayoutDevicePoint(10, 10), 0, + apzc->GetGuid(), _, _)) + .Times(0); + EXPECT_CALL(*mcc, HandleTap(TapType::eDoubleTap, LayoutDevicePoint(10, 10), 0, + apzc->GetGuid(), _, _)) + .Times(0); + + uint64_t blockIds[2]; + DoubleTapAndCheckStatus(apzc, ScreenIntPoint(10, 10), &blockIds); + + // responses to the two touchstarts + apzc->ContentReceivedInputBlock(blockIds[0], true); + apzc->ContentReceivedInputBlock(blockIds[1], true); + + apzc->AssertStateIsReset(); +} + +// Test for bug 947892 +// We test whether we dispatch tap event when the tap is followed by pinch. +// Additionally test that the pinch gesture successfully results in zooming. +TEST_F(APZCGestureDetectorTester, TapFollowedByPinch) { + MakeApzcZoomable(); + + EXPECT_CALL(*mcc, HandleTap(TapType::eSingleTap, LayoutDevicePoint(10, 10), 0, + apzc->GetGuid(), _, _)) + .Times(1); + + Tap(apzc, ScreenIntPoint(10, 10), TimeDuration::FromMilliseconds(100)); + + PinchWithTouchInput( + apzc, ScreenIntPoint(15, 15), 1.5, + PinchOptions().TimeBetweenTouchEvents( + // Time it so that the max tap timer expires while the fingers are + // down for the pinch but haven't started to move yet. + TimeDuration::FromMilliseconds(StaticPrefs::apz_max_tap_time() - + 90))); + + EXPECT_GT(apzc->GetFrameMetrics().GetZoom().scale, 1.0f); + apzc->AssertStateIsReset(); +} + +TEST_F(APZCGestureDetectorTester, TapFollowedByMultipleTouches) { + MakeApzcZoomable(); + + EXPECT_CALL(*mcc, HandleTap(TapType::eSingleTap, LayoutDevicePoint(10, 10), 0, + apzc->GetGuid(), _, _)) + .Times(1); + + Tap(apzc, ScreenIntPoint(10, 10), TimeDuration::FromMilliseconds(100)); + + int inputId = 0; + MultiTouchInput mti; + mti = CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_START, mcc->Time()); + mti.mTouches.AppendElement(SingleTouchData(inputId, ParentLayerPoint(20, 20), + ScreenSize(0, 0), 0, 0)); + apzc->ReceiveInputEvent(mti, Some(nsTArray<uint32_t>{kDefaultTouchBehavior})); + + mti = CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_START, mcc->Time()); + mti.mTouches.AppendElement(SingleTouchData(inputId, ParentLayerPoint(20, 20), + ScreenSize(0, 0), 0, 0)); + mti.mTouches.AppendElement(SingleTouchData( + inputId + 1, ParentLayerPoint(10, 10), ScreenSize(0, 0), 0, 0)); + apzc->ReceiveInputEvent(mti, Some(nsTArray<uint32_t>{kDefaultTouchBehavior})); + + mti = CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_END, mcc->Time()); + mti.mTouches.AppendElement(SingleTouchData(inputId, ParentLayerPoint(20, 20), + ScreenSize(0, 0), 0, 0)); + mti.mTouches.AppendElement(SingleTouchData( + inputId + 1, ParentLayerPoint(10, 10), ScreenSize(0, 0), 0, 0)); + apzc->ReceiveInputEvent(mti); + + apzc->AssertStateIsReset(); +} + +TEST_F(APZCGestureDetectorTester, LongPressInterruptedByWheel) { + // Since we try to allow concurrent input blocks of different types to + // co-exist, the wheel block shouldn't interrupt the long-press detection. + // But more importantly, this shouldn't crash, which is what it did at one + // point in time. + EXPECT_CALL(*mcc, HandleTap(TapType::eLongTap, _, _, _, _, _)).Times(1); + + APZEventResult result = TouchDown(apzc, ScreenIntPoint(10, 10), mcc->Time()); + uint64_t touchBlockId = result.mInputBlockId; + if (result.GetStatus() != nsEventStatus_eConsumeNoDefault) { + SetDefaultAllowedTouchBehavior(apzc, touchBlockId); + } + mcc->AdvanceByMillis(10); + uint64_t wheelBlockId = + Wheel(apzc, ScreenIntPoint(10, 10), ScreenPoint(0, -10), mcc->Time()) + .mInputBlockId; + EXPECT_NE(touchBlockId, wheelBlockId); + mcc->AdvanceByMillis(1000); +} + +TEST_F(APZCGestureDetectorTester, TapTimeoutInterruptedByWheel) { + // In this test, even though the wheel block comes right after the tap, the + // tap should still be dispatched because it completes fully before the wheel + // block arrived. + EXPECT_CALL(*mcc, HandleTap(TapType::eSingleTap, LayoutDevicePoint(10, 10), 0, + apzc->GetGuid(), _, _)) + .Times(1); + + // We make the APZC zoomable so the gesture detector needs to wait to + // distinguish between tap and double-tap. During that timeout is when we + // insert the wheel event. + MakeApzcZoomable(); + + APZEventResult result = Tap(apzc, ScreenIntPoint(10, 10), + TimeDuration::FromMilliseconds(100), nullptr); + mcc->AdvanceByMillis(10); + uint64_t wheelBlockId = + Wheel(apzc, ScreenIntPoint(10, 10), ScreenPoint(0, -10), mcc->Time()) + .mInputBlockId; + EXPECT_NE(result.mInputBlockId, wheelBlockId); + while (mcc->RunThroughDelayedTasks()) + ; +} + +TEST_F(APZCGestureDetectorTester, LongPressWithInputQueueDelay) { + // In this test, we ensure that any time spent waiting in the input queue for + // the content response is subtracted from the long-press timeout in the + // GestureEventListener. In this test the content response timeout is longer + // than the long-press timeout. + SCOPED_GFX_PREF_INT("apz.content_response_timeout", 60); + SCOPED_GFX_PREF_INT("ui.click_hold_context_menus.delay", 30); + + MakeApzcWaitForMainThread(); + + MockFunction<void(std::string checkPointName)> check; + + { + InSequence s; + EXPECT_CALL(check, Call("pre long-tap dispatch")); + EXPECT_CALL(*mcc, HandleTap(TapType::eLongTap, LayoutDevicePoint(10, 10), 0, + apzc->GetGuid(), _, _)) + .Times(1); + EXPECT_CALL(check, Call("post long-tap dispatch")); + } + + // Touch down + APZEventResult result = TouchDown(apzc, ScreenIntPoint(10, 10), mcc->Time()); + uint64_t touchBlockId = result.mInputBlockId; + // Simulate content response after 10ms + mcc->AdvanceByMillis(10); + apzc->ContentReceivedInputBlock(touchBlockId, false); + apzc->SetAllowedTouchBehavior(touchBlockId, {kDefaultTouchBehavior}); + apzc->ConfirmTarget(touchBlockId); + // Ensure long-tap event happens within 20ms after that + check.Call("pre long-tap dispatch"); + mcc->AdvanceByMillis(20); + check.Call("post long-tap dispatch"); +} + +TEST_F(APZCGestureDetectorTester, LongPressWithInputQueueDelay2) { + // Similar to the previous test, except this time we don't simulate the + // content response at all, and still expect the long-press to happen on + // schedule. + SCOPED_GFX_PREF_INT("apz.content_response_timeout", 60); + SCOPED_GFX_PREF_INT("ui.click_hold_context_menus.delay", 30); + + MakeApzcWaitForMainThread(); + + MockFunction<void(std::string checkPointName)> check; + + { + InSequence s; + EXPECT_CALL(check, Call("pre long-tap dispatch")); + EXPECT_CALL(*mcc, HandleTap(TapType::eLongTap, LayoutDevicePoint(10, 10), 0, + apzc->GetGuid(), _, _)) + .Times(1); + EXPECT_CALL(check, Call("post long-tap dispatch")); + } + + // Touch down + TouchDown(apzc, ScreenIntPoint(10, 10), mcc->Time()); + // Ensure the long-tap happens within 30ms even though there's no content + // response. + check.Call("pre long-tap dispatch"); + mcc->AdvanceByMillis(30); + check.Call("post long-tap dispatch"); +} + +TEST_F(APZCGestureDetectorTester, LongPressWithInputQueueDelay3) { + // Similar to the previous test, except now we have the long-press delay + // being longer than the content response timeout. + SCOPED_GFX_PREF_INT("apz.content_response_timeout", 30); + SCOPED_GFX_PREF_INT("ui.click_hold_context_menus.delay", 60); + + MakeApzcWaitForMainThread(); + + MockFunction<void(std::string checkPointName)> check; + + { + InSequence s; + EXPECT_CALL(check, Call("pre long-tap dispatch")); + EXPECT_CALL(*mcc, HandleTap(TapType::eLongTap, LayoutDevicePoint(10, 10), 0, + apzc->GetGuid(), _, _)) + .Times(1); + EXPECT_CALL(check, Call("post long-tap dispatch")); + } + + // Touch down + TouchDown(apzc, ScreenIntPoint(10, 10), mcc->Time()); + // Ensure the long-tap happens at the 60ms mark even though the input event + // waits in the input queue for the full content response timeout of 30ms + mcc->AdvanceByMillis(59); + check.Call("pre long-tap dispatch"); + mcc->AdvanceByMillis(1); + check.Call("post long-tap dispatch"); +} diff --git a/gfx/layers/apz/test/gtest/TestHitTesting.cpp b/gfx/layers/apz/test/gtest/TestHitTesting.cpp new file mode 100644 index 0000000000..b0609d4f0a --- /dev/null +++ b/gfx/layers/apz/test/gtest/TestHitTesting.cpp @@ -0,0 +1,352 @@ +/* -*- 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 APZHitTestingTester : public APZCTreeManagerTester { + protected: + ScreenToParentLayerMatrix4x4 transformToApzc; + ParentLayerToScreenMatrix4x4 transformToGecko; + + already_AddRefed<AsyncPanZoomController> GetTargetAPZC( + const ScreenPoint& aPoint) { + RefPtr<AsyncPanZoomController> hit = + manager->GetTargetAPZC(aPoint).mTargetApzc; + if (hit) { + transformToApzc = manager->GetScreenToApzcTransform(hit.get()); + transformToGecko = + manager->GetApzcToGeckoTransform(hit.get(), LayoutAndVisual); + } + return hit.forget(); + } + + protected: + void DisableApzOn(WebRenderLayerScrollData* aLayer) { + ModifyFrameMetrics(aLayer, [](ScrollMetadata& aSm, FrameMetrics&) { + aSm.SetForceDisableApz(true); + }); + } + + void CreateComplexMultiLayerTree() { + const char* treeShape = "x(xx(x)xx(x(x)xx))"; + // LayerID 0 12 3 45 6 7 89 + LayerIntRect layerVisibleRect[] = { + LayerIntRect(0, 0, 300, 400), // root(0) + LayerIntRect(0, 0, 100, 100), // layer(1) in top-left + LayerIntRect(50, 50, 200, 300), // layer(2) centered in root(0) + LayerIntRect(50, 50, 200, + 300), // layer(3) fully occupying parent layer(2) + LayerIntRect(0, 200, 100, 100), // layer(4) in bottom-left + LayerIntRect(200, 0, 100, + 400), // layer(5) along the right 100px of root(0) + LayerIntRect(200, 0, 100, 200), // layer(6) taking up the top + // half of parent layer(5) + LayerIntRect(200, 0, 100, + 200), // layer(7) fully occupying parent layer(6) + LayerIntRect(200, 200, 100, + 100), // layer(8) in bottom-right (below (6)) + LayerIntRect(200, 300, 100, + 100), // layer(9) in bottom-right (below (8)) + }; + CreateScrollData(treeShape, layerVisibleRect); + SetScrollableFrameMetrics(layers[1], ScrollableLayerGuid::START_SCROLL_ID); + SetScrollableFrameMetrics(layers[2], ScrollableLayerGuid::START_SCROLL_ID); + SetScrollableFrameMetrics(layers[4], + ScrollableLayerGuid::START_SCROLL_ID + 1); + SetScrollableFrameMetrics(layers[6], + ScrollableLayerGuid::START_SCROLL_ID + 1); + SetScrollableFrameMetrics(layers[7], + ScrollableLayerGuid::START_SCROLL_ID + 2); + SetScrollableFrameMetrics(layers[8], + ScrollableLayerGuid::START_SCROLL_ID + 1); + SetScrollableFrameMetrics(layers[9], + ScrollableLayerGuid::START_SCROLL_ID + 3); + } + + void CreateBug1148350LayerTree() { + const char* treeShape = "x(x)"; + // LayerID 0 1 + LayerIntRect layerVisibleRect[] = { + LayerIntRect(0, 0, 200, 200), + LayerIntRect(0, 0, 200, 200), + }; + CreateScrollData(treeShape, layerVisibleRect); + SetScrollableFrameMetrics(layers[1], ScrollableLayerGuid::START_SCROLL_ID); + } +}; + +TEST_F(APZHitTestingTester, ComplexMultiLayerTree) { + CreateComplexMultiLayerTree(); + ScopedLayerTreeRegistration registration(LayersId{0}, mcc); + UpdateHitTestingTree(); + + /* The layer tree looks like this: + + 0 + |----|--+--|----| + 1 2 4 5 + | /|\ + 3 6 8 9 + | + 7 + + Layers 1,2 have the same APZC + Layers 4,6,8 have the same APZC + Layer 7 has an APZC + Layer 9 has an APZC + */ + + TestAsyncPanZoomController* nullAPZC = nullptr; + // Ensure all the scrollable layers have an APZC + + EXPECT_FALSE(HasScrollableFrameMetrics(layers[0])); + EXPECT_NE(nullAPZC, ApzcOf(layers[1])); + EXPECT_NE(nullAPZC, ApzcOf(layers[2])); + EXPECT_FALSE(HasScrollableFrameMetrics(layers[3])); + EXPECT_NE(nullAPZC, ApzcOf(layers[4])); + EXPECT_FALSE(HasScrollableFrameMetrics(layers[5])); + EXPECT_NE(nullAPZC, ApzcOf(layers[6])); + EXPECT_NE(nullAPZC, ApzcOf(layers[7])); + EXPECT_NE(nullAPZC, ApzcOf(layers[8])); + EXPECT_NE(nullAPZC, ApzcOf(layers[9])); + // Ensure those that scroll together have the same APZCs + EXPECT_EQ(ApzcOf(layers[1]), ApzcOf(layers[2])); + EXPECT_EQ(ApzcOf(layers[4]), ApzcOf(layers[6])); + EXPECT_EQ(ApzcOf(layers[8]), ApzcOf(layers[6])); + // Ensure those that don't scroll together have different APZCs + EXPECT_NE(ApzcOf(layers[1]), ApzcOf(layers[4])); + EXPECT_NE(ApzcOf(layers[1]), ApzcOf(layers[7])); + EXPECT_NE(ApzcOf(layers[1]), ApzcOf(layers[9])); + EXPECT_NE(ApzcOf(layers[4]), ApzcOf(layers[7])); + EXPECT_NE(ApzcOf(layers[4]), ApzcOf(layers[9])); + EXPECT_NE(ApzcOf(layers[7]), ApzcOf(layers[9])); + // Ensure the APZC parent chains are set up correctly + TestAsyncPanZoomController* layers1_2 = ApzcOf(layers[1]); + TestAsyncPanZoomController* layers4_6_8 = ApzcOf(layers[4]); + TestAsyncPanZoomController* layer7 = ApzcOf(layers[7]); + TestAsyncPanZoomController* layer9 = ApzcOf(layers[9]); + EXPECT_EQ(nullptr, layers1_2->GetParent()); + EXPECT_EQ(nullptr, layers4_6_8->GetParent()); + EXPECT_EQ(layers4_6_8, layer7->GetParent()); + EXPECT_EQ(nullptr, layer9->GetParent()); + // Ensure the hit-testing tree looks like the layer tree + RefPtr<HitTestingTreeNode> root = manager->GetRootNode(); + RefPtr<HitTestingTreeNode> node5 = root->GetLastChild(); + RefPtr<HitTestingTreeNode> node4 = node5->GetPrevSibling(); + RefPtr<HitTestingTreeNode> node2 = node4->GetPrevSibling(); + RefPtr<HitTestingTreeNode> node1 = node2->GetPrevSibling(); + RefPtr<HitTestingTreeNode> node3 = node2->GetLastChild(); + RefPtr<HitTestingTreeNode> node9 = node5->GetLastChild(); + RefPtr<HitTestingTreeNode> node8 = node9->GetPrevSibling(); + RefPtr<HitTestingTreeNode> node6 = node8->GetPrevSibling(); + RefPtr<HitTestingTreeNode> node7 = node6->GetLastChild(); + EXPECT_EQ(nullptr, node1->GetPrevSibling()); + EXPECT_EQ(nullptr, node3->GetPrevSibling()); + EXPECT_EQ(nullptr, node6->GetPrevSibling()); + EXPECT_EQ(nullptr, node7->GetPrevSibling()); + EXPECT_EQ(nullptr, node1->GetLastChild()); + EXPECT_EQ(nullptr, node3->GetLastChild()); + EXPECT_EQ(nullptr, node4->GetLastChild()); + EXPECT_EQ(nullptr, node7->GetLastChild()); + EXPECT_EQ(nullptr, node8->GetLastChild()); + EXPECT_EQ(nullptr, node9->GetLastChild()); + + // Assertions about hit-testing have been ported to mochitest, + // in helper_hittest_bug1730606-4.html. +} + +TEST_F(APZHitTestingTester, TestRepaintFlushOnNewInputBlock) { + // The main purpose of this test is to verify that touch-start events (or + // anything that starts a new input block) don't ever get untransformed. This + // should always hold because the APZ code should flush repaints when we start + // a new input block and the transform to gecko space should be empty. + + CreateSimpleScrollingLayer(); + ScopedLayerTreeRegistration registration(LayersId{0}, mcc); + UpdateHitTestingTree(); + RefPtr<TestAsyncPanZoomController> apzcroot = ApzcOf(root); + + // At this point, the following holds (all coordinates in screen pixels): + // layers[0] has content from (0,0)-(500,500), clipped by composition bounds + // (0,0)-(200,200) + + MockFunction<void(std::string checkPointName)> check; + + { + InSequence s; + + EXPECT_CALL(*mcc, RequestContentRepaint(_)).Times(AtLeast(1)); + EXPECT_CALL(check, Call("post-first-touch-start")); + EXPECT_CALL(*mcc, RequestContentRepaint(_)).Times(AtLeast(1)); + EXPECT_CALL(check, Call("post-second-fling")); + EXPECT_CALL(*mcc, RequestContentRepaint(_)).Times(AtLeast(1)); + EXPECT_CALL(check, Call("post-second-touch-start")); + } + + // This first pan will move the APZC by 50 pixels, and dispatch a paint + // request. + Pan(apzcroot, 100, 50, PanOptions::NoFling); + + // Verify that a touch start doesn't get untransformed + ScreenIntPoint touchPoint(50, 50); + MultiTouchInput mti = + CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_START, mcc->Time()); + mti.mTouches.AppendElement( + SingleTouchData(0, touchPoint, ScreenSize(0, 0), 0, 0)); + + EXPECT_EQ(nsEventStatus_eConsumeDoDefault, + manager->ReceiveInputEvent(mti).GetStatus()); + EXPECT_EQ(touchPoint, mti.mTouches[0].mScreenPoint); + check.Call("post-first-touch-start"); + + // Send a touchend to clear state + mti.mType = MultiTouchInput::MULTITOUCH_END; + manager->ReceiveInputEvent(mti); + + mcc->AdvanceByMillis(1000); + + // Now do two pans. The first of these will dispatch a repaint request, as + // above. The second will get stuck in the paint throttler because the first + // one doesn't get marked as "completed", so this will result in a non-empty + // LD transform. (Note that any outstanding repaint requests from the first + // half of this test don't impact this half because we advance the time by 1 + // second, which will trigger the max-wait-exceeded codepath in the paint + // throttler). + Pan(apzcroot, 100, 50, PanOptions::NoFling); + check.Call("post-second-fling"); + Pan(apzcroot, 100, 50, PanOptions::NoFling); + + // Ensure that a touch start again doesn't get untransformed by flushing + // a repaint + mti.mType = MultiTouchInput::MULTITOUCH_START; + EXPECT_EQ(nsEventStatus_eConsumeDoDefault, + manager->ReceiveInputEvent(mti).GetStatus()); + EXPECT_EQ(touchPoint, mti.mTouches[0].mScreenPoint); + check.Call("post-second-touch-start"); + + mti.mType = MultiTouchInput::MULTITOUCH_END; + EXPECT_EQ(nsEventStatus_eConsumeDoDefault, + manager->ReceiveInputEvent(mti).GetStatus()); + EXPECT_EQ(touchPoint, mti.mTouches[0].mScreenPoint); +} + +TEST_F(APZHitTestingTester, TestRepaintFlushOnWheelEvents) { + // The purpose of this test is to ensure that wheel events trigger a repaint + // flush as per bug 1166871, and that the wheel event untransform is a no-op. + + CreateSimpleScrollingLayer(); + ScopedLayerTreeRegistration registration(LayersId{0}, mcc); + UpdateHitTestingTree(); + TestAsyncPanZoomController* apzcroot = ApzcOf(root); + + EXPECT_CALL(*mcc, RequestContentRepaint(_)).Times(AtLeast(3)); + ScreenPoint origin(100, 50); + for (int i = 0; i < 3; i++) { + ScrollWheelInput swi(mcc->Time(), 0, ScrollWheelInput::SCROLLMODE_INSTANT, + ScrollWheelInput::SCROLLDELTA_PIXEL, origin, 0, 10, + false, WheelDeltaAdjustmentStrategy::eNone); + EXPECT_EQ(nsEventStatus_eConsumeDoDefault, + manager->ReceiveInputEvent(swi).GetStatus()); + EXPECT_EQ(origin, swi.mOrigin); + + AsyncTransform viewTransform; + ParentLayerPoint point; + apzcroot->SampleContentTransformForFrame(&viewTransform, point); + EXPECT_EQ(0, point.x); + EXPECT_EQ((i + 1) * 10, point.y); + EXPECT_EQ(0, viewTransform.mTranslation.x); + EXPECT_EQ((i + 1) * -10, viewTransform.mTranslation.y); + + mcc->AdvanceByMillis(5); + } +} + +TEST_F(APZHitTestingTester, TestForceDisableApz) { + CreateSimpleScrollingLayer(); + ScopedLayerTreeRegistration registration(LayersId{0}, mcc); + UpdateHitTestingTree(); + DisableApzOn(root); + TestAsyncPanZoomController* apzcroot = ApzcOf(root); + + ScreenPoint origin(100, 50); + ScrollWheelInput swi(mcc->Time(), 0, ScrollWheelInput::SCROLLMODE_INSTANT, + ScrollWheelInput::SCROLLDELTA_PIXEL, origin, 0, 10, + false, WheelDeltaAdjustmentStrategy::eNone); + EXPECT_EQ(nsEventStatus_eConsumeDoDefault, + manager->ReceiveInputEvent(swi).GetStatus()); + EXPECT_EQ(origin, swi.mOrigin); + + AsyncTransform viewTransform; + ParentLayerPoint point; + apzcroot->SampleContentTransformForFrame(&viewTransform, point); + // Since APZ is force-disabled, we expect to see the async transform via + // the NORMAL AsyncMode, but not via the RESPECT_FORCE_DISABLE AsyncMode. + EXPECT_EQ(0, point.x); + EXPECT_EQ(10, point.y); + EXPECT_EQ(0, viewTransform.mTranslation.x); + EXPECT_EQ(-10, viewTransform.mTranslation.y); + viewTransform = apzcroot->GetCurrentAsyncTransform( + AsyncPanZoomController::eForCompositing); + point = apzcroot->GetCurrentAsyncScrollOffset( + AsyncPanZoomController::eForCompositing); + EXPECT_EQ(0, point.x); + EXPECT_EQ(0, point.y); + EXPECT_EQ(0, viewTransform.mTranslation.x); + EXPECT_EQ(0, viewTransform.mTranslation.y); + + mcc->AdvanceByMillis(10); + + // With untransforming events we should get normal behaviour (in this case, + // no noticeable untransform, because the repaint request already got + // flushed). + swi = ScrollWheelInput(mcc->Time(), 0, ScrollWheelInput::SCROLLMODE_INSTANT, + ScrollWheelInput::SCROLLDELTA_PIXEL, origin, 0, 0, + false, WheelDeltaAdjustmentStrategy::eNone); + EXPECT_EQ(nsEventStatus_eConsumeDoDefault, + manager->ReceiveInputEvent(swi).GetStatus()); + EXPECT_EQ(origin, swi.mOrigin); +} + +TEST_F(APZHitTestingTester, Bug1148350) { + CreateBug1148350LayerTree(); + ScopedLayerTreeRegistration registration(LayersId{0}, mcc); + UpdateHitTestingTree(); + + MockFunction<void(std::string checkPointName)> check; + { + InSequence s; + EXPECT_CALL(*mcc, + HandleTap(TapType::eSingleTap, LayoutDevicePoint(100, 100), 0, + ApzcOf(layers[1])->GetGuid(), _, _)) + .Times(1); + EXPECT_CALL(check, Call("Tapped without transform")); + EXPECT_CALL(*mcc, + HandleTap(TapType::eSingleTap, LayoutDevicePoint(100, 100), 0, + ApzcOf(layers[1])->GetGuid(), _, _)) + .Times(1); + EXPECT_CALL(check, Call("Tapped with interleaved transform")); + } + + Tap(manager, ScreenIntPoint(100, 100), TimeDuration::FromMilliseconds(100)); + mcc->RunThroughDelayedTasks(); + check.Call("Tapped without transform"); + + uint64_t blockId = + TouchDown(manager, ScreenIntPoint(100, 100), mcc->Time()).mInputBlockId; + SetDefaultAllowedTouchBehavior(manager, blockId); + mcc->AdvanceByMillis(100); + + layers[0]->SetVisibleRect(LayerIntRect(0, 50, 200, 150)); + layers[0]->SetTransform(Matrix4x4::Translation(0, 50, 0)); + UpdateHitTestingTree(); + + TouchUp(manager, ScreenIntPoint(100, 100), mcc->Time()); + mcc->RunThroughDelayedTasks(); + check.Call("Tapped with interleaved transform"); +} diff --git a/gfx/layers/apz/test/gtest/TestInputQueue.cpp b/gfx/layers/apz/test/gtest/TestInputQueue.cpp new file mode 100644 index 0000000000..18d1c00a2d --- /dev/null +++ b/gfx/layers/apz/test/gtest/TestInputQueue.cpp @@ -0,0 +1,48 @@ +/* -*- 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" + +// Test of scenario described in bug 1269067 - that a continuing mouse drag +// doesn't interrupt a wheel scrolling animation +TEST_F(APZCTreeManagerTester, WheelInterruptedByMouseDrag) { + // Needed because the test uses SmoothWheel() + SCOPED_GFX_PREF_BOOL("general.smoothScroll", true); + + // Set up a scrollable layer + CreateSimpleScrollingLayer(); + ScopedLayerTreeRegistration registration(LayersId{0}, mcc); + UpdateHitTestingTree(); + RefPtr<TestAsyncPanZoomController> apzc = ApzcOf(root); + + // First start the mouse drag + uint64_t dragBlockId = + MouseDown(apzc, ScreenIntPoint(5, 5), mcc->Time()).mInputBlockId; + uint64_t tmpBlockId = + MouseMove(apzc, ScreenIntPoint(6, 6), mcc->Time()).mInputBlockId; + EXPECT_EQ(dragBlockId, tmpBlockId); + + // Insert the wheel event, check that it has a new block id + uint64_t wheelBlockId = + SmoothWheel(apzc, ScreenIntPoint(6, 6), ScreenPoint(0, 1), mcc->Time()) + .mInputBlockId; + EXPECT_NE(dragBlockId, wheelBlockId); + + // Continue the drag, check that the block id is the same as before + tmpBlockId = MouseMove(apzc, ScreenIntPoint(7, 5), mcc->Time()).mInputBlockId; + EXPECT_EQ(dragBlockId, tmpBlockId); + + // Finish the wheel animation + apzc->AdvanceAnimationsUntilEnd(); + + // Check that it scrolled + ParentLayerPoint scroll = apzc->GetCurrentAsyncScrollOffset( + AsyncPanZoomController::eForEventHandling); + EXPECT_EQ(scroll.x, 0); + EXPECT_EQ(scroll.y, 10); // We scrolled 1 "line" or 10 pixels +} diff --git a/gfx/layers/apz/test/gtest/TestOverscroll.cpp b/gfx/layers/apz/test/gtest/TestOverscroll.cpp new file mode 100644 index 0000000000..7cfd8d10e1 --- /dev/null +++ b/gfx/layers/apz/test/gtest/TestOverscroll.cpp @@ -0,0 +1,2029 @@ +/* -*- 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 "APZCBasicTester.h" +#include "APZCTreeManagerTester.h" +#include "APZTestCommon.h" +#include "mozilla/layers/WebRenderScrollDataWrapper.h" + +#include "InputUtils.h" + +class APZCOverscrollTester : public APZCBasicTester { + public: + explicit APZCOverscrollTester( + AsyncPanZoomController::GestureBehavior aGestureBehavior = + AsyncPanZoomController::DEFAULT_GESTURES) + : APZCBasicTester(aGestureBehavior) {} + + protected: + UniquePtr<ScopedLayerTreeRegistration> registration; + + void TestOverscroll() { + // Pan sufficiently to hit overscroll behavior + PanIntoOverscroll(); + + // Check that we recover from overscroll via an animation. + ParentLayerPoint expectedScrollOffset(0, GetScrollRange().YMost()); + SampleAnimationUntilRecoveredFromOverscroll(expectedScrollOffset); + } + + void PanIntoOverscroll() { + int touchStart = 500; + int touchEnd = 10; + Pan(apzc, touchStart, touchEnd); + EXPECT_TRUE(apzc->IsOverscrolled()); + } + + /** + * Sample animations until we recover from overscroll. + * @param aExpectedScrollOffset the expected reported scroll offset + * throughout the animation + */ + void SampleAnimationUntilRecoveredFromOverscroll( + const ParentLayerPoint& aExpectedScrollOffset) { + const TimeDuration increment = TimeDuration::FromMilliseconds(1); + bool recoveredFromOverscroll = false; + ParentLayerPoint pointOut; + AsyncTransform viewTransformOut; + while (apzc->SampleContentTransformForFrame(&viewTransformOut, pointOut)) { + // The reported scroll offset should be the same throughout. + EXPECT_EQ(aExpectedScrollOffset, pointOut); + + // Trigger computation of the overscroll tranform, to make sure + // no assetions fire during the calculation. + apzc->GetOverscrollTransform(AsyncPanZoomController::eForEventHandling); + + if (!apzc->IsOverscrolled()) { + recoveredFromOverscroll = true; + } + + mcc->AdvanceBy(increment); + } + EXPECT_TRUE(recoveredFromOverscroll); + apzc->AssertStateIsReset(); + } + + ScrollableLayerGuid CreateSimpleRootScrollableForWebRender() { + ScrollableLayerGuid guid; + guid.mScrollId = ScrollableLayerGuid::START_SCROLL_ID; + guid.mLayersId = LayersId{0}; + + ScrollMetadata metadata; + FrameMetrics& metrics = metadata.GetMetrics(); + metrics.SetCompositionBounds(ParentLayerRect(0, 0, 100, 100)); + metrics.SetScrollableRect(CSSRect(0, 0, 100, 1000)); + metrics.SetScrollId(guid.mScrollId); + metadata.SetIsLayersIdRoot(true); + + WebRenderLayerScrollData rootLayerScrollData; + rootLayerScrollData.InitializeRoot(0); + WebRenderScrollData scrollData; + rootLayerScrollData.AppendScrollMetadata(scrollData, metadata); + scrollData.AddLayerData(std::move(rootLayerScrollData)); + + registration = MakeUnique<ScopedLayerTreeRegistration>(guid.mLayersId, mcc); + tm->UpdateHitTestingTree(WebRenderScrollDataWrapper(*updater, &scrollData), + false, guid.mLayersId, 0); + return guid; + } +}; + +#ifndef MOZ_WIDGET_ANDROID // Currently fails on Android +TEST_F(APZCOverscrollTester, FlingIntoOverscroll) { + // Enable overscrolling. + SCOPED_GFX_PREF_BOOL("apz.overscroll.enabled", true); + SCOPED_GFX_PREF_FLOAT("apz.fling_min_velocity_threshold", 0.0f); + + // Scroll down by 25 px. Don't fling for simplicity. + Pan(apzc, 50, 25, PanOptions::NoFling); + + // Now scroll back up by 20px, this time flinging after. + // The fling should cover the remaining 5 px of room to scroll, then + // go into overscroll, and finally snap-back to recover from overscroll. + Pan(apzc, 25, 45); + const TimeDuration increment = TimeDuration::FromMilliseconds(1); + bool reachedOverscroll = false; + bool recoveredFromOverscroll = false; + while (apzc->AdvanceAnimations(mcc->GetSampleTime())) { + if (!reachedOverscroll && apzc->IsOverscrolled()) { + reachedOverscroll = true; + } + if (reachedOverscroll && !apzc->IsOverscrolled()) { + recoveredFromOverscroll = true; + } + mcc->AdvanceBy(increment); + } + EXPECT_TRUE(reachedOverscroll); + EXPECT_TRUE(recoveredFromOverscroll); +} +#endif + +#ifndef MOZ_WIDGET_ANDROID // Currently fails on Android +TEST_F(APZCOverscrollTester, OverScrollPanning) { + SCOPED_GFX_PREF_BOOL("apz.overscroll.enabled", true); + + TestOverscroll(); +} +#endif + +#ifndef MOZ_WIDGET_ANDROID // Currently fails on Android +// Tests that an overscroll animation doesn't trigger an assertion failure +// in the case where a sample has a velocity of zero. +TEST_F(APZCOverscrollTester, OverScroll_Bug1152051a) { + SCOPED_GFX_PREF_BOOL("apz.overscroll.enabled", true); + + // Doctor the prefs to make the velocity zero at the end of the first sample. + + // This ensures our incoming velocity to the overscroll animation is + // a round(ish) number, 4.9 (that being the distance of the pan before + // overscroll, which is 500 - 10 = 490 pixels, divided by the duration of + // the pan, which is 100 ms). + SCOPED_GFX_PREF_FLOAT("apz.fling_friction", 0); + + TestOverscroll(); +} +#endif + +#ifndef MOZ_WIDGET_ANDROID // Currently fails on Android +// Tests that ending an overscroll animation doesn't leave around state that +// confuses the next overscroll animation. +TEST_F(APZCOverscrollTester, OverScroll_Bug1152051b) { + SCOPED_GFX_PREF_BOOL("apz.overscroll.enabled", true); + SCOPED_GFX_PREF_FLOAT("apz.overscroll.stop_distance_threshold", 0.1f); + + // Pan sufficiently to hit overscroll behavior + PanIntoOverscroll(); + + // Sample animations once, to give the fling animation started on touch-up + // a chance to realize it's overscrolled, and schedule a call to + // HandleFlingOverscroll(). + SampleAnimationOnce(); + + // This advances the time and runs the HandleFlingOverscroll task scheduled in + // the previous call, which starts an overscroll animation. It then samples + // the overscroll animation once, to get it to initialize the first overscroll + // sample. + SampleAnimationOnce(); + + // Do a touch-down to cancel the overscroll animation, and then a touch-up + // to schedule a new one since we're still overscrolled. We don't pan because + // panning can trigger functions that clear the overscroll animation state + // in other ways. + APZEventResult result = TouchDown(apzc, ScreenIntPoint(10, 10), mcc->Time()); + if (result.GetStatus() != nsEventStatus_eConsumeNoDefault) { + SetDefaultAllowedTouchBehavior(apzc, result.mInputBlockId); + } + TouchUp(apzc, ScreenIntPoint(10, 10), mcc->Time()); + + // Sample the second overscroll animation to its end. + // If the ending of the first overscroll animation fails to clear state + // properly, this will assert. + ParentLayerPoint expectedScrollOffset(0, GetScrollRange().YMost()); + SampleAnimationUntilRecoveredFromOverscroll(expectedScrollOffset); +} +#endif + +#ifndef MOZ_WIDGET_ANDROID // Currently fails on Android +// Tests that the page doesn't get stuck in an +// overscroll animation after a low-velocity pan. +TEST_F(APZCOverscrollTester, OverScrollAfterLowVelocityPan_Bug1343775) { + SCOPED_GFX_PREF_BOOL("apz.overscroll.enabled", true); + + // Pan into overscroll with a velocity less than the + // apz.fling_min_velocity_threshold preference. + Pan(apzc, 10, 30); + + EXPECT_TRUE(apzc->IsOverscrolled()); + + apzc->AdvanceAnimationsUntilEnd(); + + // Check that we recovered from overscroll. + EXPECT_FALSE(apzc->IsOverscrolled()); +} +#endif + +#ifndef MOZ_WIDGET_ANDROID // Currently fails on Android +TEST_F(APZCOverscrollTester, OverScrollAbort) { + SCOPED_GFX_PREF_BOOL("apz.overscroll.enabled", true); + + // Pan sufficiently to hit overscroll behavior + int touchStart = 500; + int touchEnd = 10; + Pan(apzc, touchStart, touchEnd); + EXPECT_TRUE(apzc->IsOverscrolled()); + + ParentLayerPoint pointOut; + AsyncTransform viewTransformOut; + + // This sample call will run to the end of the fling animation + // and will schedule the overscroll animation. + apzc->SampleContentTransformForFrame(&viewTransformOut, pointOut, + TimeDuration::FromMilliseconds(10000)); + EXPECT_TRUE(apzc->IsOverscrolled()); + + // At this point, we have an active overscroll animation. + // Check that cancelling the animation clears the overscroll. + apzc->CancelAnimation(); + EXPECT_FALSE(apzc->IsOverscrolled()); + apzc->AssertStateIsReset(); +} +#endif + +#ifndef MOZ_WIDGET_ANDROID // Currently fails on Android +TEST_F(APZCOverscrollTester, OverScrollPanningAbort) { + SCOPED_GFX_PREF_BOOL("apz.overscroll.enabled", true); + + // Pan sufficiently to hit overscroll behaviour. Keep the finger down so + // the pan does not end. + int touchStart = 500; + int touchEnd = 10; + Pan(apzc, touchStart, touchEnd, PanOptions::KeepFingerDown); + EXPECT_TRUE(apzc->IsOverscrolled()); + + // Check that calling CancelAnimation() while the user is still panning + // (and thus no fling or snap-back animation has had a chance to start) + // clears the overscroll. + apzc->CancelAnimation(); + EXPECT_FALSE(apzc->IsOverscrolled()); + apzc->AssertStateIsReset(); +} +#endif + +#ifndef MOZ_WIDGET_ANDROID // Maybe fails on Android +TEST_F(APZCOverscrollTester, OverscrollByVerticalPanGestures) { + SCOPED_GFX_PREF_BOOL("apz.overscroll.enabled", true); + + PanGesture(PanGestureInput::PANGESTURE_START, apzc, ScreenIntPoint(50, 80), + ScreenPoint(0, -2), mcc->Time()); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 80), + ScreenPoint(0, -10), mcc->Time()); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 80), + ScreenPoint(0, -2), mcc->Time()); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + PanGesture(PanGestureInput::PANGESTURE_END, apzc, ScreenIntPoint(50, 80), + ScreenPoint(0, 0), mcc->Time()); + + EXPECT_TRUE(apzc->IsOverscrolled()); + + // Check that we recover from overscroll via an animation. + ParentLayerPoint expectedScrollOffset(0, 0); + SampleAnimationUntilRecoveredFromOverscroll(expectedScrollOffset); +} +#endif + +#ifndef MOZ_WIDGET_ANDROID // Currently fails on Android +TEST_F(APZCOverscrollTester, StuckInOverscroll_Bug1767337) { + SCOPED_GFX_PREF_BOOL("apz.overscroll.enabled", true); + + PanGesture(PanGestureInput::PANGESTURE_START, apzc, ScreenIntPoint(50, 80), + ScreenPoint(0, -2), mcc->Time()); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 80), + ScreenPoint(0, -10), mcc->Time()); + mcc->AdvanceByMillis(5); + PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 80), + ScreenPoint(0, -10), mcc->Time()); + mcc->AdvanceByMillis(5); + PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 80), + ScreenPoint(0, -10), mcc->Time()); + mcc->AdvanceByMillis(5); + PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 80), + ScreenPoint(0, -10), mcc->Time()); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 80), + ScreenPoint(0, -2), mcc->Time()); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + + // Send two PANGESTURE_END in a row, to see if the second one gets us + // stuck in overscroll. + PanGesture(PanGestureInput::PANGESTURE_END, apzc, ScreenIntPoint(50, 80), + ScreenPoint(0, 0), mcc->Time(), MODIFIER_NONE, true); + SampleAnimationOnce(); + PanGesture(PanGestureInput::PANGESTURE_END, apzc, ScreenIntPoint(50, 80), + ScreenPoint(0, 0), mcc->Time(), MODIFIER_NONE, true); + + EXPECT_TRUE(apzc->IsOverscrolled()); + + // Check that we recover from overscroll via an animation. + ParentLayerPoint expectedScrollOffset(0, 0); + SampleAnimationUntilRecoveredFromOverscroll(expectedScrollOffset); +} +#endif + +#ifndef MOZ_WIDGET_ANDROID // Currently fails on Android +TEST_F(APZCOverscrollTester, OverscrollByVerticalAndHorizontalPanGestures) { + SCOPED_GFX_PREF_BOOL("apz.overscroll.enabled", true); + + PanGesture(PanGestureInput::PANGESTURE_START, apzc, ScreenIntPoint(50, 80), + ScreenPoint(0, -2), mcc->Time()); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 80), + ScreenPoint(0, -10), mcc->Time()); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 80), + ScreenPoint(0, -2), mcc->Time()); + + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 80), + ScreenPoint(-10, 0), mcc->Time()); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 80), + ScreenPoint(-2, 0), mcc->Time()); + + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + PanGesture(PanGestureInput::PANGESTURE_END, apzc, ScreenIntPoint(50, 80), + ScreenPoint(0, 0), mcc->Time()); + + EXPECT_TRUE(apzc->IsOverscrolled()); + + // Check that we recover from overscroll via an animation. + ParentLayerPoint expectedScrollOffset(0, 0); + SampleAnimationUntilRecoveredFromOverscroll(expectedScrollOffset); +} +#endif + +#ifndef MOZ_WIDGET_ANDROID // Currently fails on Android +TEST_F(APZCOverscrollTester, OverscrollByPanMomentumGestures) { + SCOPED_GFX_PREF_BOOL("apz.overscroll.enabled", true); + + PanGesture(PanGestureInput::PANGESTURE_START, apzc, ScreenIntPoint(50, 80), + ScreenPoint(0, 2), mcc->Time()); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 80), + ScreenPoint(0, 10), mcc->Time()); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 80), + ScreenPoint(0, 2), mcc->Time()); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + PanGesture(PanGestureInput::PANGESTURE_END, apzc, ScreenIntPoint(50, 80), + ScreenPoint(0, 0), mcc->Time()); + + // Make sure we are not yet in overscrolled region. + EXPECT_TRUE(!apzc->IsOverscrolled()); + + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + PanGesture(PanGestureInput::PANGESTURE_MOMENTUMSTART, apzc, + ScreenIntPoint(50, 80), ScreenPoint(0, 0), mcc->Time()); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + PanGesture(PanGestureInput::PANGESTURE_MOMENTUMPAN, apzc, + ScreenIntPoint(50, 80), ScreenPoint(0, 200), mcc->Time()); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + PanGesture(PanGestureInput::PANGESTURE_MOMENTUMPAN, apzc, + ScreenIntPoint(50, 80), ScreenPoint(0, 100), mcc->Time()); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + PanGesture(PanGestureInput::PANGESTURE_MOMENTUMPAN, apzc, + ScreenIntPoint(50, 80), ScreenPoint(0, 2), mcc->Time()); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + PanGesture(PanGestureInput::PANGESTURE_MOMENTUMEND, apzc, + ScreenIntPoint(50, 80), ScreenPoint(0, 0), mcc->Time()); + + EXPECT_TRUE(apzc->IsOverscrolled()); + + // Check that we recover from overscroll via an animation. + ParentLayerPoint expectedScrollOffset(0, GetScrollRange().YMost()); + SampleAnimationUntilRecoveredFromOverscroll(expectedScrollOffset); +} +#endif + +#ifndef MOZ_WIDGET_ANDROID // Currently fails on Android +TEST_F(APZCOverscrollTester, IgnoreMomemtumDuringOverscroll) { + SCOPED_GFX_PREF_BOOL("apz.overscroll.enabled", true); + + float yMost = GetScrollRange().YMost(); + PanGesture(PanGestureInput::PANGESTURE_START, apzc, ScreenIntPoint(50, 80), + ScreenPoint(0, yMost / 10), mcc->Time()); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 80), + ScreenPoint(0, yMost), mcc->Time()); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 80), + ScreenPoint(0, yMost / 10), mcc->Time()); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + PanGesture(PanGestureInput::PANGESTURE_END, apzc, ScreenIntPoint(50, 80), + ScreenPoint(0, 0), mcc->Time()); + + // Make sure we've started an overscroll animation. + EXPECT_TRUE(apzc->IsOverscrolled()); + EXPECT_TRUE(apzc->IsOverscrollAnimationRunning()); + + // And check the overscrolled transform value before/after calling PanGesture + // to make sure the overscroll amount isn't affected by momentum events. + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + AsyncTransformComponentMatrix overscrolledTransform = + apzc->GetOverscrollTransform(AsyncPanZoomController::eForEventHandling); + PanGesture(PanGestureInput::PANGESTURE_MOMENTUMSTART, apzc, + ScreenIntPoint(50, 80), ScreenPoint(0, 0), mcc->Time()); + EXPECT_EQ( + overscrolledTransform, + apzc->GetOverscrollTransform(AsyncPanZoomController::eForEventHandling)); + + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + overscrolledTransform = + apzc->GetOverscrollTransform(AsyncPanZoomController::eForEventHandling); + PanGesture(PanGestureInput::PANGESTURE_MOMENTUMPAN, apzc, + ScreenIntPoint(50, 80), ScreenPoint(0, 200), mcc->Time()); + EXPECT_EQ( + overscrolledTransform, + apzc->GetOverscrollTransform(AsyncPanZoomController::eForEventHandling)); + + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + overscrolledTransform = + apzc->GetOverscrollTransform(AsyncPanZoomController::eForEventHandling); + PanGesture(PanGestureInput::PANGESTURE_MOMENTUMPAN, apzc, + ScreenIntPoint(50, 80), ScreenPoint(0, 100), mcc->Time()); + EXPECT_EQ( + overscrolledTransform, + apzc->GetOverscrollTransform(AsyncPanZoomController::eForEventHandling)); + + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + overscrolledTransform = + apzc->GetOverscrollTransform(AsyncPanZoomController::eForEventHandling); + PanGesture(PanGestureInput::PANGESTURE_MOMENTUMPAN, apzc, + ScreenIntPoint(50, 80), ScreenPoint(0, 2), mcc->Time()); + EXPECT_EQ( + overscrolledTransform, + apzc->GetOverscrollTransform(AsyncPanZoomController::eForEventHandling)); + + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + overscrolledTransform = + apzc->GetOverscrollTransform(AsyncPanZoomController::eForEventHandling); + PanGesture(PanGestureInput::PANGESTURE_MOMENTUMEND, apzc, + ScreenIntPoint(50, 80), ScreenPoint(0, 0), mcc->Time()); + EXPECT_EQ( + overscrolledTransform, + apzc->GetOverscrollTransform(AsyncPanZoomController::eForEventHandling)); + + // Check that we've recovered from overscroll via an animation. + ParentLayerPoint expectedScrollOffset(0, GetScrollRange().YMost()); + SampleAnimationUntilRecoveredFromOverscroll(expectedScrollOffset); +} +#endif + +#ifndef MOZ_WIDGET_ANDROID // Currently fails on Android +TEST_F(APZCOverscrollTester, VerticalOnlyOverscroll) { + SCOPED_GFX_PREF_BOOL("apz.overscroll.enabled", true); + + // Make the content scrollable only vertically. + ScrollMetadata metadata; + FrameMetrics& metrics = metadata.GetMetrics(); + metrics.SetCompositionBounds(ParentLayerRect(0, 0, 100, 100)); + metrics.SetScrollableRect(CSSRect(0, 0, 100, 1000)); + apzc->SetFrameMetrics(metrics); + + // Scroll up into overscroll a bit. + PanGesture(PanGestureInput::PANGESTURE_START, apzc, ScreenIntPoint(50, 80), + ScreenPoint(-2, -2), mcc->Time()); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 80), + ScreenPoint(-10, -10), mcc->Time()); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 80), + ScreenPoint(-2, -2), mcc->Time()); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + PanGesture(PanGestureInput::PANGESTURE_END, apzc, ScreenIntPoint(50, 80), + ScreenPoint(0, 0), mcc->Time()); + // Now it's overscrolled. + EXPECT_TRUE(apzc->IsOverscrolled()); + AsyncTransformComponentMatrix overscrolledTransform = + apzc->GetOverscrollTransform(AsyncPanZoomController::eForEventHandling); + // The overscroll shouldn't happen horizontally. + EXPECT_TRUE(overscrolledTransform._41 == 0); + // Happens only vertically. + EXPECT_TRUE(overscrolledTransform._42 != 0); + + // Send pan momentum events including horizontal bits. + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + PanGesture(PanGestureInput::PANGESTURE_MOMENTUMSTART, apzc, + ScreenIntPoint(50, 80), ScreenPoint(0, 0), mcc->Time()); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + PanGesture(PanGestureInput::PANGESTURE_MOMENTUMPAN, apzc, + ScreenIntPoint(50, 80), ScreenPoint(-10, -100), mcc->Time()); + overscrolledTransform = + apzc->GetOverscrollTransform(AsyncPanZoomController::eForEventHandling); + // The overscroll shouldn't happen horizontally. + EXPECT_TRUE(overscrolledTransform._41 == 0); + EXPECT_TRUE(overscrolledTransform._42 != 0); + + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + PanGesture(PanGestureInput::PANGESTURE_MOMENTUMPAN, apzc, + ScreenIntPoint(50, 80), ScreenPoint(-5, -50), mcc->Time()); + overscrolledTransform = + apzc->GetOverscrollTransform(AsyncPanZoomController::eForEventHandling); + EXPECT_TRUE(overscrolledTransform._41 == 0); + EXPECT_TRUE(overscrolledTransform._42 != 0); + + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + PanGesture(PanGestureInput::PANGESTURE_MOMENTUMPAN, apzc, + ScreenIntPoint(50, 80), ScreenPoint(0, -2), mcc->Time()); + overscrolledTransform = + apzc->GetOverscrollTransform(AsyncPanZoomController::eForEventHandling); + EXPECT_TRUE(overscrolledTransform._41 == 0); + EXPECT_TRUE(overscrolledTransform._42 != 0); + + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + PanGesture(PanGestureInput::PANGESTURE_MOMENTUMEND, apzc, + ScreenIntPoint(50, 80), ScreenPoint(0, 0), mcc->Time()); + overscrolledTransform = + apzc->GetOverscrollTransform(AsyncPanZoomController::eForEventHandling); + EXPECT_TRUE(overscrolledTransform._41 == 0); + EXPECT_TRUE(overscrolledTransform._42 != 0); + + // Check that we recover from overscroll via an animation. + ParentLayerPoint expectedScrollOffset(0, 0); + SampleAnimationUntilRecoveredFromOverscroll(expectedScrollOffset); +} +#endif + +#ifndef MOZ_WIDGET_ANDROID // Currently fails on Android +TEST_F(APZCOverscrollTester, VerticalOnlyOverscrollByPanMomentum) { + SCOPED_GFX_PREF_BOOL("apz.overscroll.enabled", true); + + // Make the content scrollable only vertically. + ScrollMetadata metadata; + FrameMetrics& metrics = metadata.GetMetrics(); + metrics.SetCompositionBounds(ParentLayerRect(0, 0, 100, 100)); + metrics.SetScrollableRect(CSSRect(0, 0, 100, 1000)); + // Scrolls the content down a bit. + metrics.SetVisualScrollOffset(CSSPoint(0, 50)); + apzc->SetFrameMetrics(metrics); + + // Scroll up a bit where overscroll will not happen. + PanGesture(PanGestureInput::PANGESTURE_START, apzc, ScreenIntPoint(50, 80), + ScreenPoint(0, -2), mcc->Time()); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 80), + ScreenPoint(0, -10), mcc->Time()); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 80), + ScreenPoint(0, -2), mcc->Time()); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + PanGesture(PanGestureInput::PANGESTURE_END, apzc, ScreenIntPoint(50, 80), + ScreenPoint(0, 0), mcc->Time()); + + // Make sure it's not yet overscrolled. + EXPECT_TRUE(!apzc->IsOverscrolled()); + + // Send pan momentum events including horizontal bits. + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + PanGesture(PanGestureInput::PANGESTURE_MOMENTUMSTART, apzc, + ScreenIntPoint(50, 80), ScreenPoint(0, 0), mcc->Time()); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + PanGesture(PanGestureInput::PANGESTURE_MOMENTUMPAN, apzc, + ScreenIntPoint(50, 80), ScreenPoint(-10, -100), mcc->Time()); + // Now it's overscrolled. + EXPECT_TRUE(apzc->IsOverscrolled()); + + AsyncTransformComponentMatrix overscrolledTransform = + apzc->GetOverscrollTransform(AsyncPanZoomController::eForEventHandling); + // But the overscroll shouldn't happen horizontally. + EXPECT_TRUE(overscrolledTransform._41 == 0); + // Happens only vertically. + EXPECT_TRUE(overscrolledTransform._42 != 0); + + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + PanGesture(PanGestureInput::PANGESTURE_MOMENTUMPAN, apzc, + ScreenIntPoint(50, 80), ScreenPoint(-5, -50), mcc->Time()); + overscrolledTransform = + apzc->GetOverscrollTransform(AsyncPanZoomController::eForEventHandling); + EXPECT_TRUE(overscrolledTransform._41 == 0); + EXPECT_TRUE(overscrolledTransform._42 != 0); + + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + PanGesture(PanGestureInput::PANGESTURE_MOMENTUMPAN, apzc, + ScreenIntPoint(50, 80), ScreenPoint(0, -2), mcc->Time()); + overscrolledTransform = + apzc->GetOverscrollTransform(AsyncPanZoomController::eForEventHandling); + EXPECT_TRUE(overscrolledTransform._41 == 0); + EXPECT_TRUE(overscrolledTransform._42 != 0); + + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + PanGesture(PanGestureInput::PANGESTURE_MOMENTUMEND, apzc, + ScreenIntPoint(50, 80), ScreenPoint(0, 0), mcc->Time()); + overscrolledTransform = + apzc->GetOverscrollTransform(AsyncPanZoomController::eForEventHandling); + EXPECT_TRUE(overscrolledTransform._41 == 0); + EXPECT_TRUE(overscrolledTransform._42 != 0); + + // Check that we recover from overscroll via an animation. + ParentLayerPoint expectedScrollOffset(0, 0); + SampleAnimationUntilRecoveredFromOverscroll(expectedScrollOffset); +} +#endif + +#ifndef MOZ_WIDGET_ANDROID // Currently fails on Android +TEST_F(APZCOverscrollTester, DisallowOverscrollInSingleLineTextControl) { + SCOPED_GFX_PREF_BOOL("apz.overscroll.enabled", true); + + // Create a horizontal scrollable frame with `vertical disregarded direction`. + ScrollMetadata metadata; + FrameMetrics& metrics = metadata.GetMetrics(); + metrics.SetCompositionBounds(ParentLayerRect(0, 0, 100, 10)); + metrics.SetScrollableRect(CSSRect(0, 0, 1000, 10)); + apzc->SetFrameMetrics(metrics); + metadata.SetDisregardedDirection(Some(ScrollDirection::eVertical)); + apzc->NotifyLayersUpdated(metadata, /*aIsFirstPaint=*/false, + /*aThisLayerTreeUpdated=*/true); + + // Try to overscroll up and left with pan gestures. + PanGesture(PanGestureInput::PANGESTURE_START, apzc, ScreenIntPoint(50, 5), + ScreenPoint(-2, -2), mcc->Time()); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 5), + ScreenPoint(-10, -10), mcc->Time()); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 5), + ScreenPoint(-2, -2), mcc->Time()); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + PanGesture(PanGestureInput::PANGESTURE_END, apzc, ScreenIntPoint(50, 5), + ScreenPoint(0, 0), mcc->Time()); + + // No overscrolling should happen. + EXPECT_TRUE(!apzc->IsOverscrolled()); + + // Send pan momentum events too. + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + PanGesture(PanGestureInput::PANGESTURE_MOMENTUMSTART, apzc, + ScreenIntPoint(50, 5), ScreenPoint(0, 0), mcc->Time()); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + PanGesture(PanGestureInput::PANGESTURE_MOMENTUMPAN, apzc, + ScreenIntPoint(50, 5), ScreenPoint(-100, -100), mcc->Time()); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + PanGesture(PanGestureInput::PANGESTURE_MOMENTUMPAN, apzc, + ScreenIntPoint(50, 5), ScreenPoint(-50, -50), mcc->Time()); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + PanGesture(PanGestureInput::PANGESTURE_MOMENTUMPAN, apzc, + ScreenIntPoint(50, 5), ScreenPoint(-2, -2), mcc->Time()); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + PanGesture(PanGestureInput::PANGESTURE_MOMENTUMEND, apzc, + ScreenIntPoint(50, 5), ScreenPoint(0, 0), mcc->Time()); + // No overscrolling should happen either. + EXPECT_TRUE(!apzc->IsOverscrolled()); +} +#endif + +#ifndef MOZ_WIDGET_ANDROID // Maybe fails on Android +// Tests that horizontal overscroll animation keeps running with vertical +// pan momentum scrolling. +TEST_F(APZCOverscrollTester, + HorizontalOverscrollAnimationWithVerticalPanMomentumScrolling) { + SCOPED_GFX_PREF_BOOL("apz.overscroll.enabled", true); + + ScrollMetadata metadata; + FrameMetrics& metrics = metadata.GetMetrics(); + metrics.SetCompositionBounds(ParentLayerRect(0, 0, 100, 100)); + metrics.SetScrollableRect(CSSRect(0, 0, 1000, 5000)); + apzc->SetFrameMetrics(metrics); + + // Try to overscroll left with pan gestures. + PanGesture(PanGestureInput::PANGESTURE_START, apzc, ScreenIntPoint(50, 80), + ScreenPoint(-2, 0), mcc->Time()); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 80), + ScreenPoint(-10, 0), mcc->Time()); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 80), + ScreenPoint(-2, 0), mcc->Time()); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + PanGesture(PanGestureInput::PANGESTURE_END, apzc, ScreenIntPoint(50, 80), + ScreenPoint(0, 0), mcc->Time()); + + // Make sure we've started an overscroll animation. + EXPECT_TRUE(apzc->IsOverscrolled()); + EXPECT_TRUE(apzc->IsOverscrollAnimationRunning()); + AsyncTransformComponentMatrix initialOverscrolledTransform = + apzc->GetOverscrollTransform(AsyncPanZoomController::eForEventHandling); + + // Send lengthy downward momentums to make sure the overscroll animation + // doesn't clobber the momentums scrolling. + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + // The overscroll amount on X axis has started being managed by the overscroll + // animation. + AsyncTransformComponentMatrix currentOverscrolledTransform = + apzc->GetOverscrollTransform(AsyncPanZoomController::eForEventHandling); + EXPECT_NE(initialOverscrolledTransform._41, currentOverscrolledTransform._41); + // There is no overscroll on Y axis. + EXPECT_EQ(currentOverscrolledTransform._42, 0); + ParentLayerPoint scrollOffset = apzc->GetCurrentAsyncScrollOffset( + AsyncPanZoomController::eForEventHandling); + // The scroll offset shouldn't be changed by the overscroll animation. + EXPECT_EQ(scrollOffset.y, 0); + + // Simple gesture on the Y axis to ensure that we can send a vertical + // momentum scroll + PanGesture(PanGestureInput::PANGESTURE_START, apzc, ScreenIntPoint(50, 80), + ScreenPoint(0, -2), mcc->Time()); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 80), + ScreenPoint(0, 2), mcc->Time()); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + PanGesture(PanGestureInput::PANGESTURE_END, apzc, ScreenIntPoint(50, 80), + ScreenPoint(0, 0), mcc->Time()); + + ParentLayerPoint offsetAfterPan = apzc->GetCurrentAsyncScrollOffset( + AsyncPanZoomController::eForEventHandling); + + PanGesture(PanGestureInput::PANGESTURE_MOMENTUMSTART, apzc, + ScreenIntPoint(50, 80), ScreenPoint(0, 0), mcc->Time()); + EXPECT_TRUE(apzc->IsOverscrolled()); + EXPECT_TRUE(apzc->IsOverscrollAnimationRunning()); + // The overscroll amount on both axes shouldn't be changed by this pan + // momentum start event since the displacement is zero. + EXPECT_EQ( + currentOverscrolledTransform._41, + apzc->GetOverscrollTransform(AsyncPanZoomController::eForEventHandling) + ._41); + EXPECT_EQ( + currentOverscrolledTransform._42, + apzc->GetOverscrollTransform(AsyncPanZoomController::eForEventHandling) + ._42); + + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + // The overscroll amount should be managed by the overscroll animation. + EXPECT_NE( + currentOverscrolledTransform._41, + apzc->GetOverscrollTransform(AsyncPanZoomController::eForEventHandling) + ._41); + scrollOffset = apzc->GetCurrentAsyncScrollOffset( + AsyncPanZoomController::eForEventHandling); + // Not yet started scrolling. + EXPECT_EQ(scrollOffset.y, offsetAfterPan.y); + EXPECT_EQ(scrollOffset.x, 0); + + currentOverscrolledTransform = + apzc->GetOverscrollTransform(AsyncPanZoomController::eForEventHandling); + + // Send a long pan momentum. + PanGesture(PanGestureInput::PANGESTURE_MOMENTUMPAN, apzc, + ScreenIntPoint(50, 80), ScreenPoint(0, 200), mcc->Time()); + EXPECT_TRUE(apzc->IsOverscrolled()); + EXPECT_TRUE(apzc->IsOverscrollAnimationRunning()); + // The overscroll amount on X axis shouldn't be changed by this momentum pan. + EXPECT_EQ( + currentOverscrolledTransform._41, + apzc->GetOverscrollTransform(AsyncPanZoomController::eForEventHandling) + ._41); + // Now it started scrolling vertically. + scrollOffset = apzc->GetCurrentAsyncScrollOffset( + AsyncPanZoomController::eForEventHandling); + EXPECT_GT(scrollOffset.y, 0); + EXPECT_EQ(scrollOffset.x, 0); + + currentOverscrolledTransform = + apzc->GetOverscrollTransform(AsyncPanZoomController::eForEventHandling); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + // The overscroll on X axis keeps being managed by the overscroll animation. + EXPECT_NE( + currentOverscrolledTransform._41, + apzc->GetOverscrollTransform(AsyncPanZoomController::eForEventHandling) + ._41); + // The scroll offset on Y axis shouldn't be changed by the overscroll + // animation. + EXPECT_EQ(scrollOffset.y, apzc->GetCurrentAsyncScrollOffset( + AsyncPanZoomController::eForEventHandling) + .y); + + currentOverscrolledTransform = + apzc->GetOverscrollTransform(AsyncPanZoomController::eForEventHandling); + scrollOffset = apzc->GetCurrentAsyncScrollOffset( + AsyncPanZoomController::eForEventHandling); + PanGesture(PanGestureInput::PANGESTURE_MOMENTUMPAN, apzc, + ScreenIntPoint(50, 80), ScreenPoint(0, 100), mcc->Time()); + EXPECT_TRUE(apzc->IsOverscrolled()); + EXPECT_TRUE(apzc->IsOverscrollAnimationRunning()); + // The overscroll amount on X axis shouldn't be changed by this momentum pan. + EXPECT_EQ( + currentOverscrolledTransform._41, + apzc->GetOverscrollTransform(AsyncPanZoomController::eForEventHandling) + ._41); + // Scrolling keeps going by momentum. + EXPECT_GT(apzc->GetCurrentAsyncScrollOffset( + AsyncPanZoomController::eForEventHandling) + .y, + scrollOffset.y); + + scrollOffset = apzc->GetCurrentAsyncScrollOffset( + AsyncPanZoomController::eForEventHandling); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + PanGesture(PanGestureInput::PANGESTURE_MOMENTUMPAN, apzc, + ScreenIntPoint(50, 80), ScreenPoint(0, 10), mcc->Time()); + EXPECT_TRUE(apzc->IsOverscrolled()); + EXPECT_TRUE(apzc->IsOverscrollAnimationRunning()); + // Scrolling keeps going by momentum. + EXPECT_GT(apzc->GetCurrentAsyncScrollOffset( + AsyncPanZoomController::eForEventHandling) + .y, + scrollOffset.y); + + currentOverscrolledTransform = + apzc->GetOverscrollTransform(AsyncPanZoomController::eForEventHandling); + scrollOffset = apzc->GetCurrentAsyncScrollOffset( + AsyncPanZoomController::eForEventHandling); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + PanGesture(PanGestureInput::PANGESTURE_MOMENTUMEND, apzc, + ScreenIntPoint(50, 80), ScreenPoint(0, 0), mcc->Time()); + EXPECT_TRUE(apzc->IsOverscrolled()); + EXPECT_TRUE(apzc->IsOverscrollAnimationRunning()); + // This momentum event doesn't change the scroll offset since its + // displacement is zero. + EXPECT_EQ(apzc->GetCurrentAsyncScrollOffset( + AsyncPanZoomController::eForEventHandling) + .y, + scrollOffset.y); + + // Check that we recover from the horizontal overscroll via the animation. + ParentLayerPoint expectedScrollOffset(0, scrollOffset.y); + SampleAnimationUntilRecoveredFromOverscroll(expectedScrollOffset); +} +#endif + +#ifndef MOZ_WIDGET_ANDROID // Maybe fails on Android +// Similar to above +// HorizontalOverscrollAnimationWithVerticalPanMomentumScrolling, +// but having OverscrollAnimation on both axes initially. +TEST_F(APZCOverscrollTester, + BothAxesOverscrollAnimationWithPanMomentumScrolling) { + // TODO: This test currently requires gestures that cause movement on both + // axis, which excludes DOMINANT_AXIS locking mode. The gestures should be + // broken up into multiple gestures to cause the overscroll. + SCOPED_GFX_PREF_INT("apz.axis_lock.mode", 2); + SCOPED_GFX_PREF_BOOL("apz.overscroll.enabled", true); + + ScrollMetadata metadata; + FrameMetrics& metrics = metadata.GetMetrics(); + metrics.SetCompositionBounds(ParentLayerRect(0, 0, 100, 100)); + metrics.SetScrollableRect(CSSRect(0, 0, 1000, 5000)); + apzc->SetFrameMetrics(metrics); + + // Try to overscroll up and left with pan gestures. + PanGesture(PanGestureInput::PANGESTURE_START, apzc, ScreenIntPoint(50, 80), + ScreenPoint(-2, -2), mcc->Time()); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 80), + ScreenPoint(-10, -10), mcc->Time()); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 80), + ScreenPoint(-2, -2), mcc->Time()); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + PanGesture(PanGestureInput::PANGESTURE_END, apzc, ScreenIntPoint(50, 80), + ScreenPoint(0, 0), mcc->Time()); + + // Make sure we've started an overscroll animation. + EXPECT_TRUE(apzc->IsOverscrolled()); + EXPECT_TRUE(apzc->IsOverscrollAnimationRunning()); + AsyncTransformComponentMatrix initialOverscrolledTransform = + apzc->GetOverscrollTransform(AsyncPanZoomController::eForEventHandling); + + // Send lengthy downward momentums to make sure the overscroll animation + // doesn't clobber the momentums scrolling. + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + // The overscroll amount has started being managed by the overscroll + // animation. + AsyncTransformComponentMatrix currentOverscrolledTransform = + apzc->GetOverscrollTransform(AsyncPanZoomController::eForEventHandling); + EXPECT_NE(initialOverscrolledTransform._41, currentOverscrolledTransform._41); + EXPECT_NE(initialOverscrolledTransform._42, currentOverscrolledTransform._42); + + PanGesture(PanGestureInput::PANGESTURE_MOMENTUMSTART, apzc, + ScreenIntPoint(50, 80), ScreenPoint(0, 0), mcc->Time()); + EXPECT_TRUE(apzc->IsOverscrolled()); + EXPECT_TRUE(apzc->IsOverscrollAnimationRunning()); + // The overscroll amount on both axes shouldn't be changed by this pan + // momentum start event since the displacement is zero. + EXPECT_EQ( + currentOverscrolledTransform._41, + apzc->GetOverscrollTransform(AsyncPanZoomController::eForEventHandling) + ._41); + EXPECT_EQ( + currentOverscrolledTransform._42, + apzc->GetOverscrollTransform(AsyncPanZoomController::eForEventHandling) + ._42); + + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + // Still being managed by the overscroll animation. + EXPECT_NE( + currentOverscrolledTransform._41, + apzc->GetOverscrollTransform(AsyncPanZoomController::eForEventHandling) + ._41); + EXPECT_NE( + currentOverscrolledTransform._42, + apzc->GetOverscrollTransform(AsyncPanZoomController::eForEventHandling) + ._42); + + currentOverscrolledTransform = + apzc->GetOverscrollTransform(AsyncPanZoomController::eForEventHandling); + // Send a long pan momentum. + PanGesture(PanGestureInput::PANGESTURE_MOMENTUMPAN, apzc, + ScreenIntPoint(50, 80), ScreenPoint(0, 200), mcc->Time()); + EXPECT_TRUE(apzc->IsOverscrolled()); + EXPECT_TRUE(apzc->IsOverscrollAnimationRunning()); + // The overscroll amount on X axis shouldn't be changed by this momentum pan. + EXPECT_EQ( + currentOverscrolledTransform._41, + apzc->GetOverscrollTransform(AsyncPanZoomController::eForEventHandling) + ._41); + // But now the overscroll amount on Y axis should be changed by this momentum + // pan. + EXPECT_NE( + currentOverscrolledTransform._42, + apzc->GetOverscrollTransform(AsyncPanZoomController::eForEventHandling) + ._42); + // Actually it's no longer overscrolled. + EXPECT_EQ( + apzc->GetOverscrollTransform(AsyncPanZoomController::eForEventHandling) + ._42, + 0); + + ParentLayerPoint currentScrollOffset = apzc->GetCurrentAsyncScrollOffset( + AsyncPanZoomController::eForEventHandling); + // Now it started scrolling. + EXPECT_GT(currentScrollOffset.y, 0); + + currentOverscrolledTransform = + apzc->GetOverscrollTransform(AsyncPanZoomController::eForEventHandling); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + // The overscroll on X axis keeps being managed by the overscroll animation. + EXPECT_NE( + currentOverscrolledTransform._41, + apzc->GetOverscrollTransform(AsyncPanZoomController::eForEventHandling) + ._41); + // But the overscroll on Y axis is no longer affected by the overscroll + // animation. + EXPECT_EQ( + currentOverscrolledTransform._42, + apzc->GetOverscrollTransform(AsyncPanZoomController::eForEventHandling) + ._42); + // The scroll offset on Y axis shouldn't be changed by the overscroll + // animation. + EXPECT_EQ(currentScrollOffset.y, + apzc->GetCurrentAsyncScrollOffset( + AsyncPanZoomController::eForEventHandling) + .y); + + currentOverscrolledTransform = + apzc->GetOverscrollTransform(AsyncPanZoomController::eForEventHandling); + currentScrollOffset = apzc->GetCurrentAsyncScrollOffset( + AsyncPanZoomController::eForEventHandling); + PanGesture(PanGestureInput::PANGESTURE_MOMENTUMPAN, apzc, + ScreenIntPoint(50, 80), ScreenPoint(0, 100), mcc->Time()); + EXPECT_TRUE(apzc->IsOverscrolled()); + EXPECT_TRUE(apzc->IsOverscrollAnimationRunning()); + // The overscroll amount on X axis shouldn't be changed by this momentum pan. + EXPECT_EQ( + currentOverscrolledTransform._41, + apzc->GetOverscrollTransform(AsyncPanZoomController::eForEventHandling) + ._41); + // Keeping no overscrolling on Y axis. + EXPECT_EQ( + apzc->GetOverscrollTransform(AsyncPanZoomController::eForEventHandling) + ._42, + 0); + // Scrolling keeps going by momentum. + EXPECT_GT(apzc->GetCurrentAsyncScrollOffset( + AsyncPanZoomController::eForEventHandling) + .y, + currentScrollOffset.y); + + currentScrollOffset = apzc->GetCurrentAsyncScrollOffset( + AsyncPanZoomController::eForEventHandling); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + PanGesture(PanGestureInput::PANGESTURE_MOMENTUMPAN, apzc, + ScreenIntPoint(50, 80), ScreenPoint(0, 10), mcc->Time()); + EXPECT_TRUE(apzc->IsOverscrolled()); + EXPECT_TRUE(apzc->IsOverscrollAnimationRunning()); + // Keeping no overscrolling on Y axis. + EXPECT_EQ( + apzc->GetOverscrollTransform(AsyncPanZoomController::eForEventHandling) + ._42, + 0); + // Scrolling keeps going by momentum. + EXPECT_GT(apzc->GetCurrentAsyncScrollOffset( + AsyncPanZoomController::eForEventHandling) + .y, + currentScrollOffset.y); + + currentOverscrolledTransform = + apzc->GetOverscrollTransform(AsyncPanZoomController::eForEventHandling); + currentScrollOffset = apzc->GetCurrentAsyncScrollOffset( + AsyncPanZoomController::eForEventHandling); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + PanGesture(PanGestureInput::PANGESTURE_MOMENTUMEND, apzc, + ScreenIntPoint(50, 80), ScreenPoint(0, 0), mcc->Time()); + EXPECT_TRUE(apzc->IsOverscrolled()); + EXPECT_TRUE(apzc->IsOverscrollAnimationRunning()); + // Keeping no overscrolling on Y axis. + EXPECT_EQ( + apzc->GetOverscrollTransform(AsyncPanZoomController::eForEventHandling) + ._42, + 0); + // This momentum event doesn't change the scroll offset since its + // displacement is zero. + EXPECT_EQ(apzc->GetCurrentAsyncScrollOffset( + AsyncPanZoomController::eForEventHandling) + .y, + currentScrollOffset.y); + + // Check that we recover from the horizontal overscroll via the animation. + ParentLayerPoint expectedScrollOffset(0, currentScrollOffset.y); + SampleAnimationUntilRecoveredFromOverscroll(expectedScrollOffset); +} +#endif + +#ifndef MOZ_WIDGET_ANDROID // Maybe fails on Android +// This is another variant of +// HorizontalOverscrollAnimationWithVerticalPanMomentumScrolling. In this test, +// after a horizontal overscroll animation started, upwards pan moments happen, +// thus there should be a new vertical overscroll animation in addition to +// the horizontal one. +TEST_F( + APZCOverscrollTester, + VerticalOverscrollAnimationInAdditionToExistingHorizontalOverscrollAnimation) { + SCOPED_GFX_PREF_BOOL("apz.overscroll.enabled", true); + + ScrollMetadata metadata; + FrameMetrics& metrics = metadata.GetMetrics(); + metrics.SetCompositionBounds(ParentLayerRect(0, 0, 100, 100)); + metrics.SetScrollableRect(CSSRect(0, 0, 1000, 5000)); + // Scrolls the content 50px down. + metrics.SetVisualScrollOffset(CSSPoint(0, 50)); + apzc->SetFrameMetrics(metrics); + + // Try to overscroll left with pan gestures. + PanGesture(PanGestureInput::PANGESTURE_START, apzc, ScreenIntPoint(50, 80), + ScreenPoint(-2, 0), mcc->Time()); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 80), + ScreenPoint(-10, 0), mcc->Time()); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 80), + ScreenPoint(-2, 0), mcc->Time()); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + PanGesture(PanGestureInput::PANGESTURE_END, apzc, ScreenIntPoint(50, 80), + ScreenPoint(0, 0), mcc->Time()); + + // Make sure we've started an overscroll animation. + EXPECT_TRUE(apzc->IsOverscrolled()); + EXPECT_TRUE(apzc->IsOverscrollAnimationRunning()); + AsyncTransformComponentMatrix initialOverscrolledTransform = + apzc->GetOverscrollTransform(AsyncPanZoomController::eForEventHandling); + + // Send lengthy __upward__ momentums to make sure the overscroll animation + // doesn't clobber the momentums scrolling. + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + // The overscroll amount on X axis has started being managed by the overscroll + // animation. + AsyncTransformComponentMatrix currentOverscrolledTransform = + apzc->GetOverscrollTransform(AsyncPanZoomController::eForEventHandling); + EXPECT_NE(initialOverscrolledTransform._41, currentOverscrolledTransform._41); + // There is no overscroll on Y axis. + EXPECT_EQ( + apzc->GetOverscrollTransform(AsyncPanZoomController::eForEventHandling) + ._42, + 0); + ParentLayerPoint scrollOffset = apzc->GetCurrentAsyncScrollOffset( + AsyncPanZoomController::eForEventHandling); + // The scroll offset shouldn't be changed by the overscroll animation. + EXPECT_EQ(scrollOffset.y, 50); + + // Simple gesture on the Y axis to ensure that we can send a vertical + // momentum scroll + PanGesture(PanGestureInput::PANGESTURE_START, apzc, ScreenIntPoint(50, 80), + ScreenPoint(0, -2), mcc->Time()); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 80), + ScreenPoint(0, 2), mcc->Time()); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + PanGesture(PanGestureInput::PANGESTURE_END, apzc, ScreenIntPoint(50, 80), + ScreenPoint(0, 0), mcc->Time()); + + ParentLayerPoint offsetAfterPan = apzc->GetCurrentAsyncScrollOffset( + AsyncPanZoomController::eForEventHandling); + + PanGesture(PanGestureInput::PANGESTURE_MOMENTUMSTART, apzc, + ScreenIntPoint(50, 80), ScreenPoint(0, 0), mcc->Time()); + EXPECT_TRUE(apzc->IsOverscrolled()); + EXPECT_TRUE(apzc->IsOverscrollAnimationRunning()); + // The overscroll amount on both axes shouldn't be changed by this pan + // momentum start event since the displacement is zero. + EXPECT_EQ( + currentOverscrolledTransform._41, + apzc->GetOverscrollTransform(AsyncPanZoomController::eForEventHandling) + ._41); + EXPECT_EQ( + currentOverscrolledTransform._42, + apzc->GetOverscrollTransform(AsyncPanZoomController::eForEventHandling) + ._42); + + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + // The overscroll amount should be managed by the overscroll animation. + EXPECT_NE( + currentOverscrolledTransform._41, + apzc->GetOverscrollTransform(AsyncPanZoomController::eForEventHandling) + ._41); + scrollOffset = apzc->GetCurrentAsyncScrollOffset( + AsyncPanZoomController::eForEventHandling); + // Not yet started scrolling. + EXPECT_EQ(scrollOffset.y, offsetAfterPan.y); + EXPECT_EQ(scrollOffset.x, 0); + + currentOverscrolledTransform = + apzc->GetOverscrollTransform(AsyncPanZoomController::eForEventHandling); + + // Send a long pan momentum. + PanGesture(PanGestureInput::PANGESTURE_MOMENTUMPAN, apzc, + ScreenIntPoint(50, 80), ScreenPoint(0, -200), mcc->Time()); + EXPECT_TRUE(apzc->IsOverscrolled()); + EXPECT_TRUE(apzc->IsOverscrollAnimationRunning()); + // The overscroll amount on X axis shouldn't be changed by this momentum pan. + EXPECT_EQ( + currentOverscrolledTransform._41, + apzc->GetOverscrollTransform(AsyncPanZoomController::eForEventHandling) + ._41); + // Now it started scrolling vertically. + scrollOffset = apzc->GetCurrentAsyncScrollOffset( + AsyncPanZoomController::eForEventHandling); + EXPECT_EQ(scrollOffset.y, 0); + EXPECT_EQ(scrollOffset.x, 0); + // Actually it's also vertically overscrolled. + EXPECT_GT( + apzc->GetOverscrollTransform(AsyncPanZoomController::eForEventHandling) + ._42, + 0); + + currentOverscrolledTransform = + apzc->GetOverscrollTransform(AsyncPanZoomController::eForEventHandling); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + // The overscroll on X axis keeps being managed by the overscroll animation. + EXPECT_NE( + currentOverscrolledTransform._41, + apzc->GetOverscrollTransform(AsyncPanZoomController::eForEventHandling) + ._41); + // The overscroll on Y Axis hasn't been changed by the overscroll animation at + // this moment, sine the last displacement was consumed in the last pan + // momentum. + EXPECT_EQ( + currentOverscrolledTransform._42, + apzc->GetOverscrollTransform(AsyncPanZoomController::eForEventHandling) + ._42); + + currentOverscrolledTransform = + apzc->GetOverscrollTransform(AsyncPanZoomController::eForEventHandling); + PanGesture(PanGestureInput::PANGESTURE_MOMENTUMPAN, apzc, + ScreenIntPoint(50, 80), ScreenPoint(0, -100), mcc->Time()); + EXPECT_TRUE(apzc->IsOverscrolled()); + EXPECT_TRUE(apzc->IsOverscrollAnimationRunning()); + // The overscroll amount on X axis shouldn't be changed by this momentum pan. + EXPECT_EQ( + currentOverscrolledTransform._41, + apzc->GetOverscrollTransform(AsyncPanZoomController::eForEventHandling) + ._41); + // Now the overscroll amount on Y axis shouldn't be changed by this momentum + // pan either. + EXPECT_EQ( + currentOverscrolledTransform._42, + apzc->GetOverscrollTransform(AsyncPanZoomController::eForEventHandling) + ._42); + + currentOverscrolledTransform = + apzc->GetOverscrollTransform(AsyncPanZoomController::eForEventHandling); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + EXPECT_NE( + currentOverscrolledTransform._41, + apzc->GetOverscrollTransform(AsyncPanZoomController::eForEventHandling) + ._41); + // And now the overscroll on Y Axis should be also managed by the overscroll + // animation. + EXPECT_NE( + currentOverscrolledTransform._42, + apzc->GetOverscrollTransform(AsyncPanZoomController::eForEventHandling) + ._42); + + currentOverscrolledTransform = + apzc->GetOverscrollTransform(AsyncPanZoomController::eForEventHandling); + PanGesture(PanGestureInput::PANGESTURE_MOMENTUMPAN, apzc, + ScreenIntPoint(50, 80), ScreenPoint(0, -10), mcc->Time()); + EXPECT_TRUE(apzc->IsOverscrolled()); + EXPECT_TRUE(apzc->IsOverscrollAnimationRunning()); + // The overscroll amount on both axes shouldn't be changed by momentum event. + EXPECT_EQ( + currentOverscrolledTransform._41, + apzc->GetOverscrollTransform(AsyncPanZoomController::eForEventHandling) + ._41); + EXPECT_EQ( + currentOverscrolledTransform._42, + apzc->GetOverscrollTransform(AsyncPanZoomController::eForEventHandling) + ._42); + + currentOverscrolledTransform = + apzc->GetOverscrollTransform(AsyncPanZoomController::eForEventHandling); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + PanGesture(PanGestureInput::PANGESTURE_MOMENTUMEND, apzc, + ScreenIntPoint(50, 80), ScreenPoint(0, 0), mcc->Time()); + EXPECT_TRUE(apzc->IsOverscrolled()); + EXPECT_TRUE(apzc->IsOverscrollAnimationRunning()); + + // Check that we recover from the horizontal overscroll via the animation. + ParentLayerPoint expectedScrollOffset(0, 0); + SampleAnimationUntilRecoveredFromOverscroll(expectedScrollOffset); +} +#endif + +#ifndef MOZ_WIDGET_ANDROID // Currently fails on Android +TEST_F(APZCOverscrollTester, OverscrollByPanGesturesInterruptedByReflowZoom) { + SCOPED_GFX_PREF_BOOL("apz.overscroll.enabled", true); + SCOPED_GFX_PREF_INT("mousewheel.with_control.action", 3); // reflow zoom. + + // A sanity check that pan gestures with ctrl modifier will not be handled by + // APZ. + PanGestureInput panInput(PanGestureInput::PANGESTURE_START, mcc->Time(), + ScreenIntPoint(5, 5), ScreenPoint(0, -2), + MODIFIER_CONTROL); + WidgetWheelEvent wheelEvent = panInput.ToWidgetEvent(nullptr); + EXPECT_FALSE(APZInputBridge::ActionForWheelEvent(&wheelEvent).isSome()); + + ScrollableLayerGuid rootGuid = CreateSimpleRootScrollableForWebRender(); + RefPtr<AsyncPanZoomController> apzc = + tm->GetTargetAPZC(rootGuid.mLayersId, rootGuid.mScrollId); + + PanGesture(PanGestureInput::PANGESTURE_START, tm, ScreenIntPoint(50, 80), + ScreenPoint(0, -2), mcc->Time()); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + PanGesture(PanGestureInput::PANGESTURE_PAN, tm, ScreenIntPoint(50, 80), + ScreenPoint(0, -10), mcc->Time()); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + + // Make sure overscrolling has started. + EXPECT_TRUE(apzc->IsOverscrolled()); + + // Press ctrl until PANGESTURE_END. + PanGestureWithModifiers(PanGestureInput::PANGESTURE_PAN, MODIFIER_CONTROL, tm, + ScreenIntPoint(50, 80), ScreenPoint(0, -2), + mcc->Time()); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + // At this moment (i.e. PANGESTURE_PAN), still in overscrolling state. + EXPECT_TRUE(apzc->IsOverscrolled()); + + PanGestureWithModifiers(PanGestureInput::PANGESTURE_END, MODIFIER_CONTROL, tm, + ScreenIntPoint(50, 80), ScreenPoint(0, 0), + mcc->Time()); + // The overscrolling state should have been restored. + EXPECT_TRUE(!apzc->IsOverscrolled()); +} +#endif + +#ifndef MOZ_WIDGET_ANDROID // Only applies to GenericOverscrollEffect +TEST_F(APZCOverscrollTester, SmoothTransitionFromPanToAnimation) { + SCOPED_GFX_PREF_BOOL("apz.overscroll.enabled", true); + + ScrollMetadata metadata; + FrameMetrics& metrics = metadata.GetMetrics(); + metrics.SetCompositionBounds(ParentLayerRect(0, 0, 100, 100)); + metrics.SetScrollableRect(CSSRect(0, 0, 100, 1000)); + // Start scrolled down to y=500px. + metrics.SetVisualScrollOffset(CSSPoint(0, 500)); + apzc->SetFrameMetrics(metrics); + + int frameLength = 10; // milliseconds; 10 to keep the math simple + float panVelocity = 10; // pixels per millisecond + int panPixelsPerFrame = frameLength * panVelocity; // 100 pixels per frame + + ScreenIntPoint panPoint(50, 50); + PanGesture(PanGestureInput::PANGESTURE_START, apzc, panPoint, + ScreenPoint(0, -1), mcc->Time()); + // Pan up for 6 frames at 100 pixels per frame. This should reduce + // the vertical scroll offset from 500 to 0, and get us into overscroll. + for (int i = 0; i < 6; ++i) { + mcc->AdvanceByMillis(frameLength); + PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, panPoint, + ScreenPoint(0, -panPixelsPerFrame), mcc->Time()); + } + EXPECT_TRUE(apzc->IsOverscrolled()); + + // Pan further into overscroll at the same input velocity, enough + // for the frames while we are in overscroll to dominate the computation + // in the velocity tracker. + // Importantly, while the input velocity is still 100 pixels per frame, + // in the overscrolled state the page only visual moves by at most 8 pixels + // per frame. + int frames = StaticPrefs::apz_velocity_relevance_time_ms() / frameLength; + for (int i = 0; i < frames; ++i) { + mcc->AdvanceByMillis(frameLength); + PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, panPoint, + ScreenPoint(0, -panPixelsPerFrame), mcc->Time()); + } + EXPECT_TRUE(apzc->IsOverscrolled()); + + // End the pan, allowing an overscroll animation to start. + mcc->AdvanceByMillis(frameLength); + PanGesture(PanGestureInput::PANGESTURE_END, apzc, panPoint, ScreenPoint(0, 0), + mcc->Time()); + EXPECT_TRUE(apzc->IsOverscrollAnimationRunning()); + + // Check that the velocity reflects the actual movement (no more than 8 + // pixels/frame ==> 0.8 pixels per millisecond), not the input velocity + // (100 pixels/frame ==> 10 pixels per millisecond). This ensures that + // the transition from the pan to the animation appears smooth. + // (Note: velocities are negative since they are upwards.) + EXPECT_LT(apzc->GetVelocityVector().y, 0); + EXPECT_GT(apzc->GetVelocityVector().y, -0.8); +} +#endif + +#ifndef MOZ_WIDGET_ANDROID // Only applies to GenericOverscrollEffect +TEST_F(APZCOverscrollTester, NoOverscrollForMousewheel) { + SCOPED_GFX_PREF_BOOL("apz.overscroll.enabled", true); + + ScrollMetadata metadata; + FrameMetrics& metrics = metadata.GetMetrics(); + metrics.SetCompositionBounds(ParentLayerRect(0, 0, 100, 100)); + metrics.SetScrollableRect(CSSRect(0, 0, 100, 1000)); + // Start scrolled down just a few pixels from the top. + metrics.SetVisualScrollOffset(CSSPoint(0, 3)); + // Set line and page scroll amounts. Otherwise, even though Wheel() uses + // SCROLLDELTA_PIXEL, the wheel handling code will get confused by things + // like the "don't scroll more than one page" check. + metadata.SetPageScrollAmount(LayoutDeviceIntSize(50, 100)); + metadata.SetLineScrollAmount(LayoutDeviceIntSize(5, 10)); + apzc->SetScrollMetadata(metadata); + + // Send a wheel with enough delta to scrollto y=0 *and* overscroll. + Wheel(apzc, ScreenIntPoint(10, 10), ScreenPoint(0, -10), mcc->Time()); + + // Check that we did not actually go into overscroll. + EXPECT_FALSE(apzc->IsOverscrolled()); +} +#endif + +#ifndef MOZ_WIDGET_ANDROID // Only applies to GenericOverscrollEffect +TEST_F(APZCOverscrollTester, ClickWhileOverscrolled) { + SCOPED_GFX_PREF_BOOL("apz.overscroll.enabled", true); + + ScrollMetadata metadata; + FrameMetrics& metrics = metadata.GetMetrics(); + metrics.SetCompositionBounds(ParentLayerRect(0, 0, 100, 100)); + metrics.SetScrollableRect(CSSRect(0, 0, 100, 1000)); + metrics.SetVisualScrollOffset(CSSPoint(0, 0)); + apzc->SetFrameMetrics(metrics); + + // Pan into overscroll at the top. + ScreenIntPoint panPoint(50, 50); + PanGesture(PanGestureInput::PANGESTURE_START, apzc, panPoint, + ScreenPoint(0, -1), mcc->Time()); + mcc->AdvanceByMillis(10); + PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, panPoint, + ScreenPoint(0, -100), mcc->Time()); + EXPECT_TRUE(apzc->IsOverscrolled()); + EXPECT_TRUE(apzc->GetOverscrollAmount().y < 0); // overscrolled at top + + // End the pan. This should start an overscroll animation. + mcc->AdvanceByMillis(10); + PanGesture(PanGestureInput::PANGESTURE_END, apzc, panPoint, ScreenPoint(0, 0), + mcc->Time()); + EXPECT_TRUE(apzc->GetOverscrollAmount().y < 0); // overscrolled at top + EXPECT_TRUE(apzc->IsOverscrollAnimationRunning()); + + // Send a mouse-down. This should interrupt the animation but not relieve + // overscroll yet. + ParentLayerPoint overscrollBefore = apzc->GetOverscrollAmount(); + MouseDown(apzc, panPoint, mcc->Time()); + EXPECT_FALSE(apzc->IsOverscrollAnimationRunning()); + EXPECT_EQ(overscrollBefore, apzc->GetOverscrollAmount()); + + // Send a mouse-up. This should start an overscroll animation again. + MouseUp(apzc, panPoint, mcc->Time()); + EXPECT_TRUE(apzc->IsOverscrollAnimationRunning()); + + SampleAnimationUntilRecoveredFromOverscroll(ParentLayerPoint(0, 0)); +} +#endif + +#ifndef MOZ_WIDGET_ANDROID // Only applies to GenericOverscrollEffect +TEST_F(APZCOverscrollTester, DynamicallyLoadingContent) { + SCOPED_GFX_PREF_BOOL("apz.overscroll.enabled", true); + + ScrollMetadata metadata; + FrameMetrics& metrics = metadata.GetMetrics(); + metrics.SetCompositionBounds(ParentLayerRect(0, 0, 100, 100)); + metrics.SetScrollableRect(CSSRect(0, 0, 100, 1000)); + metrics.SetVisualScrollOffset(CSSPoint(0, 0)); + apzc->SetFrameMetrics(metrics); + + // Pan to the bottom of the page, and further, into overscroll. + ScreenIntPoint panPoint(50, 50); + PanGesture(PanGestureInput::PANGESTURE_START, apzc, panPoint, + ScreenPoint(0, 1), mcc->Time()); + for (int i = 0; i < 12; ++i) { + mcc->AdvanceByMillis(10); + PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, panPoint, + ScreenPoint(0, 100), mcc->Time()); + } + EXPECT_TRUE(apzc->IsOverscrolled()); + EXPECT_TRUE(apzc->GetOverscrollAmount().y > 0); // overscrolled at bottom + + // Grow the scrollable rect at the bottom, simulating the page loading content + // dynamically. + CSSRect scrollableRect = metrics.GetScrollableRect(); + scrollableRect.height += 500; + metrics.SetScrollableRect(scrollableRect); + apzc->NotifyLayersUpdated(metadata, false, true); + + // Check that the modified scrollable rect cleared the overscroll. + EXPECT_FALSE(apzc->IsOverscrolled()); + + // Pan back up to the top, and further, into overscroll. + PanGesture(PanGestureInput::PANGESTURE_START, apzc, panPoint, + ScreenPoint(0, -1), mcc->Time()); + for (int i = 0; i < 12; ++i) { + mcc->AdvanceByMillis(10); + PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, panPoint, + ScreenPoint(0, -100), mcc->Time()); + } + EXPECT_TRUE(apzc->IsOverscrolled()); + ParentLayerPoint overscrollAmount = apzc->GetOverscrollAmount(); + EXPECT_TRUE(overscrollAmount.y < 0); // overscrolled at top + + // Grow the scrollable rect at the bottom again. + scrollableRect = metrics.GetScrollableRect(); + scrollableRect.height += 500; + metrics.SetScrollableRect(scrollableRect); + apzc->NotifyLayersUpdated(metadata, false, true); + + // Check that the modified scrollable rect did NOT clear overscroll at the + // top. + EXPECT_TRUE(apzc->IsOverscrolled()); + EXPECT_EQ(overscrollAmount, + apzc->GetOverscrollAmount()); // overscroll did not change at all +} +#endif + +#ifndef MOZ_WIDGET_ANDROID // Only applies to GenericOverscrollEffect +TEST_F(APZCOverscrollTester, SmallAmountOfOverscroll) { + SCOPED_GFX_PREF_BOOL("apz.overscroll.enabled", true); + + ScrollMetadata metadata; + FrameMetrics& metrics = metadata.GetMetrics(); + metrics.SetCompositionBounds(ParentLayerRect(0, 0, 100, 100)); + metrics.SetScrollableRect(CSSRect(0, 0, 100, 1000)); + + // Do vertical overscroll first. + ScreenIntPoint panPoint(50, 50); + PanGesture(PanGestureInput::PANGESTURE_START, apzc, panPoint, + ScreenPoint(0, -10), mcc->Time()); + mcc->AdvanceByMillis(10); + PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, panPoint, + ScreenPoint(0, -10), mcc->Time()); + mcc->AdvanceByMillis(10); + PanGesture(PanGestureInput::PANGESTURE_END, apzc, panPoint, ScreenPoint(0, 0), + mcc->Time()); + mcc->AdvanceByMillis(10); + + // Then do small horizontal overscroll which will be considered as "finished" + // by our overscroll animation physics model. + PanGesture(PanGestureInput::PANGESTURE_START, apzc, panPoint, + ScreenPoint(-0.1, 0), mcc->Time()); + mcc->AdvanceByMillis(10); + PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, panPoint, + ScreenPoint(-0.2, 0), mcc->Time()); + mcc->AdvanceByMillis(10); + PanGesture(PanGestureInput::PANGESTURE_END, apzc, panPoint, ScreenPoint(0, 0), + mcc->Time()); + mcc->AdvanceByMillis(10); + + EXPECT_TRUE(apzc->IsOverscrolled()); + EXPECT_TRUE(apzc->GetOverscrollAmount().y < 0); // overscrolled at top + EXPECT_TRUE(apzc->GetOverscrollAmount().x < 0); // and overscrolled at left + + // Then do vertical scroll. + PanGesture(PanGestureInput::PANGESTURE_START, apzc, panPoint, + ScreenPoint(0, 10), mcc->Time()); + mcc->AdvanceByMillis(10); + PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, panPoint, + ScreenPoint(0, 100), mcc->Time()); + mcc->AdvanceByMillis(10); + PanGesture(PanGestureInput::PANGESTURE_END, apzc, panPoint, ScreenPoint(0, 0), + mcc->Time()); + + ParentLayerPoint scrollOffset = apzc->GetCurrentAsyncScrollOffset( + AsyncPanZoomController::eForEventHandling); + EXPECT_GT(scrollOffset.y, 0); // Make sure the vertical scroll offset is + // greater than zero. + + // The small horizontal overscroll amount should be restored to zero. + ParentLayerPoint expectedScrollOffset(0, scrollOffset.y); + SampleAnimationUntilRecoveredFromOverscroll(expectedScrollOffset); +} +#endif + +#ifdef MOZ_WIDGET_ANDROID // Only applies to WidgetOverscrollEffect +TEST_F(APZCOverscrollTester, StuckInOverscroll_Bug1786452) { + SCOPED_GFX_PREF_BOOL("apz.overscroll.enabled", true); + + ScrollMetadata metadata; + FrameMetrics& metrics = metadata.GetMetrics(); + metrics.SetCompositionBounds(ParentLayerRect(0, 0, 100, 100)); + metrics.SetScrollableRect(CSSRect(0, 0, 100, 1000)); + + // Over the course of the test, expect one or more calls to + // UpdateOverscrollOffset(), followed by a call to UpdateOverscrollVelocity(). + // The latter ensures the widget has a chance to end its overscroll effect. + InSequence s; + EXPECT_CALL(*mcc, UpdateOverscrollOffset(_, _, _, _)).Times(AtLeast(1)); + EXPECT_CALL(*mcc, UpdateOverscrollVelocity(_, _, _, _)).Times(1); + + // Pan into overscroll, keeping the finger down + ScreenIntPoint startPoint(10, 500); + ScreenIntPoint endPoint(10, 10); + Pan(apzc, startPoint, endPoint, PanOptions::KeepFingerDown); + EXPECT_TRUE(apzc->IsOverscrolled()); + + // Linger a while to cause the velocity to drop to very low or zero + mcc->AdvanceByMillis(100); + TouchMove(apzc, endPoint, mcc->Time()); + EXPECT_LT(apzc->GetVelocityVector().Length(), + StaticPrefs::apz_fling_min_velocity_threshold()); + EXPECT_TRUE(apzc->IsOverscrolled()); + + // Lift the finger + mcc->AdvanceByMillis(20); + TouchUp(apzc, endPoint, mcc->Time()); + EXPECT_FALSE(apzc->IsOverscrolled()); +} +#endif + +class APZCOverscrollTesterMock : public APZCTreeManagerTester { + public: + APZCOverscrollTesterMock() { CreateMockHitTester(); } + + UniquePtr<ScopedLayerTreeRegistration> registration; + TestAsyncPanZoomController* rootApzc; +}; + +#ifndef MOZ_WIDGET_ANDROID // Currently fails on Android +TEST_F(APZCOverscrollTesterMock, OverscrollHandoff) { + SCOPED_GFX_PREF_BOOL("apz.overscroll.enabled", true); + + const char* treeShape = "x(x)"; + LayerIntRect layerVisibleRect[] = {LayerIntRect(0, 0, 100, 100), + LayerIntRect(0, 0, 100, 50)}; + CreateScrollData(treeShape, layerVisibleRect); + SetScrollableFrameMetrics(root, ScrollableLayerGuid::START_SCROLL_ID, + CSSRect(0, 0, 200, 200)); + SetScrollableFrameMetrics(layers[1], ScrollableLayerGuid::START_SCROLL_ID + 1, + // same size as the visible region so that + // the container is not scrollable in any directions + // actually. This is simulating overflow: hidden + // iframe document in Fission, though we don't set + // a different layers id. + CSSRect(0, 0, 100, 50)); + + SetScrollHandoff(layers[1], root); + + registration = MakeUnique<ScopedLayerTreeRegistration>(LayersId{0}, mcc); + UpdateHitTestingTree(); + rootApzc = ApzcOf(root); + rootApzc->GetFrameMetrics().SetIsRootContent(true); + + // A pan gesture on the child scroller (which is not scrollable though). + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1); + PanGesture(PanGestureInput::PANGESTURE_START, manager, ScreenIntPoint(50, 20), + ScreenPoint(0, -2), mcc->Time()); + EXPECT_TRUE(rootApzc->IsOverscrolled()); +} +#endif + +#ifndef MOZ_WIDGET_ANDROID // Currently fails on Android +TEST_F(APZCOverscrollTesterMock, VerticalOverscrollHandoffToScrollableRoot) { + SCOPED_GFX_PREF_BOOL("apz.overscroll.enabled", true); + + // Create a layer tree having two vertical scrollable layers. + const char* treeShape = "x(x)"; + LayerIntRect layerVisibleRect[] = {LayerIntRect(0, 0, 100, 100), + LayerIntRect(0, 0, 100, 50)}; + CreateScrollData(treeShape, layerVisibleRect); + SetScrollableFrameMetrics(root, ScrollableLayerGuid::START_SCROLL_ID, + CSSRect(0, 0, 100, 200)); + SetScrollableFrameMetrics(layers[1], ScrollableLayerGuid::START_SCROLL_ID + 1, + CSSRect(0, 0, 100, 200)); + + SetScrollHandoff(layers[1], root); + + registration = MakeUnique<ScopedLayerTreeRegistration>(LayersId{0}, mcc); + UpdateHitTestingTree(); + rootApzc = ApzcOf(root); + rootApzc->GetFrameMetrics().SetIsRootContent(true); + + // A vertical pan gesture on the child scroller which will be handed off to + // the root APZC. + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1); + PanGesture(PanGestureInput::PANGESTURE_START, manager, ScreenIntPoint(50, 20), + ScreenPoint(0, -2), mcc->Time()); + EXPECT_TRUE(rootApzc->IsOverscrolled()); + EXPECT_FALSE(ApzcOf(layers[1])->IsOverscrolled()); +} +#endif + +#ifndef MOZ_WIDGET_ANDROID // Currently fails on Android +TEST_F(APZCOverscrollTesterMock, NoOverscrollHandoffToNonScrollableRoot) { + SCOPED_GFX_PREF_BOOL("apz.overscroll.enabled", true); + + // Create a layer tree having non-scrollable root and a vertical scrollable + // child. + const char* treeShape = "x(x)"; + LayerIntRect layerVisibleRect[] = {LayerIntRect(0, 0, 100, 100), + LayerIntRect(0, 0, 100, 50)}; + 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<ScopedLayerTreeRegistration>(LayersId{0}, mcc); + UpdateHitTestingTree(); + rootApzc = ApzcOf(root); + rootApzc->GetFrameMetrics().SetIsRootContent(true); + + // A vertical pan gesture on the child scroller which should not be handed + // off the root APZC. + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1); + PanGesture(PanGestureInput::PANGESTURE_START, manager, ScreenIntPoint(50, 20), + ScreenPoint(0, -2), mcc->Time()); + EXPECT_FALSE(rootApzc->IsOverscrolled()); + EXPECT_TRUE(ApzcOf(layers[1])->IsOverscrolled()); +} +#endif + +#ifndef MOZ_WIDGET_ANDROID // Currently fails on Android +TEST_F(APZCOverscrollTesterMock, NoOverscrollHandoffOrthogonalPanGesture) { + SCOPED_GFX_PREF_BOOL("apz.overscroll.enabled", true); + + // Create a layer tree having horizontal scrollable root and a vertical + // scrollable child. + const char* treeShape = "x(x)"; + LayerIntRect layerVisibleRect[] = {LayerIntRect(0, 0, 100, 100), + LayerIntRect(0, 0, 100, 50)}; + 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<ScopedLayerTreeRegistration>(LayersId{0}, mcc); + UpdateHitTestingTree(); + rootApzc = ApzcOf(root); + rootApzc->GetFrameMetrics().SetIsRootContent(true); + + // A vertical pan gesture on the child scroller which should not be handed + // off the root APZC because the root APZC is not scrollable vertically. + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1); + PanGesture(PanGestureInput::PANGESTURE_START, manager, ScreenIntPoint(50, 20), + ScreenPoint(0, -2), mcc->Time()); + EXPECT_FALSE(rootApzc->IsOverscrolled()); + EXPECT_TRUE(ApzcOf(layers[1])->IsOverscrolled()); +} +#endif + +#ifndef MOZ_WIDGET_ANDROID // Only applies to GenericOverscrollEffect +TEST_F(APZCOverscrollTesterMock, + RetriggerCancelledOverscrollAnimationByNewPanGesture) { + SCOPED_GFX_PREF_BOOL("apz.overscroll.enabled", true); + + // Create a layer tree having vertical scrollable root and a horizontal + // scrollable child. + const char* treeShape = "x(x)"; + LayerIntRect layerVisibleRect[] = {LayerIntRect(0, 0, 100, 100), + LayerIntRect(0, 0, 100, 50)}; + CreateScrollData(treeShape, layerVisibleRect); + SetScrollableFrameMetrics(root, ScrollableLayerGuid::START_SCROLL_ID, + CSSRect(0, 0, 100, 200)); + SetScrollableFrameMetrics(layers[1], ScrollableLayerGuid::START_SCROLL_ID + 1, + CSSRect(0, 0, 200, 50)); + + SetScrollHandoff(layers[1], root); + + registration = MakeUnique<ScopedLayerTreeRegistration>(LayersId{0}, mcc); + UpdateHitTestingTree(); + rootApzc = ApzcOf(root); + rootApzc->GetFrameMetrics().SetIsRootContent(true); + + ScreenIntPoint panPoint(50, 20); + // A vertical pan gesture on the child scroller which should be handed off the + // root APZC. + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1); + PanGesture(PanGestureInput::PANGESTURE_START, manager, panPoint, + ScreenPoint(0, -2), mcc->Time()); + mcc->AdvanceByMillis(10); + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1); + PanGesture(PanGestureInput::PANGESTURE_PAN, manager, panPoint, + ScreenPoint(0, -10), mcc->Time()); + mcc->AdvanceByMillis(10); + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1); + PanGesture(PanGestureInput::PANGESTURE_END, manager, panPoint, + ScreenPoint(0, 0), mcc->Time()); + + // The root APZC should be overscrolled and the child APZC should not be. + EXPECT_TRUE(rootApzc->IsOverscrolled()); + EXPECT_FALSE(ApzcOf(layers[1])->IsOverscrolled()); + + mcc->AdvanceByMillis(10); + + // Make sure the root APZC is still overscrolled. + EXPECT_TRUE(rootApzc->IsOverscrolled()); + + // Start a new horizontal pan gesture on the child scroller which should be + // handled by the child APZC now. + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1); + APZEventResult result = PanGesture(PanGestureInput::PANGESTURE_START, manager, + panPoint, ScreenPoint(-2, 0), mcc->Time()); + // The above horizontal pan start event was flagged as "this event may trigger + // swipe" and either the root scrollable frame or the horizontal child + // scrollable frame is not scrollable in the pan start direction, thus the pan + // start event run into the short circuit path for swipe-to-navigation in + // InputQueue::ReceivePanGestureInput, which means it's waiting for the + // content response, so we need to respond explicitly here. + manager->ContentReceivedInputBlock(result.mInputBlockId, false); + mcc->AdvanceByMillis(10); + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1); + PanGesture(PanGestureInput::PANGESTURE_PAN, manager, panPoint, + ScreenPoint(-10, 0), mcc->Time()); + mcc->AdvanceByMillis(10); + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1); + PanGesture(PanGestureInput::PANGESTURE_END, manager, panPoint, + ScreenPoint(0, 0), mcc->Time()); + + // Now both APZCs should be overscrolled. + EXPECT_TRUE(rootApzc->IsOverscrolled()); + EXPECT_TRUE(ApzcOf(layers[1])->IsOverscrolled()); + + // Sample all animations until all of them have been finished. + while (SampleAnimationsOnce()) + ; + + // After the animations finished, all overscrolled states should have been + // restored. + EXPECT_FALSE(rootApzc->IsOverscrolled()); + EXPECT_FALSE(ApzcOf(layers[1])->IsOverscrolled()); +} +#endif + +#ifndef MOZ_WIDGET_ANDROID // Only applies to GenericOverscrollEffect +TEST_F(APZCOverscrollTesterMock, RetriggeredOverscrollAnimationVelocity) { + SCOPED_GFX_PREF_BOOL("apz.overscroll.enabled", true); + + // Setup two nested vertical scrollable frames. + const char* treeShape = "x(x)"; + LayerIntRect layerVisibleRect[] = {LayerIntRect(0, 0, 100, 100), + LayerIntRect(0, 0, 100, 50)}; + CreateScrollData(treeShape, layerVisibleRect); + SetScrollableFrameMetrics(root, ScrollableLayerGuid::START_SCROLL_ID, + CSSRect(0, 0, 100, 200)); + SetScrollableFrameMetrics(layers[1], ScrollableLayerGuid::START_SCROLL_ID + 1, + CSSRect(0, 0, 100, 200)); + + SetScrollHandoff(layers[1], root); + + registration = MakeUnique<ScopedLayerTreeRegistration>(LayersId{0}, mcc); + UpdateHitTestingTree(); + rootApzc = ApzcOf(root); + rootApzc->GetFrameMetrics().SetIsRootContent(true); + + ScreenIntPoint panPoint(50, 20); + // A vertical upward pan gesture on the child scroller which should be handed + // off the root APZC. + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1); + PanGesture(PanGestureInput::PANGESTURE_START, manager, panPoint, + ScreenPoint(0, -2), mcc->Time()); + mcc->AdvanceByMillis(10); + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1); + PanGesture(PanGestureInput::PANGESTURE_PAN, manager, panPoint, + ScreenPoint(0, -10), mcc->Time()); + mcc->AdvanceByMillis(10); + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1); + PanGesture(PanGestureInput::PANGESTURE_END, manager, panPoint, + ScreenPoint(0, 0), mcc->Time()); + + // The root APZC should be overscrolled and the child APZC should not be. + EXPECT_TRUE(rootApzc->IsOverscrolled()); + EXPECT_FALSE(ApzcOf(layers[1])->IsOverscrolled()); + + mcc->AdvanceByMillis(10); + + // Make sure the root APZC is still overscrolled and there's an overscroll + // animation. + EXPECT_TRUE(rootApzc->IsOverscrolled()); + EXPECT_TRUE(rootApzc->IsOverscrollAnimationRunning()); + + // And make sure the overscroll animation's velocity is a certain amount in + // the upward direction. + EXPECT_LT(rootApzc->GetVelocityVector().y, 0); + + // Start a new downward pan gesture on the child scroller which + // should be handled by the child APZC now. + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1); + PanGesture(PanGestureInput::PANGESTURE_START, manager, panPoint, + ScreenPoint(0, 2), mcc->Time()); + mcc->AdvanceByMillis(10); + // The new pan-start gesture stops the overscroll animation at this moment. + EXPECT_TRUE(!rootApzc->IsOverscrollAnimationRunning()); + + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1); + PanGesture(PanGestureInput::PANGESTURE_PAN, manager, panPoint, + ScreenPoint(0, 10), mcc->Time()); + mcc->AdvanceByMillis(10); + // There's no overscroll animation yet even if the root APZC is still + // overscrolled. + EXPECT_TRUE(!rootApzc->IsOverscrollAnimationRunning()); + EXPECT_TRUE(rootApzc->IsOverscrolled()); + + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1); + PanGesture(PanGestureInput::PANGESTURE_END, manager, panPoint, + ScreenPoint(0, 10), mcc->Time()); + + // Now an overscroll animation should have been triggered by the pan-end + // gesture. + EXPECT_TRUE(rootApzc->IsOverscrollAnimationRunning()); + EXPECT_TRUE(rootApzc->IsOverscrolled()); + // And the newly created overscroll animation's positions should never exceed + // 0. + while (SampleAnimationsOnce()) { + EXPECT_LE(rootApzc->GetOverscrollAmount().y, 0); + } +} +#endif + +#ifndef MOZ_WIDGET_ANDROID // Only applies to GenericOverscrollEffect +TEST_F(APZCOverscrollTesterMock, OverscrollIntoPreventDefault) { + SCOPED_GFX_PREF_BOOL("apz.overscroll.enabled", true); + + const char* treeShape = "x"; + LayerIntRect layerVisibleRects[] = {LayerIntRect(0, 0, 100, 100)}; + CreateScrollData(treeShape, layerVisibleRects); + SetScrollableFrameMetrics(root, ScrollableLayerGuid::START_SCROLL_ID, + CSSRect(0, 0, 100, 200)); + + registration = MakeUnique<ScopedLayerTreeRegistration>(LayersId{0}, mcc); + UpdateHitTestingTree(); + rootApzc = ApzcOf(root); + + // Start a pan gesture a few pixels below the 20px DTC region. + ScreenIntPoint cursorLocation(10, 25); + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID); + APZEventResult result = + PanGesture(PanGestureInput::PANGESTURE_START, manager, cursorLocation, + ScreenPoint(0, -2), mcc->Time()); + + // At this point, we should be overscrolled. + EXPECT_TRUE(rootApzc->IsOverscrolled()); + + // Pan further, until the DTC region is under the cursor. + // Note that, due to ApplyResistance(), we need a large input delta to cause a + // visual transform enough to bridge the 5px to the DTC region. + mcc->AdvanceByMillis(10); + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID); + PanGesture(PanGestureInput::PANGESTURE_PAN, manager, cursorLocation, + ScreenPoint(0, -100), mcc->Time()); + + // At this point, we are still overscrolled. Record the overscroll amount. + EXPECT_TRUE(rootApzc->IsOverscrolled()); + float overscrollY = rootApzc->GetOverscrollAmount().y; + + // Send a content response with preventDefault = true. + manager->SetAllowedTouchBehavior(result.mInputBlockId, + {AllowedTouchBehavior::VERTICAL_PAN}); + manager->SetTargetAPZC(result.mInputBlockId, {result.mTargetGuid}); + manager->ContentReceivedInputBlock(result.mInputBlockId, + /*aPreventDefault=*/true); + + // The content response has the effect of interrupting the input block + // but no processing happens yet (as there are no events in the block). + EXPECT_TRUE(rootApzc->IsOverscrolled()); + EXPECT_EQ(overscrollY, rootApzc->GetOverscrollAmount().y); + + // Send one more pan event. This starts a new, *unconfirmed* input block + // (via the "transmogrify" codepath). + mcc->AdvanceByMillis(10); + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID, + {CompositorHitTestFlags::eVisibleToHitTest, + CompositorHitTestFlags::eIrregularArea}); + result = PanGesture(PanGestureInput::PANGESTURE_PAN, manager, cursorLocation, + ScreenPoint(0, -10), mcc->Time()); + + // No overscroll occurs (the event is waiting in the queue for confirmation). + EXPECT_TRUE(rootApzc->IsOverscrolled()); + EXPECT_EQ(overscrollY, rootApzc->GetOverscrollAmount().y); + + // preventDefault the new event as well + manager->SetAllowedTouchBehavior(result.mInputBlockId, + {AllowedTouchBehavior::VERTICAL_PAN}); + manager->SetTargetAPZC(result.mInputBlockId, {result.mTargetGuid}); + manager->ContentReceivedInputBlock(result.mInputBlockId, + /*aPreventDefault=*/true); + + // This should trigger clearing the overscrolling and resetting the state. + EXPECT_FALSE(rootApzc->IsOverscrolled()); + rootApzc->AssertStateIsReset(); + + // If there are momentum events after this point, they should not cause + // further scrolling or overscorll. + mcc->AdvanceByMillis(10); + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID); + result = PanGesture(PanGestureInput::PANGESTURE_MOMENTUMSTART, manager, + cursorLocation, ScreenPoint(0, -100), mcc->Time()); + mcc->AdvanceByMillis(10); + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID); + result = PanGesture(PanGestureInput::PANGESTURE_MOMENTUMPAN, manager, + cursorLocation, ScreenPoint(0, -100), mcc->Time()); + EXPECT_FALSE(rootApzc->IsOverscrolled()); + EXPECT_EQ(rootApzc->GetFrameMetrics().GetVisualScrollOffset(), + CSSPoint(0, 0)); +} +#endif diff --git a/gfx/layers/apz/test/gtest/TestPanning.cpp b/gfx/layers/apz/test/gtest/TestPanning.cpp new file mode 100644 index 0000000000..886b0fec99 --- /dev/null +++ b/gfx/layers/apz/test/gtest/TestPanning.cpp @@ -0,0 +1,251 @@ +/* -*- 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 "APZCBasicTester.h" +#include "APZTestCommon.h" +#include "InputUtils.h" +#include "gtest/gtest.h" + +class APZCPanningTester : public APZCBasicTester { + protected: + void DoPanTest(bool aShouldTriggerScroll, bool aShouldBeConsumed, + uint32_t aBehavior) { + if (aShouldTriggerScroll) { + // Three repaint request for each pan. + EXPECT_CALL(*mcc, RequestContentRepaint(_)).Times(6); + } else { + EXPECT_CALL(*mcc, RequestContentRepaint(_)).Times(0); + } + + int touchStart = 50; + int touchEnd = 10; + ParentLayerPoint pointOut; + AsyncTransform viewTransformOut; + + nsTArray<uint32_t> allowedTouchBehaviors; + allowedTouchBehaviors.AppendElement(aBehavior); + + // Pan down + PanAndCheckStatus(apzc, touchStart, touchEnd, aShouldBeConsumed, + &allowedTouchBehaviors); + apzc->SampleContentTransformForFrame(&viewTransformOut, pointOut); + + if (aShouldTriggerScroll) { + EXPECT_EQ(ParentLayerPoint(0, -(touchEnd - touchStart)), pointOut); + EXPECT_NE(AsyncTransform(), viewTransformOut); + } else { + EXPECT_EQ(ParentLayerPoint(), pointOut); + EXPECT_EQ(AsyncTransform(), viewTransformOut); + } + + // Clear the fling from the previous pan, or stopping it will + // consume the next touchstart + apzc->CancelAnimation(); + + // Pan back + PanAndCheckStatus(apzc, touchEnd, touchStart, aShouldBeConsumed, + &allowedTouchBehaviors); + apzc->SampleContentTransformForFrame(&viewTransformOut, pointOut); + + EXPECT_EQ(ParentLayerPoint(), pointOut); + EXPECT_EQ(AsyncTransform(), viewTransformOut); + } + + void DoPanWithPreventDefaultTest() { + MakeApzcWaitForMainThread(); + + int touchStart = 50; + int touchEnd = 10; + ParentLayerPoint pointOut; + AsyncTransform viewTransformOut; + uint64_t blockId = 0; + + // Pan down + nsTArray<uint32_t> allowedTouchBehaviors; + allowedTouchBehaviors.AppendElement( + mozilla::layers::AllowedTouchBehavior::VERTICAL_PAN); + PanAndCheckStatus(apzc, touchStart, touchEnd, true, &allowedTouchBehaviors, + &blockId); + + // Send the signal that content has handled and preventDefaulted the touch + // events. This flushes the event queue. + apzc->ContentReceivedInputBlock(blockId, true); + + apzc->SampleContentTransformForFrame(&viewTransformOut, pointOut); + EXPECT_EQ(ParentLayerPoint(), pointOut); + EXPECT_EQ(AsyncTransform(), viewTransformOut); + + apzc->AssertStateIsReset(); + } +}; + +// In the each of the following 4 pan tests we are performing two pan gestures: +// vertical pan from top to bottom and back - from bottom to top. According to +// the pointer-events/touch-action spec AUTO and PAN_Y touch-action values allow +// vertical scrolling while NONE and PAN_X forbid it. The first parameter of +// DoPanTest method specifies this behavior. However, the events will be marked +// as consumed even if the behavior in PAN_X, because the user could move their +// finger horizontally too - APZ has no way of knowing beforehand and so must +// consume the events. +TEST_F(APZCPanningTester, PanWithTouchActionAuto) { + // Velocity bias can cause extra repaint requests. + SCOPED_GFX_PREF_FLOAT("apz.velocity_bias", 0.0); + DoPanTest(true, true, + mozilla::layers::AllowedTouchBehavior::HORIZONTAL_PAN | + mozilla::layers::AllowedTouchBehavior::VERTICAL_PAN); +} + +TEST_F(APZCPanningTester, PanWithTouchActionNone) { + // Velocity bias can cause extra repaint requests. + SCOPED_GFX_PREF_FLOAT("apz.velocity_bias", 0.0); + DoPanTest(false, false, 0); +} + +TEST_F(APZCPanningTester, PanWithTouchActionPanX) { + // Velocity bias can cause extra repaint requests. + SCOPED_GFX_PREF_FLOAT("apz.velocity_bias", 0.0); + DoPanTest(false, false, + mozilla::layers::AllowedTouchBehavior::HORIZONTAL_PAN); +} + +TEST_F(APZCPanningTester, PanWithTouchActionPanY) { + // Velocity bias can cause extra repaint requests. + SCOPED_GFX_PREF_FLOAT("apz.velocity_bias", 0.0); + DoPanTest(true, true, mozilla::layers::AllowedTouchBehavior::VERTICAL_PAN); +} + +TEST_F(APZCPanningTester, PanWithPreventDefault) { + DoPanWithPreventDefaultTest(); +} + +TEST_F(APZCPanningTester, PanWithHistoricalTouchData) { + SCOPED_GFX_PREF_FLOAT("apz.fling_min_velocity_threshold", 0.0); + + // Simulate the same pan gesture, in three different ways. + // We start at y=50, with a 50ms resting period at the start of the pan. + // Then we accelerate the finger upwards towards y=10, reaching a 10px/10ms + // velocity towards the end of the panning motion. + // + // The first simulation fires touch move events with 10ms gaps. + // The second simulation skips two of the touch move events, simulating + // "jank". The third simulation also skips those two events, but reports the + // missed positions in the following event's historical coordinates. + // + // Consequently, the first and third simulation should estimate the same + // velocities, whereas the second simulation should estimate a different + // velocity because it is missing data. + + // First simulation: full data + + APZEventResult result = TouchDown(apzc, ScreenIntPoint(0, 50), mcc->Time()); + if (result.GetStatus() != nsEventStatus_eConsumeNoDefault) { + SetDefaultAllowedTouchBehavior(apzc, result.mInputBlockId); + } + + mcc->AdvanceByMillis(50); + result = TouchMove(apzc, ScreenIntPoint(0, 45), mcc->Time()); + mcc->AdvanceByMillis(10); + result = TouchMove(apzc, ScreenIntPoint(0, 40), mcc->Time()); + mcc->AdvanceByMillis(10); + result = TouchMove(apzc, ScreenIntPoint(0, 30), mcc->Time()); + mcc->AdvanceByMillis(10); + result = TouchMove(apzc, ScreenIntPoint(0, 20), mcc->Time()); + result = TouchUp(apzc, ScreenIntPoint(0, 20), mcc->Time()); + auto velocityFromFullDataAsSeparateEvents = apzc->GetVelocityVector(); + apzc->CancelAnimation(); + + mcc->AdvanceByMillis(100); + + // Second simulation: partial data + + result = TouchDown(apzc, ScreenIntPoint(0, 50), mcc->Time()); + if (result.GetStatus() != nsEventStatus_eConsumeNoDefault) { + SetDefaultAllowedTouchBehavior(apzc, result.mInputBlockId); + } + + mcc->AdvanceByMillis(50); + result = TouchMove(apzc, ScreenIntPoint(0, 45), mcc->Time()); + mcc->AdvanceByMillis(30); + result = TouchMove(apzc, ScreenIntPoint(0, 20), mcc->Time()); + result = TouchUp(apzc, ScreenIntPoint(0, 20), mcc->Time()); + auto velocityFromPartialData = apzc->GetVelocityVector(); + apzc->CancelAnimation(); + + mcc->AdvanceByMillis(100); + + // Third simulation: full data via historical data + + result = TouchDown(apzc, ScreenIntPoint(0, 50), mcc->Time()); + if (result.GetStatus() != nsEventStatus_eConsumeNoDefault) { + SetDefaultAllowedTouchBehavior(apzc, result.mInputBlockId); + } + + mcc->AdvanceByMillis(50); + result = TouchMove(apzc, ScreenIntPoint(0, 45), mcc->Time()); + mcc->AdvanceByMillis(30); + + MultiTouchInput mti = + CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_MOVE, mcc->Time()); + auto singleTouchData = CreateSingleTouchData(0, ScreenIntPoint(0, 20)); + singleTouchData.mHistoricalData.AppendElement( + SingleTouchData::HistoricalTouchData{ + mcc->Time() - TimeDuration::FromMilliseconds(20), + ScreenIntPoint(0, 40), + {}, + {}, + 0.0f, + 0.0f}); + singleTouchData.mHistoricalData.AppendElement( + SingleTouchData::HistoricalTouchData{ + mcc->Time() - TimeDuration::FromMilliseconds(10), + ScreenIntPoint(0, 30), + {}, + {}, + 0.0f, + 0.0f}); + mti.mTouches.AppendElement(singleTouchData); + result = apzc->ReceiveInputEvent(mti); + + result = TouchUp(apzc, ScreenIntPoint(0, 20), mcc->Time()); + auto velocityFromFullDataViaHistory = apzc->GetVelocityVector(); + apzc->CancelAnimation(); + + EXPECT_EQ(velocityFromFullDataAsSeparateEvents, + velocityFromFullDataViaHistory); + EXPECT_NE(velocityFromPartialData, velocityFromFullDataViaHistory); +} + +TEST_F(APZCPanningTester, DuplicatePanEndEvents_Bug1833950) { + // Send a pan gesture that triggers a fling animation at the end. + // Note that we need at least two _PAN events to have enough samples + // in the velocity tracker to compute a fling velocity. + PanGesture(PanGestureInput::PANGESTURE_START, apzc, ScreenIntPoint(50, 80), + ScreenPoint(0, 2), mcc->Time()); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 80), + ScreenPoint(0, 10), mcc->Time()); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 80), + ScreenPoint(0, 10), mcc->Time()); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + PanGesture(PanGestureInput::PANGESTURE_END, apzc, ScreenIntPoint(50, 80), + ScreenPoint(0, 0), mcc->Time(), MODIFIER_NONE, + /*aSimulateMomentum=*/true); + + // Give the fling animation a chance to start. + SampleAnimationOnce(); + apzc->AssertStateIsFling(); + + // Send a duplicate pan-end event. + // This test is just intended to check that doing this doesn't + // trigger an assertion failure in debug mode. + PanGesture(PanGestureInput::PANGESTURE_END, apzc, ScreenIntPoint(50, 80), + ScreenPoint(0, 0), mcc->Time(), MODIFIER_NONE, + /*aSimulateMomentum=*/true); +} diff --git a/gfx/layers/apz/test/gtest/TestPinching.cpp b/gfx/layers/apz/test/gtest/TestPinching.cpp new file mode 100644 index 0000000000..a22d742eb0 --- /dev/null +++ b/gfx/layers/apz/test/gtest/TestPinching.cpp @@ -0,0 +1,664 @@ +/* -*- 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 "APZCBasicTester.h" +#include "APZTestCommon.h" +#include "InputUtils.h" +#include "mozilla/StaticPrefs_apz.h" + +class APZCPinchTester : public APZCBasicTester { + private: + // This (multiplied by apz.touch_start_tolerance) needs to be the hypotenuse + // in a Pythagorean triple, along with overcomeTouchToleranceX and + // overcomeTouchToleranceY from APZCTesterBase::Pan(). + // This is because APZCTesterBase::Pan(), when run without the + // PanOptions::ExactCoordinates option, will need to first overcome the + // touch start tolerance by performing a move of exactly + // (apz.touch_start_tolerance * DPI) length. + // When moving on both axes at once, we need to use integers for both legs + // (overcomeTouchToleranceX and overcomeTouchToleranceY) while making sure + // that the hypotenuse is also a round integer number (hence Pythagorean + // triples). (The hypotenuse is the length of the movement in this case.) + static const int mDPI = 100; + + public: + explicit APZCPinchTester( + AsyncPanZoomController::GestureBehavior aGestureBehavior = + AsyncPanZoomController::DEFAULT_GESTURES) + : APZCBasicTester(aGestureBehavior) {} + + void SetUp() override { + APZCBasicTester::SetUp(); + tm->SetDPI(mDPI); + } + + protected: + FrameMetrics GetPinchableFrameMetrics() { + FrameMetrics fm; + fm.SetCompositionBounds(ParentLayerRect(0, 0, 100, 200)); + fm.SetScrollableRect(CSSRect(0, 0, 980, 1000)); + fm.SetVisualScrollOffset(CSSPoint(300, 300)); + fm.SetLayoutViewport(CSSRect(300, 300, 100, 200)); + fm.SetZoom(CSSToParentLayerScale(2.0)); + // APZC only allows zooming on the root scrollable frame. + fm.SetIsRootContent(true); + // the visible area of the document in CSS pixels is x=300 y=300 w=50 h=100 + return fm; + } + + void DoPinchTest(bool aShouldTriggerPinch, + nsTArray<uint32_t>* aAllowedTouchBehaviors = nullptr) { + apzc->SetFrameMetrics(GetPinchableFrameMetrics()); + MakeApzcZoomable(); + + if (aShouldTriggerPinch) { + // One repaint request for each gesture. + EXPECT_CALL(*mcc, RequestContentRepaint(_)).Times(2); + } else { + EXPECT_CALL(*mcc, RequestContentRepaint(_)).Times(0); + } + + int touchInputId = 0; + if (mGestureBehavior == AsyncPanZoomController::USE_GESTURE_DETECTOR) { + PinchWithTouchInputAndCheckStatus(apzc, ScreenIntPoint(250, 300), 1.25, + touchInputId, aShouldTriggerPinch, + aAllowedTouchBehaviors); + } else { + PinchWithPinchInputAndCheckStatus(apzc, ScreenIntPoint(250, 300), 1.25, + aShouldTriggerPinch); + } + + apzc->AssertStateIsReset(); + + FrameMetrics fm = apzc->GetFrameMetrics(); + + if (aShouldTriggerPinch) { + // the visible area of the document in CSS pixels is now x=325 y=330 w=40 + // h=80 + EXPECT_EQ(2.5f, fm.GetZoom().scale); + EXPECT_EQ(325, fm.GetVisualScrollOffset().x); + EXPECT_EQ(330, fm.GetVisualScrollOffset().y); + } else { + // The frame metrics should stay the same since touch-action:none makes + // apzc ignore pinch gestures. + EXPECT_EQ(2.0f, fm.GetZoom().scale); + EXPECT_EQ(300, fm.GetVisualScrollOffset().x); + EXPECT_EQ(300, fm.GetVisualScrollOffset().y); + } + + // part 2 of the test, move to the top-right corner of the page and pinch + // and make sure we stay in the correct spot + fm.SetZoom(CSSToParentLayerScale(2.0)); + fm.SetVisualScrollOffset(CSSPoint(930, 5)); + apzc->SetFrameMetrics(fm); + // the visible area of the document in CSS pixels is x=930 y=5 w=50 h=100 + + if (mGestureBehavior == AsyncPanZoomController::USE_GESTURE_DETECTOR) { + PinchWithTouchInputAndCheckStatus(apzc, ScreenIntPoint(250, 300), 0.5, + touchInputId, aShouldTriggerPinch, + aAllowedTouchBehaviors); + } else { + PinchWithPinchInputAndCheckStatus(apzc, ScreenIntPoint(250, 300), 0.5, + aShouldTriggerPinch); + } + + apzc->AssertStateIsReset(); + + fm = apzc->GetFrameMetrics(); + + if (aShouldTriggerPinch) { + // the visible area of the document in CSS pixels is now x=805 y=0 w=100 + // h=200 + EXPECT_EQ(1.0f, fm.GetZoom().scale); + EXPECT_EQ(805, fm.GetVisualScrollOffset().x); + EXPECT_EQ(0, fm.GetVisualScrollOffset().y); + } else { + EXPECT_EQ(2.0f, fm.GetZoom().scale); + EXPECT_EQ(930, fm.GetVisualScrollOffset().x); + EXPECT_EQ(5, fm.GetVisualScrollOffset().y); + } + } +}; + +class APZCPinchGestureDetectorTester : public APZCPinchTester { + public: + APZCPinchGestureDetectorTester() + : APZCPinchTester(AsyncPanZoomController::USE_GESTURE_DETECTOR) {} + + void DoPinchWithPreventDefaultTest() { + FrameMetrics originalMetrics = GetPinchableFrameMetrics(); + apzc->SetFrameMetrics(originalMetrics); + + MakeApzcWaitForMainThread(); + MakeApzcZoomable(); + + uint64_t blockId = 0; + PinchWithTouchInput(apzc, ScreenIntPoint(250, 300), 1.25, + PinchOptions().OutInputBlockId(&blockId)); + + // Send the prevent-default notification for the touch block + apzc->ContentReceivedInputBlock(blockId, true); + + // verify the metrics didn't change (i.e. the pinch was ignored) + FrameMetrics fm = apzc->GetFrameMetrics(); + EXPECT_EQ(originalMetrics.GetZoom(), fm.GetZoom()); + EXPECT_EQ(originalMetrics.GetVisualScrollOffset().x, + fm.GetVisualScrollOffset().x); + EXPECT_EQ(originalMetrics.GetVisualScrollOffset().y, + fm.GetVisualScrollOffset().y); + + apzc->AssertStateIsReset(); + } +}; + +class APZCPinchLockingTester : public APZCPinchTester { + private: + ScreenIntPoint mFocus; + float mSpan; + int mPinchLockBufferMaxAge; + + public: + APZCPinchLockingTester() + : APZCPinchTester(AsyncPanZoomController::USE_GESTURE_DETECTOR), + mFocus(ScreenIntPoint(200, 300)), + mSpan(10.0) {} + + virtual void SetUp() { + mPinchLockBufferMaxAge = + StaticPrefs::apz_pinch_lock_buffer_max_age_AtStartup(); + + APZCPinchTester::SetUp(); + apzc->SetFrameMetrics(GetPinchableFrameMetrics()); + MakeApzcZoomable(); + + auto event = CreatePinchGestureInput(PinchGestureInput::PINCHGESTURE_START, + mFocus, mSpan, mSpan, mcc->Time()); + apzc->ReceiveInputEvent(event); + mcc->AdvanceBy(TimeDuration::FromMilliseconds(mPinchLockBufferMaxAge + 1)); + } + + void twoFingerPan() { + ScreenCoord panDistance = + StaticPrefs::apz_pinch_lock_scroll_lock_threshold() * 1.2 * + tm->GetDPI(); + + mFocus = ScreenIntPoint((int)(mFocus.x.value + panDistance), + (int)(mFocus.y.value)); + + auto event = CreatePinchGestureInput(PinchGestureInput::PINCHGESTURE_SCALE, + mFocus, mSpan, mSpan, mcc->Time()); + apzc->ReceiveInputEvent(event); + mcc->AdvanceBy(TimeDuration::FromMilliseconds(mPinchLockBufferMaxAge + 1)); + } + + void twoFingerZoom() { + float pinchDistance = + StaticPrefs::apz_pinch_lock_span_breakout_threshold() * 1.2 * + tm->GetDPI(); + + float newSpan = mSpan + pinchDistance; + + auto event = CreatePinchGestureInput(PinchGestureInput::PINCHGESTURE_SCALE, + mFocus, newSpan, mSpan, mcc->Time()); + apzc->ReceiveInputEvent(event); + mcc->AdvanceBy(TimeDuration::FromMilliseconds(mPinchLockBufferMaxAge + 1)); + mSpan = newSpan; + } + + bool isPinchLockActive() { + FrameMetrics originalMetrics = apzc->GetFrameMetrics(); + + // Send a small scale input to the APZC + float pinchDistance = + StaticPrefs::apz_pinch_lock_span_breakout_threshold() * 0.8 * + tm->GetDPI(); + auto event = + CreatePinchGestureInput(PinchGestureInput::PINCHGESTURE_SCALE, mFocus, + mSpan + pinchDistance, mSpan, mcc->Time()); + apzc->ReceiveInputEvent(event); + + FrameMetrics result = apzc->GetFrameMetrics(); + bool lockActive = originalMetrics.GetZoom() == result.GetZoom() && + originalMetrics.GetVisualScrollOffset().x == + result.GetVisualScrollOffset().x && + originalMetrics.GetVisualScrollOffset().y == + result.GetVisualScrollOffset().y; + + // Avoid side effects, reset to original frame metrics + apzc->SetFrameMetrics(originalMetrics); + return lockActive; + } +}; + +TEST_F(APZCPinchGestureDetectorTester, + Pinch_UseGestureDetector_TouchActionNone) { + nsTArray<uint32_t> behaviors = {mozilla::layers::AllowedTouchBehavior::NONE, + mozilla::layers::AllowedTouchBehavior::NONE}; + DoPinchTest(false, &behaviors); +} + +TEST_F(APZCPinchGestureDetectorTester, + Pinch_UseGestureDetector_TouchActionZoom) { + nsTArray<uint32_t> behaviors; + behaviors.AppendElement(mozilla::layers::AllowedTouchBehavior::PINCH_ZOOM); + behaviors.AppendElement(mozilla::layers::AllowedTouchBehavior::PINCH_ZOOM); + DoPinchTest(true, &behaviors); +} + +TEST_F(APZCPinchGestureDetectorTester, + Pinch_UseGestureDetector_TouchActionNotAllowZoom) { + nsTArray<uint32_t> behaviors; + behaviors.AppendElement(mozilla::layers::AllowedTouchBehavior::NONE); + behaviors.AppendElement(mozilla::layers::AllowedTouchBehavior::PINCH_ZOOM); + DoPinchTest(false, &behaviors); +} + +TEST_F(APZCPinchGestureDetectorTester, + Pinch_UseGestureDetector_TouchActionNone_NoAPZZoom) { + SCOPED_GFX_PREF_BOOL("apz.allow_zooming", false); + + // Since we are preventing the pinch action via touch-action we should not be + // sending the pinch gesture notifications that would normally be sent when + // apz_allow_zooming is false. + EXPECT_CALL(*mcc, NotifyPinchGesture(_, _, _, _, _)).Times(0); + nsTArray<uint32_t> behaviors = {mozilla::layers::AllowedTouchBehavior::NONE, + mozilla::layers::AllowedTouchBehavior::NONE}; + DoPinchTest(false, &behaviors); +} + +TEST_F(APZCPinchGestureDetectorTester, Pinch_PreventDefault) { + DoPinchWithPreventDefaultTest(); +} + +TEST_F(APZCPinchGestureDetectorTester, Pinch_PreventDefault_NoAPZZoom) { + SCOPED_GFX_PREF_BOOL("apz.allow_zooming", false); + + // Since we are preventing the pinch action we should not be sending the pinch + // gesture notifications that would normally be sent when apz_allow_zooming is + // false. + EXPECT_CALL(*mcc, NotifyPinchGesture(_, _, _, _, _)).Times(0); + + DoPinchWithPreventDefaultTest(); +} + +TEST_F(APZCPinchGestureDetectorTester, Panning_TwoFingerFling_ZoomDisabled) { + SCOPED_GFX_PREF_FLOAT("apz.fling_min_velocity_threshold", 0.0f); + + apzc->SetFrameMetrics(GetPinchableFrameMetrics()); + MakeApzcUnzoomable(); + + // Perform a two finger pan + PinchWithTouchInput(apzc, ScreenIntPoint(100, 200), 1, + PinchOptions().SecondFocus(ScreenIntPoint(100, 100))); + + // Expect to be in a flinging state + apzc->AssertStateIsFling(); +} + +TEST_F(APZCPinchGestureDetectorTester, Pinch_DoesntFling_ZoomDisabled) { + SCOPED_GFX_PREF_FLOAT("apz.fling_min_velocity_threshold", 0.0f); + + apzc->SetFrameMetrics(GetPinchableFrameMetrics()); + MakeApzcUnzoomable(); + + // Perform a pinch + PinchWithTouchInput(apzc, ScreenIntPoint(100, 200), 2, + PinchOptions() + .Flags(PinchFlags::LiftFinger2) + .Vertical(true) + .SecondFocus(ScreenIntPoint(100, 100))); + + // Lift second finger after a pause + mcc->AdvanceBy(TimeDuration::FromMilliseconds(50)); + TouchUp(apzc, ScreenIntPoint(100, 100), mcc->Time()); + + // Pinch should not trigger a fling + EXPECT_EQ(apzc->GetVelocityVector().y, 0); +} + +TEST_F(APZCPinchGestureDetectorTester, Panning_TwoFingerFling_ZoomEnabled) { + SCOPED_GFX_PREF_FLOAT("apz.fling_min_velocity_threshold", 0.0f); + + apzc->SetFrameMetrics(GetPinchableFrameMetrics()); + MakeApzcZoomable(); + + // Perform a two finger pan + PinchWithTouchInput(apzc, ScreenIntPoint(100, 200), 1, + PinchOptions().SecondFocus(ScreenIntPoint(100, 100))); + + // Expect to NOT be in flinging state + apzc->AssertStateIsReset(); +} + +TEST_F(APZCPinchGestureDetectorTester, + Panning_TwoThenOneFingerFling_ZoomEnabled) { + SCOPED_GFX_PREF_FLOAT("apz.fling_min_velocity_threshold", 0.0f); + + apzc->SetFrameMetrics(GetPinchableFrameMetrics()); + MakeApzcZoomable(); + + // Perform a two finger pan lifting only the first finger + PinchWithTouchInput(apzc, ScreenIntPoint(100, 200), 1, + PinchOptions() + .Flags(PinchFlags::LiftFinger2) + .SecondFocus(ScreenIntPoint(100, 100))); + + // Lift second finger after a pause + mcc->AdvanceBy(TimeDuration::FromMilliseconds(50)); + TouchUp(apzc, ScreenIntPoint(100, 100), mcc->Time()); + + // This gesture should activate the pinch lock, and result + // in a fling even if the page is zoomable. + apzc->AssertStateIsFling(); +} + +TEST_F(APZCPinchGestureDetectorTester, + Panning_TwoThenOneFingerFling_ZoomDisabled) { + SCOPED_GFX_PREF_FLOAT("apz.fling_min_velocity_threshold", 0.0f); + + apzc->SetFrameMetrics(GetPinchableFrameMetrics()); + MakeApzcUnzoomable(); + + // Perform a two finger pan lifting only the first finger + PinchWithTouchInput(apzc, ScreenIntPoint(100, 200), 1, + PinchOptions() + .Flags(PinchFlags::LiftFinger2) + .SecondFocus(ScreenIntPoint(100, 100))); + + // Lift second finger after a pause + mcc->AdvanceBy(TimeDuration::FromMilliseconds(50)); + TouchUp(apzc, ScreenIntPoint(100, 100), mcc->Time()); + + // This gesture should activate the pinch lock and result in a fling + apzc->AssertStateIsFling(); +} + +TEST_F(APZCPinchTester, Panning_TwoFinger_ZoomDisabled) { + // set up APZ + apzc->SetFrameMetrics(GetPinchableFrameMetrics()); + MakeApzcUnzoomable(); + + nsEventStatus statuses[3]; // scalebegin, scale, scaleend + PinchWithPinchInput(apzc, ScreenIntPoint(250, 350), ScreenIntPoint(200, 300), + 10, &statuses); + + FrameMetrics fm = apzc->GetFrameMetrics(); + + // It starts from (300, 300), then moves the focus point from (250, 350) to + // (200, 300) pans by (50, 50) screen pixels, but there is a 2x zoom, which + // causes the scroll offset to change by half of that (25, 25) pixels. + EXPECT_EQ(325, fm.GetVisualScrollOffset().x); + EXPECT_EQ(325, fm.GetVisualScrollOffset().y); + EXPECT_EQ(2.0, fm.GetZoom().scale); +} + +TEST_F(APZCPinchTester, Panning_Beyond_LayoutViewport) { + SCOPED_GFX_PREF_INT("apz.axis_lock.mode", 0); + + apzc->SetFrameMetrics(GetPinchableFrameMetrics()); + MakeApzcZoomable(); + + // Case 1 - visual viewport is still inside layout viewport. + Pan(apzc, 350, 300, PanOptions::NoFling); + FrameMetrics fm = apzc->GetFrameMetrics(); + // It starts from (300, 300) pans by (0, 50) screen pixels, but there is a + // 2x zoom, which causes the scroll offset to change by half of that (0, 25). + // But the visual viewport is still inside the layout viewport. + EXPECT_EQ(300, fm.GetVisualScrollOffset().x); + EXPECT_EQ(325, fm.GetVisualScrollOffset().y); + EXPECT_EQ(300, fm.GetLayoutViewport().X()); + EXPECT_EQ(300, fm.GetLayoutViewport().Y()); + + // Case 2 - visual viewport crosses the bottom boundary of the layout + // viewport. + Pan(apzc, 525, 325, PanOptions::NoFling); + fm = apzc->GetFrameMetrics(); + // It starts from (300, 325) pans by (0, 200) screen pixels, but there is a + // 2x zoom, which causes the scroll offset to change by half of that + // (0, 100). The visual viewport crossed the bottom boundary of the layout + // viewport by 25px. + EXPECT_EQ(300, fm.GetVisualScrollOffset().x); + EXPECT_EQ(425, fm.GetVisualScrollOffset().y); + EXPECT_EQ(300, fm.GetLayoutViewport().X()); + EXPECT_EQ(325, fm.GetLayoutViewport().Y()); + + // Case 3 - visual viewport crosses the top boundary of the layout viewport. + Pan(apzc, 425, 775, PanOptions::NoFling); + fm = apzc->GetFrameMetrics(); + // It starts from (300, 425) pans by (0, -350) screen pixels, but there is a + // 2x zoom, which causes the scroll offset to change by half of that + // (0, -175). The visual viewport crossed the top of the layout viewport by + // 75px. + EXPECT_EQ(300, fm.GetVisualScrollOffset().x); + EXPECT_EQ(250, fm.GetVisualScrollOffset().y); + EXPECT_EQ(300, fm.GetLayoutViewport().X()); + EXPECT_EQ(250, fm.GetLayoutViewport().Y()); + + // Case 4 - visual viewport crosses the left boundary of the layout viewport. + Pan(apzc, ScreenIntPoint(150, 10), ScreenIntPoint(350, 10), + PanOptions::NoFling); + fm = apzc->GetFrameMetrics(); + // It starts from (300, 250) pans by (-200, 0) screen pixels, but there is a + // 2x zoom, which causes the scroll offset to change by half of that + // (-100, 0). The visual viewport crossed the left boundary of the layout + // viewport by 100px. + EXPECT_EQ(200, fm.GetVisualScrollOffset().x); + EXPECT_EQ(250, fm.GetVisualScrollOffset().y); + EXPECT_EQ(200, fm.GetLayoutViewport().X()); + EXPECT_EQ(250, fm.GetLayoutViewport().Y()); + + // Case 5 - visual viewport crosses the right boundary of the layout viewport. + Pan(apzc, ScreenIntPoint(350, 10), ScreenIntPoint(150, 10), + PanOptions::NoFling); + fm = apzc->GetFrameMetrics(); + // It starts from (200, 250) pans by (200, 0) screen pixels, but there is a + // 2x zoom, which causes the scroll offset to change by half of that + // (100, 0). The visual viewport crossed the right boundary of the layout + // viewport by 50px. + EXPECT_EQ(300, fm.GetVisualScrollOffset().x); + EXPECT_EQ(250, fm.GetVisualScrollOffset().y); + EXPECT_EQ(250, fm.GetLayoutViewport().X()); + EXPECT_EQ(250, fm.GetLayoutViewport().Y()); + + // Case 6 - visual viewport crosses both the vertical and horizontal + // boundaries of the layout viewport by moving diagonally towards the + // top-right corner. + Pan(apzc, ScreenIntPoint(350, 200), ScreenIntPoint(150, 400), + PanOptions::NoFling); + fm = apzc->GetFrameMetrics(); + // It starts from (300, 250) pans by (200, -200) screen pixels, but there is + // a 2x zoom, which causes the scroll offset to change by half of that + // (100, -100). The visual viewport moved by (100, -100) outside the + // boundary of the layout viewport. + EXPECT_EQ(400, fm.GetVisualScrollOffset().x); + EXPECT_EQ(150, fm.GetVisualScrollOffset().y); + EXPECT_EQ(350, fm.GetLayoutViewport().X()); + EXPECT_EQ(150, fm.GetLayoutViewport().Y()); +} + +TEST_F(APZCPinchGestureDetectorTester, Pinch_APZZoom_Disabled) { + SCOPED_GFX_PREF_BOOL("apz.allow_zooming", false); + + FrameMetrics originalMetrics = GetPinchableFrameMetrics(); + apzc->SetFrameMetrics(originalMetrics); + + // When apz_allow_zooming is false, the ZoomConstraintsClient produces + // ZoomConstraints with mAllowZoom set to false. + MakeApzcUnzoomable(); + + // With apz_allow_zooming false, we expect the NotifyPinchGesture function to + // get called as the pinch progresses, but the metrics shouldn't change. + EXPECT_CALL(*mcc, + NotifyPinchGesture(PinchGestureInput::PINCHGESTURE_START, + apzc->GetGuid(), _, LayoutDeviceCoord(0), _)) + .Times(1); + EXPECT_CALL(*mcc, NotifyPinchGesture(PinchGestureInput::PINCHGESTURE_SCALE, + apzc->GetGuid(), _, _, _)) + .Times(AtLeast(1)); + EXPECT_CALL(*mcc, + NotifyPinchGesture(PinchGestureInput::PINCHGESTURE_END, + apzc->GetGuid(), _, LayoutDeviceCoord(0), _)) + .Times(1); + + PinchWithTouchInput(apzc, ScreenIntPoint(250, 300), 1.25); + + // verify the metrics didn't change (i.e. the pinch was ignored inside APZ) + FrameMetrics fm = apzc->GetFrameMetrics(); + EXPECT_EQ(originalMetrics.GetZoom(), fm.GetZoom()); + EXPECT_EQ(originalMetrics.GetVisualScrollOffset().x, + fm.GetVisualScrollOffset().x); + EXPECT_EQ(originalMetrics.GetVisualScrollOffset().y, + fm.GetVisualScrollOffset().y); + + apzc->AssertStateIsReset(); +} + +TEST_F(APZCPinchGestureDetectorTester, Pinch_NoSpan) { + SCOPED_GFX_PREF_BOOL("apz.allow_zooming", false); + + FrameMetrics originalMetrics = GetPinchableFrameMetrics(); + apzc->SetFrameMetrics(originalMetrics); + + // When apz_allow_zooming is false, the ZoomConstraintsClient produces + // ZoomConstraints with mAllowZoom set to false. + MakeApzcUnzoomable(); + + // With apz_allow_zooming false, we expect the NotifyPinchGesture function to + // get called as the pinch progresses, but the metrics shouldn't change. + EXPECT_CALL(*mcc, + NotifyPinchGesture(PinchGestureInput::PINCHGESTURE_START, + apzc->GetGuid(), _, LayoutDeviceCoord(0), _)) + .Times(1); + EXPECT_CALL(*mcc, NotifyPinchGesture(PinchGestureInput::PINCHGESTURE_SCALE, + apzc->GetGuid(), _, _, _)) + .Times(AtLeast(1)); + EXPECT_CALL(*mcc, + NotifyPinchGesture(PinchGestureInput::PINCHGESTURE_END, + apzc->GetGuid(), _, LayoutDeviceCoord(0), _)) + .Times(1); + + int inputId = 0; + ScreenIntPoint focus(250, 300); + + // Do a pinch holding a zero span and moving the focus by y=100 + + const TimeDuration TIME_BETWEEN_TOUCH_EVENT = + TimeDuration::FromMilliseconds(50); + const auto touchBehaviors = Some(nsTArray<uint32_t>{kDefaultTouchBehavior}); + + MultiTouchInput mtiStart = + MultiTouchInput(MultiTouchInput::MULTITOUCH_START, 0, mcc->Time(), 0); + mtiStart.mTouches.AppendElement(CreateSingleTouchData(inputId, focus)); + mtiStart.mTouches.AppendElement(CreateSingleTouchData(inputId + 1, focus)); + apzc->ReceiveInputEvent(mtiStart, touchBehaviors); + mcc->AdvanceBy(TIME_BETWEEN_TOUCH_EVENT); + + focus.y -= 35 + 1; // this is to get over the PINCH_START_THRESHOLD in + // GestureEventListener.cpp + MultiTouchInput mtiMove1 = + MultiTouchInput(MultiTouchInput::MULTITOUCH_MOVE, 0, mcc->Time(), 0); + mtiMove1.mTouches.AppendElement(CreateSingleTouchData(inputId, focus)); + mtiMove1.mTouches.AppendElement(CreateSingleTouchData(inputId + 1, focus)); + apzc->ReceiveInputEvent(mtiMove1); + mcc->AdvanceBy(TIME_BETWEEN_TOUCH_EVENT); + + focus.y -= 100; // do a two-finger scroll of 100 screen pixels + MultiTouchInput mtiMove2 = + MultiTouchInput(MultiTouchInput::MULTITOUCH_MOVE, 0, mcc->Time(), 0); + mtiMove2.mTouches.AppendElement(CreateSingleTouchData(inputId, focus)); + mtiMove2.mTouches.AppendElement(CreateSingleTouchData(inputId + 1, focus)); + apzc->ReceiveInputEvent(mtiMove2); + mcc->AdvanceBy(TIME_BETWEEN_TOUCH_EVENT); + + MultiTouchInput mtiEnd = + MultiTouchInput(MultiTouchInput::MULTITOUCH_END, 0, mcc->Time(), 0); + mtiEnd.mTouches.AppendElement(CreateSingleTouchData(inputId, focus)); + mtiEnd.mTouches.AppendElement(CreateSingleTouchData(inputId + 1, focus)); + apzc->ReceiveInputEvent(mtiEnd); + + // Done, check the metrics to make sure we scrolled by 100 screen pixels, + // which is 50 CSS pixels for the pinchable frame metrics. + + FrameMetrics fm = apzc->GetFrameMetrics(); + EXPECT_EQ(originalMetrics.GetZoom(), fm.GetZoom()); + EXPECT_EQ(originalMetrics.GetVisualScrollOffset().x, + fm.GetVisualScrollOffset().x); + EXPECT_EQ(originalMetrics.GetVisualScrollOffset().y + 50, + fm.GetVisualScrollOffset().y); + + apzc->AssertStateIsReset(); +} + +TEST_F(APZCPinchTester, Pinch_TwoFinger_APZZoom_Disabled_Bug1354185) { + // Set up APZ such that mZoomConstraints.mAllowZoom is false. + SCOPED_GFX_PREF_BOOL("apz.allow_zooming", false); + apzc->SetFrameMetrics(GetPinchableFrameMetrics()); + MakeApzcUnzoomable(); + + // We expect a repaint request for scrolling. + EXPECT_CALL(*mcc, RequestContentRepaint(_)).Times(1); + + // Send only the PINCHGESTURE_START and PINCHGESTURE_SCALE events, + // in order to trigger a call to AsyncPanZoomController::OnScale + // but not to AsyncPanZoomController::OnScaleEnd. + ScreenIntPoint aFocus(250, 350); + ScreenIntPoint aSecondFocus(200, 300); + float aScale = 10; + auto event = CreatePinchGestureInput(PinchGestureInput::PINCHGESTURE_START, + aFocus, 10.0, 10.0, mcc->Time()); + apzc->ReceiveInputEvent(event); + + event = + CreatePinchGestureInput(PinchGestureInput::PINCHGESTURE_SCALE, + aSecondFocus, 10.0f * aScale, 10.0, mcc->Time()); + apzc->ReceiveInputEvent(event); +} + +TEST_F(APZCPinchLockingTester, Pinch_Locking_Free) { + SCOPED_GFX_PREF_INT("apz.pinch_lock.mode", 0); // PINCH_FREE + + twoFingerPan(); + EXPECT_FALSE(isPinchLockActive()); +} + +TEST_F(APZCPinchLockingTester, Pinch_Locking_Normal_Lock) { + SCOPED_GFX_PREF_INT("apz.pinch_lock.mode", 1); // PINCH_NORMAL + + twoFingerPan(); + EXPECT_TRUE(isPinchLockActive()); +} + +TEST_F(APZCPinchLockingTester, Pinch_Locking_Normal_Lock_Break) { + SCOPED_GFX_PREF_INT("apz.pinch_lock.mode", 1); // PINCH_NORMAL + + twoFingerPan(); + twoFingerZoom(); + EXPECT_TRUE(isPinchLockActive()); +} + +TEST_F(APZCPinchLockingTester, Pinch_Locking_Sticky_Lock) { + SCOPED_GFX_PREF_INT("apz.pinch_lock.mode", 2); // PINCH_STICKY + + twoFingerPan(); + EXPECT_TRUE(isPinchLockActive()); +} + +TEST_F(APZCPinchLockingTester, Pinch_Locking_Sticky_Lock_Break) { + SCOPED_GFX_PREF_INT("apz.pinch_lock.mode", 2); // PINCH_STICKY + + twoFingerPan(); + twoFingerZoom(); + EXPECT_FALSE(isPinchLockActive()); +} + +TEST_F(APZCPinchLockingTester, Pinch_Locking_Sticky_Lock_Break_Lock) { + SCOPED_GFX_PREF_INT("apz.pinch_lock.mode", 2); // PINCH_STICKY + + twoFingerPan(); + twoFingerZoom(); + twoFingerPan(); + EXPECT_TRUE(isPinchLockActive()); +} diff --git a/gfx/layers/apz/test/gtest/TestPointerEventsConsumable.cpp b/gfx/layers/apz/test/gtest/TestPointerEventsConsumable.cpp new file mode 100644 index 0000000000..e96a5df6e4 --- /dev/null +++ b/gfx/layers/apz/test/gtest/TestPointerEventsConsumable.cpp @@ -0,0 +1,500 @@ +/* -*- 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 "gtest/gtest.h" + +#include "APZCTreeManagerTester.h" +#include "APZTestCommon.h" +#include "InputUtils.h" +#include "apz/src/AsyncPanZoomController.h" +#include "apz/src/InputBlockState.h" +#include "apz/src/OverscrollHandoffState.h" +#include "mozilla/layers/IAPZCTreeManager.h" + +class APZCArePointerEventsConsumable : public APZCTreeManagerTester { + public: + APZCArePointerEventsConsumable() { CreateMockHitTester(); } + + void CreateSingleElementTree() { + const char* treeShape = "x"; + LayerIntRect layerVisibleRect[] = { + LayerIntRect(0, 0, 100, 100), + }; + CreateScrollData(treeShape, layerVisibleRect); + SetScrollableFrameMetrics(root, ScrollableLayerGuid::START_SCROLL_ID, + CSSRect(0, 0, 500, 500)); + + registration = MakeUnique<ScopedLayerTreeRegistration>(LayersId{0}, mcc); + + UpdateHitTestingTree(); + + ApzcOf(root)->GetFrameMetrics().SetIsRootContent(true); + } + + void CreateScrollHandoffTree() { + const char* treeShape = "x(x)"; + LayerIntRect layerVisibleRect[] = {LayerIntRect(0, 0, 200, 200), + LayerIntRect(50, 50, 100, 100)}; + CreateScrollData(treeShape, layerVisibleRect); + SetScrollableFrameMetrics(root, ScrollableLayerGuid::START_SCROLL_ID, + CSSRect(0, 0, 300, 300)); + SetScrollableFrameMetrics(layers[1], + ScrollableLayerGuid::START_SCROLL_ID + 1, + CSSRect(0, 0, 200, 200)); + SetScrollHandoff(layers[1], root); + registration = MakeUnique<ScopedLayerTreeRegistration>(LayersId{0}, mcc); + UpdateHitTestingTree(); + + ApzcOf(root)->GetFrameMetrics().SetIsRootContent(true); + } + + RefPtr<TouchBlockState> CreateTouchBlockStateForApzc( + const RefPtr<TestAsyncPanZoomController>& aApzc) { + TouchCounter counter{}; + TargetConfirmationFlags flags{true}; + + return new TouchBlockState(aApzc, flags, counter); + } + + void UpdateOverscrollBehavior(ScrollableLayerGuid::ViewID aScrollId, + OverscrollBehavior aX, OverscrollBehavior aY) { + auto* layer = layers[aScrollId - ScrollableLayerGuid::START_SCROLL_ID]; + ModifyFrameMetrics(layer, [aX, aY](ScrollMetadata& sm, FrameMetrics& _) { + OverscrollBehaviorInfo overscroll; + overscroll.mBehaviorX = aX; + overscroll.mBehaviorY = aY; + sm.SetOverscrollBehavior(overscroll); + }); + UpdateHitTestingTree(); + } + + UniquePtr<ScopedLayerTreeRegistration> registration; +}; + +TEST_F(APZCArePointerEventsConsumable, EmptyInput) { + CreateSingleElementTree(); + + RefPtr<TestAsyncPanZoomController> apzc = ApzcOf(root); + RefPtr<TouchBlockState> blockState = CreateTouchBlockStateForApzc(apzc); + + MultiTouchInput touchInput = + CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_START, mcc->Time()); + + const PointerEventsConsumableFlags expected{false, false}; + const PointerEventsConsumableFlags actual = + apzc->ArePointerEventsConsumable(blockState, touchInput); + EXPECT_EQ(expected, actual); +} + +TEST_F(APZCArePointerEventsConsumable, ScrollHorizontally) { + CreateSingleElementTree(); + + RefPtr<TestAsyncPanZoomController> apzc = ApzcOf(root); + RefPtr<TouchBlockState> blockState = CreateTouchBlockStateForApzc(apzc); + + // Create touch with horizontal 20 unit scroll + MultiTouchInput touchStart = + CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_START, mcc->Time()); + touchStart.mTouches.AppendElement( + SingleTouchData(0, ScreenIntPoint(10, 10), ScreenSize(0, 0), 0, 0)); + + MultiTouchInput touchMove = + CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_MOVE, mcc->Time()); + touchMove.mTouches.AppendElement( + SingleTouchData(0, ScreenIntPoint(30, 10), ScreenSize(0, 0), 0, 0)); + + blockState->UpdateSlopState(touchStart, false); + + PointerEventsConsumableFlags actual{}; + PointerEventsConsumableFlags expected{}; + + // Scroll area 500x500, room to pan x, room to pan y + expected = {true, true}; + actual = apzc->ArePointerEventsConsumable(blockState, touchMove); + EXPECT_EQ(expected, actual); + + // Scroll area 100x100, no room to pan x, no room to pan y + apzc->GetFrameMetrics().SetScrollableRect(CSSRect{0, 0, 100, 100}); + expected = {false, true}; + actual = apzc->ArePointerEventsConsumable(blockState, touchMove); + EXPECT_EQ(expected, actual); + + // Scroll area 500x100, room to pan x, no room to pan y + apzc->GetFrameMetrics().SetScrollableRect(CSSRect{0, 0, 500, 100}); + expected = {true, true}; + actual = apzc->ArePointerEventsConsumable(blockState, touchMove); + EXPECT_EQ(expected, actual); + + // Scroll area 100x500, no room to pan x, room to pan y + apzc->GetFrameMetrics().SetScrollableRect(CSSRect{0, 0, 100, 500}); + expected = {false, true}; + actual = apzc->ArePointerEventsConsumable(blockState, touchMove); + EXPECT_EQ(expected, actual); +} + +TEST_F(APZCArePointerEventsConsumable, ScrollVertically) { + CreateSingleElementTree(); + + RefPtr<TestAsyncPanZoomController> apzc = ApzcOf(root); + RefPtr<TouchBlockState> blockState = CreateTouchBlockStateForApzc(apzc); + + // Create touch with vertical 20 unit scroll + MultiTouchInput touchStart = + CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_START, mcc->Time()); + touchStart.mTouches.AppendElement( + SingleTouchData(0, ScreenIntPoint(10, 10), ScreenSize(0, 0), 0, 0)); + + MultiTouchInput touchMove = + CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_MOVE, mcc->Time()); + touchMove.mTouches.AppendElement( + SingleTouchData(0, ScreenIntPoint(10, 30), ScreenSize(0, 0), 0, 0)); + + blockState->UpdateSlopState(touchStart, false); + + PointerEventsConsumableFlags actual{}; + PointerEventsConsumableFlags expected{}; + + // Scroll area 500x500, room to pan x, room to pan y + expected = {true, true}; + actual = apzc->ArePointerEventsConsumable(blockState, touchMove); + EXPECT_EQ(expected, actual); + + // Scroll area 100x100, no room to pan x, no room to pan y + apzc->GetFrameMetrics().SetScrollableRect(CSSRect{0, 0, 100, 100}); + expected = {false, true}; + actual = apzc->ArePointerEventsConsumable(blockState, touchMove); + EXPECT_EQ(expected, actual); + + // Scroll area 500x100, room to pan x, no room to pan y + apzc->GetFrameMetrics().SetScrollableRect(CSSRect{0, 0, 500, 100}); + expected = {false, true}; + actual = apzc->ArePointerEventsConsumable(blockState, touchMove); + EXPECT_EQ(expected, actual); + + // Scroll area 100x500, no room to pan x, room to pan y + apzc->GetFrameMetrics().SetScrollableRect(CSSRect{0, 0, 100, 500}); + expected = {true, true}; + actual = apzc->ArePointerEventsConsumable(blockState, touchMove); + EXPECT_EQ(expected, actual); +} + +TEST_F(APZCArePointerEventsConsumable, NestedElementCanScroll) { + CreateScrollHandoffTree(); + + RefPtr<TestAsyncPanZoomController> apzc = ApzcOf(layers[1]); + RefPtr<TouchBlockState> blockState = CreateTouchBlockStateForApzc(apzc); + + // Create touch with vertical 20 unit scroll + MultiTouchInput touchStart = + CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_START, mcc->Time()); + touchStart.mTouches.AppendElement( + SingleTouchData(0, ScreenIntPoint(60, 60), ScreenSize(0, 0), 0, 0)); + + MultiTouchInput touchMove = + CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_MOVE, mcc->Time()); + touchMove.mTouches.AppendElement( + SingleTouchData(0, ScreenIntPoint(60, 80), ScreenSize(0, 0), 0, 0)); + + blockState->UpdateSlopState(touchStart, false); + + const PointerEventsConsumableFlags expected{true, true}; + const PointerEventsConsumableFlags actual = + apzc->ArePointerEventsConsumable(blockState, touchMove); + EXPECT_EQ(expected, actual); +} + +TEST_F(APZCArePointerEventsConsumable, NestedElementCannotScroll) { + CreateScrollHandoffTree(); + + RefPtr<TestAsyncPanZoomController> apzc = ApzcOf(layers[1]); + RefPtr<TouchBlockState> blockState = CreateTouchBlockStateForApzc(apzc); + + // Create touch with vertical 20 unit scroll + MultiTouchInput touchStart = + CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_START, mcc->Time()); + touchStart.mTouches.AppendElement( + SingleTouchData(0, ScreenIntPoint(60, 60), ScreenSize(0, 0), 0, 0)); + + MultiTouchInput touchMove = + CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_MOVE, mcc->Time()); + touchMove.mTouches.AppendElement( + SingleTouchData(0, ScreenIntPoint(60, 80), ScreenSize(0, 0), 0, 0)); + + blockState->UpdateSlopState(touchStart, false); + + PointerEventsConsumableFlags actual{}; + PointerEventsConsumableFlags expected{}; + + // Set the nested element to have no room to scroll. + // Because of the overscroll handoff, we still have room to scroll + // in the parent element. + apzc->GetFrameMetrics().SetScrollableRect(CSSRect{0, 0, 100, 100}); + expected = {true, true}; + actual = apzc->ArePointerEventsConsumable(blockState, touchMove); + EXPECT_EQ(expected, actual); + + // Set overscroll handoff for the nested element to none. + // Because no handoff will happen, we are not able to use the parent's + // room to scroll. + // Bug 1814886: Once fixed, change expected value to {false, true}. + UpdateOverscrollBehavior(ScrollableLayerGuid::START_SCROLL_ID + 1, + OverscrollBehavior::None, OverscrollBehavior::None); + expected = {true, true}; + actual = apzc->ArePointerEventsConsumable(blockState, touchMove); + EXPECT_EQ(expected, actual); +} + +TEST_F(APZCArePointerEventsConsumable, NotScrollableButZoomable) { + CreateSingleElementTree(); + + RefPtr<TestAsyncPanZoomController> apzc = ApzcOf(root); + RefPtr<TouchBlockState> blockState = CreateTouchBlockStateForApzc(apzc); + + // Create touch with vertical 20 unit scroll + MultiTouchInput touchStart = + CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_START, mcc->Time()); + touchStart.mTouches.AppendElement( + SingleTouchData(0, ScreenIntPoint(60, 60), ScreenSize(0, 0), 0, 0)); + + MultiTouchInput touchMove = + CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_MOVE, mcc->Time()); + touchMove.mTouches.AppendElement( + SingleTouchData(0, ScreenIntPoint(60, 80), ScreenSize(0, 0), 0, 0)); + + blockState->UpdateSlopState(touchStart, false); + + // Make the root have no room to scroll + apzc->GetFrameMetrics().SetScrollableRect(CSSRect{0, 0, 100, 100}); + + // Make zoomable + apzc->UpdateZoomConstraints(ZoomConstraints( + true, true, CSSToParentLayerScale(0.25f), CSSToParentLayerScale(4.0f))); + + PointerEventsConsumableFlags actual{}; + PointerEventsConsumableFlags expected{}; + + expected = {false, true}; + actual = apzc->ArePointerEventsConsumable(blockState, touchMove); + EXPECT_EQ(expected, actual); + + // Add a second touch point and therefore make the APZC consider + // zoom use cases as well. + touchMove.mTouches.AppendElement( + SingleTouchData(0, ScreenIntPoint(60, 90), ScreenSize(0, 0), 0, 0)); + + expected = {true, true}; + actual = apzc->ArePointerEventsConsumable(blockState, touchMove); + EXPECT_EQ(expected, actual); +} + +TEST_F(APZCArePointerEventsConsumable, TouchActionsProhibitAll) { + CreateSingleElementTree(); + + RefPtr<TestAsyncPanZoomController> apzc = ApzcOf(root); + + // Create touch with vertical 20 unit scroll + MultiTouchInput touchStart = + CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_START, mcc->Time()); + touchStart.mTouches.AppendElement( + SingleTouchData(0, ScreenIntPoint(60, 60), ScreenSize(0, 0), 0, 0)); + + MultiTouchInput touchMove = + CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_MOVE, mcc->Time()); + touchMove.mTouches.AppendElement( + SingleTouchData(0, ScreenIntPoint(60, 80), ScreenSize(0, 0), 0, 0)); + + PointerEventsConsumableFlags expected{}; + PointerEventsConsumableFlags actual{}; + + { + RefPtr<TouchBlockState> blockState = CreateTouchBlockStateForApzc(apzc); + blockState->UpdateSlopState(touchStart, false); + + blockState->SetAllowedTouchBehaviors({AllowedTouchBehavior::NONE}); + expected = {true, false}; + actual = apzc->ArePointerEventsConsumable(blockState, touchMove); + EXPECT_EQ(expected, actual); + } + + // Convert touch input to two-finger pinch + touchStart.mTouches.AppendElement( + SingleTouchData(1, ScreenIntPoint(80, 80), ScreenSize(0, 0), 0, 0)); + touchMove.mTouches.AppendElement( + SingleTouchData(1, ScreenIntPoint(90, 90), ScreenSize(0, 0), 0, 0)); + + { + RefPtr<TouchBlockState> blockState = CreateTouchBlockStateForApzc(apzc); + blockState->UpdateSlopState(touchStart, false); + + blockState->SetAllowedTouchBehaviors({AllowedTouchBehavior::NONE}); + expected = {true, false}; + actual = apzc->ArePointerEventsConsumable(blockState, touchMove); + EXPECT_EQ(expected, actual); + } +} + +TEST_F(APZCArePointerEventsConsumable, TouchActionsAllowVerticalScrolling) { + CreateSingleElementTree(); + + RefPtr<TestAsyncPanZoomController> apzc = ApzcOf(root); + + // Create touch with vertical 20 unit scroll + MultiTouchInput touchStart = + CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_START, mcc->Time()); + touchStart.mTouches.AppendElement( + SingleTouchData(0, ScreenIntPoint(60, 60), ScreenSize(0, 0), 0, 0)); + + MultiTouchInput touchMove = + CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_MOVE, mcc->Time()); + touchMove.mTouches.AppendElement( + SingleTouchData(0, ScreenIntPoint(60, 80), ScreenSize(0, 0), 0, 0)); + + PointerEventsConsumableFlags expected{}; + PointerEventsConsumableFlags actual{}; + + { + RefPtr<TouchBlockState> blockState = CreateTouchBlockStateForApzc(apzc); + blockState->UpdateSlopState(touchStart, false); + + blockState->SetAllowedTouchBehaviors({AllowedTouchBehavior::NONE}); + expected = {true, false}; + actual = apzc->ArePointerEventsConsumable(blockState, touchMove); + EXPECT_EQ(expected, actual); + } + + { + RefPtr<TouchBlockState> blockState = CreateTouchBlockStateForApzc(apzc); + blockState->UpdateSlopState(touchStart, false); + + blockState->SetAllowedTouchBehaviors({AllowedTouchBehavior::VERTICAL_PAN}); + expected = {true, true}; + actual = apzc->ArePointerEventsConsumable(blockState, touchMove); + EXPECT_EQ(expected, actual); + } +} + +TEST_F(APZCArePointerEventsConsumable, TouchActionsAllowHorizontalScrolling) { + CreateSingleElementTree(); + + RefPtr<TestAsyncPanZoomController> apzc = ApzcOf(root); + + // Create touch with horizontal 20 unit scroll + MultiTouchInput touchStart = + CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_START, mcc->Time()); + touchStart.mTouches.AppendElement( + SingleTouchData(0, ScreenIntPoint(60, 60), ScreenSize(0, 0), 0, 0)); + + MultiTouchInput touchMove = + CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_MOVE, mcc->Time()); + touchMove.mTouches.AppendElement( + SingleTouchData(0, ScreenIntPoint(80, 60), ScreenSize(0, 0), 0, 0)); + + PointerEventsConsumableFlags expected{}; + PointerEventsConsumableFlags actual{}; + + { + RefPtr<TouchBlockState> blockState = CreateTouchBlockStateForApzc(apzc); + blockState->UpdateSlopState(touchStart, false); + + blockState->SetAllowedTouchBehaviors({AllowedTouchBehavior::NONE}); + expected = {true, false}; + actual = apzc->ArePointerEventsConsumable(blockState, touchMove); + EXPECT_EQ(expected, actual); + } + + { + RefPtr<TouchBlockState> blockState = CreateTouchBlockStateForApzc(apzc); + blockState->UpdateSlopState(touchStart, false); + + blockState->SetAllowedTouchBehaviors( + {AllowedTouchBehavior::HORIZONTAL_PAN}); + expected = {true, true}; + actual = apzc->ArePointerEventsConsumable(blockState, touchMove); + EXPECT_EQ(expected, actual); + } +} + +TEST_F(APZCArePointerEventsConsumable, TouchActionsAllowPinchZoom) { + CreateSingleElementTree(); + + RefPtr<TestAsyncPanZoomController> apzc = ApzcOf(root); + + // Create two-finger pinch + MultiTouchInput touchStart = + CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_START, mcc->Time()); + touchStart.mTouches.AppendElement( + SingleTouchData(0, ScreenIntPoint(60, 60), ScreenSize(0, 0), 0, 0)); + touchStart.mTouches.AppendElement( + SingleTouchData(1, ScreenIntPoint(80, 80), ScreenSize(0, 0), 0, 0)); + + MultiTouchInput touchMove = + CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_MOVE, mcc->Time()); + touchMove.mTouches.AppendElement( + SingleTouchData(0, ScreenIntPoint(50, 50), ScreenSize(0, 0), 0, 0)); + touchMove.mTouches.AppendElement( + SingleTouchData(1, ScreenIntPoint(90, 90), ScreenSize(0, 0), 0, 0)); + + PointerEventsConsumableFlags expected{}; + PointerEventsConsumableFlags actual{}; + + { + RefPtr<TouchBlockState> blockState = CreateTouchBlockStateForApzc(apzc); + blockState->UpdateSlopState(touchStart, false); + + blockState->SetAllowedTouchBehaviors({AllowedTouchBehavior::NONE}); + expected = {true, false}; + actual = apzc->ArePointerEventsConsumable(blockState, touchMove); + EXPECT_EQ(expected, actual); + } + + { + RefPtr<TouchBlockState> blockState = CreateTouchBlockStateForApzc(apzc); + blockState->UpdateSlopState(touchStart, false); + + blockState->SetAllowedTouchBehaviors({AllowedTouchBehavior::PINCH_ZOOM}); + expected = {true, true}; + actual = apzc->ArePointerEventsConsumable(blockState, touchMove); + EXPECT_EQ(expected, actual); + } +} + +TEST_F(APZCArePointerEventsConsumable, DynamicToolbar) { + CreateSingleElementTree(); + + RefPtr<TestAsyncPanZoomController> apzc = ApzcOf(root); + RefPtr<TouchBlockState> blockState = CreateTouchBlockStateForApzc(apzc); + + // Create touch with vertical 20 unit scroll + MultiTouchInput touchStart = + CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_START, mcc->Time()); + touchStart.mTouches.AppendElement( + SingleTouchData(0, ScreenIntPoint(60, 30), ScreenSize(0, 0), 0, 0)); + + MultiTouchInput touchMove = + CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_MOVE, mcc->Time()); + touchMove.mTouches.AppendElement( + SingleTouchData(0, ScreenIntPoint(60, 40), ScreenSize(0, 0), 0, 0)); + + blockState->UpdateSlopState(touchStart, false); + + // Restrict size of scrollable area: No room to pan X, no room to pan Y + apzc->GetFrameMetrics().SetScrollableRect(CSSRect{0, 0, 100, 100}); + + PointerEventsConsumableFlags actual{}; + PointerEventsConsumableFlags expected{}; + + expected = {false, true}; + actual = apzc->ArePointerEventsConsumable(blockState, touchMove); + EXPECT_EQ(expected, actual); + + apzc->GetFrameMetrics().SetCompositionSizeWithoutDynamicToolbar( + ParentLayerSize{100, 90}); + UpdateHitTestingTree(); + + expected = {true, true}; + actual = apzc->ArePointerEventsConsumable(blockState, touchMove); + EXPECT_EQ(expected, actual); +} diff --git a/gfx/layers/apz/test/gtest/TestScrollHandoff.cpp b/gfx/layers/apz/test/gtest/TestScrollHandoff.cpp new file mode 100644 index 0000000000..ae1bb65960 --- /dev/null +++ b/gfx/layers/apz/test/gtest/TestScrollHandoff.cpp @@ -0,0 +1,809 @@ +/* -*- 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<ScopedLayerTreeRegistration> 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<ScopedLayerTreeRegistration>(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<ScopedLayerTreeRegistration>(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<ScopedLayerTreeRegistration>(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<ScopedLayerTreeRegistration>(LayersId{0}, mcc); + UpdateHitTestingTree(); + rootApzc = ApzcOf(root); + } + + void CreateScrollgrabLayerTree(bool makeParentScrollable = true) { + const char* treeShape = "x(x)"; + LayerIntRect layerVisibleRect[] = { + LayerIntRect(0, 0, 100, 100), // scroll-grabbing parent + LayerIntRect(0, 20, 100, 80) // child + }; + CreateScrollData(treeShape, layerVisibleRect); + float parentHeight = makeParentScrollable ? 120 : 100; + SetScrollableFrameMetrics(root, ScrollableLayerGuid::START_SCROLL_ID, + CSSRect(0, 0, 100, parentHeight)); + SetScrollableFrameMetrics(layers[1], + ScrollableLayerGuid::START_SCROLL_ID + 1, + CSSRect(0, 0, 100, 800)); + SetScrollHandoff(layers[1], root); + registration = MakeUnique<ScopedLayerTreeRegistration>(LayersId{0}, mcc); + UpdateHitTestingTree(); + rootApzc = ApzcOf(root); + rootApzc->GetScrollMetadata().SetHasScrollgrab(true); + } + + void TestFlingAcceleration() { + // Jack up the fling acceleration multiplier so we can easily determine + // whether acceleration occured. + const float kAcceleration = 100.0f; + SCOPED_GFX_PREF_FLOAT("apz.fling_accel_base_mult", kAcceleration); + SCOPED_GFX_PREF_FLOAT("apz.fling_accel_min_fling_velocity", 0.0); + SCOPED_GFX_PREF_FLOAT("apz.fling_accel_min_pan_velocity", 0.0); + + RefPtr<TestAsyncPanZoomController> childApzc = ApzcOf(layers[1]); + + // Pan once, enough to fully scroll the scrollgrab parent and then scroll + // and fling the child. + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1); + Pan(manager, 70, 40); + + // Give the fling animation a chance to start. + SampleAnimationsOnce(); + + float childVelocityAfterFling1 = childApzc->GetVelocityVector().y; + + // Pan again. + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1); + Pan(manager, 70, 40); + + // Give the fling animation a chance to start. + // This time it should be accelerated. + SampleAnimationsOnce(); + + float childVelocityAfterFling2 = childApzc->GetVelocityVector().y; + + // We should have accelerated once. + // The division by 2 is to account for friction. + EXPECT_GT(childVelocityAfterFling2, + childVelocityAfterFling1 * kAcceleration / 2); + + // We should not have accelerated twice. + // The division by 4 is to account for friction. + EXPECT_LE(childVelocityAfterFling2, + childVelocityAfterFling1 * kAcceleration * kAcceleration / 4); + } + + void TestCrossApzcAxisLock() { + SCOPED_GFX_PREF_INT("apz.axis_lock.mode", 1); + + CreateScrollHandoffLayerTree1(); + + RefPtr<TestAsyncPanZoomController> 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(); } +}; + +#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<TestAsyncPanZoomController> 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<TestAsyncPanZoomController> 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, + TimeStamp(), 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<TestAsyncPanZoomController> 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 that flinging in a direction where one component of the fling goes into +// overscroll but the other doesn't, results in just the one component being +// handed off to the parent, while the original APZC continues flinging in the +// other direction. +TEST_F(APZScrollHandoffTesterMock, PartialFlingHandoff) { + 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, so the horizontal component of the fling should be handed + // off to the parent APZC. + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1); + Pan(manager, ScreenIntPoint(90, 90), ScreenIntPoint(55, 55)); + + RefPtr<TestAsyncPanZoomController> parent = ApzcOf(layers[0]); + RefPtr<TestAsyncPanZoomController> child = ApzcOf(layers[1]); + + // Advance the child's fling animation once to give the partial handoff + // a chance to occur. + mcc->AdvanceByMillis(10); + child->AdvanceAnimations(mcc->GetSampleTime()); + + // Assert that partial handoff has occurred. + child->AssertStateIsFling(); + parent->AssertStateIsFling(); +} + +// 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<TestAsyncPanZoomController> parent1 = ApzcOf(layers[1]); + RefPtr<TestAsyncPanZoomController> child1 = ApzcOf(layers[2]); + RefPtr<TestAsyncPanZoomController> parent2 = ApzcOf(layers[3]); + RefPtr<TestAsyncPanZoomController> 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(); +} + +#ifndef MOZ_WIDGET_ANDROID // Currently fails on Android +TEST_F(APZScrollHandoffTester, Scrollgrab) { + SCOPED_GFX_PREF_BOOL("apz.allow_immediate_handoff", true); + + // Set up the layer tree + CreateScrollgrabLayerTree(); + + RefPtr<TestAsyncPanZoomController> childApzc = ApzcOf(layers[1]); + + // Pan on the child, enough to fully scroll the scrollgrab parent (20 px) + // and leave some more (another 15 px) for the child. + Pan(childApzc, 80, 45); + + // Check that the parent and child have scrolled as much as we expect. + EXPECT_EQ(20, rootApzc->GetFrameMetrics().GetVisualScrollOffset().y); + EXPECT_EQ(15, childApzc->GetFrameMetrics().GetVisualScrollOffset().y); +} +#endif + +TEST_F(APZScrollHandoffTester, ScrollgrabFling) { + SCOPED_GFX_PREF_BOOL("apz.allow_immediate_handoff", true); + SCOPED_GFX_PREF_FLOAT("apz.fling_min_velocity_threshold", 0.0f); + + // Set up the layer tree + CreateScrollgrabLayerTree(); + + RefPtr<TestAsyncPanZoomController> childApzc = ApzcOf(layers[1]); + + // Pan on the child, not enough to fully scroll the scrollgrab parent. + Pan(childApzc, 80, 70); + + // Check that it is the scrollgrab parent that's in a fling, not the child. + rootApzc->AssertStateIsFling(); + childApzc->AssertStateIsReset(); +} + +TEST_F(APZScrollHandoffTesterMock, ScrollgrabFlingAcceleration1) { + SCOPED_GFX_PREF_BOOL("apz.allow_immediate_handoff", true); + SCOPED_GFX_PREF_FLOAT("apz.fling_min_velocity_threshold", 0.0f); + CreateScrollgrabLayerTree(true /* make parent scrollable */); + + // Note: Usually, fling acceleration does not work across handoff, because our + // fling acceleration code does not propagate the "fling cancel velocity" + // across handoff. However, this test sets apz.fling_min_velocity_threshold to + // zero, so the "fling cancel velocity" is allowed to be zero, and fling + // acceleration succeeds, almost by accident. + TestFlingAcceleration(); +} + +TEST_F(APZScrollHandoffTesterMock, ScrollgrabFlingAcceleration2) { + SCOPED_GFX_PREF_BOOL("apz.allow_immediate_handoff", true); + SCOPED_GFX_PREF_FLOAT("apz.fling_min_velocity_threshold", 0.0f); + CreateScrollgrabLayerTree(false /* do not make parent scrollable */); + TestFlingAcceleration(); +} + +TEST_F(APZScrollHandoffTester, ImmediateHandoffDisallowed_Pan) { + SCOPED_GFX_PREF_BOOL("apz.allow_immediate_handoff", false); + + CreateScrollHandoffLayerTree1(); + + RefPtr<TestAsyncPanZoomController> parentApzc = ApzcOf(layers[0]); + RefPtr<TestAsyncPanZoomController> 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<TestAsyncPanZoomController> parentApzc = ApzcOf(layers[0]); + RefPtr<TestAsyncPanZoomController> 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<TestAsyncPanZoomController> 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<TestAsyncPanZoomController> 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<TestAsyncPanZoomController> 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); +} diff --git a/gfx/layers/apz/test/gtest/TestScrollbarDragging.cpp b/gfx/layers/apz/test/gtest/TestScrollbarDragging.cpp new file mode 100644 index 0000000000..014eb0a4ba --- /dev/null +++ b/gfx/layers/apz/test/gtest/TestScrollbarDragging.cpp @@ -0,0 +1,102 @@ +/* -*- 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 APZScrollbarDraggingTester : public APZCTreeManagerTester { + public: + APZScrollbarDraggingTester() { CreateMockHitTester(); } + + protected: + UniquePtr<ScopedLayerTreeRegistration> registration; + ScrollableLayerGuid::ViewID scrollId = ScrollableLayerGuid::START_SCROLL_ID; + TestAsyncPanZoomController* apzc = nullptr; + + ParentLayerCoord ScrollY() const { + return apzc + ->GetCurrentAsyncScrollOffset(AsyncPanZoomController::eForEventHandling) + .y; + } + + void QueueHitOnVerticalScrollbar() { + mMockHitTester->QueueScrollbarThumbHitResult(scrollId, + ScrollDirection::eVertical); + } + + void CreateLayerTreeWithVerticalScrollbar() { + // The first child is the scrollable node, the second child is the + // scrollbar. + const char* treeShape = "x(xx)"; + LayerIntRect layerVisibleRect[] = {LayerIntRect(0, 0, 100, 100), + LayerIntRect(0, 0, 50, 100), + LayerIntRect(50, 0, 50, 10)}; + CreateScrollData(treeShape, layerVisibleRect); + SetScrollableFrameMetrics(layers[1], scrollId, CSSRect(0, 0, 50, 1000)); + registration = MakeUnique<ScopedLayerTreeRegistration>(LayersId{0}, mcc); + layers[2]->SetScrollbarData(ScrollbarData::CreateForThumb( + ScrollDirection::eVertical, 0.1, 0, 10, 10, true, 0, 100, scrollId)); + UpdateHitTestingTree(); + apzc = ApzcOf(layers[1]); + } +}; + +// Test that the scrollable rect shrinking during dragging does not result +// in scrolling out of bounds. +TEST_F(APZScrollbarDraggingTester, ScrollableRectShrinksDuringDragging) { + // Explicitly enable scrollbar dragging. This allows the test to run on + // Android as well. + SCOPED_GFX_PREF_BOOL("apz.drag.enabled", true); + + CreateLayerTreeWithVerticalScrollbar(); + EXPECT_EQ(ScrollY(), 0); + + // Start a scrollbar drag at y=5. + QueueHitOnVerticalScrollbar(); + auto dragBlockId = + MouseDown(manager, ScreenIntPoint(75, 5), mcc->Time()).mInputBlockId; + manager->StartScrollbarDrag(apzc->GetGuid(), + AsyncDragMetrics(scrollId, 0, dragBlockId, 5, + ScrollDirection::eVertical)); + + // Drag the scrollbar down to y=75. (The total height is 100.) + for (int mouseY = 10; mouseY <= 75; mouseY += 5) { + mcc->AdvanceByMillis(10); + // We do a hit test for every mouse event, including mousemoves. + QueueHitOnVerticalScrollbar(); + MouseMove(manager, ScreenIntPoint(75, mouseY), mcc->Time()); + } + + // We should have scrolled past y>500 at least (total scrollable rect height + // is 1000). + EXPECT_GT(ScrollY(), 500); + + // Shrink the scrollable rect height to 500. + ModifyFrameMetrics(layers[1], [](ScrollMetadata&, FrameMetrics& aMetrics) { + aMetrics.SetScrollableRect(CSSRect(0, 0, 50, 500)); + }); + UpdateHitTestingTree(); + + // Continue the drag to near the bottom, y=95. + // Check that the scroll position never gets out of bounds. (With the + // scrollable rect height now 500, the max vertical scroll position is 400.) + for (int mouseY = 80; mouseY <= 95; mouseY += 5) { + mcc->AdvanceByMillis(10); + QueueHitOnVerticalScrollbar(); + MouseMove(manager, ScreenIntPoint(75, mouseY), mcc->Time()); + EXPECT_LE(ScrollY(), 400); + } + + // End the drag. + mcc->AdvanceByMillis(10); + QueueHitOnVerticalScrollbar(); + MouseUp(manager, ScreenIntPoint(75, 95), mcc->Time()); + + // We should end up at the bottom of the new scroll range (and not out of + // bounds). + EXPECT_EQ(ScrollY(), 400); +} diff --git a/gfx/layers/apz/test/gtest/TestSnapping.cpp b/gfx/layers/apz/test/gtest/TestSnapping.cpp new file mode 100644 index 0000000000..60dcf0bee6 --- /dev/null +++ b/gfx/layers/apz/test/gtest/TestSnapping.cpp @@ -0,0 +1,302 @@ +/* -*- 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" +#include "mozilla/StaticPrefs_layout.h" +#include "mozilla/StaticPrefs_mousewheel.h" + +class APZCSnappingTesterMock : public APZCTreeManagerTester { + public: + APZCSnappingTesterMock() { CreateMockHitTester(); } +}; + +TEST_F(APZCSnappingTesterMock, Bug1265510) { + // Needed because the test uses SmoothWheel() + SCOPED_GFX_PREF_BOOL("general.smoothScroll", true); + + const char* treeShape = "x(x)"; + LayerIntRect layerVisibleRect[] = {LayerIntRect(0, 0, 100, 100), + LayerIntRect(0, 100, 100, 100)}; + CreateScrollData(treeShape, layerVisibleRect); + SetScrollableFrameMetrics(root, ScrollableLayerGuid::START_SCROLL_ID, + CSSRect(0, 0, 100, 200)); + SetScrollableFrameMetrics(layers[1], ScrollableLayerGuid::START_SCROLL_ID + 1, + CSSRect(0, 0, 100, 200)); + SetScrollHandoff(layers[1], root); + + ScrollSnapInfo snap; + snap.mScrollSnapStrictnessY = StyleScrollSnapStrictness::Mandatory; + snap.mSnapportSize = + CSSSize::ToAppUnits(layerVisibleRect[0].Size() * LayerToCSSScale(1.0)); + + snap.mSnapTargets.AppendElement(ScrollSnapInfo::SnapTarget( + Nothing(), Some(0 * AppUnitsPerCSSPixel()), + CSSRect::ToAppUnits(CSSRect(0, 0, 10, 10)), StyleScrollSnapStop::Normal, + ScrollSnapTargetId{1})); + snap.mSnapTargets.AppendElement(ScrollSnapInfo::SnapTarget( + Nothing(), Some(100 * AppUnitsPerCSSPixel()), + CSSRect::ToAppUnits(CSSRect(0, 100, 10, 10)), StyleScrollSnapStop::Normal, + ScrollSnapTargetId{2})); + + ModifyFrameMetrics(root, [&](ScrollMetadata& aSm, FrameMetrics&) { + aSm.SetSnapInfo(ScrollSnapInfo(snap)); + }); + + UniquePtr<ScopedLayerTreeRegistration> registration = + MakeUnique<ScopedLayerTreeRegistration>(LayersId{0}, mcc); + UpdateHitTestingTree(); + + TestAsyncPanZoomController* outer = ApzcOf(layers[0]); + TestAsyncPanZoomController* inner = ApzcOf(layers[1]); + + // Position the mouse near the bottom of the outer frame and scroll by 60px. + // (6 lines of 10px each). APZC will actually scroll to y=100 because of the + // mandatory snap coordinate there. + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID); + SmoothWheel(manager, ScreenIntPoint(50, 80), ScreenPoint(0, 6), mcc->Time()); + // Advance in 5ms increments until we've scrolled by 70px. At this point, the + // closest snap point is y=100, and the inner frame should be under the mouse + // cursor. + while (outer + ->GetCurrentAsyncScrollOffset( + AsyncTransformConsumer::eForEventHandling) + .y < 70) { + mcc->AdvanceByMillis(5); + outer->AdvanceAnimations(mcc->GetSampleTime()); + } + // Now do another wheel in a new transaction. This should start scrolling the + // inner frame; we verify that it does by checking the inner scroll position. + mcc->AdvanceBy(TimeDuration::FromMilliseconds( + StaticPrefs::mousewheel_transaction_timeout() + 100)); + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1); + SmoothWheel(manager, ScreenIntPoint(50, 80), ScreenPoint(0, 6), mcc->Time()); + mcc->AdvanceByMillis(5); + inner->AdvanceAnimationsUntilEnd(); + EXPECT_LT(0.0f, inner + ->GetCurrentAsyncScrollOffset( + AsyncTransformConsumer::eForEventHandling) + .y); + + // However, the outer frame should also continue to the snap point, otherwise + // it is demonstrating incorrect behaviour by violating the mandatory + // snapping. + outer->AdvanceAnimationsUntilEnd(); + EXPECT_EQ(100.0f, outer + ->GetCurrentAsyncScrollOffset( + AsyncTransformConsumer::eForEventHandling) + .y); +} + +TEST_F(APZCSnappingTesterMock, Snap_After_Pinch) { + const char* treeShape = "x"; + LayerIntRect layerVisibleRect[] = { + LayerIntRect(0, 0, 100, 100), + }; + CreateScrollData(treeShape, layerVisibleRect); + SetScrollableFrameMetrics(root, ScrollableLayerGuid::START_SCROLL_ID, + CSSRect(0, 0, 100, 200)); + + // Set up some basic scroll snapping + ScrollSnapInfo snap; + snap.mScrollSnapStrictnessY = StyleScrollSnapStrictness::Mandatory; + snap.mSnapportSize = + CSSSize::ToAppUnits(layerVisibleRect[0].Size() * LayerToCSSScale(1.0)); + + snap.mSnapTargets.AppendElement(ScrollSnapInfo::SnapTarget( + Nothing(), Some(0 * AppUnitsPerCSSPixel()), + CSSRect::ToAppUnits(CSSRect(0, 0, 10, 10)), StyleScrollSnapStop::Normal, + ScrollSnapTargetId{1})); + snap.mSnapTargets.AppendElement(ScrollSnapInfo::SnapTarget( + Nothing(), Some(100 * AppUnitsPerCSSPixel()), + CSSRect::ToAppUnits(CSSRect(0, 100, 10, 10)), StyleScrollSnapStop::Normal, + ScrollSnapTargetId{2})); + + // Save the scroll snap info on the root APZC. + // Also mark the root APZC as "root content", since APZC only allows + // zooming on the root content APZC. + ModifyFrameMetrics(root, [&](ScrollMetadata& aSm, FrameMetrics& aMetrics) { + aSm.SetSnapInfo(ScrollSnapInfo(snap)); + aMetrics.SetIsRootContent(true); + }); + + UniquePtr<ScopedLayerTreeRegistration> registration = + MakeUnique<ScopedLayerTreeRegistration>(LayersId{0}, mcc); + UpdateHitTestingTree(); + + RefPtr<TestAsyncPanZoomController> apzc = ApzcOf(root); + + // Allow zooming + apzc->UpdateZoomConstraints(ZoomConstraints( + true, true, CSSToParentLayerScale(0.25f), CSSToParentLayerScale(4.0f))); + + PinchWithPinchInput(apzc, ScreenIntPoint(50, 50), ScreenIntPoint(50, 50), + 1.2f); + + apzc->AssertStateIsSmoothMsdScroll(); +} + +// Currently fails on Android because on the platform we have a different +// VelocityTracker. +#ifndef MOZ_WIDGET_ANDROID +TEST_F(APZCSnappingTesterMock, SnapOnPanEndWithZeroVelocity) { + // Use pref values for desktop everywhere. + SCOPED_GFX_PREF_FLOAT("apz.fling_friction", 0.002); + SCOPED_GFX_PREF_FLOAT("apz.fling_stopped_threshold", 0.01); + SCOPED_GFX_PREF_FLOAT("apz.fling_curve_function_x1", 0.0); + SCOPED_GFX_PREF_FLOAT("apz.fling_curve_function_x2", 1.0); + SCOPED_GFX_PREF_FLOAT("apz.fling_curve_function_y1", 0.0); + SCOPED_GFX_PREF_FLOAT("apz.fling_curve_function_y2", 1.0); + SCOPED_GFX_PREF_INT("apz.velocity_relevance_time_ms", 100); + + const char* treeShape = "x"; + LayerIntRect layerVisibleRect[] = { + LayerIntRect(0, 0, 100, 100), + }; + CreateScrollData(treeShape, layerVisibleRect); + SetScrollableFrameMetrics(root, ScrollableLayerGuid::START_SCROLL_ID, + CSSRect(0, 0, 100, 400)); + + // Set up two snap points, 30 and 100. + ScrollSnapInfo snap; + snap.mScrollSnapStrictnessY = StyleScrollSnapStrictness::Mandatory; + snap.mSnapportSize = + CSSSize::ToAppUnits(layerVisibleRect[0].Size() * LayerToCSSScale(1.0)); + snap.mSnapTargets.AppendElement(ScrollSnapInfo::SnapTarget( + Nothing(), Some(30 * AppUnitsPerCSSPixel()), + CSSRect::ToAppUnits(CSSRect(0, 30, 10, 10)), StyleScrollSnapStop::Normal, + ScrollSnapTargetId{1})); + snap.mSnapTargets.AppendElement(ScrollSnapInfo::SnapTarget( + Nothing(), Some(100 * AppUnitsPerCSSPixel()), + CSSRect::ToAppUnits(CSSRect(0, 100, 10, 10)), StyleScrollSnapStop::Normal, + ScrollSnapTargetId{2})); + + // Save the scroll snap info on the root APZC. + ModifyFrameMetrics(root, [&](ScrollMetadata& aSm, FrameMetrics& aMetrics) { + aSm.SetSnapInfo(ScrollSnapInfo(snap)); + }); + + UniquePtr<ScopedLayerTreeRegistration> registration = + MakeUnique<ScopedLayerTreeRegistration>(LayersId{0}, mcc); + UpdateHitTestingTree(); + + RefPtr<TestAsyncPanZoomController> apzc = ApzcOf(root); + + // Send a series of pan gestures to scroll to position at 50. + const ScreenIntPoint position = ScreenIntPoint(50, 30); + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID); + PanGesture(PanGestureInput::PANGESTURE_START, manager, position, + ScreenPoint(0, 10), mcc->Time()); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID); + PanGesture(PanGestureInput::PANGESTURE_PAN, manager, position, + ScreenPoint(0, 40), mcc->Time()); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + + // Make sure the velocity just before sending a pan-end is zero. + EXPECT_EQ(apzc->GetVelocityVector().y, 0); + + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID); + PanGesture(PanGestureInput::PANGESTURE_END, manager, position, + ScreenPoint(0, 0), mcc->Time()); + + // Now a smooth animation has been triggered for snapping to 30. + apzc->AssertStateIsSmoothMsdScroll(); + + apzc->AdvanceAnimationsUntilEnd(); + // The snapped position should be 30 rather than 100 because it's the nearest + // snap point. + EXPECT_EQ(apzc->GetCurrentAsyncScrollOffset( + AsyncPanZoomController::eForEventHandling) + .y, + 30); +} + +// Smililar to above SnapOnPanEndWithZeroVelocity but with positive velocity so +// that the snap position would be the one in the scrolling direction. +TEST_F(APZCSnappingTesterMock, SnapOnPanEndWithPositiveVelocity) { + // Use pref values for desktop everywhere. + SCOPED_GFX_PREF_FLOAT("apz.fling_friction", 0.002); + SCOPED_GFX_PREF_FLOAT("apz.fling_stopped_threshold", 0.01); + SCOPED_GFX_PREF_FLOAT("apz.fling_curve_function_x1", 0.0); + SCOPED_GFX_PREF_FLOAT("apz.fling_curve_function_x2", 1.0); + SCOPED_GFX_PREF_FLOAT("apz.fling_curve_function_y1", 0.0); + SCOPED_GFX_PREF_FLOAT("apz.fling_curve_function_y2", 1.0); + SCOPED_GFX_PREF_INT("apz.velocity_relevance_time_ms", 100); + + const char* treeShape = "x"; + LayerIntRect layerVisibleRect[] = { + LayerIntRect(0, 0, 100, 100), + }; + CreateScrollData(treeShape, layerVisibleRect); + SetScrollableFrameMetrics(root, ScrollableLayerGuid::START_SCROLL_ID, + CSSRect(0, 0, 100, 400)); + + // Set up two snap points, 30 and 100. + ScrollSnapInfo snap; + snap.mScrollSnapStrictnessY = StyleScrollSnapStrictness::Mandatory; + snap.mSnapportSize = + CSSSize::ToAppUnits(layerVisibleRect[0].Size() * LayerToCSSScale(1.0)); + snap.mSnapTargets.AppendElement(ScrollSnapInfo::SnapTarget( + Nothing(), Some(30 * AppUnitsPerCSSPixel()), + CSSRect::ToAppUnits(CSSRect(0, 30, 10, 10)), StyleScrollSnapStop::Normal, + ScrollSnapTargetId{1})); + snap.mSnapTargets.AppendElement(ScrollSnapInfo::SnapTarget( + Nothing(), Some(100 * AppUnitsPerCSSPixel()), + CSSRect::ToAppUnits(CSSRect(0, 100, 10, 10)), StyleScrollSnapStop::Normal, + ScrollSnapTargetId{2})); + + // Save the scroll snap info on the root APZC. + ModifyFrameMetrics(root, [&](ScrollMetadata& aSm, FrameMetrics& aMetrics) { + aSm.SetSnapInfo(ScrollSnapInfo(snap)); + }); + + UniquePtr<ScopedLayerTreeRegistration> registration = + MakeUnique<ScopedLayerTreeRegistration>(LayersId{0}, mcc); + UpdateHitTestingTree(); + + RefPtr<TestAsyncPanZoomController> apzc = ApzcOf(root); + + // Send a series of pan gestures that a pan-end event happens at 65 + const ScreenIntPoint position = ScreenIntPoint(50, 30); + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID); + PanGesture(PanGestureInput::PANGESTURE_START, manager, position, + ScreenPoint(0, 10), mcc->Time()); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID); + PanGesture(PanGestureInput::PANGESTURE_PAN, manager, position, + ScreenPoint(0, 35), mcc->Time()); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID); + PanGesture(PanGestureInput::PANGESTURE_PAN, manager, position, + ScreenPoint(0, 20), mcc->Time()); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + + // There should be positive velocity in this case. + EXPECT_GT(apzc->GetVelocityVector().y, 0); + + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID); + PanGesture(PanGestureInput::PANGESTURE_END, manager, position, + ScreenPoint(0, 0), mcc->Time()); + mcc->AdvanceByMillis(5); + + // A smooth animation has been triggered by the pan-end event above. + apzc->AssertStateIsSmoothMsdScroll(); + + apzc->AdvanceAnimationsUntilEnd(); + EXPECT_EQ(apzc->GetCurrentAsyncScrollOffset( + AsyncPanZoomController::eForEventHandling) + .y, + 100); +} +#endif diff --git a/gfx/layers/apz/test/gtest/TestSnappingOnMomentum.cpp b/gfx/layers/apz/test/gtest/TestSnappingOnMomentum.cpp new file mode 100644 index 0000000000..a76d1b2d60 --- /dev/null +++ b/gfx/layers/apz/test/gtest/TestSnappingOnMomentum.cpp @@ -0,0 +1,102 @@ +/* -*- 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" +#include "mozilla/StaticPrefs_layout.h" + +class APZCSnappingOnMomentumTesterMock : public APZCTreeManagerTester { + public: + APZCSnappingOnMomentumTesterMock() { CreateMockHitTester(); } +}; + +TEST_F(APZCSnappingOnMomentumTesterMock, Snap_On_Momentum) { + const char* treeShape = "x"; + LayerIntRect layerVisibleRect[] = { + LayerIntRect(0, 0, 100, 100), + }; + CreateScrollData(treeShape, layerVisibleRect); + SetScrollableFrameMetrics(root, ScrollableLayerGuid::START_SCROLL_ID, + CSSRect(0, 0, 100, 500)); + + // Set up some basic scroll snapping + ScrollSnapInfo snap; + snap.mScrollSnapStrictnessY = StyleScrollSnapStrictness::Mandatory; + snap.mSnapportSize = + CSSSize::ToAppUnits(layerVisibleRect[0].Size() * LayerToCSSScale(1.0)); + snap.mSnapTargets.AppendElement(ScrollSnapInfo::SnapTarget( + Nothing(), Some(0 * AppUnitsPerCSSPixel()), + CSSRect::ToAppUnits(CSSRect(0, 0, 10, 10)), StyleScrollSnapStop::Normal, + ScrollSnapTargetId{1})); + snap.mSnapTargets.AppendElement(ScrollSnapInfo::SnapTarget( + Nothing(), Some(100 * AppUnitsPerCSSPixel()), + CSSRect::ToAppUnits(CSSRect(0, 100, 10, 10)), StyleScrollSnapStop::Normal, + ScrollSnapTargetId{2})); + + ModifyFrameMetrics(root, [&](ScrollMetadata& aSm, FrameMetrics&) { + aSm.SetSnapInfo(ScrollSnapInfo(snap)); + }); + + UniquePtr<ScopedLayerTreeRegistration> registration = + MakeUnique<ScopedLayerTreeRegistration>(LayersId{0}, mcc); + UpdateHitTestingTree(); + + RefPtr<TestAsyncPanZoomController> apzc = ApzcOf(root); + + TimeStamp now = mcc->Time(); + + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID); + PanGesture(PanGestureInput::PANGESTURE_START, manager, ScreenIntPoint(50, 80), + ScreenPoint(0, 2), now); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID); + PanGesture(PanGestureInput::PANGESTURE_PAN, manager, ScreenIntPoint(50, 80), + ScreenPoint(0, 25), mcc->Time()); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID); + PanGesture(PanGestureInput::PANGESTURE_PAN, manager, ScreenIntPoint(50, 80), + ScreenPoint(0, 25), mcc->Time()); + + // The velocity should be positive when panning with positive displacement. + EXPECT_GT(apzc->GetVelocityVector().y, 3.0); + + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID); + PanGesture(PanGestureInput::PANGESTURE_END, manager, ScreenIntPoint(50, 80), + ScreenPoint(0, 0), mcc->Time()); + + // After lifting the fingers, the velocity should be zero and a smooth + // animation should have been triggered for scroll snap. + EXPECT_EQ(apzc->GetVelocityVector().y, 0); + apzc->AssertStateIsSmoothMsdScroll(); + + mcc->AdvanceByMillis(5); + + apzc->AdvanceAnimations(mcc->GetSampleTime()); + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID); + PanGesture(PanGestureInput::PANGESTURE_MOMENTUMSTART, manager, + ScreenIntPoint(50, 80), ScreenPoint(0, 200), mcc->Time()); + mcc->AdvanceByMillis(10); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID); + PanGesture(PanGestureInput::PANGESTURE_MOMENTUMPAN, manager, + ScreenIntPoint(50, 80), ScreenPoint(0, 50), mcc->Time()); + mcc->AdvanceByMillis(10); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID); + PanGesture(PanGestureInput::PANGESTURE_MOMENTUMEND, manager, + ScreenIntPoint(50, 80), ScreenPoint(0, 0), mcc->Time()); + + apzc->AdvanceAnimationsUntilEnd(); + EXPECT_EQ(100.0f, apzc->GetCurrentAsyncScrollOffset( + AsyncTransformConsumer::eForEventHandling) + .y); +} diff --git a/gfx/layers/apz/test/gtest/TestTransformNotifications.cpp b/gfx/layers/apz/test/gtest/TestTransformNotifications.cpp new file mode 100644 index 0000000000..f9eeba66e6 --- /dev/null +++ b/gfx/layers/apz/test/gtest/TestTransformNotifications.cpp @@ -0,0 +1,569 @@ +/* -*- 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 "APZCBasicTester.h" +#include "APZCTreeManagerTester.h" +#include "APZTestCommon.h" +#include "mozilla/layers/WebRenderScrollDataWrapper.h" +#include "apz/util/APZEventState.h" + +#include "InputUtils.h" + +class APZCTransformNotificationTester : public APZCTreeManagerTester { + public: + explicit APZCTransformNotificationTester() { CreateMockHitTester(); } + + UniquePtr<ScopedLayerTreeRegistration> mRegistration; + + RefPtr<TestAsyncPanZoomController> mRootApzc; + + void SetupBasicTest() { + const char* treeShape = "x"; + LayerIntRect layerVisibleRect[] = { + LayerIntRect(0, 0, 100, 100), + }; + CreateScrollData(treeShape, layerVisibleRect); + SetScrollableFrameMetrics(root, ScrollableLayerGuid::START_SCROLL_ID, + CSSRect(0, 0, 500, 500)); + + mRegistration = MakeUnique<ScopedLayerTreeRegistration>(LayersId{0}, mcc); + + UpdateHitTestingTree(); + + mRootApzc = ApzcOf(root); + } + + void SetupNonScrollableTest() { + const char* treeShape = "x"; + LayerIntRect layerVisibleRect[] = { + LayerIntRect(0, 0, 100, 100), + }; + CreateScrollData(treeShape, layerVisibleRect); + SetScrollableFrameMetrics(root, ScrollableLayerGuid::START_SCROLL_ID, + CSSRect(0, 0, 100, 100)); + + mRegistration = MakeUnique<ScopedLayerTreeRegistration>(LayersId{0}, mcc); + + UpdateHitTestingTree(); + + mRootApzc = ApzcOf(root); + + mRootApzc->GetFrameMetrics().SetIsRootContent(true); + } +}; + +TEST_F(APZCTransformNotificationTester, PanningTransformNotifications) { + SCOPED_GFX_PREF_BOOL("apz.overscroll.enabled", true); + + SetupBasicTest(); + + // Scroll down by 25 px. Ensure we only get one set of + // state change notifications. + // + // Then, scroll back up by 20px, this time flinging after. + // The fling should cover the remaining 5 px of room to scroll, then + // go into overscroll, and finally snap-back to recover from overscroll. + // Again, ensure we only get one set of state change notifications for + // this entire procedure. + + MockFunction<void(std::string checkPointName)> check; + { + InSequence s; + EXPECT_CALL(check, Call("Simple pan")); + EXPECT_CALL( + *mcc, NotifyAPZStateChange( + _, GeckoContentController::APZStateChange::eStartTouch, _, _)) + .Times(1); + EXPECT_CALL( + *mcc, + NotifyAPZStateChange( + _, GeckoContentController::APZStateChange::eTransformBegin, _, _)) + .Times(1); + EXPECT_CALL( + *mcc, + NotifyAPZStateChange( + _, GeckoContentController::APZStateChange::eStartPanning, _, _)) + .Times(1); + EXPECT_CALL(*mcc, + NotifyAPZStateChange( + _, GeckoContentController::APZStateChange::eEndTouch, _, _)) + .Times(1); + EXPECT_CALL( + *mcc, + NotifyAPZStateChange( + _, GeckoContentController::APZStateChange::eTransformEnd, _, _)) + .Times(1); + EXPECT_CALL(check, Call("Complex pan")); + EXPECT_CALL( + *mcc, NotifyAPZStateChange( + _, GeckoContentController::APZStateChange::eStartTouch, _, _)) + .Times(1); + EXPECT_CALL( + *mcc, + NotifyAPZStateChange( + _, GeckoContentController::APZStateChange::eTransformBegin, _, _)) + .Times(1); + EXPECT_CALL( + *mcc, + NotifyAPZStateChange( + _, GeckoContentController::APZStateChange::eStartPanning, _, _)) + .Times(1); + EXPECT_CALL(*mcc, + NotifyAPZStateChange( + _, GeckoContentController::APZStateChange::eEndTouch, _, _)) + .Times(1); + EXPECT_CALL( + *mcc, + NotifyAPZStateChange( + _, GeckoContentController::APZStateChange::eTransformEnd, _, _)) + .Times(1); + EXPECT_CALL(check, Call("Done")); + } + + check.Call("Simple pan"); + Pan(mRootApzc, 50, 25, PanOptions::NoFling); + check.Call("Complex pan"); + Pan(mRootApzc, 25, 45); + mRootApzc->AdvanceAnimationsUntilEnd(); + check.Call("Done"); +} + +TEST_F(APZCTransformNotificationTester, PanWithMomentumTransformNotifications) { + SetupBasicTest(); + + MockFunction<void(std::string checkPointName)> check; + { + InSequence s; + EXPECT_CALL(check, Call("Pan Start")); + EXPECT_CALL( + *mcc, + NotifyAPZStateChange( + _, GeckoContentController::APZStateChange::eTransformBegin, _, _)) + .Times(1); + + EXPECT_CALL(check, Call("Panning")); + EXPECT_CALL(check, Call("Pan End")); + EXPECT_CALL(check, Call("Momentum Start")); + + EXPECT_CALL(check, Call("Momentum Pan")); + EXPECT_CALL(check, Call("Momentum End")); + // The TransformEnd should only be sent after the momentum pan. + EXPECT_CALL( + *mcc, + NotifyAPZStateChange( + _, GeckoContentController::APZStateChange::eTransformEnd, _, _)) + .Times(1); + + EXPECT_CALL(check, Call("Done")); + } + + check.Call("Pan Start"); + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID); + PanGesture(PanGestureInput::PANGESTURE_START, manager, ScreenIntPoint(50, 50), + ScreenIntPoint(1, 2), mcc->Time()); + mcc->AdvanceByMillis(5); + mRootApzc->AdvanceAnimations(mcc->GetSampleTime()); + + check.Call("Panning"); + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID); + PanGesture(PanGestureInput::PANGESTURE_PAN, mRootApzc, ScreenIntPoint(50, 50), + ScreenPoint(15, 30), mcc->Time()); + mcc->AdvanceByMillis(5); + mRootApzc->AdvanceAnimations(mcc->GetSampleTime()); + + check.Call("Pan End"); + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID); + PanGesture(PanGestureInput::PANGESTURE_END, manager, ScreenIntPoint(50, 50), + ScreenPoint(0, 0), mcc->Time()); + mcc->AdvanceByMillis(5); + mRootApzc->AdvanceAnimations(mcc->GetSampleTime()); + + check.Call("Momentum Start"); + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID); + PanGesture(PanGestureInput::PANGESTURE_MOMENTUMSTART, manager, + ScreenIntPoint(50, 50), ScreenPoint(30, 90), mcc->Time()); + mcc->AdvanceByMillis(10); + mRootApzc->AdvanceAnimations(mcc->GetSampleTime()); + + check.Call("Momentum Pan"); + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID); + PanGesture(PanGestureInput::PANGESTURE_MOMENTUMPAN, manager, + ScreenIntPoint(50, 50), ScreenPoint(10, 30), mcc->Time()); + mcc->AdvanceByMillis(10); + mRootApzc->AdvanceAnimations(mcc->GetSampleTime()); + + check.Call("Momentum End"); + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID); + PanGesture(PanGestureInput::PANGESTURE_MOMENTUMEND, manager, + ScreenIntPoint(50, 50), ScreenPoint(0, 0), mcc->Time()); + mcc->AdvanceByMillis(10); + mRootApzc->AdvanceAnimations(mcc->GetSampleTime()); + + check.Call("Done"); +} + +TEST_F(APZCTransformNotificationTester, + PanWithoutMomentumTransformNotifications) { + // Ensure that the TransformEnd delay is 100ms. + SCOPED_GFX_PREF_INT("apz.scrollend-event.content.delay_ms", 100); + + SetupBasicTest(); + + MockFunction<void(std::string checkPointName)> check; + { + InSequence s; + EXPECT_CALL(check, Call("Pan Start")); + EXPECT_CALL( + *mcc, + NotifyAPZStateChange( + _, GeckoContentController::APZStateChange::eTransformBegin, _, _)) + .Times(1); + + EXPECT_CALL(check, Call("Panning")); + EXPECT_CALL(check, Call("Pan End")); + EXPECT_CALL(check, Call("TransformEnd delay")); + // The TransformEnd should only be sent after the pan gesture and 100ms + // timer fire. + EXPECT_CALL( + *mcc, + NotifyAPZStateChange( + _, GeckoContentController::APZStateChange::eTransformEnd, _, _)) + .Times(1); + + EXPECT_CALL(check, Call("Done")); + } + + check.Call("Pan Start"); + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID); + PanGesture(PanGestureInput::PANGESTURE_START, manager, ScreenIntPoint(50, 50), + ScreenIntPoint(1, 2), mcc->Time()); + mcc->AdvanceByMillis(5); + mRootApzc->AdvanceAnimations(mcc->GetSampleTime()); + + check.Call("Panning"); + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID); + PanGesture(PanGestureInput::PANGESTURE_PAN, mRootApzc, ScreenIntPoint(50, 50), + ScreenPoint(15, 30), mcc->Time()); + mcc->AdvanceByMillis(5); + mRootApzc->AdvanceAnimations(mcc->GetSampleTime()); + + check.Call("Pan End"); + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID); + PanGesture(PanGestureInput::PANGESTURE_END, manager, ScreenIntPoint(50, 50), + ScreenPoint(0, 0), mcc->Time()); + mcc->AdvanceByMillis(55); + mRootApzc->AdvanceAnimations(mcc->GetSampleTime()); + + check.Call("TransformEnd delay"); + mcc->AdvanceByMillis(55); + mRootApzc->AdvanceAnimations(mcc->GetSampleTime()); + + check.Call("Done"); +} + +TEST_F(APZCTransformNotificationTester, + PanFollowedByNewPanTransformNotifications) { + // Ensure that the TransformEnd delay is 100ms. + SCOPED_GFX_PREF_INT("apz.scrollend-event.content.delay_ms", 100); + + SetupBasicTest(); + + MockFunction<void(std::string checkPointName)> check; + { + InSequence s; + EXPECT_CALL(check, Call("Pan Start")); + EXPECT_CALL( + *mcc, + NotifyAPZStateChange( + _, GeckoContentController::APZStateChange::eTransformBegin, _, _)) + .Times(1); + + EXPECT_CALL(check, Call("Panning")); + EXPECT_CALL(check, Call("Pan End")); + // The TransformEnd delay should be cut short and delivered before the + // new pan gesture begins. + EXPECT_CALL(check, Call("New Pan Start")); + EXPECT_CALL( + *mcc, + NotifyAPZStateChange( + _, GeckoContentController::APZStateChange::eTransformEnd, _, _)) + .Times(1); + EXPECT_CALL( + *mcc, + NotifyAPZStateChange( + _, GeckoContentController::APZStateChange::eTransformBegin, _, _)) + .Times(1); + EXPECT_CALL(check, Call("New Pan End")); + EXPECT_CALL( + *mcc, + NotifyAPZStateChange( + _, GeckoContentController::APZStateChange::eTransformEnd, _, _)) + .Times(1); + + EXPECT_CALL(check, Call("Done")); + } + + check.Call("Pan Start"); + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID); + PanGesture(PanGestureInput::PANGESTURE_START, manager, ScreenIntPoint(50, 50), + ScreenIntPoint(1, 2), mcc->Time()); + mcc->AdvanceByMillis(5); + mRootApzc->AdvanceAnimations(mcc->GetSampleTime()); + + check.Call("Panning"); + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID); + PanGesture(PanGestureInput::PANGESTURE_PAN, mRootApzc, ScreenIntPoint(50, 50), + ScreenPoint(15, 30), mcc->Time()); + mcc->AdvanceByMillis(5); + mRootApzc->AdvanceAnimations(mcc->GetSampleTime()); + + check.Call("Pan End"); + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID); + PanGesture(PanGestureInput::PANGESTURE_END, manager, ScreenIntPoint(50, 50), + ScreenPoint(0, 0), mcc->Time()); + mcc->AdvanceByMillis(55); + mRootApzc->AdvanceAnimations(mcc->GetSampleTime()); + + check.Call("New Pan Start"); + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID); + PanGesture(PanGestureInput::PANGESTURE_START, manager, ScreenIntPoint(50, 50), + ScreenIntPoint(1, 2), mcc->Time()); + mcc->AdvanceByMillis(5); + mRootApzc->AdvanceAnimations(mcc->GetSampleTime()); + + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID); + PanGesture(PanGestureInput::PANGESTURE_PAN, mRootApzc, ScreenIntPoint(50, 50), + ScreenPoint(15, 30), mcc->Time()); + mcc->AdvanceByMillis(5); + mRootApzc->AdvanceAnimations(mcc->GetSampleTime()); + + check.Call("New Pan End"); + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID); + PanGesture(PanGestureInput::PANGESTURE_END, manager, ScreenIntPoint(50, 50), + ScreenPoint(0, 0), mcc->Time()); + mcc->AdvanceByMillis(105); + mRootApzc->AdvanceAnimations(mcc->GetSampleTime()); + + check.Call("Done"); +} + +TEST_F(APZCTransformNotificationTester, + PanFollowedByWheelTransformNotifications) { + // Needed because the test uses SmoothWheel() + SCOPED_GFX_PREF_BOOL("general.smoothScroll", true); + // Ensure that the TransformEnd delay is 100ms. + SCOPED_GFX_PREF_INT("apz.scrollend-event.content.delay_ms", 100); + + SetupBasicTest(); + + MockFunction<void(std::string checkPointName)> check; + { + InSequence s; + EXPECT_CALL(check, Call("Pan Start")); + EXPECT_CALL( + *mcc, + NotifyAPZStateChange( + _, GeckoContentController::APZStateChange::eTransformBegin, _, _)) + .Times(1); + + EXPECT_CALL(check, Call("Panning")); + EXPECT_CALL(check, Call("Pan End")); + // The TransformEnd delay should be cut short and delivered before the + // new wheel event begins. + EXPECT_CALL(check, Call("Wheel Start")); + EXPECT_CALL( + *mcc, + NotifyAPZStateChange( + _, GeckoContentController::APZStateChange::eTransformEnd, _, _)) + .Times(1); + EXPECT_CALL( + *mcc, + NotifyAPZStateChange( + _, GeckoContentController::APZStateChange::eTransformBegin, _, _)) + .Times(1); + EXPECT_CALL(check, Call("Wheel End")); + EXPECT_CALL( + *mcc, + NotifyAPZStateChange( + _, GeckoContentController::APZStateChange::eTransformEnd, _, _)) + .Times(1); + EXPECT_CALL(check, Call("Done")); + } + + check.Call("Pan Start"); + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID); + PanGesture(PanGestureInput::PANGESTURE_START, manager, ScreenIntPoint(50, 50), + ScreenIntPoint(1, 2), mcc->Time()); + mcc->AdvanceByMillis(5); + mRootApzc->AdvanceAnimations(mcc->GetSampleTime()); + + check.Call("Panning"); + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID); + PanGesture(PanGestureInput::PANGESTURE_PAN, mRootApzc, ScreenIntPoint(50, 50), + ScreenPoint(15, 30), mcc->Time()); + mcc->AdvanceByMillis(5); + mRootApzc->AdvanceAnimations(mcc->GetSampleTime()); + + check.Call("Pan End"); + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID); + PanGesture(PanGestureInput::PANGESTURE_END, manager, ScreenIntPoint(50, 50), + ScreenPoint(0, 0), mcc->Time()); + mcc->AdvanceByMillis(55); + mRootApzc->AdvanceAnimations(mcc->GetSampleTime()); + + check.Call("Wheel Start"); + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID); + SmoothWheel(manager, ScreenIntPoint(50, 50), ScreenPoint(10, 10), + mcc->Time()); + mcc->AdvanceByMillis(10); + mRootApzc->AdvanceAnimations(mcc->GetSampleTime()); + + check.Call("Wheel End"); + + mRootApzc->AdvanceAnimationsUntilEnd(); + + check.Call("Done"); +} + +#ifndef MOZ_WIDGET_ANDROID // Currently fails on Android +TEST_F(APZCTransformNotificationTester, PanOverscrollTransformNotifications) { + SCOPED_GFX_PREF_BOOL("apz.overscroll.enabled", true); + + SetupBasicTest(); + + MockFunction<void(std::string checkPointName)> check; + { + InSequence s; + EXPECT_CALL(check, Call("Pan Start")); + EXPECT_CALL( + *mcc, + NotifyAPZStateChange( + _, GeckoContentController::APZStateChange::eTransformBegin, _, _)) + .Times(1); + + EXPECT_CALL(check, Call("Panning Into Overscroll")); + EXPECT_CALL(check, Call("Pan End")); + EXPECT_CALL(check, Call("Overscroll Animation End")); + // The TransformEnd should only be sent after the overscroll animation + // completes. + EXPECT_CALL( + *mcc, + NotifyAPZStateChange( + _, GeckoContentController::APZStateChange::eTransformEnd, _, _)) + .Times(1); + EXPECT_CALL(check, Call("Done")); + } + + check.Call("Pan Start"); + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID); + PanGesture(PanGestureInput::PANGESTURE_START, manager, ScreenIntPoint(50, 50), + ScreenIntPoint(1, 2), mcc->Time()); + mcc->AdvanceByMillis(5); + mRootApzc->AdvanceAnimations(mcc->GetSampleTime()); + + check.Call("Panning Into Overscroll"); + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID); + PanGesture(PanGestureInput::PANGESTURE_PAN, mRootApzc, ScreenIntPoint(50, 50), + ScreenPoint(15, -30), mcc->Time()); + mcc->AdvanceByMillis(5); + mRootApzc->AdvanceAnimations(mcc->GetSampleTime()); + + // Ensure that we have overscrolled. + EXPECT_TRUE(mRootApzc->IsOverscrolled()); + + check.Call("Pan End"); + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID); + PanGesture(PanGestureInput::PANGESTURE_END, manager, ScreenIntPoint(50, 50), + ScreenPoint(0, 0), mcc->Time()); + mcc->AdvanceByMillis(5); + mRootApzc->AdvanceAnimations(mcc->GetSampleTime()); + + // Wait for the overscroll animation to complete and the TransformEnd + // notification to be sent. + check.Call("Overscroll Animation End"); + mcc->AdvanceByMillis(5); + mRootApzc->AdvanceAnimationsUntilEnd(); + EXPECT_FALSE(mRootApzc->IsOverscrolled()); + + check.Call("Done"); +} +#endif + +TEST_F(APZCTransformNotificationTester, ScrollableTouchStateChange) { + // Create a scroll frame with available space for a scroll. + SetupBasicTest(); + + MockFunction<void(std::string checkPointName)> check; + { + EXPECT_CALL(check, Call("Start")); + // We receive a touch-start with the flag indicating that the + // touch-start occurred over a scrollable element. + EXPECT_CALL( + *mcc, NotifyAPZStateChange( + _, GeckoContentController::APZStateChange::eStartTouch, 1, _)) + .Times(1); + + EXPECT_CALL(*mcc, + NotifyAPZStateChange( + _, GeckoContentController::APZStateChange::eEndTouch, 1, _)) + .Times(1); + EXPECT_CALL(check, Call("Done")); + } + + check.Call("Start"); + + // Conduct a touch down and touch up in the scrollable element, + // and ensure the correct state change notifications are sent. + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID); + TouchDown(mRootApzc, ScreenIntPoint(10, 10), mcc->Time()); + mcc->AdvanceByMillis(5); + mRootApzc->AdvanceAnimations(mcc->GetSampleTime()); + + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID); + TouchUp(mRootApzc, ScreenIntPoint(10, 10), mcc->Time()); + mcc->AdvanceByMillis(5); + mRootApzc->AdvanceAnimations(mcc->GetSampleTime()); + + check.Call("Done"); +} + +TEST_F(APZCTransformNotificationTester, NonScrollableTouchStateChange) { + // Create a non-scrollable frame with no space to scroll. + SetupNonScrollableTest(); + + MockFunction<void(std::string checkPointName)> check; + { + EXPECT_CALL(check, Call("Start")); + // We receive a touch-start with the flag indicating that the + // touch-start occurred over a non-scrollable element. + EXPECT_CALL( + *mcc, NotifyAPZStateChange( + _, GeckoContentController::APZStateChange::eStartTouch, 0, _)) + .Times(1); + + EXPECT_CALL(*mcc, + NotifyAPZStateChange( + _, GeckoContentController::APZStateChange::eEndTouch, 1, _)) + .Times(1); + EXPECT_CALL(check, Call("Done")); + } + + check.Call("Start"); + + // Conduct a touch down and touch up in the non-scrollable element, + // and ensure the correct state change notifications are sent. + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID); + TouchDown(mRootApzc, ScreenIntPoint(10, 10), mcc->Time()); + mcc->AdvanceByMillis(5); + mRootApzc->AdvanceAnimations(mcc->GetSampleTime()); + + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID); + TouchUp(mRootApzc, ScreenIntPoint(10, 10), mcc->Time()); + mcc->AdvanceByMillis(5); + mRootApzc->AdvanceAnimations(mcc->GetSampleTime()); + + check.Call("Done"); +} diff --git a/gfx/layers/apz/test/gtest/TestTreeManager.cpp b/gfx/layers/apz/test/gtest/TestTreeManager.cpp new file mode 100644 index 0000000000..8d8cda8729 --- /dev/null +++ b/gfx/layers/apz/test/gtest/TestTreeManager.cpp @@ -0,0 +1,411 @@ +/* -*- 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" +#include "Units.h" +#include "mozilla/StaticPrefs_apz.h" + +class APZCTreeManagerGenericTester : public APZCTreeManagerTester { + protected: + void CreateSimpleScrollingLayer() { + const char* treeShape = "x"; + LayerIntRect layerVisibleRect[] = { + LayerIntRect(0, 0, 200, 200), + }; + CreateScrollData(treeShape, layerVisibleRect); + SetScrollableFrameMetrics(layers[0], ScrollableLayerGuid::START_SCROLL_ID, + CSSRect(0, 0, 500, 500)); + } + + void CreateSimpleMultiLayerTree() { + const char* treeShape = "x(xx)"; + // LayerID 0 12 + LayerIntRect layerVisibleRect[] = { + LayerIntRect(0, 0, 100, 100), + LayerIntRect(0, 0, 100, 50), + LayerIntRect(0, 50, 100, 50), + }; + CreateScrollData(treeShape, layerVisibleRect); + } + + void CreatePotentiallyLeakingTree() { + const char* treeShape = "x(x(x(x))x(x(x)))"; + // LayerID 0 1 2 3 4 5 6 + CreateScrollData(treeShape); + SetScrollableFrameMetrics(layers[0], ScrollableLayerGuid::START_SCROLL_ID); + SetScrollableFrameMetrics(layers[2], + ScrollableLayerGuid::START_SCROLL_ID + 1); + SetScrollableFrameMetrics(layers[5], + ScrollableLayerGuid::START_SCROLL_ID + 1); + SetScrollableFrameMetrics(layers[3], + ScrollableLayerGuid::START_SCROLL_ID + 2); + SetScrollableFrameMetrics(layers[6], + ScrollableLayerGuid::START_SCROLL_ID + 3); + } + + void CreateTwoLayerTree(int32_t aRootContentLayerIndex) { + const char* treeShape = "x(x)"; + // LayerID 0 1 + LayerIntRect layerVisibleRect[] = { + LayerIntRect(0, 0, 100, 100), + LayerIntRect(0, 0, 100, 100), + }; + CreateScrollData(treeShape, layerVisibleRect); + SetScrollableFrameMetrics(layers[0], ScrollableLayerGuid::START_SCROLL_ID); + SetScrollableFrameMetrics(layers[1], + ScrollableLayerGuid::START_SCROLL_ID + 1); + SetScrollHandoff(layers[1], layers[0]); + + // Make layers[aRootContentLayerIndex] the root content + ModifyFrameMetrics(layers[aRootContentLayerIndex], + [](ScrollMetadata& sm, FrameMetrics& fm) { + fm.SetIsRootContent(true); + }); + } +}; + +TEST_F(APZCTreeManagerGenericTester, ScrollablePaintedLayers) { + CreateSimpleMultiLayerTree(); + ScopedLayerTreeRegistration registration(LayersId{0}, mcc); + + // both layers have the same scrollId + SetScrollableFrameMetrics(layers[1], ScrollableLayerGuid::START_SCROLL_ID); + SetScrollableFrameMetrics(layers[2], ScrollableLayerGuid::START_SCROLL_ID); + UpdateHitTestingTree(); + + TestAsyncPanZoomController* nullAPZC = nullptr; + // so they should have the same APZC + EXPECT_FALSE(HasScrollableFrameMetrics(layers[0])); + EXPECT_NE(nullAPZC, ApzcOf(layers[1])); + EXPECT_NE(nullAPZC, ApzcOf(layers[2])); + EXPECT_EQ(ApzcOf(layers[1]), ApzcOf(layers[2])); +} + +TEST_F(APZCTreeManagerGenericTester, Bug1068268) { + CreatePotentiallyLeakingTree(); + ScopedLayerTreeRegistration registration(LayersId{0}, mcc); + + UpdateHitTestingTree(); + RefPtr<HitTestingTreeNode> root = manager->GetRootNode(); + RefPtr<HitTestingTreeNode> node2 = root->GetFirstChild()->GetFirstChild(); + RefPtr<HitTestingTreeNode> node5 = root->GetLastChild()->GetLastChild(); + + EXPECT_EQ(ApzcOf(layers[2]), node5->GetApzc()); + EXPECT_EQ(ApzcOf(layers[2]), node2->GetApzc()); + EXPECT_EQ(ApzcOf(layers[0]), ApzcOf(layers[2])->GetParent()); + EXPECT_EQ(ApzcOf(layers[2]), ApzcOf(layers[5])); + + EXPECT_EQ(node2->GetFirstChild(), node2->GetLastChild()); + EXPECT_EQ(ApzcOf(layers[3]), node2->GetLastChild()->GetApzc()); + EXPECT_EQ(node5->GetFirstChild(), node5->GetLastChild()); + EXPECT_EQ(ApzcOf(layers[6]), node5->GetLastChild()->GetApzc()); + EXPECT_EQ(ApzcOf(layers[2]), ApzcOf(layers[3])->GetParent()); + EXPECT_EQ(ApzcOf(layers[5]), ApzcOf(layers[6])->GetParent()); +} + +class APZCTreeManagerGenericTesterMock : public APZCTreeManagerGenericTester { + public: + APZCTreeManagerGenericTesterMock() { CreateMockHitTester(); } +}; + +TEST_F(APZCTreeManagerGenericTesterMock, Bug1194876) { + // Create a layer tree with parent and child scrollable layers, with the + // child being the root content. + CreateTwoLayerTree(1); + ScopedLayerTreeRegistration registration(LayersId{0}, mcc); + UpdateHitTestingTree(); + + uint64_t blockId; + nsTArray<ScrollableLayerGuid> targets; + + // First touch goes down, APZCTM will hit layers[1] because it is on top of + // layers[0], but we tell it the real target APZC is layers[0]. + MultiTouchInput mti; + mti = CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_START, mcc->Time()); + mti.mTouches.AppendElement( + SingleTouchData(0, ScreenIntPoint(25, 50), ScreenSize(0, 0), 0, 0)); + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1, + {CompositorHitTestFlags::eVisibleToHitTest, + CompositorHitTestFlags::eIrregularArea}); + blockId = manager->ReceiveInputEvent(mti).mInputBlockId; + manager->ContentReceivedInputBlock(blockId, false); + targets.AppendElement(ApzcOf(layers[0])->GetGuid()); + manager->SetTargetAPZC(blockId, targets); + + // Around here, the above touch will get processed by ApzcOf(layers[0]) + + // Second touch goes down (first touch remains down), APZCTM will again hit + // layers[1]. Again we tell it both touches landed on layers[0], but because + // layers[1] is the RCD layer, it will end up being the multitouch target. + mti.mTouches.AppendElement( + SingleTouchData(1, ScreenIntPoint(75, 50), ScreenSize(0, 0), 0, 0)); + // Each touch will get hit-tested, so queue two hit-test results. + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1, + {CompositorHitTestFlags::eVisibleToHitTest, + CompositorHitTestFlags::eIrregularArea}); + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1, + {CompositorHitTestFlags::eVisibleToHitTest, + CompositorHitTestFlags::eIrregularArea}); + blockId = manager->ReceiveInputEvent(mti).mInputBlockId; + manager->ContentReceivedInputBlock(blockId, false); + targets.AppendElement(ApzcOf(layers[0])->GetGuid()); + manager->SetTargetAPZC(blockId, targets); + + // Around here, the above multi-touch will get processed by ApzcOf(layers[1]). + // We want to ensure that ApzcOf(layers[0]) has had its state cleared, because + // otherwise it will do things like dispatch spurious long-tap events. + + EXPECT_CALL(*mcc, HandleTap(TapType::eLongTap, _, _, _, _, _)).Times(0); +} + +TEST_F(APZCTreeManagerGenericTesterMock, TargetChangesMidGesture_Bug1570559) { + // Create a layer tree with parent and child scrollable layers, with the + // parent being the root content. + CreateTwoLayerTree(0); + ScopedLayerTreeRegistration registration(LayersId{0}, mcc); + UpdateHitTestingTree(); + + uint64_t blockId; + nsTArray<ScrollableLayerGuid> targets; + + // First touch goes down. APZCTM hits the child layer because it is on top + // (and we confirm this target), but do not prevent-default the event, causing + // the child APZC's gesture detector to start a long-tap timeout task. + MultiTouchInput mti = + CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_START, mcc->Time()); + mti.mTouches.AppendElement( + SingleTouchData(0, ScreenIntPoint(25, 50), ScreenSize(0, 0), 0, 0)); + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1, + {CompositorHitTestFlags::eVisibleToHitTest, + CompositorHitTestFlags::eIrregularArea}); + blockId = manager->ReceiveInputEvent(mti).mInputBlockId; + manager->ContentReceivedInputBlock(blockId, /* default prevented = */ false); + targets.AppendElement(ApzcOf(layers[1])->GetGuid()); + manager->SetTargetAPZC(blockId, targets); + + // Second touch goes down (first touch remains down). APZCTM again hits the + // child and we confirm this, but multi-touch events are routed to the root + // content APZC which is the parent. This event is prevent-defaulted, so we + // clear the parent's gesture state. The bug is that we fail to clear the + // child's gesture state. + mti.mTouches.AppendElement( + SingleTouchData(1, ScreenIntPoint(75, 50), ScreenSize(0, 0), 0, 0)); + // Each touch will get hit-tested, so queue two hit-test results. + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1, + {CompositorHitTestFlags::eVisibleToHitTest, + CompositorHitTestFlags::eIrregularArea}); + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1, + {CompositorHitTestFlags::eVisibleToHitTest, + CompositorHitTestFlags::eIrregularArea}); + blockId = manager->ReceiveInputEvent(mti).mInputBlockId; + manager->ContentReceivedInputBlock(blockId, /* default prevented = */ true); + targets.AppendElement(ApzcOf(layers[1])->GetGuid()); + manager->SetTargetAPZC(blockId, targets); + + // If we've failed to clear the child's gesture state, then the long tap + // timeout task will fire in TearDown() and a long-tap will be dispatched. + EXPECT_CALL(*mcc, HandleTap(TapType::eLongTap, _, _, _, _, _)).Times(0); +} + +TEST_F(APZCTreeManagerGenericTesterMock, Bug1198900) { + // This is just a test that cancels a wheel event to make sure it doesn't + // crash. + CreateSimpleScrollingLayer(); + ScopedLayerTreeRegistration registration(LayersId{0}, mcc); + UpdateHitTestingTree(); + + ScreenPoint origin(100, 50); + ScrollWheelInput swi(mcc->Time(), 0, ScrollWheelInput::SCROLLMODE_INSTANT, + ScrollWheelInput::SCROLLDELTA_PIXEL, origin, 0, 10, + false, WheelDeltaAdjustmentStrategy::eNone); + uint64_t blockId; + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID, + {CompositorHitTestFlags::eVisibleToHitTest, + CompositorHitTestFlags::eIrregularArea}); + blockId = manager->ReceiveInputEvent(swi).mInputBlockId; + manager->ContentReceivedInputBlock(blockId, /* preventDefault= */ true); +} + +// The next two tests check that APZ clamps the scroll offset it composites even +// if the main thread fails to do so. (The main thread will always clamp its +// scroll offset internally, but it may not send APZ the clamped version for +// scroll offset synchronization reasons.) +TEST_F(APZCTreeManagerTester, Bug1551582) { + // The simple layer tree has a scrollable rect of 500x500 and a composition + // bounds of 200x200, leading to a scroll range of (0,0,300,300). + CreateSimpleScrollingLayer(); + ScopedLayerTreeRegistration registration(LayersId{0}, mcc); + UpdateHitTestingTree(); + + // Simulate the main thread scrolling to the end of the scroll range. + ModifyFrameMetrics(root, [](ScrollMetadata& aSm, FrameMetrics& aMetrics) { + aMetrics.SetLayoutScrollOffset(CSSPoint(300, 300)); + nsTArray<ScrollPositionUpdate> scrollUpdates; + scrollUpdates.AppendElement(ScrollPositionUpdate::NewScroll( + ScrollOrigin::Other, CSSPoint::ToAppUnits(CSSPoint(300, 300)))); + aSm.SetScrollUpdates(scrollUpdates); + aMetrics.SetScrollGeneration(scrollUpdates.LastElement().GetGeneration()); + }); + UpdateHitTestingTree(); + + // Sanity check. + RefPtr<TestAsyncPanZoomController> apzc = ApzcOf(root); + CSSPoint compositedScrollOffset = apzc->GetCompositedScrollOffset(); + EXPECT_EQ(CSSPoint(300, 300), compositedScrollOffset); + + // Simulate the main thread shrinking the scrollable rect to 400x400 (and + // thereby the scroll range to (0,0,200,200) without sending a new scroll + // offset update for the clamped scroll position (200,200). + ModifyFrameMetrics(root, [](ScrollMetadata& aSm, FrameMetrics& aMetrics) { + aMetrics.SetScrollableRect(CSSRect(0, 0, 400, 400)); + }); + UpdateHitTestingTree(); + + // Check that APZ has clamped the scroll offset to (200,200) for us. + compositedScrollOffset = apzc->GetCompositedScrollOffset(); + EXPECT_EQ(CSSPoint(200, 200), compositedScrollOffset); +} +TEST_F(APZCTreeManagerTester, Bug1557424) { + // The simple layer tree has a scrollable rect of 500x500 and a composition + // bounds of 200x200, leading to a scroll range of (0,0,300,300). + CreateSimpleScrollingLayer(); + ScopedLayerTreeRegistration registration(LayersId{0}, mcc); + UpdateHitTestingTree(); + + // Simulate the main thread scrolling to the end of the scroll range. + ModifyFrameMetrics(root, [](ScrollMetadata& aSm, FrameMetrics& aMetrics) { + aMetrics.SetLayoutScrollOffset(CSSPoint(300, 300)); + nsTArray<ScrollPositionUpdate> scrollUpdates; + scrollUpdates.AppendElement(ScrollPositionUpdate::NewScroll( + ScrollOrigin::Other, CSSPoint::ToAppUnits(CSSPoint(300, 300)))); + aSm.SetScrollUpdates(scrollUpdates); + aMetrics.SetScrollGeneration(scrollUpdates.LastElement().GetGeneration()); + }); + UpdateHitTestingTree(); + + // Sanity check. + RefPtr<TestAsyncPanZoomController> apzc = ApzcOf(root); + CSSPoint compositedScrollOffset = apzc->GetCompositedScrollOffset(); + EXPECT_EQ(CSSPoint(300, 300), compositedScrollOffset); + + // Simulate the main thread expanding the composition bounds to 300x300 (and + // thereby shrinking the scroll range to (0,0,200,200) without sending a new + // scroll offset update for the clamped scroll position (200,200). + ModifyFrameMetrics(root, [](ScrollMetadata& aSm, FrameMetrics& aMetrics) { + aMetrics.SetCompositionBounds(ParentLayerRect(0, 0, 300, 300)); + }); + UpdateHitTestingTree(); + + // Check that APZ has clamped the scroll offset to (200,200) for us. + compositedScrollOffset = apzc->GetCompositedScrollOffset(); + EXPECT_EQ(CSSPoint(200, 200), compositedScrollOffset); +} + +TEST_F(APZCTreeManagerTester, Bug1805601) { + // The simple layer tree has a scrollable rect of 500x500 and a composition + // bounds of 200x200, leading to a scroll range of (0,0,300,300) at unit zoom. + CreateSimpleScrollingLayer(); + ScopedLayerTreeRegistration registration(LayersId{0}, mcc); + UpdateHitTestingTree(); + RefPtr<TestAsyncPanZoomController> apzc = ApzcOf(root); + FrameMetrics& compositorMetrics = apzc->GetFrameMetrics(); + EXPECT_EQ(CSSRect(0, 0, 300, 300), compositorMetrics.CalculateScrollRange()); + + // Zoom the page in by 2x. This needs to be reflected in each of the pres + // shell resolution, cumulative resolution, and zoom. This makes the scroll + // range (0,0,400,400). + compositorMetrics.SetZoom(CSSToParentLayerScale(2.0)); + EXPECT_EQ(CSSRect(0, 0, 400, 400), compositorMetrics.CalculateScrollRange()); + + // Scroll to an area inside the 2x scroll range but outside the original one. + compositorMetrics.ClampAndSetVisualScrollOffset(CSSPoint(350, 350)); + EXPECT_EQ(CSSPoint(350, 350), compositorMetrics.GetVisualScrollOffset()); + + // Simulate a main-thread update where the zoom is reset to 1x but the visual + // scroll offset is unmodified. + ModifyFrameMetrics(root, [](ScrollMetadata& aSm, FrameMetrics& aMetrics) { + // Changes to |compositorMetrics| are not reflected in |aMetrics|, which + // is the "layer tree" copy, so we don't need to explicitly set the zoom to + // 1.0 (it still has that as the initial value), but we do need to set + // the visual scroll offset to the same value the APZ copy has. + aMetrics.SetVisualScrollOffset(CSSPoint(350, 350)); + + // Needed to get APZ to accept the 1.0 zoom in |aMetrics|, otherwise + // it will act as though its zoom is newer (e.g. an async zoom that hasn't + // been repainted yet) and ignore ours. + aSm.SetResolutionUpdated(true); + }); + UpdateHitTestingTree(); + + // Check that APZ clamped the scroll offset. + EXPECT_EQ(CSSRect(0, 0, 300, 300), compositorMetrics.CalculateScrollRange()); + EXPECT_EQ(CSSPoint(300, 300), compositorMetrics.GetVisualScrollOffset()); +} + +TEST_F(APZCTreeManagerTester, + InstantKeyScrollBetweenTwoSamplingsWithSameTimeStamp) { + if (!StaticPrefs::apz_keyboard_enabled_AtStartup()) { + // On Android apz.keyboard.enabled is false by default and it's can't be + // changed here since it's `mirror: once`, so we just skip this test. + return; + } + + // For instant scrolling, i.e. no async animation should not be involved. + SCOPED_GFX_PREF_BOOL("general.smoothScroll", false); + + // Set up a keyboard shortcuts map to scroll page down. + AutoTArray<KeyboardShortcut, 1> shortcuts{KeyboardShortcut( + KeyboardInput::KEY_DOWN, 0, 0, 0, 0, + KeyboardScrollAction( + KeyboardScrollAction::KeyboardScrollActionType::eScrollPage, true))}; + KeyboardMap keyboardMap(std::move(shortcuts)); + manager->SetKeyboardMap(keyboardMap); + + // Set up a scrollable layer. + CreateSimpleScrollingLayer(); + ScopedLayerTreeRegistration registration(LayersId{0}, mcc); + UpdateHitTestingTree(); + + // Setup the scrollable layer is scrollable by key events. + FocusTarget focusTarget; + focusTarget.mSequenceNumber = 1; + focusTarget.mData = AsVariant<FocusTarget::ScrollTargets>( + {ScrollableLayerGuid::START_SCROLL_ID, + ScrollableLayerGuid::START_SCROLL_ID}); + manager->UpdateFocusState(LayersId{0}, LayersId{0}, focusTarget); + + // A vsync tick happens. + mcc->AdvanceByMillis(16); + + // The first sampling happens, there's no change have happened, thus no need + // to composite. + EXPECT_FALSE(manager->AdvanceAnimations(mcc->GetSampleTime())); + + // A key event causing scroll page down happens. + WidgetKeyboardEvent widgetEvent(true, eKeyDown, nullptr); + KeyboardInput input(widgetEvent); + Unused << manager->ReceiveInputEvent(input); + + // Simulate WebRender compositing frames until APZ tells it the scroll offset + // has stopped changing. + // Important to trigger the bug: the first composite has the same time stamp + // as the earlier one above. + ParentLayerPoint compositedScrollOffset; + while (true) { + bool needMoreFrames = manager->AdvanceAnimations(mcc->GetSampleTime()); + compositedScrollOffset = ApzcOf(root)->GetCurrentAsyncScrollOffset( + AsyncPanZoomController::eForCompositing); + if (!needMoreFrames) { + break; + } + mcc->AdvanceBy(TimeDuration::FromMilliseconds(16)); + } + + // Check that the effect of the keyboard scroll has been composited. + EXPECT_GT(compositedScrollOffset.y, 0); +} diff --git a/gfx/layers/apz/test/gtest/TestWRScrollData.cpp b/gfx/layers/apz/test/gtest/TestWRScrollData.cpp new file mode 100644 index 0000000000..6d07a91a24 --- /dev/null +++ b/gfx/layers/apz/test/gtest/TestWRScrollData.cpp @@ -0,0 +1,273 @@ +/* -*- 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 "TestWRScrollData.h" +#include "APZTestAccess.h" +#include "gtest/gtest.h" +#include "FrameMetrics.h" +#include "gfxPlatform.h" +#include "mozilla/layers/APZUpdater.h" +#include "mozilla/layers/LayersTypes.h" +#include "mozilla/layers/ScrollableLayerGuid.h" +#include "mozilla/layers/WebRenderScrollDataWrapper.h" +#include "mozilla/UniquePtr.h" +#include "apz/src/APZCTreeManager.h" + +using mozilla::layers::APZCTreeManager; +using mozilla::layers::APZUpdater; +using mozilla::layers::LayersId; +using mozilla::layers::ScrollableLayerGuid; +using mozilla::layers::ScrollMetadata; +using mozilla::layers::TestWRScrollData; +using mozilla::layers::WebRenderLayerScrollData; +using mozilla::layers::WebRenderScrollDataWrapper; + +/* static */ +TestWRScrollData TestWRScrollData::Create(const char* aTreeShape, + const APZUpdater& aUpdater, + const LayerIntRect* aVisibleRects, + const gfx::Matrix4x4* aTransforms) { + // The WebRenderLayerScrollData tree needs to be created in a fairly + // particular way (for example, each node needs to know the number of + // descendants it has), so this function takes care to create the nodes + // in the same order as WebRenderCommandBuilder would. + TestWRScrollData result; + const size_t len = strlen(aTreeShape); + // "Layer index" in this function refers to the index by which a layer will + // be accessible via TestWRScrollData::GetLayer(), and matches the order + // in which the layer appears in |aTreeShape|. + size_t currentLayerIndex = 0; + struct LayerEntry { + size_t mLayerIndex; + int32_t mDescendantCount = 0; + }; + // Layers we have encountered in |aTreeShape|, but have not built a + // WebRenderLayerScrollData for. (It can only be built after its + // descendants have been encountered and counted.) + std::stack<LayerEntry> pendingLayers; + std::vector<WebRenderLayerScrollData> finishedLayers; + // Tracks the level of nesting of '(' characters. Starts at 1 to account + // for the root layer. + size_t depth = 1; + // Helper function for finishing a layer once all its descendants have been + // encountered. + auto finishLayer = [&] { + MOZ_ASSERT(!pendingLayers.empty()); + LayerEntry entry = pendingLayers.top(); + + WebRenderLayerScrollData layer; + APZTestAccess::InitializeForTest(layer, entry.mDescendantCount); + if (aVisibleRects) { + layer.SetVisibleRect(aVisibleRects[entry.mLayerIndex]); + } + if (aTransforms) { + layer.SetTransform(aTransforms[entry.mLayerIndex]); + } + finishedLayers.push_back(std::move(layer)); + + // |finishedLayers| stores the layers in a different order than they + // appeared in |aTreeShape|. To be able to access layers by their layer + // index, keep a mapping from layer index to index in |finishedLayers|. + result.mIndexMap.emplace(entry.mLayerIndex, finishedLayers.size() - 1); + + pendingLayers.pop(); + + // Keep track of descendant counts. The +1 is for the layer just finished. + if (!pendingLayers.empty()) { + pendingLayers.top().mDescendantCount += (entry.mDescendantCount + 1); + } + }; + for (size_t i = 0; i < len; ++i) { + if (aTreeShape[i] == '(') { + ++depth; + } else if (aTreeShape[i] == ')') { + if (pendingLayers.size() <= 1) { + printf("Invalid tree shape: too many ')'\n"); + MOZ_CRASH(); + } + finishLayer(); // finish last layer at current depth + --depth; + } else { + if (aTreeShape[i] != 'x') { + printf("The only allowed character to represent a layer is 'x'\n"); + MOZ_CRASH(); + } + if (depth == pendingLayers.size()) { + // We have a previous layer at this same depth to finish. + if (depth <= 1) { + printf("The tree is only allowed to have one root\n"); + MOZ_CRASH(); + } + finishLayer(); + } + MOZ_ASSERT(depth == pendingLayers.size() + 1); + pendingLayers.push({currentLayerIndex}); + ++currentLayerIndex; + } + } + if (pendingLayers.size() != 1) { + printf("Invalid tree shape: '(' and ')' not balanced\n"); + MOZ_CRASH(); + } + finishLayer(); // finish root layer + + // As in WebRenderCommandBuilder, the layers need to be added to the + // WebRenderScrollData in reverse of the order in which they were built. + for (auto it = finishedLayers.rbegin(); it != finishedLayers.rend(); ++it) { + result.AddLayerData(std::move(*it)); + } + // mIndexMap also needs to be adjusted to accout for the reversal above. + for (auto& [layerIndex, storedIndex] : result.mIndexMap) { + (void)layerIndex; // suppress -Werror=unused-variable + storedIndex = result.GetLayerCount() - storedIndex - 1; + } + + return result; +} + +const WebRenderLayerScrollData* TestWRScrollData::operator[]( + size_t aLayerIndex) const { + auto it = mIndexMap.find(aLayerIndex); + if (it == mIndexMap.end()) { + return nullptr; + } + return GetLayerData(it->second); +} + +WebRenderLayerScrollData* TestWRScrollData::operator[](size_t aLayerIndex) { + auto it = mIndexMap.find(aLayerIndex); + if (it == mIndexMap.end()) { + return nullptr; + } + return GetLayerData(it->second); +} + +void TestWRScrollData::SetScrollMetadata( + size_t aLayerIndex, const nsTArray<ScrollMetadata>& aMetadata) { + WebRenderLayerScrollData* layer = operator[](aLayerIndex); + MOZ_ASSERT(layer); + for (const ScrollMetadata& metadata : aMetadata) { + layer->AppendScrollMetadata(*this, metadata); + } +} + +class WebRenderScrollDataWrapperTester : public ::testing::Test { + protected: + virtual void SetUp() { + // This ensures ScrollMetadata::sNullMetadata is initialized. + gfxPlatform::GetPlatform(); + + mManager = APZCTreeManager::Create(LayersId{0}); + mUpdater = new APZUpdater(mManager, false); + } + + RefPtr<APZCTreeManager> mManager; + RefPtr<APZUpdater> mUpdater; +}; + +TEST_F(WebRenderScrollDataWrapperTester, SimpleTree) { + auto layers = TestWRScrollData::Create("x(x(x(xx)x(x)))", *mUpdater); + WebRenderScrollDataWrapper w0(*mUpdater, &layers); + + ASSERT_EQ(layers[0], w0.GetLayer()); + WebRenderScrollDataWrapper w1 = w0.GetLastChild(); + ASSERT_EQ(layers[1], w1.GetLayer()); + ASSERT_FALSE(w1.GetPrevSibling().IsValid()); + WebRenderScrollDataWrapper w5 = w1.GetLastChild(); + ASSERT_EQ(layers[5], w5.GetLayer()); + WebRenderScrollDataWrapper w6 = w5.GetLastChild(); + ASSERT_EQ(layers[6], w6.GetLayer()); + ASSERT_FALSE(w6.GetLastChild().IsValid()); + WebRenderScrollDataWrapper w2 = w5.GetPrevSibling(); + ASSERT_EQ(layers[2], w2.GetLayer()); + ASSERT_FALSE(w2.GetPrevSibling().IsValid()); + WebRenderScrollDataWrapper w4 = w2.GetLastChild(); + ASSERT_EQ(layers[4], w4.GetLayer()); + ASSERT_FALSE(w4.GetLastChild().IsValid()); + WebRenderScrollDataWrapper w3 = w4.GetPrevSibling(); + ASSERT_EQ(layers[3], w3.GetLayer()); + ASSERT_FALSE(w3.GetLastChild().IsValid()); + ASSERT_FALSE(w3.GetPrevSibling().IsValid()); +} + +static ScrollMetadata MakeMetadata(ScrollableLayerGuid::ViewID aId) { + ScrollMetadata metadata; + metadata.GetMetrics().SetScrollId(aId); + return metadata; +} + +TEST_F(WebRenderScrollDataWrapperTester, MultiFramemetricsTree) { + auto layers = TestWRScrollData::Create("x(x(x(xx)x(x)))", *mUpdater); + + nsTArray<ScrollMetadata> metadata; + metadata.InsertElementAt(0, + MakeMetadata(ScrollableLayerGuid::START_SCROLL_ID + + 0)); // topmost of root layer + metadata.InsertElementAt(0, + MakeMetadata(ScrollableLayerGuid::NULL_SCROLL_ID)); + metadata.InsertElementAt( + 0, MakeMetadata(ScrollableLayerGuid::START_SCROLL_ID + 1)); + metadata.InsertElementAt( + 0, MakeMetadata(ScrollableLayerGuid::START_SCROLL_ID + 2)); + metadata.InsertElementAt(0, + MakeMetadata(ScrollableLayerGuid::NULL_SCROLL_ID)); + metadata.InsertElementAt( + 0, MakeMetadata( + ScrollableLayerGuid::NULL_SCROLL_ID)); // bottom of root layer + layers.SetScrollMetadata(0, metadata); + + metadata.Clear(); + metadata.InsertElementAt( + 0, MakeMetadata(ScrollableLayerGuid::START_SCROLL_ID + 3)); + layers.SetScrollMetadata(1, metadata); + + metadata.Clear(); + metadata.InsertElementAt( + 0, MakeMetadata(ScrollableLayerGuid::START_SCROLL_ID + 4)); + layers.SetScrollMetadata(2, metadata); + + metadata.Clear(); + metadata.InsertElementAt( + 0, MakeMetadata(ScrollableLayerGuid::START_SCROLL_ID + 5)); + layers.SetScrollMetadata(4, metadata); + + metadata.Clear(); + metadata.InsertElementAt(0, + MakeMetadata(ScrollableLayerGuid::NULL_SCROLL_ID)); + metadata.InsertElementAt( + 0, MakeMetadata(ScrollableLayerGuid::START_SCROLL_ID + 6)); + layers.SetScrollMetadata(5, metadata); + + WebRenderScrollDataWrapper wrapper(*mUpdater, &layers); + nsTArray<WebRenderLayerScrollData*> expectedLayers; + expectedLayers.AppendElement(layers[0]); + expectedLayers.AppendElement(layers[0]); + expectedLayers.AppendElement(layers[0]); + expectedLayers.AppendElement(layers[0]); + expectedLayers.AppendElement(layers[0]); + expectedLayers.AppendElement(layers[0]); + expectedLayers.AppendElement(layers[1]); + expectedLayers.AppendElement(layers[5]); + expectedLayers.AppendElement(layers[5]); + expectedLayers.AppendElement(layers[6]); + nsTArray<ScrollableLayerGuid::ViewID> expectedIds; + expectedIds.AppendElement(ScrollableLayerGuid::START_SCROLL_ID + 0); + expectedIds.AppendElement(ScrollableLayerGuid::NULL_SCROLL_ID); + expectedIds.AppendElement(ScrollableLayerGuid::START_SCROLL_ID + 1); + expectedIds.AppendElement(ScrollableLayerGuid::START_SCROLL_ID + 2); + expectedIds.AppendElement(ScrollableLayerGuid::NULL_SCROLL_ID); + expectedIds.AppendElement(ScrollableLayerGuid::NULL_SCROLL_ID); + expectedIds.AppendElement(ScrollableLayerGuid::START_SCROLL_ID + 3); + expectedIds.AppendElement(ScrollableLayerGuid::NULL_SCROLL_ID); + expectedIds.AppendElement(ScrollableLayerGuid::START_SCROLL_ID + 6); + expectedIds.AppendElement(ScrollableLayerGuid::NULL_SCROLL_ID); + for (int i = 0; i < 10; i++) { + ASSERT_EQ(expectedLayers[i], wrapper.GetLayer()); + ASSERT_EQ(expectedIds[i], wrapper.Metrics().GetScrollId()); + wrapper = wrapper.GetLastChild(); + } + ASSERT_FALSE(wrapper.IsValid()); +} diff --git a/gfx/layers/apz/test/gtest/TestWRScrollData.h b/gfx/layers/apz/test/gtest/TestWRScrollData.h new file mode 100644 index 0000000000..c0a6a78e3a --- /dev/null +++ b/gfx/layers/apz/test/gtest/TestWRScrollData.h @@ -0,0 +1,63 @@ +/* -*- 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/. */ + +#ifndef mozilla_layers_TestWRScrollData_h +#define mozilla_layers_TestWRScrollData_h + +#include "mozilla/gfx/MatrixFwd.h" +#include "mozilla/layers/WebRenderScrollData.h" + +namespace mozilla { +namespace layers { + +class APZUpdater; + +// Extends WebRenderScrollData with some methods useful for gtests. +class TestWRScrollData : public WebRenderScrollData { + public: + TestWRScrollData() = default; + TestWRScrollData(TestWRScrollData&& aOther) = default; + TestWRScrollData& operator=(TestWRScrollData&& aOther) = default; + + /* + * Create a WebRenderLayerScrollData tree described by |aTreeShape|. + * |aTreeShape| is expected to be a string where each character is + * either 'x' to indicate a node in the tree, or a '(' or ')' to indicate + * the start/end of a subtree. + * + * Example "x(x(x(xx)x))" would yield: + * x + * | + * x + * / \ + * x x + * / \ + * x x + * + * The caller may optionally provide visible rects and/or transforms + * for the nodes. If provided, the array should contain one element + * for each node, in the same order as in |aTreeShape|. + */ + static TestWRScrollData Create(const char* aTreeShape, + const APZUpdater& aUpdater, + const LayerIntRect* aVisibleRects = nullptr, + const gfx::Matrix4x4* aTransforms = nullptr); + + // These methods allow accessing and manipulating layers based on an index + // representing the order in which they appear in |aTreeShape|. + WebRenderLayerScrollData* operator[](size_t aLayerIndex); + const WebRenderLayerScrollData* operator[](size_t aLayerIndex) const; + void SetScrollMetadata(size_t aLayerIndex, + const nsTArray<ScrollMetadata>& aMetadata); + + private: + std::map<size_t, size_t> mIndexMap; // Used to implement GetLayer() +}; + +} // namespace layers +} // namespace mozilla + +#endif diff --git a/gfx/layers/apz/test/gtest/moz.build b/gfx/layers/apz/test/gtest/moz.build new file mode 100644 index 0000000000..5c9231fd21 --- /dev/null +++ b/gfx/layers/apz/test/gtest/moz.build @@ -0,0 +1,40 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +UNIFIED_SOURCES += [ + "APZTestAccess.cpp", + "APZTestCommon.cpp", + "MockHitTester.cpp", + "TestAxisLock.cpp", + "TestBasic.cpp", + "TestEventRegions.cpp", + "TestEventResult.cpp", + "TestFlingAcceleration.cpp", + "TestGestureDetector.cpp", + "TestHitTesting.cpp", + "TestInputQueue.cpp", + "TestOverscroll.cpp", + "TestPanning.cpp", + "TestPinching.cpp", + "TestPointerEventsConsumable.cpp", + "TestScrollbarDragging.cpp", + "TestScrollHandoff.cpp", + "TestSnapping.cpp", + "TestSnappingOnMomentum.cpp", + "TestTransformNotifications.cpp", + "TestTreeManager.cpp", + "TestWRScrollData.cpp", +] + +include("/ipc/chromium/chromium-config.mozbuild") + +LOCAL_INCLUDES += [ + "/gfx/2d", + "/gfx/cairo/cairo/src", + "/gfx/layers", +] + +FINAL_LIBRARY = "xul-gtest" diff --git a/gfx/layers/apz/test/gtest/mvm/TestMobileViewportManager.cpp b/gfx/layers/apz/test/gtest/mvm/TestMobileViewportManager.cpp new file mode 100644 index 0000000000..e5b2dc7af8 --- /dev/null +++ b/gfx/layers/apz/test/gtest/mvm/TestMobileViewportManager.cpp @@ -0,0 +1,221 @@ +/* -*- 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 "gtest/gtest.h" +#include "gmock/gmock.h" + +#include <functional> + +#include "MobileViewportManager.h" +#include "mozilla/MVMContext.h" +#include "mozilla/dom/Event.h" + +using namespace mozilla; + +class MockMVMContext : public MVMContext { + using AutoSizeFlag = nsViewportInfo::AutoSizeFlag; + using AutoScaleFlag = nsViewportInfo::AutoScaleFlag; + using ZoomFlag = nsViewportInfo::ZoomFlag; + + // A "layout function" is a function that computes the content size + // as a function of the ICB size. + using LayoutFunction = std::function<CSSSize(CSSSize aICBSize)>; + + public: + // MVMContext methods we don't care to implement. + MOCK_METHOD3(AddEventListener, + void(const nsAString& aType, nsIDOMEventListener* aListener, + bool aUseCapture)); + MOCK_METHOD3(RemoveEventListener, + void(const nsAString& aType, nsIDOMEventListener* aListener, + bool aUseCapture)); + MOCK_METHOD3(AddObserver, void(nsIObserver* aObserver, const char* aTopic, + bool aOwnsWeak)); + MOCK_METHOD2(RemoveObserver, + void(nsIObserver* aObserver, const char* aTopic)); + MOCK_METHOD0(Destroy, void()); + + MOCK_METHOD1(SetVisualViewportSize, void(const CSSSize& aSize)); + MOCK_METHOD0(PostVisualViewportResizeEventByDynamicToolbar, void()); + MOCK_METHOD0(UpdateDisplayPortMargins, void()); + MOCK_METHOD0(GetDynamicToolbarOffset, ScreenIntCoord()); + + void SetMVM(MobileViewportManager* aMVM) { mMVM = aMVM; } + + // MVMContext method implementations. + nsViewportInfo GetViewportInfo(const ScreenIntSize& aDisplaySize) const { + // This is a very basic approximation of what Document::GetViewportInfo() + // does in the most common cases. + // Ideally, we would invoke the algorithm in Document::GetViewportInfo() + // itself, but that would require refactoring it a bit to remove + // dependencies on the actual Document which we don't have available in + // this test harness. + CSSSize viewportSize = mDisplaySize / mDeviceScale; + if (mAutoSizeFlag == AutoSizeFlag::FixedSize) { + viewportSize = CSSSize(mFixedViewportWidth, + mFixedViewportWidth * (float(mDisplaySize.height) / + mDisplaySize.width)); + } + return nsViewportInfo(mDefaultScale, mMinScale, mMaxScale, viewportSize, + mAutoSizeFlag, mAutoScaleFlag, mZoomFlag, + dom::ViewportFitType::Auto); + } + CSSToLayoutDeviceScale CSSToDevPixelScale() const { return mDeviceScale; } + float GetResolution() const { return mResolution; } + bool SubjectMatchesDocument(nsISupports* aSubject) const { return true; } + Maybe<CSSRect> CalculateScrollableRectForRSF() const { + return Some(CSSRect(CSSPoint(), mContentSize)); + } + bool IsResolutionUpdatedByApz() const { return false; } + LayoutDeviceMargin ScrollbarAreaToExcludeFromCompositionBounds() const { + return LayoutDeviceMargin(); + } + Maybe<LayoutDeviceIntSize> GetDocumentViewerSize() const { + return Some(mDisplaySize); + } + bool AllowZoomingForDocument() const { return true; } + bool IsInReaderMode() const { return false; } + bool IsDocumentLoading() const { return false; } + + void SetResolutionAndScaleTo(float aResolution, + ResolutionChangeOrigin aOrigin) { + mResolution = aResolution; + mMVM->ResolutionUpdated(aOrigin); + } + void Reflow(const CSSSize& aNewSize) { + mICBSize = aNewSize; + mContentSize = mLayoutFunction(mICBSize); + } + + // Allow test code to modify the input metrics. + void SetMinScale(CSSToScreenScale aMinScale) { mMinScale = aMinScale; } + void SetMaxScale(CSSToScreenScale aMaxScale) { mMaxScale = aMaxScale; } + void SetInitialScale(CSSToScreenScale aInitialScale) { + mDefaultScale = aInitialScale; + mAutoScaleFlag = AutoScaleFlag::FixedScale; + } + void SetFixedViewportWidth(CSSCoord aWidth) { + mFixedViewportWidth = aWidth; + mAutoSizeFlag = AutoSizeFlag::FixedSize; + } + void SetDisplaySize(const LayoutDeviceIntSize& aNewDisplaySize) { + mDisplaySize = aNewDisplaySize; + } + void SetLayoutFunction(const LayoutFunction& aLayoutFunction) { + mLayoutFunction = aLayoutFunction; + } + + // Allow test code to query the output metrics. + CSSSize GetICBSize() const { return mICBSize; } + CSSSize GetContentSize() const { return mContentSize; } + + private: + // Input metrics, with some sensible defaults. + LayoutDeviceIntSize mDisplaySize{300, 600}; + CSSToScreenScale mDefaultScale{1.0f}; + CSSToScreenScale mMinScale{0.25f}; + CSSToScreenScale mMaxScale{10.0f}; + CSSToLayoutDeviceScale mDeviceScale{1.0f}; + CSSCoord mFixedViewportWidth; + AutoSizeFlag mAutoSizeFlag = AutoSizeFlag::AutoSize; + AutoScaleFlag mAutoScaleFlag = AutoScaleFlag::AutoScale; + ZoomFlag mZoomFlag = ZoomFlag::AllowZoom; + // As a default layout function, just set the content size to the ICB size. + LayoutFunction mLayoutFunction = [](CSSSize aICBSize) { return aICBSize; }; + + // Output metrics. + float mResolution = 1.0f; + CSSSize mICBSize; + CSSSize mContentSize; + + MobileViewportManager* mMVM = nullptr; +}; + +class MVMTester : public ::testing::Test { + public: + MVMTester() + : mMVMContext(new MockMVMContext()), + mMVM(new MobileViewportManager( + mMVMContext, + MobileViewportManager::ManagerType::VisualAndMetaViewport)) { + mMVMContext->SetMVM(mMVM.get()); + } + + void Resize(const LayoutDeviceIntSize& aNewDisplaySize) { + mMVMContext->SetDisplaySize(aNewDisplaySize); + mMVM->RequestReflow(false); + } + + protected: + RefPtr<MockMVMContext> mMVMContext; + RefPtr<MobileViewportManager> mMVM; +}; + +TEST_F(MVMTester, ZoomBoundsRespectedAfterRotation_Bug1536755) { + // Set up initial conditions. + mMVMContext->SetDisplaySize(LayoutDeviceIntSize(600, 300)); + mMVMContext->SetInitialScale(CSSToScreenScale(1.0f)); + mMVMContext->SetMinScale(CSSToScreenScale(1.0f)); + mMVMContext->SetMaxScale(CSSToScreenScale(1.0f)); + // Set a layout function that simulates a page which is twice + // as tall as it is wide. + mMVMContext->SetLayoutFunction([](CSSSize aICBSize) { + return CSSSize(aICBSize.width, aICBSize.width * 2); + }); + + // Perform an initial viewport computation and reflow, and + // sanity-check the results. + mMVM->SetInitialViewport(); + EXPECT_EQ(CSSSize(600, 300), mMVMContext->GetICBSize()); + EXPECT_EQ(CSSSize(600, 1200), mMVMContext->GetContentSize()); + EXPECT_EQ(1.0f, mMVMContext->GetResolution()); + + // Now rotate the screen, and check that the minimum and maximum + // scales are still respected after the rotation. + Resize(LayoutDeviceIntSize(300, 600)); + EXPECT_EQ(CSSSize(300, 600), mMVMContext->GetICBSize()); + EXPECT_EQ(CSSSize(300, 600), mMVMContext->GetContentSize()); + EXPECT_EQ(1.0f, mMVMContext->GetResolution()); +} + +TEST_F(MVMTester, LandscapeToPortraitRotation_Bug1523844) { + // Set up initial conditions. + mMVMContext->SetDisplaySize(LayoutDeviceIntSize(300, 600)); + // Set a layout function that simulates a page with a fixed + // content size that's as wide as the screen in one orientation + // (and wider in the other). + mMVMContext->SetLayoutFunction( + [](CSSSize aICBSize) { return CSSSize(600, 1200); }); + + // Simulate a "DOMMetaAdded" event being fired before calling + // SetInitialViewport(). This matches what typically happens + // during real usage (the MVM receives the "DOMMetaAdded" + // before the "load", and it's the "load" that calls + // SetInitialViewport()), and is important to trigger this + // bug, because it causes the MVM to be stuck with an + // "mRestoreResolution" (prior to the fix). + mMVM->HandleDOMMetaAdded(); + + // Perform an initial viewport computation and reflow, and + // sanity-check the results. + mMVM->SetInitialViewport(); + EXPECT_EQ(CSSSize(300, 600), mMVMContext->GetICBSize()); + EXPECT_EQ(CSSSize(600, 1200), mMVMContext->GetContentSize()); + EXPECT_EQ(0.5f, mMVMContext->GetResolution()); + + // Rotate to landscape. + Resize(LayoutDeviceIntSize(600, 300)); + EXPECT_EQ(CSSSize(600, 300), mMVMContext->GetICBSize()); + EXPECT_EQ(CSSSize(600, 1200), mMVMContext->GetContentSize()); + EXPECT_EQ(1.0f, mMVMContext->GetResolution()); + + // Rotate back to portrait and check that we have returned + // to the portrait resolution. + Resize(LayoutDeviceIntSize(300, 600)); + EXPECT_EQ(CSSSize(300, 600), mMVMContext->GetICBSize()); + EXPECT_EQ(CSSSize(600, 1200), mMVMContext->GetContentSize()); + EXPECT_EQ(0.5f, mMVMContext->GetResolution()); +} diff --git a/gfx/layers/apz/test/gtest/mvm/moz.build b/gfx/layers/apz/test/gtest/mvm/moz.build new file mode 100644 index 0000000000..0fa985307b --- /dev/null +++ b/gfx/layers/apz/test/gtest/mvm/moz.build @@ -0,0 +1,13 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +UNIFIED_SOURCES += [ + "TestMobileViewportManager.cpp", +] + +include("/ipc/chromium/chromium-config.mozbuild") + +FINAL_LIBRARY = "xul-gtest" |