summaryrefslogtreecommitdiffstats
path: root/gfx/layers/apz/src
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 19:33:14 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 19:33:14 +0000
commit36d22d82aa202bb199967e9512281e9a53db42c9 (patch)
tree105e8c98ddea1c1e4784a60a5a6410fa416be2de /gfx/layers/apz/src
parentInitial commit. (diff)
downloadfirefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.tar.xz
firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.zip
Adding upstream version 115.7.0esr.upstream/115.7.0esr
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-rw-r--r--gfx/layers/apz/src/APZCTreeManager.cpp3742
-rw-r--r--gfx/layers/apz/src/APZCTreeManager.h1064
-rw-r--r--gfx/layers/apz/src/APZInputBridge.cpp435
-rw-r--r--gfx/layers/apz/src/APZPublicUtils.cpp111
-rw-r--r--gfx/layers/apz/src/APZSampler.cpp216
-rw-r--r--gfx/layers/apz/src/APZUpdater.cpp546
-rw-r--r--gfx/layers/apz/src/APZUtils.cpp118
-rw-r--r--gfx/layers/apz/src/APZUtils.h220
-rw-r--r--gfx/layers/apz/src/AndroidAPZ.cpp36
-rw-r--r--gfx/layers/apz/src/AndroidAPZ.h34
-rw-r--r--gfx/layers/apz/src/AndroidFlingPhysics.cpp218
-rw-r--r--gfx/layers/apz/src/AndroidFlingPhysics.h45
-rw-r--r--gfx/layers/apz/src/AndroidVelocityTracker.cpp288
-rw-r--r--gfx/layers/apz/src/AndroidVelocityTracker.h42
-rw-r--r--gfx/layers/apz/src/AsyncDragMetrics.h54
-rw-r--r--gfx/layers/apz/src/AsyncPanZoomAnimation.h101
-rw-r--r--gfx/layers/apz/src/AsyncPanZoomController.cpp6654
-rw-r--r--gfx/layers/apz/src/AsyncPanZoomController.h1943
-rw-r--r--gfx/layers/apz/src/AutoDirWheelDeltaAdjuster.h89
-rw-r--r--gfx/layers/apz/src/AutoscrollAnimation.cpp93
-rw-r--r--gfx/layers/apz/src/AutoscrollAnimation.h42
-rw-r--r--gfx/layers/apz/src/Axis.cpp733
-rw-r--r--gfx/layers/apz/src/Axis.h462
-rw-r--r--gfx/layers/apz/src/CheckerboardEvent.cpp195
-rw-r--r--gfx/layers/apz/src/CheckerboardEvent.h218
-rw-r--r--gfx/layers/apz/src/DesktopFlingPhysics.h67
-rw-r--r--gfx/layers/apz/src/DragTracker.cpp59
-rw-r--r--gfx/layers/apz/src/DragTracker.h39
-rw-r--r--gfx/layers/apz/src/ExpectedGeckoMetrics.cpp26
-rw-r--r--gfx/layers/apz/src/ExpectedGeckoMetrics.h44
-rw-r--r--gfx/layers/apz/src/FlingAccelerator.cpp128
-rw-r--r--gfx/layers/apz/src/FlingAccelerator.h59
-rw-r--r--gfx/layers/apz/src/FocusState.cpp225
-rw-r--r--gfx/layers/apz/src/FocusState.h175
-rw-r--r--gfx/layers/apz/src/FocusTarget.cpp233
-rw-r--r--gfx/layers/apz/src/FocusTarget.h71
-rw-r--r--gfx/layers/apz/src/GenericFlingAnimation.h207
-rw-r--r--gfx/layers/apz/src/GenericScrollAnimation.cpp120
-rw-r--r--gfx/layers/apz/src/GenericScrollAnimation.h59
-rw-r--r--gfx/layers/apz/src/GestureEventListener.cpp663
-rw-r--r--gfx/layers/apz/src/GestureEventListener.h285
-rw-r--r--gfx/layers/apz/src/HitTestingTreeNode.cpp419
-rw-r--r--gfx/layers/apz/src/HitTestingTreeNode.h270
-rw-r--r--gfx/layers/apz/src/IAPZHitTester.cpp78
-rw-r--r--gfx/layers/apz/src/IAPZHitTester.h91
-rw-r--r--gfx/layers/apz/src/InputBlockState.cpp840
-rw-r--r--gfx/layers/apz/src/InputBlockState.h544
-rw-r--r--gfx/layers/apz/src/InputQueue.cpp1090
-rw-r--r--gfx/layers/apz/src/InputQueue.h277
-rw-r--r--gfx/layers/apz/src/KeyboardMap.cpp170
-rw-r--r--gfx/layers/apz/src/KeyboardMap.h118
-rw-r--r--gfx/layers/apz/src/KeyboardScrollAction.cpp37
-rw-r--r--gfx/layers/apz/src/KeyboardScrollAction.h48
-rw-r--r--gfx/layers/apz/src/Overscroll.h250
-rw-r--r--gfx/layers/apz/src/OverscrollHandoffState.cpp228
-rw-r--r--gfx/layers/apz/src/OverscrollHandoffState.h203
-rw-r--r--gfx/layers/apz/src/PotentialCheckerboardDurationTracker.cpp74
-rw-r--r--gfx/layers/apz/src/PotentialCheckerboardDurationTracker.h61
-rw-r--r--gfx/layers/apz/src/QueuedInput.cpp44
-rw-r--r--gfx/layers/apz/src/QueuedInput.h63
-rw-r--r--gfx/layers/apz/src/RecentEventsBuffer.h83
-rw-r--r--gfx/layers/apz/src/SampledAPZCState.cpp111
-rw-r--r--gfx/layers/apz/src/SampledAPZCState.h72
-rw-r--r--gfx/layers/apz/src/ScrollThumbUtils.cpp341
-rw-r--r--gfx/layers/apz/src/ScrollThumbUtils.h51
-rw-r--r--gfx/layers/apz/src/SimpleVelocityTracker.cpp135
-rw-r--r--gfx/layers/apz/src/SimpleVelocityTracker.h54
-rw-r--r--gfx/layers/apz/src/SmoothMsdScrollAnimation.cpp139
-rw-r--r--gfx/layers/apz/src/SmoothMsdScrollAnimation.h61
-rw-r--r--gfx/layers/apz/src/SmoothScrollAnimation.cpp46
-rw-r--r--gfx/layers/apz/src/SmoothScrollAnimation.h37
-rw-r--r--gfx/layers/apz/src/WRHitTester.cpp247
-rw-r--r--gfx/layers/apz/src/WRHitTester.h26
-rw-r--r--gfx/layers/apz/src/WheelScrollAnimation.cpp64
-rw-r--r--gfx/layers/apz/src/WheelScrollAnimation.h30
75 files changed, 26561 insertions, 0 deletions
diff --git a/gfx/layers/apz/src/APZCTreeManager.cpp b/gfx/layers/apz/src/APZCTreeManager.cpp
new file mode 100644
index 0000000000..9dd296dd97
--- /dev/null
+++ b/gfx/layers/apz/src/APZCTreeManager.cpp
@@ -0,0 +1,3742 @@
+/* -*- 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 <stack>
+#include <unordered_set>
+#include "APZCTreeManager.h"
+#include "AsyncPanZoomController.h"
+#include "Compositor.h" // for Compositor
+#include "DragTracker.h" // for DragTracker
+#include "GenericFlingAnimation.h" // for FLING_LOG
+#include "HitTestingTreeNode.h" // for HitTestingTreeNode
+#include "InputBlockState.h" // for InputBlockState
+#include "InputData.h" // for InputData, etc
+#include "WRHitTester.h" // for WRHitTester
+#include "mozilla/RecursiveMutex.h"
+#include "mozilla/dom/MouseEventBinding.h" // for MouseEvent constants
+#include "mozilla/dom/BrowserParent.h" // for AreRecordReplayTabsActive
+#include "mozilla/dom/Touch.h" // for Touch
+#include "mozilla/gfx/CompositorHitTestInfo.h"
+#include "mozilla/gfx/LoggingConstants.h"
+#include "mozilla/gfx/gfxVars.h" // for gfxVars
+#include "mozilla/gfx/GPUParent.h" // for GPUParent
+#include "mozilla/gfx/Logging.h" // for gfx::TreeLog
+#include "mozilla/gfx/Point.h" // for Point
+#include "mozilla/layers/APZSampler.h" // for APZSampler
+#include "mozilla/layers/APZThreadUtils.h" // for AssertOnControllerThread, etc
+#include "mozilla/layers/APZUpdater.h" // for APZUpdater
+#include "mozilla/layers/APZUtils.h" // for AsyncTransform
+#include "mozilla/layers/AsyncDragMetrics.h" // for AsyncDragMetrics
+#include "mozilla/layers/CompositorBridgeParent.h" // for CompositorBridgeParent, etc
+#include "mozilla/layers/DoubleTapToZoom.h" // for ZoomTarget
+#include "mozilla/layers/MatrixMessage.h"
+#include "mozilla/layers/UiCompositorControllerParent.h"
+#include "mozilla/layers/WebRenderScrollDataWrapper.h"
+#include "mozilla/MouseEvents.h"
+#include "mozilla/mozalloc.h" // for operator new
+#include "mozilla/Preferences.h" // for Preferences
+#include "mozilla/StaticPrefs_accessibility.h"
+#include "mozilla/StaticPrefs_apz.h"
+#include "mozilla/StaticPrefs_layout.h"
+#include "mozilla/ToString.h"
+#include "mozilla/TouchEvents.h"
+#include "mozilla/EventStateManager.h" // for WheelPrefs
+#include "mozilla/webrender/WebRenderAPI.h"
+#include "nsDebug.h" // for NS_WARNING
+#include "nsPoint.h" // for nsIntPoint
+#include "nsThreadUtils.h" // for NS_IsMainThread
+#include "ScrollThumbUtils.h" // for ComputeTransformForScrollThumb
+#include "OverscrollHandoffState.h" // for OverscrollHandoffState
+#include "TreeTraversal.h" // for ForEachNode, BreadthFirstSearch, etc
+#include "Units.h" // for ParentlayerPixel
+#include "GestureEventListener.h" // for GestureEventListener::setLongTapEnabled
+#include "UnitTransforms.h" // for ViewAs
+
+mozilla::LazyLogModule mozilla::layers::APZCTreeManager::sLog("apz.manager");
+#define APZCTM_LOG(...) \
+ MOZ_LOG(APZCTreeManager::sLog, LogLevel::Debug, (__VA_ARGS__))
+
+static mozilla::LazyLogModule sApzKeyLog("apz.key");
+#define APZ_KEY_LOG(...) MOZ_LOG(sApzKeyLog, LogLevel::Debug, (__VA_ARGS__))
+
+namespace mozilla {
+namespace layers {
+
+using mozilla::gfx::CompositorHitTestFlags;
+using mozilla::gfx::CompositorHitTestInfo;
+using mozilla::gfx::CompositorHitTestInvisibleToHit;
+using mozilla::gfx::LOG_DEFAULT;
+
+typedef mozilla::gfx::Point Point;
+typedef mozilla::gfx::Point4D Point4D;
+typedef mozilla::gfx::Matrix4x4 Matrix4x4;
+
+typedef CompositorBridgeParent::LayerTreeState LayerTreeState;
+
+struct APZCTreeManager::TreeBuildingState {
+ TreeBuildingState(LayersId aRootLayersId, bool aIsFirstPaint,
+ LayersId aOriginatingLayersId, APZTestData* aTestData,
+ uint32_t aPaintSequence)
+ : mIsFirstPaint(aIsFirstPaint),
+ mOriginatingLayersId(aOriginatingLayersId),
+ mPaintLogger(aTestData, aPaintSequence) {
+ CompositorBridgeParent::CallWithIndirectShadowTree(
+ aRootLayersId, [this](LayerTreeState& aState) -> void {
+ mCompositorController = aState.GetCompositorController();
+ });
+ }
+
+ typedef std::unordered_map<AsyncPanZoomController*, gfx::Matrix4x4>
+ DeferredTransformMap;
+
+ // State that doesn't change as we recurse in the tree building
+ RefPtr<CompositorController> mCompositorController;
+ const bool mIsFirstPaint;
+ const LayersId mOriginatingLayersId;
+ const APZPaintLogHelper mPaintLogger;
+
+ // State that is updated as we perform the tree build
+
+ // A list of nodes that need to be destroyed at the end of the tree building.
+ // This is initialized with all nodes in the old tree, and nodes are removed
+ // from it as we reuse them in the new tree.
+ nsTArray<RefPtr<HitTestingTreeNode>> mNodesToDestroy;
+
+ // This map is populated as we place APZCs into the new tree. Its purpose is
+ // to facilitate re-using the same APZC for different layers that scroll
+ // together (and thus have the same ScrollableLayerGuid). The presShellId
+ // doesn't matter for this purpose, and we move the map to the APZCTreeManager
+ // after we're done building, so it's useful to have the presshell-ignoring
+ // map for that.
+ std::unordered_map<ScrollableLayerGuid, ApzcMapData,
+ ScrollableLayerGuid::HashIgnoringPresShellFn,
+ ScrollableLayerGuid::EqualIgnoringPresShellFn>
+ mApzcMap;
+
+ // This is populated with all the HitTestingTreeNodes that are scroll thumbs
+ // and have a scrollthumb animation id (which indicates that they need to be
+ // sampled for WebRender on the sampler thread).
+ std::vector<HitTestingTreeNode*> mScrollThumbs;
+ // This is populated with all the scroll target nodes. We use in conjunction
+ // with mScrollThumbs to build APZCTreeManager::mScrollThumbInfo.
+ std::unordered_map<ScrollableLayerGuid, HitTestingTreeNode*,
+ ScrollableLayerGuid::HashIgnoringPresShellFn,
+ ScrollableLayerGuid::EqualIgnoringPresShellFn>
+ mScrollTargets;
+
+ // During the tree building process, the perspective transform component
+ // of the ancestor transforms of some APZCs can be "deferred" to their
+ // children, meaning they are added to the children's ancestor transforms
+ // instead. Those deferred transforms are tracked here.
+ DeferredTransformMap mPerspectiveTransformsDeferredToChildren;
+
+ // As we recurse down through the tree, this picks up the zoom animation id
+ // from a node in the layer tree, and propagates it downwards to the nearest
+ // APZC instance that is for an RCD node. Generally it will be set on the
+ // root node of the layers (sub-)tree, which may not be same as the RCD node
+ // for the subtree, and so we need this mechanism to ensure it gets propagated
+ // to the RCD's APZC instance. Once it is set on the APZC instance, the value
+ // is cleared back to Nothing(). Note that this is only used in the WebRender
+ // codepath.
+ Maybe<uint64_t> mZoomAnimationId;
+
+ // See corresponding members of APZCTreeManager. These are the same thing, but
+ // on the tree-walking state. They are populated while walking the tree in
+ // a layers update, and then moved into APZCTreeManager.
+ std::vector<FixedPositionInfo> mFixedPositionInfo;
+ std::vector<RootScrollbarInfo> mRootScrollbarInfo;
+ std::vector<StickyPositionInfo> mStickyPositionInfo;
+
+ // As we recurse down through reflayers in the tree, this picks up the
+ // cumulative EventRegionsOverride flags from the reflayers, and is used to
+ // apply them to descendant layers.
+ std::stack<EventRegionsOverride> mOverrideFlags;
+};
+
+class APZCTreeManager::CheckerboardFlushObserver : public nsIObserver {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIOBSERVER
+
+ explicit CheckerboardFlushObserver(APZCTreeManager* aTreeManager)
+ : mTreeManager(aTreeManager) {
+ MOZ_ASSERT(NS_IsMainThread());
+ nsCOMPtr<nsIObserverService> obsSvc =
+ mozilla::services::GetObserverService();
+ MOZ_ASSERT(obsSvc);
+ if (obsSvc) {
+ obsSvc->AddObserver(this, "APZ:FlushActiveCheckerboard", false);
+ }
+ }
+
+ void Unregister() {
+ MOZ_ASSERT(NS_IsMainThread());
+ nsCOMPtr<nsIObserverService> obsSvc =
+ mozilla::services::GetObserverService();
+ if (obsSvc) {
+ obsSvc->RemoveObserver(this, "APZ:FlushActiveCheckerboard");
+ }
+ mTreeManager = nullptr;
+ }
+
+ protected:
+ virtual ~CheckerboardFlushObserver() = default;
+
+ private:
+ RefPtr<APZCTreeManager> mTreeManager;
+};
+
+NS_IMPL_ISUPPORTS(APZCTreeManager::CheckerboardFlushObserver, nsIObserver)
+
+NS_IMETHODIMP
+APZCTreeManager::CheckerboardFlushObserver::Observe(nsISupports* aSubject,
+ const char* aTopic,
+ const char16_t*) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(mTreeManager.get());
+
+ RecursiveMutexAutoLock lock(mTreeManager->mTreeLock);
+ if (mTreeManager->mRootNode) {
+ ForEachNode<ReverseIterator>(
+ mTreeManager->mRootNode.get(), [](HitTestingTreeNode* aNode) {
+ if (aNode->IsPrimaryHolder()) {
+ MOZ_ASSERT(aNode->GetApzc());
+ aNode->GetApzc()->FlushActiveCheckerboardReport();
+ }
+ });
+ }
+ if (XRE_IsGPUProcess()) {
+ if (gfx::GPUParent* gpu = gfx::GPUParent::GetSingleton()) {
+ nsCString topic("APZ:FlushActiveCheckerboard:Done");
+ Unused << gpu->SendNotifyUiObservers(topic);
+ }
+ } else {
+ MOZ_ASSERT(XRE_IsParentProcess());
+ nsCOMPtr<nsIObserverService> obsSvc =
+ mozilla::services::GetObserverService();
+ if (obsSvc) {
+ obsSvc->NotifyObservers(nullptr, "APZ:FlushActiveCheckerboard:Done",
+ nullptr);
+ }
+ }
+ return NS_OK;
+}
+
+/**
+ * A RAII class used for setting the focus sequence number on input events
+ * as they are being processed. Any input event is assumed to be potentially
+ * focus changing unless explicitly marked otherwise.
+ */
+class MOZ_RAII AutoFocusSequenceNumberSetter {
+ public:
+ AutoFocusSequenceNumberSetter(FocusState& aFocusState, InputData& aEvent)
+ : mFocusState(aFocusState), mEvent(aEvent), mMayChangeFocus(true) {}
+
+ void MarkAsNonFocusChanging() { mMayChangeFocus = false; }
+
+ ~AutoFocusSequenceNumberSetter() {
+ if (mMayChangeFocus) {
+ mFocusState.ReceiveFocusChangingEvent();
+
+ APZ_KEY_LOG(
+ "Marking input with type=%d as focus changing with seq=%" PRIu64 "\n",
+ static_cast<int>(mEvent.mInputType),
+ mFocusState.LastAPZProcessedEvent());
+ } else {
+ APZ_KEY_LOG(
+ "Marking input with type=%d as non focus changing with seq=%" PRIu64
+ "\n",
+ static_cast<int>(mEvent.mInputType),
+ mFocusState.LastAPZProcessedEvent());
+ }
+
+ mEvent.mFocusSequenceNumber = mFocusState.LastAPZProcessedEvent();
+ }
+
+ private:
+ FocusState& mFocusState;
+ InputData& mEvent;
+ bool mMayChangeFocus;
+};
+
+APZCTreeManager::APZCTreeManager(LayersId aRootLayersId,
+ UniquePtr<IAPZHitTester> aHitTester)
+ : mTestSampleTime(Nothing(), "APZCTreeManager::mTestSampleTime"),
+ mInputQueue(new InputQueue()),
+ mRootLayersId(aRootLayersId),
+ mSampler(nullptr),
+ mUpdater(nullptr),
+ mTreeLock("APZCTreeLock"),
+ mMapLock("APZCMapLock"),
+ mRetainedTouchIdentifier(-1),
+ mInScrollbarTouchDrag(false),
+ mCurrentMousePosition(ScreenPoint(),
+ "APZCTreeManager::mCurrentMousePosition"),
+ mApzcTreeLog("apzctree"),
+ mTestDataLock("APZTestDataLock"),
+ mDPI(160.0),
+ mHitTester(std::move(aHitTester)),
+ mScrollGenerationLock("APZScrollGenerationLock") {
+ RefPtr<APZCTreeManager> self(this);
+ NS_DispatchToMainThread(NS_NewRunnableFunction(
+ "layers::APZCTreeManager::APZCTreeManager",
+ [self] { self->mFlushObserver = new CheckerboardFlushObserver(self); }));
+ AsyncPanZoomController::InitializeGlobalState();
+ mApzcTreeLog.ConditionOnPrefFunction(StaticPrefs::apz_printtree);
+
+ if (!mHitTester) {
+ mHitTester = MakeUnique<WRHitTester>();
+ }
+ mHitTester->Initialize(this);
+}
+
+APZCTreeManager::~APZCTreeManager() = default;
+
+void APZCTreeManager::SetSampler(APZSampler* aSampler) {
+ // We're either setting the sampler or clearing it
+ MOZ_ASSERT((mSampler == nullptr) != (aSampler == nullptr));
+ mSampler = aSampler;
+}
+
+void APZCTreeManager::SetUpdater(APZUpdater* aUpdater) {
+ // We're either setting the updater or clearing it
+ MOZ_ASSERT((mUpdater == nullptr) != (aUpdater == nullptr));
+ mUpdater = aUpdater;
+}
+
+void APZCTreeManager::NotifyLayerTreeAdopted(
+ LayersId aLayersId, const RefPtr<APZCTreeManager>& aOldApzcTreeManager) {
+ AssertOnUpdaterThread();
+
+ if (aOldApzcTreeManager) {
+ aOldApzcTreeManager->mFocusState.RemoveFocusTarget(aLayersId);
+ // While we could move the focus target information from the old APZC tree
+ // manager into this one, it's safer to not do that, as we'll probably have
+ // that information repopulated soon anyway (on the next layers update).
+ }
+
+ UniquePtr<APZTestData> adoptedData;
+ if (aOldApzcTreeManager) {
+ MutexAutoLock lock(aOldApzcTreeManager->mTestDataLock);
+ auto it = aOldApzcTreeManager->mTestData.find(aLayersId);
+ if (it != aOldApzcTreeManager->mTestData.end()) {
+ adoptedData = std::move(it->second);
+ aOldApzcTreeManager->mTestData.erase(it);
+ }
+ }
+ if (adoptedData) {
+ MutexAutoLock lock(mTestDataLock);
+ mTestData[aLayersId] = std::move(adoptedData);
+ }
+}
+
+void APZCTreeManager::NotifyLayerTreeRemoved(LayersId aLayersId) {
+ AssertOnUpdaterThread();
+
+ mFocusState.RemoveFocusTarget(aLayersId);
+
+ { // scope lock
+ MutexAutoLock lock(mTestDataLock);
+ mTestData.erase(aLayersId);
+ }
+}
+
+AsyncPanZoomController* APZCTreeManager::NewAPZCInstance(
+ LayersId aLayersId, GeckoContentController* aController) {
+ return new AsyncPanZoomController(
+ aLayersId, this, mInputQueue, aController,
+ AsyncPanZoomController::USE_GESTURE_DETECTOR);
+}
+
+void APZCTreeManager::SetTestSampleTime(const Maybe<TimeStamp>& aTime) {
+ auto testSampleTime = mTestSampleTime.Lock();
+ testSampleTime.ref() = aTime;
+}
+
+SampleTime APZCTreeManager::GetFrameTime() {
+ auto testSampleTime = mTestSampleTime.Lock();
+ if (testSampleTime.ref()) {
+ return SampleTime::FromTest(*testSampleTime.ref());
+ }
+ return SampleTime::FromNow();
+}
+
+void APZCTreeManager::SetAllowedTouchBehavior(
+ uint64_t aInputBlockId, const nsTArray<TouchBehaviorFlags>& aValues) {
+ if (!APZThreadUtils::IsControllerThread()) {
+ APZThreadUtils::RunOnControllerThread(
+ NewRunnableMethod<uint64_t,
+ StoreCopyPassByLRef<nsTArray<TouchBehaviorFlags>>>(
+ "layers::APZCTreeManager::SetAllowedTouchBehavior", this,
+ &APZCTreeManager::SetAllowedTouchBehavior, aInputBlockId,
+ aValues.Clone()));
+ return;
+ }
+
+ APZThreadUtils::AssertOnControllerThread();
+
+ mInputQueue->SetAllowedTouchBehavior(aInputBlockId, aValues);
+}
+
+void APZCTreeManager::SetBrowserGestureResponse(
+ uint64_t aInputBlockId, BrowserGestureResponse aResponse) {
+ if (!APZThreadUtils::IsControllerThread()) {
+ APZThreadUtils::RunOnControllerThread(
+ NewRunnableMethod<uint64_t, BrowserGestureResponse>(
+ "layers::APZCTreeManager::SetBrowserGestureResponse", this,
+ &APZCTreeManager::SetBrowserGestureResponse, aInputBlockId,
+ aResponse));
+ return;
+ }
+
+ APZThreadUtils::AssertOnControllerThread();
+
+ mInputQueue->SetBrowserGestureResponse(aInputBlockId, aResponse);
+}
+
+void APZCTreeManager::UpdateHitTestingTree(
+ const WebRenderScrollDataWrapper& aRoot, bool aIsFirstPaint,
+ LayersId aOriginatingLayersId, uint32_t aPaintSequenceNumber) {
+ AssertOnUpdaterThread();
+
+ RecursiveMutexAutoLock lock(mTreeLock);
+
+ // For testing purposes, we log some data to the APZTestData associated with
+ // the layers id that originated this update.
+ APZTestData* testData = nullptr;
+ if (StaticPrefs::apz_test_logging_enabled()) {
+ MutexAutoLock lock(mTestDataLock);
+ UniquePtr<APZTestData> ptr = MakeUnique<APZTestData>();
+ auto result =
+ mTestData.insert(std::make_pair(aOriginatingLayersId, std::move(ptr)));
+ testData = result.first->second.get();
+ testData->StartNewPaint(aPaintSequenceNumber);
+ }
+
+ TreeBuildingState state(mRootLayersId, aIsFirstPaint, aOriginatingLayersId,
+ testData, aPaintSequenceNumber);
+
+ // We do this business with collecting the entire tree into an array because
+ // otherwise it's very hard to determine which APZC instances need to be
+ // destroyed. In the worst case, there are two scenarios: (a) a layer with an
+ // APZC is removed from the layer tree and (b) a layer with an APZC is moved
+ // in the layer tree from one place to a completely different place. In
+ // scenario (a) we would want to destroy the APZC while walking the layer tree
+ // and noticing that the layer/APZC is no longer there. But if we do that then
+ // we run into a problem in scenario (b) because we might encounter that layer
+ // later during the walk. To handle both of these we have to 'remember' that
+ // the layer was not found, and then do the destroy only at the end of the
+ // tree walk after we are sure that the layer was removed and not just
+ // transplanted elsewhere. Doing that as part of a recursive tree walk is hard
+ // and so maintaining a list and removing APZCs that are still alive is much
+ // simpler.
+ ForEachNode<ReverseIterator>(mRootNode.get(),
+ [&state](HitTestingTreeNode* aNode) {
+ state.mNodesToDestroy.AppendElement(aNode);
+ });
+ mRootNode = nullptr;
+ mAsyncZoomContainerSubtree = Nothing();
+ int asyncZoomContainerNestingDepth = 0;
+ bool haveNestedAsyncZoomContainers = false;
+ nsTArray<LayersId> subtreesWithRootContentOutsideAsyncZoomContainer;
+
+ if (aRoot) {
+ std::unordered_set<LayersId, LayersId::HashFn> seenLayersIds;
+ std::stack<gfx::TreeAutoIndent<gfx::LOG_CRITICAL>> indents;
+ std::stack<AncestorTransform> ancestorTransforms;
+ HitTestingTreeNode* parent = nullptr;
+ HitTestingTreeNode* next = nullptr;
+ LayersId layersId = mRootLayersId;
+ seenLayersIds.insert(mRootLayersId);
+ ancestorTransforms.push(AncestorTransform());
+ state.mOverrideFlags.push(EventRegionsOverride::NoOverride);
+ nsTArray<Maybe<ZoomConstraints>> zoomConstraintsStack;
+
+ // push a nothing to be used for anything outside an async zoom container
+ zoomConstraintsStack.AppendElement(Nothing());
+
+ mApzcTreeLog << "[start]\n";
+ mTreeLock.AssertCurrentThreadIn();
+
+ ForEachNode<ReverseIterator>(
+ aRoot,
+ [&](ScrollNode aLayerMetrics) {
+ if (auto asyncZoomContainerId =
+ aLayerMetrics.GetAsyncZoomContainerId()) {
+ if (asyncZoomContainerNestingDepth > 0) {
+ haveNestedAsyncZoomContainers = true;
+ }
+ mAsyncZoomContainerSubtree = Some(layersId);
+ ++asyncZoomContainerNestingDepth;
+
+ auto it = mZoomConstraints.find(
+ ScrollableLayerGuid(layersId, 0, *asyncZoomContainerId));
+ if (it != mZoomConstraints.end()) {
+ zoomConstraintsStack.AppendElement(Some(it->second));
+ } else {
+ zoomConstraintsStack.AppendElement(Nothing());
+ }
+ }
+
+ if (aLayerMetrics.Metrics().IsRootContent()) {
+ MutexAutoLock lock(mMapLock);
+ mGeckoFixedLayerMargins =
+ aLayerMetrics.Metrics().GetFixedLayerMargins();
+ } else {
+ MOZ_ASSERT(aLayerMetrics.Metrics().GetFixedLayerMargins() ==
+ ScreenMargin(),
+ "fixed-layer-margins should be 0 on non-root layer");
+ }
+
+ // Note that this check happens after the potential increment of
+ // asyncZoomContainerNestingDepth, to allow the root content
+ // metadata to be on the same node as the async zoom container.
+ if (aLayerMetrics.Metrics().IsRootContent() &&
+ asyncZoomContainerNestingDepth == 0) {
+ subtreesWithRootContentOutsideAsyncZoomContainer.AppendElement(
+ layersId);
+ }
+
+ HitTestingTreeNode* node = PrepareNodeForLayer(
+ lock, aLayerMetrics, aLayerMetrics.Metrics(), layersId,
+ zoomConstraintsStack.LastElement(), ancestorTransforms.top(),
+ parent, next, state);
+ MOZ_ASSERT(node);
+ AsyncPanZoomController* apzc = node->GetApzc();
+ aLayerMetrics.SetApzc(apzc);
+
+ // GetScrollbarAnimationId is only set when webrender is enabled,
+ // which limits the extra thumb mapping work to the webrender-enabled
+ // case where it is needed.
+ // Note also that when webrender is enabled, a "valid" animation id
+ // is always nonzero, so we don't need to worry about handling the
+ // case where WR is enabled and the animation id is zero.
+ if (node->GetScrollbarAnimationId()) {
+ if (node->IsScrollThumbNode()) {
+ state.mScrollThumbs.push_back(node);
+ } else if (node->IsScrollbarContainerNode()) {
+ // Only scrollbar containers for the root have an animation id.
+ state.mRootScrollbarInfo.emplace_back(
+ *(node->GetScrollbarAnimationId()),
+ node->GetScrollbarDirection());
+ }
+ }
+
+ // GetFixedPositionAnimationId is only set when webrender is enabled.
+ if (node->GetFixedPositionAnimationId().isSome()) {
+ state.mFixedPositionInfo.emplace_back(node);
+ }
+ // GetStickyPositionAnimationId is only set when webrender is enabled.
+ if (node->GetStickyPositionAnimationId().isSome()) {
+ state.mStickyPositionInfo.emplace_back(node);
+ }
+ if (apzc && node->IsPrimaryHolder()) {
+ state.mScrollTargets[apzc->GetGuid()] = node;
+ }
+
+ // Accumulate the CSS transform between layers that have an APZC.
+ // In the terminology of the big comment above
+ // APZCTreeManager::GetScreenToApzcTransform, if we are at layer M,
+ // then aAncestorTransform is NC * OC * PC, and we left-multiply MC
+ // and compute ancestorTransform to be MC * NC * OC * PC. This gets
+ // passed down as the ancestor transform to layer L when we recurse
+ // into the children below. If we are at a layer with an APZC, such as
+ // P, then we reset the ancestorTransform to just PC, to start the new
+ // accumulation as we go down.
+ AncestorTransform currentTransform{
+ aLayerMetrics.GetTransform(),
+ aLayerMetrics.TransformIsPerspective()};
+ if (!apzc) {
+ currentTransform = currentTransform * ancestorTransforms.top();
+ }
+ ancestorTransforms.push(currentTransform);
+
+ // Note that |node| at this point will not have any children,
+ // otherwise we we would have to set next to node->GetFirstChild().
+ MOZ_ASSERT(!node->GetFirstChild());
+ parent = node;
+ next = nullptr;
+
+ // Update the layersId if we have a new one
+ if (Maybe<LayersId> newLayersId = aLayerMetrics.GetReferentId()) {
+ layersId = *newLayersId;
+ seenLayersIds.insert(layersId);
+
+ // Propagate any event region override flags down into all
+ // descendant nodes from the reflayer that has the flag. This is an
+ // optimization to avoid having to walk up the tree to check the
+ // override flags. Note that we don't keep the flags on the reflayer
+ // itself, because the semantics of the flags are that they apply
+ // to all content in the layer subtree being referenced. This
+ // matters with the WR hit-test codepath, because this reflayer may
+ // be just one of many nodes associated with a particular APZC, and
+ // calling GetTargetNode with a guid may return any one of the
+ // nodes. If different nodes have different flags on them that can
+ // make the WR hit-test result incorrect, but being strict about
+ // only putting the flags on descendant layers avoids this problem.
+ state.mOverrideFlags.push(state.mOverrideFlags.top() |
+ aLayerMetrics.GetEventRegionsOverride());
+ }
+
+ indents.push(gfx::TreeAutoIndent<gfx::LOG_CRITICAL>(mApzcTreeLog));
+ },
+ [&](ScrollNode aLayerMetrics) {
+ if (aLayerMetrics.GetAsyncZoomContainerId()) {
+ --asyncZoomContainerNestingDepth;
+ zoomConstraintsStack.RemoveLastElement();
+ }
+ if (aLayerMetrics.GetReferentId()) {
+ state.mOverrideFlags.pop();
+ }
+
+ next = parent;
+ parent = parent->GetParent();
+ layersId = next->GetLayersId();
+ ancestorTransforms.pop();
+ indents.pop();
+ });
+
+ mApzcTreeLog << "[end]\n";
+
+ MOZ_ASSERT(
+ !mAsyncZoomContainerSubtree ||
+ !subtreesWithRootContentOutsideAsyncZoomContainer.Contains(
+ *mAsyncZoomContainerSubtree),
+ "If there is an async zoom container, all scroll nodes with root "
+ "content scroll metadata should be inside it");
+ MOZ_ASSERT(!haveNestedAsyncZoomContainers,
+ "Should not have nested async zoom container");
+
+ // If we have perspective transforms deferred to children, do another
+ // walk of the tree and actually apply them to the children.
+ // We can't do this "as we go" in the previous traversal, because by the
+ // time we realize we need to defer a perspective transform for an APZC,
+ // we may already have processed a previous layer (including children
+ // found in its subtree) that shares that APZC.
+ if (!state.mPerspectiveTransformsDeferredToChildren.empty()) {
+ ForEachNode<ReverseIterator>(
+ mRootNode.get(), [&state](HitTestingTreeNode* aNode) {
+ AsyncPanZoomController* apzc = aNode->GetApzc();
+ if (!apzc) {
+ return;
+ }
+ if (!aNode->IsPrimaryHolder()) {
+ return;
+ }
+
+ AsyncPanZoomController* parent = apzc->GetParent();
+ if (!parent) {
+ return;
+ }
+
+ auto it =
+ state.mPerspectiveTransformsDeferredToChildren.find(parent);
+ if (it != state.mPerspectiveTransformsDeferredToChildren.end()) {
+ apzc->SetAncestorTransform(AncestorTransform{
+ it->second * apzc->GetAncestorTransform(), false});
+ }
+ });
+ }
+
+ // Remove any layers ids for which we no longer have content from
+ // mDetachedLayersIds.
+ for (auto iter = mDetachedLayersIds.begin();
+ iter != mDetachedLayersIds.end();) {
+ // unordered_set::erase() invalidates the iterator pointing to the
+ // element being erased, but returns an iterator to the next element.
+ if (seenLayersIds.find(*iter) == seenLayersIds.end()) {
+ iter = mDetachedLayersIds.erase(iter);
+ } else {
+ ++iter;
+ }
+ }
+ }
+
+ // We do not support tree structures where the root node has siblings.
+ MOZ_ASSERT(!(mRootNode && mRootNode->GetPrevSibling()));
+
+ { // scope lock and update our mApzcMap before we destroy all the unused
+ // APZC instances
+ MutexAutoLock lock(mMapLock);
+ mApzcMap = std::move(state.mApzcMap);
+
+ for (auto& mapping : mApzcMap) {
+ AsyncPanZoomController* parent = mapping.second.apzc->GetParent();
+ mapping.second.parent = parent ? Some(parent->GetGuid()) : Nothing();
+ }
+
+ mScrollThumbInfo.clear();
+ // For non-webrender, state.mScrollThumbs will be empty so this will be a
+ // no-op.
+ for (HitTestingTreeNode* thumb : state.mScrollThumbs) {
+ MOZ_ASSERT(thumb->IsScrollThumbNode());
+ ScrollableLayerGuid targetGuid(thumb->GetLayersId(), 0,
+ thumb->GetScrollTargetId());
+ auto it = state.mScrollTargets.find(targetGuid);
+ if (it == state.mScrollTargets.end()) {
+ // It could be that |thumb| is a scrollthumb for content which didn't
+ // have an APZC, for example if the content isn't layerized. Regardless,
+ // we can't async-scroll it so we don't need to worry about putting it
+ // in mScrollThumbInfo.
+ continue;
+ }
+ HitTestingTreeNode* target = it->second;
+ mScrollThumbInfo.emplace_back(
+ *(thumb->GetScrollbarAnimationId()), thumb->GetTransform(),
+ thumb->GetScrollbarData(), targetGuid, target->GetTransform(),
+ target->IsAncestorOf(thumb));
+ }
+
+ mRootScrollbarInfo = std::move(state.mRootScrollbarInfo);
+ mFixedPositionInfo = std::move(state.mFixedPositionInfo);
+ mStickyPositionInfo = std::move(state.mStickyPositionInfo);
+ }
+
+ for (size_t i = 0; i < state.mNodesToDestroy.Length(); i++) {
+ APZCTM_LOG("Destroying node at %p with APZC %p\n",
+ state.mNodesToDestroy[i].get(),
+ state.mNodesToDestroy[i]->GetApzc());
+ state.mNodesToDestroy[i]->Destroy();
+ }
+
+ APZCTM_LOG("APZCTreeManager (%p)\n", this);
+ if (mRootNode && MOZ_LOG_TEST(sLog, LogLevel::Debug)) {
+ mRootNode->Dump(" ");
+ }
+ SendSubtreeTransformsToChromeMainThread(nullptr);
+}
+
+void APZCTreeManager::UpdateFocusState(LayersId aRootLayerTreeId,
+ LayersId aOriginatingLayersId,
+ const FocusTarget& aFocusTarget) {
+ AssertOnUpdaterThread();
+
+ if (!StaticPrefs::apz_keyboard_enabled_AtStartup()) {
+ return;
+ }
+
+ mFocusState.Update(aRootLayerTreeId, aOriginatingLayersId, aFocusTarget);
+}
+
+void APZCTreeManager::SampleForWebRender(const Maybe<VsyncId>& aVsyncId,
+ wr::TransactionWrapper& aTxn,
+ const SampleTime& aSampleTime) {
+ AssertOnSamplerThread();
+ MutexAutoLock lock(mMapLock);
+
+ RefPtr<WebRenderBridgeParent> wrBridgeParent;
+ RefPtr<CompositorController> controller;
+ CompositorBridgeParent::CallWithIndirectShadowTree(
+ mRootLayersId, [&](LayerTreeState& aState) -> void {
+ controller = aState.GetCompositorController();
+ wrBridgeParent = aState.mWrBridge;
+ });
+
+ bool activeAnimations = AdvanceAnimationsInternal(lock, aSampleTime);
+ if (activeAnimations && controller) {
+ controller->ScheduleRenderOnCompositorThread(
+ wr::RenderReasons::ANIMATED_PROPERTY);
+ }
+
+ nsTArray<wr::WrTransformProperty> transforms;
+
+ // Sample async transforms on scrollable layers.
+ for (const auto& mapping : mApzcMap) {
+ AsyncPanZoomController* apzc = mapping.second.apzc;
+
+ if (Maybe<CompositionPayload> payload = apzc->NotifyScrollSampling()) {
+ if (wrBridgeParent && aVsyncId) {
+ wrBridgeParent->AddPendingScrollPayload(*payload, *aVsyncId);
+ }
+ }
+
+ if (StaticPrefs::apz_test_logging_enabled()) {
+ MutexAutoLock lock(mTestDataLock);
+
+ ScrollableLayerGuid guid = apzc->GetGuid();
+ auto it = mTestData.find(guid.mLayersId);
+ if (it != mTestData.end()) {
+ it->second->RecordSampledResult(
+ apzc->GetCurrentAsyncVisualViewport(
+ AsyncPanZoomController::eForCompositing)
+ .TopLeft(),
+ (aSampleTime.Time() - TimeStamp::ProcessCreation())
+ .ToMicroseconds(),
+ guid.mLayersId, guid.mScrollId);
+ }
+ }
+
+ if (Maybe<uint64_t> zoomAnimationId = apzc->GetZoomAnimationId()) {
+ // for now we only support zooming on root content APZCs
+ MOZ_ASSERT(apzc->IsRootContent());
+
+ LayoutDeviceToParentLayerScale zoom = apzc->GetCurrentPinchZoomScale(
+ AsyncPanZoomController::eForCompositing);
+
+ AsyncTransform asyncVisualTransform = apzc->GetCurrentAsyncTransform(
+ AsyncPanZoomController::eForCompositing,
+ AsyncTransformComponents{AsyncTransformComponent::eVisual});
+
+ transforms.AppendElement(wr::ToWrTransformProperty(
+ *zoomAnimationId, LayoutDeviceToParentLayerMatrix4x4::Scaling(
+ zoom.scale, zoom.scale, 1.0f) *
+ AsyncTransformComponentMatrix::Translation(
+ asyncVisualTransform.mTranslation)));
+
+ aTxn.UpdateIsTransformAsyncZooming(*zoomAnimationId,
+ apzc->IsAsyncZooming());
+ }
+
+ nsTArray<wr::SampledScrollOffset> sampledOffsets =
+ apzc->GetSampledScrollOffsets();
+ aTxn.UpdateScrollPosition(wr::AsPipelineId(apzc->GetGuid().mLayersId),
+ apzc->GetGuid().mScrollId, sampledOffsets);
+
+#if defined(MOZ_WIDGET_ANDROID)
+ // Send the root frame metrics to java through the UIController
+ RefPtr<UiCompositorControllerParent> uiController =
+ UiCompositorControllerParent::GetFromRootLayerTreeId(mRootLayersId);
+ if (uiController &&
+ apzc->UpdateRootFrameMetricsIfChanged(mLastRootMetrics)) {
+ uiController->NotifyUpdateScreenMetrics(mLastRootMetrics);
+ }
+#endif
+ }
+
+ // Now collect all the async transforms needed for the scrollthumbs.
+ for (const ScrollThumbInfo& info : mScrollThumbInfo) {
+ auto it = mApzcMap.find(info.mTargetGuid);
+ if (it == mApzcMap.end()) {
+ // It could be that |info| is a scrollthumb for content which didn't
+ // have an APZC, for example if the content isn't layerized. Regardless,
+ // we can't async-scroll it so we don't need to worry about putting it
+ // in mScrollThumbInfo.
+ continue;
+ }
+ AsyncPanZoomController* scrollTargetApzc = it->second.apzc;
+ MOZ_ASSERT(scrollTargetApzc);
+ LayerToParentLayerMatrix4x4 transform =
+ scrollTargetApzc->CallWithLastContentPaintMetrics(
+ [&](const FrameMetrics& aMetrics) {
+ return ComputeTransformForScrollThumb(
+ info.mThumbTransform * AsyncTransformMatrix(),
+ info.mTargetTransform.ToUnknownMatrix(), scrollTargetApzc,
+ aMetrics, info.mThumbData, info.mTargetIsAncestor);
+ });
+ transforms.AppendElement(
+ wr::ToWrTransformProperty(info.mThumbAnimationId, transform));
+ }
+
+ // Move the root scrollbar in response to the dynamic toolbar transition.
+ for (const RootScrollbarInfo& info : mRootScrollbarInfo) {
+ // We only care about the horizontal scrollbar.
+ if (info.mScrollDirection == ScrollDirection::eHorizontal) {
+ ScreenPoint translation =
+ apz::ComputeFixedMarginsOffset(GetCompositorFixedLayerMargins(lock),
+ SideBits::eBottom, ScreenMargin());
+
+ LayerToParentLayerMatrix4x4 transform =
+ LayerToParentLayerMatrix4x4::Translation(ViewAs<ParentLayerPixel>(
+ translation, PixelCastJustification::ScreenIsParentLayerForRoot));
+
+ transforms.AppendElement(
+ wr::ToWrTransformProperty(info.mScrollbarAnimationId, transform));
+ }
+ }
+
+ for (const FixedPositionInfo& info : mFixedPositionInfo) {
+ MOZ_ASSERT(info.mFixedPositionAnimationId.isSome());
+ if (!IsFixedToRootContent(info, lock)) {
+ continue;
+ }
+
+ ScreenPoint translation = apz::ComputeFixedMarginsOffset(
+ GetCompositorFixedLayerMargins(lock), info.mFixedPosSides,
+ mGeckoFixedLayerMargins);
+
+ LayerToParentLayerMatrix4x4 transform =
+ LayerToParentLayerMatrix4x4::Translation(ViewAs<ParentLayerPixel>(
+ translation, PixelCastJustification::ScreenIsParentLayerForRoot));
+
+ transforms.AppendElement(
+ wr::ToWrTransformProperty(*info.mFixedPositionAnimationId, transform));
+ }
+
+ for (const StickyPositionInfo& info : mStickyPositionInfo) {
+ MOZ_ASSERT(info.mStickyPositionAnimationId.isSome());
+ SideBits sides = SidesStuckToRootContent(info, lock);
+ if (sides == SideBits::eNone) {
+ continue;
+ }
+
+ ScreenPoint translation = apz::ComputeFixedMarginsOffset(
+ GetCompositorFixedLayerMargins(lock), sides,
+ // For sticky layers, we don't need to factor
+ // mGeckoFixedLayerMargins because Gecko doesn't shift the
+ // position of sticky elements for dynamic toolbar movements.
+ ScreenMargin());
+
+ LayerToParentLayerMatrix4x4 transform =
+ LayerToParentLayerMatrix4x4::Translation(ViewAs<ParentLayerPixel>(
+ translation, PixelCastJustification::ScreenIsParentLayerForRoot));
+
+ transforms.AppendElement(
+ wr::ToWrTransformProperty(*info.mStickyPositionAnimationId, transform));
+ }
+
+ aTxn.AppendTransformProperties(transforms);
+}
+
+ParentLayerRect APZCTreeManager::ComputeClippedCompositionBounds(
+ const MutexAutoLock& aProofOfMapLock, ClippedCompositionBoundsMap& aDestMap,
+ ScrollableLayerGuid aGuid) {
+ if (auto iter = aDestMap.find(aGuid); iter != aDestMap.end()) {
+ // We already computed it for this one, early-exit. This might happen
+ // because on a later iteration of mApzcMap we might encounter an ancestor
+ // of an APZC that we processed on an earlier iteration. In this case we
+ // would have computed the ancestor's clipped composition bounds when
+ // recursing up on the earlier iteration.
+ return iter->second;
+ }
+
+ ParentLayerRect bounds = mApzcMap[aGuid].apzc->GetCompositionBounds();
+ const auto& mapEntry = mApzcMap.find(aGuid);
+ MOZ_ASSERT(mapEntry != mApzcMap.end());
+ if (mapEntry->second.parent.isNothing()) {
+ // Recursion base case, where the APZC with guid `aGuid` has no parent.
+ // In this case, we don't need to clip `bounds` any further and can just
+ // early exit.
+ aDestMap.emplace(aGuid, bounds);
+ return bounds;
+ }
+
+ ScrollableLayerGuid parentGuid = mapEntry->second.parent.value();
+ auto parentBoundsEntry = aDestMap.find(parentGuid);
+ // If aDestMap doesn't contain the parent entry yet, we recurse to compute
+ // that one first.
+ ParentLayerRect parentClippedBounds =
+ (parentBoundsEntry == aDestMap.end())
+ ? ComputeClippedCompositionBounds(aProofOfMapLock, aDestMap,
+ parentGuid)
+ : parentBoundsEntry->second;
+
+ // The parent layer's async transform applies to the current layer to take
+ // `bounds` into the same coordinate space as `parentClippedBounds`. However,
+ // we're going to do the inverse operation and unapply this transform to
+ // `parentClippedBounds` to bring it into the same coordinate space as
+ // `bounds`.
+ AsyncTransform appliesToLayer =
+ mApzcMap[parentGuid].apzc->GetCurrentAsyncTransform(
+ AsyncPanZoomController::eForCompositing);
+
+ // Do the unapplication
+ LayerRect parentClippedBoundsInParentLayerSpace =
+ (parentClippedBounds - appliesToLayer.mTranslation) /
+ appliesToLayer.mScale;
+
+ // And then clip `bounds` by the parent's comp bounds in the current space.
+ bounds = bounds.Intersect(
+ ViewAs<ParentLayerPixel>(parentClippedBoundsInParentLayerSpace,
+ PixelCastJustification::MovingDownToChildren));
+
+ // Done!
+ aDestMap.emplace(aGuid, bounds);
+ return bounds;
+}
+
+bool APZCTreeManager::AdvanceAnimationsInternal(
+ const MutexAutoLock& aProofOfMapLock, const SampleTime& aSampleTime) {
+ ClippedCompositionBoundsMap clippedCompBounds;
+ bool activeAnimations = false;
+ for (const auto& mapping : mApzcMap) {
+ AsyncPanZoomController* apzc = mapping.second.apzc;
+ // Note that this call is recursive, but it early-exits if called again
+ // with the same guid. So this loop is still amortized O(n) with respect to
+ // the number of APZCs.
+ ParentLayerRect clippedBounds = ComputeClippedCompositionBounds(
+ aProofOfMapLock, clippedCompBounds, mapping.first);
+
+ apzc->ReportCheckerboard(aSampleTime, clippedBounds);
+ activeAnimations |= apzc->AdvanceAnimations(aSampleTime);
+ }
+ return activeAnimations;
+}
+
+void APZCTreeManager::PrintLayerInfo(const ScrollNode& aLayer) {
+ if (StaticPrefs::apz_printtree() && aLayer.Dump(mApzcTreeLog) > 0) {
+ mApzcTreeLog << "\n";
+ }
+}
+
+// mTreeLock is held, and checked with static analysis
+void APZCTreeManager::AttachNodeToTree(HitTestingTreeNode* aNode,
+ HitTestingTreeNode* aParent,
+ HitTestingTreeNode* aNextSibling) {
+ if (aNextSibling) {
+ aNextSibling->SetPrevSibling(aNode);
+ } else if (aParent) {
+ aParent->SetLastChild(aNode);
+ } else {
+ MOZ_ASSERT(!mRootNode);
+ mRootNode = aNode;
+ aNode->MakeRoot();
+ }
+}
+
+already_AddRefed<HitTestingTreeNode> APZCTreeManager::RecycleOrCreateNode(
+ const RecursiveMutexAutoLock& aProofOfTreeLock, TreeBuildingState& aState,
+ AsyncPanZoomController* aApzc, LayersId aLayersId) {
+ // Find a node without an APZC and return it. Note that unless the layer tree
+ // actually changes, this loop should generally do an early-return on the
+ // first iteration, so it should be cheap in the common case.
+ for (int32_t i = aState.mNodesToDestroy.Length() - 1; i >= 0; i--) {
+ RefPtr<HitTestingTreeNode> node = aState.mNodesToDestroy[i];
+ if (node->IsRecyclable(aProofOfTreeLock)) {
+ aState.mNodesToDestroy.RemoveElementAt(i);
+ node->RecycleWith(aProofOfTreeLock, aApzc, aLayersId);
+ return node.forget();
+ }
+ }
+ RefPtr<HitTestingTreeNode> node =
+ new HitTestingTreeNode(aApzc, false, aLayersId);
+ return node.forget();
+}
+
+void APZCTreeManager::StartScrollbarDrag(const ScrollableLayerGuid& aGuid,
+ const AsyncDragMetrics& aDragMetrics) {
+ if (!APZThreadUtils::IsControllerThread()) {
+ APZThreadUtils::RunOnControllerThread(
+ NewRunnableMethod<ScrollableLayerGuid, AsyncDragMetrics>(
+ "layers::APZCTreeManager::StartScrollbarDrag", this,
+ &APZCTreeManager::StartScrollbarDrag, aGuid, aDragMetrics));
+ return;
+ }
+
+ APZThreadUtils::AssertOnControllerThread();
+
+ RefPtr<AsyncPanZoomController> apzc = GetTargetAPZC(aGuid);
+ if (!apzc) {
+ NotifyScrollbarDragRejected(aGuid);
+ return;
+ }
+
+ uint64_t inputBlockId = aDragMetrics.mDragStartSequenceNumber;
+ mInputQueue->ConfirmDragBlock(inputBlockId, apzc, aDragMetrics);
+}
+
+bool APZCTreeManager::StartAutoscroll(const ScrollableLayerGuid& aGuid,
+ const ScreenPoint& aAnchorLocation) {
+ APZThreadUtils::AssertOnControllerThread();
+
+ RefPtr<AsyncPanZoomController> apzc = GetTargetAPZC(aGuid);
+ if (!apzc) {
+ if (XRE_IsGPUProcess()) {
+ // If we're in the compositor process, the "return false" will be
+ // ignored because the query comes over the PAPZCTreeManager protocol
+ // via an async message. In this case, send an explicit rejection
+ // message to content.
+ NotifyAutoscrollRejected(aGuid);
+ }
+ return false;
+ }
+
+ apzc->StartAutoscroll(aAnchorLocation);
+ return true;
+}
+
+void APZCTreeManager::StopAutoscroll(const ScrollableLayerGuid& aGuid) {
+ APZThreadUtils::AssertOnControllerThread();
+
+ if (RefPtr<AsyncPanZoomController> apzc = GetTargetAPZC(aGuid)) {
+ apzc->StopAutoscroll();
+ }
+}
+
+void APZCTreeManager::NotifyScrollbarDragInitiated(
+ uint64_t aDragBlockId, const ScrollableLayerGuid& aGuid,
+ ScrollDirection aDirection) const {
+ RefPtr<GeckoContentController> controller =
+ GetContentController(aGuid.mLayersId);
+ if (controller) {
+ controller->NotifyAsyncScrollbarDragInitiated(aDragBlockId, aGuid.mScrollId,
+ aDirection);
+ }
+}
+
+void APZCTreeManager::NotifyScrollbarDragRejected(
+ const ScrollableLayerGuid& aGuid) const {
+ RefPtr<GeckoContentController> controller =
+ GetContentController(aGuid.mLayersId);
+ if (controller) {
+ controller->NotifyAsyncScrollbarDragRejected(aGuid.mScrollId);
+ }
+}
+
+void APZCTreeManager::NotifyAutoscrollRejected(
+ const ScrollableLayerGuid& aGuid) const {
+ RefPtr<GeckoContentController> controller =
+ GetContentController(aGuid.mLayersId);
+ MOZ_ASSERT(controller);
+ controller->NotifyAsyncAutoscrollRejected(aGuid.mScrollId);
+}
+
+void SetHitTestData(HitTestingTreeNode* aNode,
+ const WebRenderScrollDataWrapper& aLayer,
+ const EventRegionsOverride& aOverrideFlags) {
+ aNode->SetHitTestData(aLayer.GetVisibleRegion(),
+ aLayer.GetRemoteDocumentSize(),
+ aLayer.GetTransformTyped(), aOverrideFlags,
+ aLayer.GetAsyncZoomContainerId());
+}
+
+HitTestingTreeNode* APZCTreeManager::PrepareNodeForLayer(
+ const RecursiveMutexAutoLock& aProofOfTreeLock, const ScrollNode& aLayer,
+ const FrameMetrics& aMetrics, LayersId aLayersId,
+ const Maybe<ZoomConstraints>& aZoomConstraints,
+ const AncestorTransform& aAncestorTransform, HitTestingTreeNode* aParent,
+ HitTestingTreeNode* aNextSibling, TreeBuildingState& aState) {
+ mTreeLock.AssertCurrentThreadIn(); // for static analysis
+ bool needsApzc = true;
+ if (!aMetrics.IsScrollable()) {
+ needsApzc = false;
+ }
+
+ // XXX: As a future optimization we can probably stick these things on the
+ // TreeBuildingState, and update them as we change layers id during the
+ // traversal
+ RefPtr<GeckoContentController> geckoContentController;
+ CompositorBridgeParent::CallWithIndirectShadowTree(
+ aLayersId, [&](LayerTreeState& lts) -> void {
+ geckoContentController = lts.mController;
+ });
+
+ if (!geckoContentController) {
+ needsApzc = false;
+ }
+
+ if (Maybe<uint64_t> zoomAnimationId = aLayer.GetZoomAnimationId()) {
+ aState.mZoomAnimationId = zoomAnimationId;
+ }
+
+ RefPtr<HitTestingTreeNode> node = nullptr;
+ if (!needsApzc) {
+ // Note: if layer properties must be propagated to nodes, RecvUpdate in
+ // LayerTransactionParent.cpp must ensure that APZ will be notified
+ // when those properties change.
+ node = RecycleOrCreateNode(aProofOfTreeLock, aState, nullptr, aLayersId);
+ AttachNodeToTree(node, aParent, aNextSibling);
+ SetHitTestData(node, aLayer, aState.mOverrideFlags.top());
+ node->SetScrollbarData(aLayer.GetScrollbarAnimationId(),
+ aLayer.GetScrollbarData());
+ node->SetFixedPosData(aLayer.GetFixedPositionScrollContainerId(),
+ aLayer.GetFixedPositionSides(),
+ aLayer.GetFixedPositionAnimationId());
+ node->SetStickyPosData(aLayer.GetStickyScrollContainerId(),
+ aLayer.GetStickyScrollRangeOuter(),
+ aLayer.GetStickyScrollRangeInner(),
+ aLayer.GetStickyPositionAnimationId());
+ PrintLayerInfo(aLayer);
+ return node;
+ }
+
+ AsyncPanZoomController* apzc = nullptr;
+ // If we get here, aLayer is a scrollable layer and somebody
+ // has registered a GeckoContentController for it, so we need to ensure
+ // it has an APZC instance to manage its scrolling.
+
+ // aState.mApzcMap allows reusing the exact same APZC instance for different
+ // layers with the same FrameMetrics data. This is needed because in some
+ // cases content that is supposed to scroll together is split into multiple
+ // layers because of e.g. non-scrolling content interleaved in z-index order.
+ ScrollableLayerGuid guid(aLayersId, aMetrics.GetPresShellId(),
+ aMetrics.GetScrollId());
+ auto insertResult = aState.mApzcMap.insert(std::make_pair(
+ guid,
+ ApzcMapData{static_cast<AsyncPanZoomController*>(nullptr), Nothing()}));
+ if (!insertResult.second) {
+ apzc = insertResult.first->second.apzc;
+ PrintLayerInfo(aLayer);
+ }
+ APZCTM_LOG("Found APZC %p for layer %p with identifiers %" PRIx64 " %" PRId64
+ "\n",
+ apzc, aLayer.GetLayer(), uint64_t(guid.mLayersId), guid.mScrollId);
+
+ // If we haven't encountered a layer already with the same metrics, then we
+ // need to do the full reuse-or-make-an-APZC algorithm, which is contained
+ // inside the block below.
+ if (apzc == nullptr) {
+ apzc = aLayer.GetApzc();
+
+ // If the content represented by the scrollable layer has changed (which may
+ // be possible because of DLBI heuristics) then we don't want to keep using
+ // the same old APZC for the new content. Also, when reparenting a tab into
+ // a new window a layer might get moved to a different layer tree with a
+ // different APZCTreeManager. In these cases we don't want to reuse the same
+ // APZC, so null it out so we run through the code to find another one or
+ // create one.
+ if (apzc && (!apzc->Matches(guid) || !apzc->HasTreeManager(this))) {
+ apzc = nullptr;
+ }
+
+ // See if we can find an APZC from the previous tree that matches the
+ // ScrollableLayerGuid from this layer. If there is one, then we know that
+ // the layout of the page changed causing the layer tree to be rebuilt, but
+ // the underlying content for the APZC is still there somewhere. Therefore,
+ // we want to find the APZC instance and continue using it here.
+ //
+ // We particularly want to find the primary-holder node from the previous
+ // tree that matches, because we don't want that node to get destroyed. If
+ // it does get destroyed, then the APZC will get destroyed along with it by
+ // definition, but we want to keep that APZC around in the new tree.
+ // We leave non-primary-holder nodes in the destroy list because we don't
+ // care about those nodes getting destroyed.
+ for (size_t i = 0; i < aState.mNodesToDestroy.Length(); i++) {
+ RefPtr<HitTestingTreeNode> n = aState.mNodesToDestroy[i];
+ if (n->IsPrimaryHolder() && n->GetApzc() && n->GetApzc()->Matches(guid)) {
+ node = n;
+ if (apzc != nullptr) {
+ // If there is an APZC already then it should match the one from the
+ // old primary-holder node
+ MOZ_ASSERT(apzc == node->GetApzc());
+ }
+ apzc = node->GetApzc();
+ break;
+ }
+ }
+
+ // The APZC we get off the layer may have been destroyed previously if the
+ // layer was inactive or omitted from the layer tree for whatever reason
+ // from a layers update. If it later comes back it will have a reference to
+ // a destroyed APZC and so we need to throw that out and make a new one.
+ bool newApzc = (apzc == nullptr || apzc->IsDestroyed());
+ if (newApzc) {
+ apzc = NewAPZCInstance(aLayersId, geckoContentController);
+ apzc->SetCompositorController(aState.mCompositorController.get());
+ MOZ_ASSERT(node == nullptr);
+ node = new HitTestingTreeNode(apzc, true, aLayersId);
+ } else {
+ // If we are re-using a node for this layer clear the tree pointers
+ // so that it doesn't continue pointing to nodes that might no longer
+ // be in the tree. These pointers will get reset properly as we continue
+ // building the tree. Also remove it from the set of nodes that are going
+ // to be destroyed, because it's going to remain active.
+ aState.mNodesToDestroy.RemoveElement(node);
+ node->SetPrevSibling(nullptr);
+ node->SetLastChild(nullptr);
+ }
+
+ if (aMetrics.IsRootContent()) {
+ apzc->SetZoomAnimationId(aState.mZoomAnimationId);
+ aState.mZoomAnimationId = Nothing();
+ }
+
+ APZCTM_LOG(
+ "Using APZC %p for layer %p with identifiers %" PRIx64 " %" PRId64 "\n",
+ apzc, aLayer.GetLayer(), uint64_t(aLayersId), aMetrics.GetScrollId());
+
+ apzc->NotifyLayersUpdated(aLayer.Metadata(), aState.mIsFirstPaint,
+ aLayersId == aState.mOriginatingLayersId);
+
+ // Since this is the first time we are encountering an APZC with this guid,
+ // the node holding it must be the primary holder. It may be newly-created
+ // or not, depending on whether it went through the newApzc branch above.
+ MOZ_ASSERT(node->IsPrimaryHolder() && node->GetApzc() &&
+ node->GetApzc()->Matches(guid));
+
+ SetHitTestData(node, aLayer, aState.mOverrideFlags.top());
+ apzc->SetAncestorTransform(aAncestorTransform);
+
+ PrintLayerInfo(aLayer);
+
+ // Bind the APZC instance into the tree of APZCs
+ AttachNodeToTree(node, aParent, aNextSibling);
+
+ // For testing, log the parent scroll id of every APZC that has a
+ // parent. This allows test code to reconstruct the APZC tree.
+ // Note that we currently only do this for APZCs in the layer tree
+ // that originated the update, because the only identifying information
+ // we are logging about APZCs is the scroll id, and otherwise we could
+ // confuse APZCs from different layer trees with the same scroll id.
+ if (aLayersId == aState.mOriginatingLayersId) {
+ if (apzc->HasNoParentWithSameLayersId()) {
+ aState.mPaintLogger.LogTestData(aMetrics.GetScrollId(),
+ "hasNoParentWithSameLayersId", true);
+ } else {
+ MOZ_ASSERT(apzc->GetParent());
+ aState.mPaintLogger.LogTestData(aMetrics.GetScrollId(),
+ "parentScrollId",
+ apzc->GetParent()->GetGuid().mScrollId);
+ }
+ if (aMetrics.IsRootContent()) {
+ aState.mPaintLogger.LogTestData(aMetrics.GetScrollId(), "isRootContent",
+ true);
+ }
+ // Note that the async scroll offset is in ParentLayer pixels
+ aState.mPaintLogger.LogTestData(
+ aMetrics.GetScrollId(), "asyncScrollOffset",
+ apzc->GetCurrentAsyncScrollOffset(
+ AsyncPanZoomController::eForHitTesting));
+ aState.mPaintLogger.LogTestData(aMetrics.GetScrollId(),
+ "hasAsyncKeyScrolled",
+ apzc->TestHasAsyncKeyScrolled());
+ }
+
+ // We must update the zoom constraints even if the apzc isn't new because it
+ // might have moved.
+ if (node->IsPrimaryHolder()) {
+ if (aZoomConstraints) {
+ apzc->UpdateZoomConstraints(*aZoomConstraints);
+
+#ifdef DEBUG
+ auto it = mZoomConstraints.find(guid);
+ if (it != mZoomConstraints.end()) {
+ MOZ_ASSERT(it->second == *aZoomConstraints);
+ }
+ } else {
+ // We'd like to assert these things (if the first doesn't hold then at
+ // least the second) but neither are not true because xul root content
+ // gets zoomable zoom constraints, but which is not zoomable because it
+ // doesn't have a root scroll frame.
+ // clang-format off
+ // MOZ_ASSERT(mZoomConstraints.find(guid) == mZoomConstraints.end());
+ // auto it = mZoomConstraints.find(guid);
+ // if (it != mZoomConstraints.end()) {
+ // MOZ_ASSERT(!it->second.mAllowZoom && !it->second.mAllowDoubleTapZoom);
+ // }
+ // clang-format on
+#endif
+ }
+ }
+
+ // Add a guid -> APZC mapping for the newly created APZC.
+ insertResult.first->second.apzc = apzc;
+ } else {
+ // We already built an APZC earlier in this tree walk, but we have another
+ // layer now that will also be using that APZC. The hit-test region on the
+ // APZC needs to be updated to deal with the new layer's hit region.
+
+ node = RecycleOrCreateNode(aProofOfTreeLock, aState, apzc, aLayersId);
+ AttachNodeToTree(node, aParent, aNextSibling);
+
+ // Even though different layers associated with a given APZC may be at
+ // different levels in the layer tree (e.g. one being an uncle of another),
+ // we require from Layout that the CSS transforms up to their common
+ // ancestor be roughly the same. There are cases in which the transforms
+ // are not exactly the same, for example if the parent is container layer
+ // for an opacity, and this container layer has a resolution-induced scale
+ // as its base transform and a prescale that is supposed to undo that scale.
+ // Due to floating point inaccuracies those transforms can end up not quite
+ // canceling each other. That's why we're using a fuzzy comparison here
+ // instead of an exact one.
+ // In addition, two ancestor transforms are allowed to differ if one of
+ // them contains a perspective transform component and the other does not.
+ // This represents situations where some content in a scrollable frame
+ // is subject to a perspective transform and other content does not.
+ // In such cases, go with the one that does not include the perspective
+ // component; the perspective transform is remembered and applied to the
+ // children instead.
+ auto ancestorTransform = aAncestorTransform.CombinedTransform();
+ auto existingAncestorTransform = apzc->GetAncestorTransform();
+ if (!ancestorTransform.FuzzyEqualsMultiplicative(
+ existingAncestorTransform)) {
+ typedef TreeBuildingState::DeferredTransformMap::value_type PairType;
+ if (!aAncestorTransform.ContainsPerspectiveTransform() &&
+ !apzc->AncestorTransformContainsPerspective()) {
+ // If this content is being presented in a paginated fashion (e.g.
+ // print preview), the multiple layers may reflect multiple instances
+ // of the same display item being rendered on different pages. In such
+ // cases, it's expected that different instances can have different
+ // transforms, since each page renders a different part of the item.
+ if (!aLayer.Metadata().IsPaginatedPresentation()) {
+ if (ancestorTransform.IsFinite() &&
+ existingAncestorTransform.IsFinite()) {
+ MOZ_ASSERT(
+ false,
+ "Two layers that scroll together have different ancestor "
+ "transforms");
+ } else {
+ MOZ_ASSERT(ancestorTransform.IsFinite() ==
+ existingAncestorTransform.IsFinite());
+ }
+ }
+ } else if (!aAncestorTransform.ContainsPerspectiveTransform()) {
+ aState.mPerspectiveTransformsDeferredToChildren.insert(
+ PairType{apzc, apzc->GetAncestorTransformPerspective()});
+ apzc->SetAncestorTransform(aAncestorTransform);
+ } else {
+ aState.mPerspectiveTransformsDeferredToChildren.insert(
+ PairType{apzc, aAncestorTransform.GetPerspectiveTransform()});
+ }
+ }
+
+ SetHitTestData(node, aLayer, aState.mOverrideFlags.top());
+ }
+
+ // Note: if layer properties must be propagated to nodes, RecvUpdate in
+ // LayerTransactionParent.cpp must ensure that APZ will be notified
+ // when those properties change.
+ node->SetScrollbarData(aLayer.GetScrollbarAnimationId(),
+ aLayer.GetScrollbarData());
+ node->SetFixedPosData(aLayer.GetFixedPositionScrollContainerId(),
+ aLayer.GetFixedPositionSides(),
+ aLayer.GetFixedPositionAnimationId());
+ node->SetStickyPosData(aLayer.GetStickyScrollContainerId(),
+ aLayer.GetStickyScrollRangeOuter(),
+ aLayer.GetStickyScrollRangeInner(),
+ aLayer.GetStickyPositionAnimationId());
+ return node;
+}
+
+template <typename PanGestureOrScrollWheelInput>
+static bool WillHandleInput(const PanGestureOrScrollWheelInput& aPanInput) {
+ if (!XRE_IsParentProcess() || !NS_IsMainThread()) {
+ return true;
+ }
+
+ WidgetWheelEvent wheelEvent = aPanInput.ToWidgetEvent(nullptr);
+ return APZInputBridge::ActionForWheelEvent(&wheelEvent).isSome();
+}
+
+/*static*/
+void APZCTreeManager::FlushApzRepaints(LayersId aLayersId) {
+ // Previously, paints were throttled and therefore this method was used to
+ // ensure any pending paints were flushed. Now, paints are flushed
+ // immediately, so it is safe to simply send a notification now.
+ APZCTM_LOG("Flushing repaints for layers id 0x%" PRIx64 "\n",
+ uint64_t(aLayersId));
+ RefPtr<GeckoContentController> controller = GetContentController(aLayersId);
+#ifndef MOZ_WIDGET_ANDROID
+ // On Android, this code is run in production and may actually get a nullptr
+ // controller here. On other platforms this code is test-only and should never
+ // get a nullptr.
+ MOZ_ASSERT(controller);
+#endif
+ if (controller) {
+ controller->DispatchToRepaintThread(NewRunnableMethod(
+ "layers::GeckoContentController::NotifyFlushComplete", controller,
+ &GeckoContentController::NotifyFlushComplete));
+ }
+}
+
+void APZCTreeManager::MarkAsDetached(LayersId aLayersId) {
+ RecursiveMutexAutoLock lock(mTreeLock);
+ mDetachedLayersIds.insert(aLayersId);
+}
+
+static bool HasNonLockModifier(Modifiers aModifiers) {
+ return (aModifiers & (MODIFIER_ALT | MODIFIER_ALTGRAPH | MODIFIER_CONTROL |
+ MODIFIER_FN | MODIFIER_META | MODIFIER_SHIFT |
+ MODIFIER_SYMBOL | MODIFIER_OS)) != 0;
+}
+
+APZEventResult APZCTreeManager::ReceiveInputEvent(
+ InputData& aEvent, InputBlockCallback&& aCallback) {
+ APZThreadUtils::AssertOnControllerThread();
+ InputHandlingState state{aEvent};
+
+ // Use a RAII class for updating the focus sequence number of this event
+ AutoFocusSequenceNumberSetter focusSetter(mFocusState, aEvent);
+
+ switch (aEvent.mInputType) {
+ case MULTITOUCH_INPUT: {
+ MultiTouchInput& touchInput = aEvent.AsMultiTouchInput();
+ ProcessTouchInput(state, touchInput);
+ break;
+ }
+ case MOUSE_INPUT: {
+ MouseInput& mouseInput = aEvent.AsMouseInput();
+ mouseInput.mHandledByAPZ = true;
+
+ SetCurrentMousePosition(mouseInput.mOrigin);
+
+ bool startsDrag = DragTracker::StartsDrag(mouseInput);
+ if (startsDrag) {
+ // If this is the start of a drag we need to unambiguously know if it's
+ // going to land on a scrollbar or not. We can't apply an untransform
+ // here without knowing that, so we need to ensure the untransform is
+ // a no-op.
+ FlushRepaintsToClearScreenToGeckoTransform();
+ }
+
+ state.mHit = GetTargetAPZC(mouseInput.mOrigin);
+ bool hitScrollbar = (bool)state.mHit.mScrollbarNode;
+
+ // When the mouse is outside the window we still want to handle dragging
+ // but we won't find an APZC. Fallback to root APZC then.
+ { // scope lock
+ RecursiveMutexAutoLock lock(mTreeLock);
+ if (!state.mHit.mTargetApzc && mRootNode) {
+ state.mHit.mTargetApzc = mRootNode->GetApzc();
+ }
+ }
+
+ if (state.mHit.mTargetApzc) {
+ if (StaticPrefs::apz_test_logging_enabled() &&
+ mouseInput.mType == MouseInput::MOUSE_HITTEST) {
+ ScrollableLayerGuid guid = state.mHit.mTargetApzc->GetGuid();
+
+ MutexAutoLock lock(mTestDataLock);
+ auto it = mTestData.find(guid.mLayersId);
+ MOZ_ASSERT(it != mTestData.end());
+ it->second->RecordHitResult(mouseInput.mOrigin, state.mHit.mHitResult,
+ guid.mLayersId, guid.mScrollId);
+ }
+
+ TargetConfirmationFlags confFlags{state.mHit.mHitResult};
+ state.mResult = mInputQueue->ReceiveInputEvent(state.mHit.mTargetApzc,
+ confFlags, mouseInput);
+
+ // If we're starting an async scrollbar drag
+ bool apzDragEnabled = StaticPrefs::apz_drag_enabled();
+ if (apzDragEnabled && startsDrag && state.mHit.mScrollbarNode &&
+ state.mHit.mScrollbarNode->IsScrollThumbNode() &&
+ state.mHit.mScrollbarNode->GetScrollbarData()
+ .mThumbIsAsyncDraggable) {
+ SetupScrollbarDrag(mouseInput, state.mHit.mScrollbarNode,
+ state.mHit.mTargetApzc.get());
+ }
+
+ if (state.mResult.GetStatus() == nsEventStatus_eConsumeDoDefault) {
+ // This input event is part of a drag block, so whether or not it is
+ // directed at a scrollbar depends on whether the drag block started
+ // on a scrollbar.
+ hitScrollbar = mInputQueue->IsDragOnScrollbar(hitScrollbar);
+ }
+
+ if (!hitScrollbar) {
+ // The input was not targeted at a scrollbar, so we untransform it
+ // like we do for other content. Scrollbars are "special" because they
+ // have special handling in AsyncCompositionManager when resolution is
+ // applied. TODO: we should find a better way to deal with this.
+ ScreenToParentLayerMatrix4x4 transformToApzc =
+ GetScreenToApzcTransform(state.mHit.mTargetApzc);
+ ParentLayerToScreenMatrix4x4 transformToGecko =
+ GetApzcToGeckoTransformForHit(state.mHit);
+ ScreenToScreenMatrix4x4 outTransform =
+ transformToApzc * transformToGecko;
+ Maybe<ScreenPoint> untransformedRefPoint =
+ UntransformBy(outTransform, mouseInput.mOrigin);
+ if (untransformedRefPoint) {
+ mouseInput.mOrigin = *untransformedRefPoint;
+ }
+ } else {
+ // Likewise, if the input was targeted at a scrollbar, we don't want
+ // to apply the callback transform in the main thread, so we remove
+ // the scrollid from the guid. We need to keep the layersId intact so
+ // that the response from the child process doesn't get discarded.
+ state.mResult.mTargetGuid.mScrollId =
+ ScrollableLayerGuid::NULL_SCROLL_ID;
+ }
+ }
+ break;
+ }
+ case SCROLLWHEEL_INPUT: {
+ FlushRepaintsToClearScreenToGeckoTransform();
+
+ // Do this before early return for Fission hit testing.
+ ScrollWheelInput& wheelInput = aEvent.AsScrollWheelInput();
+ state.mHit = GetTargetAPZC(wheelInput.mOrigin);
+
+ wheelInput.mHandledByAPZ = WillHandleInput(wheelInput);
+ if (!wheelInput.mHandledByAPZ) {
+ return state.Finish(*this, std::move(aCallback));
+ }
+
+ if (state.mHit.mTargetApzc) {
+ MOZ_ASSERT(state.mHit.mHitResult != CompositorHitTestInvisibleToHit);
+
+ if (wheelInput.mAPZAction == APZWheelAction::PinchZoom) {
+ // The mousewheel may have hit a subframe, but we want to send the
+ // pinch-zoom events to the root-content APZC.
+ {
+ RecursiveMutexAutoLock lock(mTreeLock);
+ state.mHit.mTargetApzc = FindRootContentApzcForLayersId(
+ state.mHit.mTargetApzc->GetLayersId());
+ }
+ if (state.mHit.mTargetApzc) {
+ SynthesizePinchGestureFromMouseWheel(wheelInput,
+ state.mHit.mTargetApzc);
+ }
+ state.mResult.SetStatusAsConsumeNoDefault();
+ return state.Finish(*this, std::move(aCallback));
+ }
+
+ MOZ_ASSERT(wheelInput.mAPZAction == APZWheelAction::Scroll);
+
+ // For wheel events, the call to ReceiveInputEvent below may result in
+ // scrolling, which changes the async transform. However, the event we
+ // want to pass to gecko should be the pre-scroll event coordinates,
+ // transformed into the gecko space. (pre-scroll because the mouse
+ // cursor is stationary during wheel scrolling, unlike touchmove
+ // events). Since we just flushed the pending repaints the transform to
+ // gecko space should only consist of overscroll-cancelling transforms.
+ ScreenToScreenMatrix4x4 transformToGecko =
+ GetScreenToApzcTransform(state.mHit.mTargetApzc) *
+ GetApzcToGeckoTransformForHit(state.mHit);
+ Maybe<ScreenPoint> untransformedOrigin =
+ UntransformBy(transformToGecko, wheelInput.mOrigin);
+
+ if (!untransformedOrigin) {
+ return state.Finish(*this, std::move(aCallback));
+ }
+
+ state.mResult = mInputQueue->ReceiveInputEvent(
+ state.mHit.mTargetApzc,
+ TargetConfirmationFlags{state.mHit.mHitResult}, wheelInput);
+
+ // Update the out-parameters so they are what the caller expects.
+ wheelInput.mOrigin = *untransformedOrigin;
+ }
+ break;
+ }
+ case PANGESTURE_INPUT: {
+ FlushRepaintsToClearScreenToGeckoTransform();
+
+ // Do this before early return for Fission hit testing.
+ PanGestureInput& panInput = aEvent.AsPanGestureInput();
+ state.mHit = GetTargetAPZC(panInput.mPanStartPoint);
+
+ panInput.mHandledByAPZ = WillHandleInput(panInput);
+ if (!panInput.mHandledByAPZ) {
+ if (mInputQueue->GetCurrentPanGestureBlock()) {
+ if (state.mHit.mTargetApzc &&
+ (panInput.mType == PanGestureInput::PANGESTURE_END ||
+ panInput.mType == PanGestureInput::PANGESTURE_CANCELLED)) {
+ // If we've already been processing a pan gesture in an APZC but
+ // fall into this _if_ branch, which means this pan-end or
+ // pan-cancelled event will not be proccessed in the APZC, send a
+ // pan-interrupted event to stop any on-going work for the pan
+ // gesture, otherwise we will get stuck at an intermidiate state
+ // becasue we might no longer receive any events which will be
+ // handled by the APZC.
+ PanGestureInput panInterrupted(
+ PanGestureInput::PANGESTURE_INTERRUPTED, panInput.mTimeStamp,
+ panInput.mPanStartPoint, panInput.mPanDisplacement,
+ panInput.modifiers);
+ Unused << mInputQueue->ReceiveInputEvent(
+ state.mHit.mTargetApzc,
+ TargetConfirmationFlags{state.mHit.mHitResult}, panInterrupted);
+ }
+ }
+ return state.Finish(*this, std::move(aCallback));
+ }
+
+ // If/when we enable support for pan inputs off-main-thread, we'll need
+ // to duplicate this EventStateManager code or something. See the call to
+ // GetUserPrefsForWheelEvent in APZInputBridge.cpp for why these fields
+ // are stored separately.
+ MOZ_ASSERT(NS_IsMainThread());
+ WidgetWheelEvent wheelEvent = panInput.ToWidgetEvent(nullptr);
+ EventStateManager::GetUserPrefsForWheelEvent(
+ &wheelEvent, &panInput.mUserDeltaMultiplierX,
+ &panInput.mUserDeltaMultiplierY);
+
+ if (state.mHit.mTargetApzc) {
+ MOZ_ASSERT(state.mHit.mHitResult != CompositorHitTestInvisibleToHit);
+
+ // For pan gesture events, the call to ReceiveInputEvent below may
+ // result in scrolling, which changes the async transform. However, the
+ // event we want to pass to gecko should be the pre-scroll event
+ // coordinates, transformed into the gecko space. (pre-scroll because
+ // the mouse cursor is stationary during pan gesture scrolling, unlike
+ // touchmove events). Since we just flushed the pending repaints the
+ // transform to gecko space should only consist of overscroll-cancelling
+ // transforms.
+ ScreenToScreenMatrix4x4 transformToGecko =
+ GetScreenToApzcTransform(state.mHit.mTargetApzc) *
+ GetApzcToGeckoTransformForHit(state.mHit);
+ Maybe<ScreenPoint> untransformedStartPoint =
+ UntransformBy(transformToGecko, panInput.mPanStartPoint);
+ Maybe<ScreenPoint> untransformedDisplacement =
+ UntransformVector(transformToGecko, panInput.mPanDisplacement,
+ panInput.mPanStartPoint);
+
+ if (!untransformedStartPoint || !untransformedDisplacement) {
+ return state.Finish(*this, std::move(aCallback));
+ }
+
+ panInput.mOverscrollBehaviorAllowsSwipe =
+ state.mHit.mTargetApzc->OverscrollBehaviorAllowsSwipe();
+
+ state.mResult = mInputQueue->ReceiveInputEvent(
+ state.mHit.mTargetApzc,
+ TargetConfirmationFlags{state.mHit.mHitResult}, panInput);
+
+ // Update the out-parameters so they are what the caller expects.
+ panInput.mPanStartPoint = *untransformedStartPoint;
+ panInput.mPanDisplacement = *untransformedDisplacement;
+ }
+ break;
+ }
+ case PINCHGESTURE_INPUT: {
+ PinchGestureInput& pinchInput = aEvent.AsPinchGestureInput();
+ if (HasNonLockModifier(pinchInput.modifiers)) {
+ APZCTM_LOG("Discarding pinch input due to modifiers 0x%x\n",
+ pinchInput.modifiers);
+ return state.Finish(*this, std::move(aCallback));
+ }
+
+ state.mHit = GetTargetAPZC(pinchInput.mFocusPoint);
+
+ // We always handle pinch gestures as pinch zooms.
+ pinchInput.mHandledByAPZ = true;
+
+ if (state.mHit.mTargetApzc) {
+ MOZ_ASSERT(state.mHit.mHitResult != CompositorHitTestInvisibleToHit);
+
+ if (!state.mHit.mTargetApzc->IsRootContent()) {
+ state.mHit.mTargetApzc = FindZoomableApzc(state.mHit.mTargetApzc);
+ }
+ }
+
+ if (state.mHit.mTargetApzc) {
+ ScreenToScreenMatrix4x4 outTransform =
+ GetScreenToApzcTransform(state.mHit.mTargetApzc) *
+ GetApzcToGeckoTransformForHit(state.mHit);
+ Maybe<ScreenPoint> untransformedFocusPoint =
+ UntransformBy(outTransform, pinchInput.mFocusPoint);
+
+ if (!untransformedFocusPoint) {
+ return state.Finish(*this, std::move(aCallback));
+ }
+
+ state.mResult = mInputQueue->ReceiveInputEvent(
+ state.mHit.mTargetApzc,
+ TargetConfirmationFlags{state.mHit.mHitResult}, pinchInput);
+
+ // Update the out-parameters so they are what the caller expects.
+ pinchInput.mFocusPoint = *untransformedFocusPoint;
+ }
+ break;
+ }
+ case TAPGESTURE_INPUT: { // note: no one currently sends these
+ TapGestureInput& tapInput = aEvent.AsTapGestureInput();
+ state.mHit = GetTargetAPZC(tapInput.mPoint);
+
+ if (state.mHit.mTargetApzc) {
+ MOZ_ASSERT(state.mHit.mHitResult != CompositorHitTestInvisibleToHit);
+
+ ScreenToScreenMatrix4x4 outTransform =
+ GetScreenToApzcTransform(state.mHit.mTargetApzc) *
+ GetApzcToGeckoTransformForHit(state.mHit);
+ Maybe<ScreenIntPoint> untransformedPoint =
+ UntransformBy(outTransform, tapInput.mPoint);
+
+ if (!untransformedPoint) {
+ return state.Finish(*this, std::move(aCallback));
+ }
+
+ // Tap gesture events are not grouped into input blocks, and they're
+ // never queued in InputQueue, but processed right away. So, we only
+ // need to set |mTapGestureHitResult| for the duration of the
+ // InputQueue::ReceiveInputEvent() call.
+ {
+ RecursiveMutexAutoLock lock(mTreeLock);
+ mTapGestureHitResult =
+ mHitTester->CloneHitTestResult(lock, state.mHit);
+ }
+
+ state.mResult = mInputQueue->ReceiveInputEvent(
+ state.mHit.mTargetApzc,
+ TargetConfirmationFlags{state.mHit.mHitResult}, tapInput);
+
+ mTapGestureHitResult = HitTestResult();
+
+ // Update the out-parameters so they are what the caller expects.
+ tapInput.mPoint = *untransformedPoint;
+ }
+ break;
+ }
+ case KEYBOARD_INPUT: {
+ // Disable async keyboard scrolling when accessibility.browsewithcaret is
+ // enabled
+ if (!StaticPrefs::apz_keyboard_enabled_AtStartup() ||
+ StaticPrefs::accessibility_browsewithcaret()) {
+ APZ_KEY_LOG("Skipping key input from invalid prefs\n");
+ return state.Finish(*this, std::move(aCallback));
+ }
+
+ KeyboardInput& keyInput = aEvent.AsKeyboardInput();
+
+ // Try and find a matching shortcut for this keyboard input
+ Maybe<KeyboardShortcut> shortcut = mKeyboardMap.FindMatch(keyInput);
+
+ if (!shortcut) {
+ APZ_KEY_LOG("Skipping key input with no shortcut\n");
+
+ // If we don't have a shortcut for this key event, then we can keep our
+ // focus only if we know there are no key event listeners for this
+ // target
+ if (mFocusState.CanIgnoreKeyboardShortcutMisses()) {
+ focusSetter.MarkAsNonFocusChanging();
+ }
+ return state.Finish(*this, std::move(aCallback));
+ }
+
+ // Check if this shortcut needs to be dispatched to content. Anything
+ // matching this is assumed to be able to change focus.
+ if (shortcut->mDispatchToContent) {
+ APZ_KEY_LOG("Skipping key input with dispatch-to-content shortcut\n");
+ return state.Finish(*this, std::move(aCallback));
+ }
+
+ // We know we have an action to execute on whatever is the current focus
+ // target
+ const KeyboardScrollAction& action = shortcut->mAction;
+
+ // The current focus target depends on which direction the scroll is to
+ // happen
+ Maybe<ScrollableLayerGuid> targetGuid;
+ switch (action.mType) {
+ case KeyboardScrollAction::eScrollCharacter: {
+ targetGuid = mFocusState.GetHorizontalTarget();
+ break;
+ }
+ case KeyboardScrollAction::eScrollLine:
+ case KeyboardScrollAction::eScrollPage:
+ case KeyboardScrollAction::eScrollComplete: {
+ targetGuid = mFocusState.GetVerticalTarget();
+ break;
+ }
+ }
+
+ // If we don't have a scroll target then either we have a stale focus
+ // target, the focused element has event listeners, or the focused element
+ // doesn't have a layerized scroll frame. In any case we need to dispatch
+ // to content.
+ if (!targetGuid) {
+ APZ_KEY_LOG("Skipping key input with no current focus target\n");
+ return state.Finish(*this, std::move(aCallback));
+ }
+
+ RefPtr<AsyncPanZoomController> targetApzc =
+ GetTargetAPZC(targetGuid->mLayersId, targetGuid->mScrollId);
+
+ if (!targetApzc) {
+ APZ_KEY_LOG("Skipping key input with focus target but no APZC\n");
+ return state.Finish(*this, std::move(aCallback));
+ }
+
+ // Attach the keyboard scroll action to the input event for processing
+ // by the input queue.
+ keyInput.mAction = action;
+
+ APZ_KEY_LOG("Dispatching key input with apzc=%p\n", targetApzc.get());
+
+ // Dispatch the event to the input queue.
+ state.mResult = mInputQueue->ReceiveInputEvent(
+ targetApzc, TargetConfirmationFlags{true}, keyInput);
+
+ // Any keyboard event that is dispatched to the input queue at this point
+ // should have been consumed
+ MOZ_ASSERT(state.mResult.GetStatus() == nsEventStatus_eConsumeDoDefault ||
+ state.mResult.GetStatus() == nsEventStatus_eConsumeNoDefault);
+
+ keyInput.mHandledByAPZ = true;
+ focusSetter.MarkAsNonFocusChanging();
+
+ break;
+ }
+ }
+ return state.Finish(*this, std::move(aCallback));
+}
+
+static TouchBehaviorFlags ConvertToTouchBehavior(
+ const CompositorHitTestInfo& info) {
+ TouchBehaviorFlags result = AllowedTouchBehavior::UNKNOWN;
+ if (info == CompositorHitTestInvisibleToHit) {
+ result = AllowedTouchBehavior::NONE;
+ } else if (info.contains(CompositorHitTestFlags::eIrregularArea)) {
+ // Note that eApzAwareListeners and eInactiveScrollframe are similar
+ // to eIrregularArea in some respects, but are not relevant for the
+ // purposes of this function, which deals specifically with touch-action.
+ result = AllowedTouchBehavior::UNKNOWN;
+ } else {
+ result = AllowedTouchBehavior::VERTICAL_PAN |
+ AllowedTouchBehavior::HORIZONTAL_PAN |
+ AllowedTouchBehavior::PINCH_ZOOM |
+ AllowedTouchBehavior::ANIMATING_ZOOM;
+ if (info.contains(CompositorHitTestFlags::eTouchActionPanXDisabled)) {
+ result &= ~AllowedTouchBehavior::HORIZONTAL_PAN;
+ }
+ if (info.contains(CompositorHitTestFlags::eTouchActionPanYDisabled)) {
+ result &= ~AllowedTouchBehavior::VERTICAL_PAN;
+ }
+ if (info.contains(CompositorHitTestFlags::eTouchActionPinchZoomDisabled)) {
+ result &= ~AllowedTouchBehavior::PINCH_ZOOM;
+ }
+ if (info.contains(
+ CompositorHitTestFlags::eTouchActionAnimatingZoomDisabled)) {
+ result &= ~AllowedTouchBehavior::ANIMATING_ZOOM;
+ }
+ }
+ return result;
+}
+
+APZCTreeManager::HitTestResult APZCTreeManager::GetTouchInputBlockAPZC(
+ const MultiTouchInput& aEvent,
+ nsTArray<TouchBehaviorFlags>* aOutTouchBehaviors) {
+ HitTestResult hit;
+ if (aEvent.mTouches.Length() == 0) {
+ return hit;
+ }
+
+ FlushRepaintsToClearScreenToGeckoTransform();
+
+ hit = GetTargetAPZC(aEvent.mTouches[0].mScreenPoint);
+ // Don't set a layers id on multi-touch events.
+ if (aEvent.mTouches.Length() != 1) {
+ hit.mLayersId = LayersId{0};
+ }
+
+ if (aOutTouchBehaviors) {
+ aOutTouchBehaviors->AppendElement(ConvertToTouchBehavior(hit.mHitResult));
+ }
+ for (size_t i = 1; i < aEvent.mTouches.Length(); i++) {
+ HitTestResult hit2 = GetTargetAPZC(aEvent.mTouches[i].mScreenPoint);
+ if (aOutTouchBehaviors) {
+ aOutTouchBehaviors->AppendElement(
+ ConvertToTouchBehavior(hit2.mHitResult));
+ }
+ hit.mTargetApzc = GetZoomableTarget(hit.mTargetApzc, hit2.mTargetApzc);
+ APZCTM_LOG("Using APZC %p as the root APZC for multi-touch\n",
+ hit.mTargetApzc.get());
+ // A multi-touch gesture will not be a scrollbar drag, even if the
+ // first touch point happened to hit a scrollbar.
+ hit.mScrollbarNode.Clear();
+
+ // XXX we should probably be combining the hit results from the different
+ // touch points somehow, instead of just using the last one.
+ hit.mHitResult = hit2.mHitResult;
+ }
+
+ return hit;
+}
+
+APZEventResult APZCTreeManager::InputHandlingState::Finish(
+ APZCTreeManager& aTreeManager, InputBlockCallback&& aCallback) {
+ // The validity check here handles both the case where mHit was
+ // never populated (because this event did not trigger a hit-test),
+ // and the case where it was populated with an invalid LayersId
+ // (which can happen e.g. for multi-touch events).
+ if (mHit.mLayersId.IsValid()) {
+ mEvent.mLayersId = mHit.mLayersId;
+ }
+
+ // Absorb events that are in targetted at a position in the gutter,
+ // unless they are fixed position elements.
+ if (mHit.mHitOverscrollGutter && mHit.mFixedPosSides == SideBits::eNone) {
+ mResult.SetStatusAsConsumeNoDefault();
+ }
+
+ // If the event will have a delayed result then add the callback to the
+ // APZCTreeManager.
+ if (aCallback && mResult.WillHaveDelayedResult()) {
+ aTreeManager.AddInputBlockCallback(
+ mResult.mInputBlockId, {mResult.GetStatus(), std::move(aCallback)});
+ }
+
+ return mResult;
+}
+
+void APZCTreeManager::ProcessTouchInput(InputHandlingState& aState,
+ MultiTouchInput& aInput) {
+ aInput.mHandledByAPZ = true;
+ nsTArray<TouchBehaviorFlags> touchBehaviors;
+ HitTestingTreeNodeAutoLock hitScrollbarNode;
+ if (aInput.mType == MultiTouchInput::MULTITOUCH_START) {
+ // If we are panned into overscroll and a second finger goes down,
+ // ignore that second touch point completely. The touch-start for it is
+ // dropped completely; subsequent touch events until the touch-end for it
+ // will have this touch point filtered out.
+ // (By contrast, if we're in overscroll but not panning, such as after
+ // putting two fingers down during an overscroll animation, we process the
+ // second touch and proceed to pinch.)
+ if (mTouchBlockHitResult.mTargetApzc &&
+ mTouchBlockHitResult.mTargetApzc->IsInPanningState() &&
+ BuildOverscrollHandoffChain(mTouchBlockHitResult.mTargetApzc)
+ ->HasOverscrolledApzc()) {
+ if (mRetainedTouchIdentifier == -1) {
+ mRetainedTouchIdentifier =
+ mTouchBlockHitResult.mTargetApzc->GetLastTouchIdentifier();
+ }
+
+ aState.mResult.SetStatusAsConsumeNoDefault();
+ return;
+ }
+
+ aState.mHit = GetTouchInputBlockAPZC(aInput, &touchBehaviors);
+ RecursiveMutexAutoLock lock(mTreeLock);
+ // Repopulate mTouchBlockHitResult from the input state.
+ mTouchBlockHitResult = mHitTester->CloneHitTestResult(lock, aState.mHit);
+ hitScrollbarNode = std::move(aState.mHit.mScrollbarNode);
+
+ // Check if this event starts a scrollbar touch-drag. The conditions
+ // checked are similar to the ones we check for MOUSE_INPUT starting
+ // a scrollbar mouse-drag.
+ mInScrollbarTouchDrag =
+ StaticPrefs::apz_drag_enabled() &&
+ StaticPrefs::apz_drag_touch_enabled() && hitScrollbarNode &&
+ hitScrollbarNode->IsScrollThumbNode() &&
+ hitScrollbarNode->GetScrollbarData().mThumbIsAsyncDraggable;
+
+ MOZ_ASSERT(touchBehaviors.Length() == aInput.mTouches.Length());
+ for (size_t i = 0; i < touchBehaviors.Length(); i++) {
+ APZCTM_LOG("Touch point has allowed behaviours 0x%02x\n",
+ touchBehaviors[i]);
+ if (touchBehaviors[i] == AllowedTouchBehavior::UNKNOWN) {
+ // If there's any unknown items in the list, throw it out and we'll
+ // wait for the main thread to send us a notification.
+ touchBehaviors.Clear();
+ break;
+ }
+ }
+ } else if (mTouchBlockHitResult.mTargetApzc) {
+ APZCTM_LOG("Re-using APZC %p as continuation of event block\n",
+ mTouchBlockHitResult.mTargetApzc.get());
+ RecursiveMutexAutoLock lock(mTreeLock);
+ aState.mHit = mHitTester->CloneHitTestResult(lock, mTouchBlockHitResult);
+ }
+
+ if (mInScrollbarTouchDrag) {
+ aState.mResult = ProcessTouchInputForScrollbarDrag(
+ aInput, hitScrollbarNode, mTouchBlockHitResult.mHitResult);
+ } else {
+ // If we receive a touch-cancel, it means all touches are finished, so we
+ // can stop ignoring any that we were ignoring.
+ if (aInput.mType == MultiTouchInput::MULTITOUCH_CANCEL) {
+ mRetainedTouchIdentifier = -1;
+ }
+
+ // If we are currently ignoring any touch points, filter them out from the
+ // set of touch points included in this event. Note that we modify aInput
+ // itself, so that the touch points are also filtered out when the caller
+ // passes the event on to content.
+ if (mRetainedTouchIdentifier != -1) {
+ for (size_t j = 0; j < aInput.mTouches.Length(); ++j) {
+ if (aInput.mTouches[j].mIdentifier != mRetainedTouchIdentifier) {
+ aInput.mTouches.RemoveElementAt(j);
+ if (!touchBehaviors.IsEmpty()) {
+ MOZ_ASSERT(touchBehaviors.Length() > j);
+ touchBehaviors.RemoveElementAt(j);
+ }
+ --j;
+ }
+ }
+ if (aInput.mTouches.IsEmpty()) {
+ aState.mResult.SetStatusAsConsumeNoDefault();
+ return;
+ }
+ }
+
+ if (mTouchBlockHitResult.mTargetApzc) {
+ MOZ_ASSERT(mTouchBlockHitResult.mHitResult !=
+ CompositorHitTestInvisibleToHit);
+
+ aState.mResult = mInputQueue->ReceiveInputEvent(
+ mTouchBlockHitResult.mTargetApzc,
+ TargetConfirmationFlags{mTouchBlockHitResult.mHitResult}, aInput,
+ touchBehaviors.IsEmpty() ? Nothing()
+ : Some(std::move(touchBehaviors)));
+
+ // For computing the event to pass back to Gecko, use up-to-date
+ // transforms (i.e. not anything cached in an input block). This ensures
+ // that transformToApzc and transformToGecko are in sync.
+ // Note: we are not using ConvertToGecko() here, because we don't
+ // want to multiply transformToApzc and transformToGecko once
+ // for each touch point.
+ ScreenToParentLayerMatrix4x4 transformToApzc =
+ GetScreenToApzcTransform(mTouchBlockHitResult.mTargetApzc);
+ ParentLayerToScreenMatrix4x4 transformToGecko =
+ GetApzcToGeckoTransformForHit(mTouchBlockHitResult);
+ ScreenToScreenMatrix4x4 outTransform = transformToApzc * transformToGecko;
+
+ for (size_t i = 0; i < aInput.mTouches.Length(); i++) {
+ SingleTouchData& touchData = aInput.mTouches[i];
+ Maybe<ScreenIntPoint> untransformedScreenPoint =
+ UntransformBy(outTransform, touchData.mScreenPoint);
+ if (!untransformedScreenPoint) {
+ aState.mResult.SetStatusAsIgnore();
+ return;
+ }
+ touchData.mScreenPoint = *untransformedScreenPoint;
+ AdjustEventPointForDynamicToolbar(touchData.mScreenPoint,
+ mTouchBlockHitResult);
+ }
+ }
+ }
+
+ mTouchCounter.Update(aInput);
+
+ // If it's the end of the touch sequence then clear out variables so we
+ // don't keep dangling references and leak things.
+ if (mTouchCounter.GetActiveTouchCount() == 0) {
+ mTouchBlockHitResult = HitTestResult();
+ mRetainedTouchIdentifier = -1;
+ mInScrollbarTouchDrag = false;
+ }
+}
+
+void APZCTreeManager::AdjustEventPointForDynamicToolbar(
+ ScreenIntPoint& aEventPoint, const HitTestResult& aHit) {
+ if (aHit.mFixedPosSides != SideBits::eNone) {
+ MutexAutoLock lock(mMapLock);
+ aEventPoint -= RoundedToInt(apz::ComputeFixedMarginsOffset(
+ GetCompositorFixedLayerMargins(lock), aHit.mFixedPosSides,
+ mGeckoFixedLayerMargins));
+ } else if (aHit.mNode && aHit.mNode->GetStickyPositionAnimationId()) {
+ SideBits sideBits = SideBits::eNone;
+ {
+ RecursiveMutexAutoLock lock(mTreeLock);
+ sideBits = SidesStuckToRootContent(aHit.mNode.Get(lock));
+ }
+ MutexAutoLock lock(mMapLock);
+ aEventPoint -= RoundedToInt(apz::ComputeFixedMarginsOffset(
+ GetCompositorFixedLayerMargins(lock), sideBits, ScreenMargin()));
+ }
+}
+
+static MouseInput::MouseType MultiTouchTypeToMouseType(
+ MultiTouchInput::MultiTouchType aType) {
+ switch (aType) {
+ case MultiTouchInput::MULTITOUCH_START:
+ return MouseInput::MOUSE_DOWN;
+ case MultiTouchInput::MULTITOUCH_MOVE:
+ return MouseInput::MOUSE_MOVE;
+ case MultiTouchInput::MULTITOUCH_END:
+ case MultiTouchInput::MULTITOUCH_CANCEL:
+ return MouseInput::MOUSE_UP;
+ }
+ MOZ_ASSERT_UNREACHABLE("Invalid multi-touch type");
+ return MouseInput::MOUSE_NONE;
+}
+
+APZEventResult APZCTreeManager::ProcessTouchInputForScrollbarDrag(
+ MultiTouchInput& aTouchInput,
+ const HitTestingTreeNodeAutoLock& aScrollThumbNode,
+ const gfx::CompositorHitTestInfo& aHitInfo) {
+ MOZ_ASSERT(mRetainedTouchIdentifier == -1);
+ MOZ_ASSERT(mTouchBlockHitResult.mTargetApzc);
+ MOZ_ASSERT(aTouchInput.mTouches.Length() == 1);
+
+ // Synthesize a mouse event based on the touch event, so that we can
+ // reuse code in InputQueue and APZC for handling scrollbar mouse-drags.
+ MouseInput mouseInput{MultiTouchTypeToMouseType(aTouchInput.mType),
+ MouseInput::PRIMARY_BUTTON,
+ dom::MouseEvent_Binding::MOZ_SOURCE_TOUCH,
+ MouseButtonsFlag::ePrimaryFlag,
+ aTouchInput.mTouches[0].mScreenPoint,
+ aTouchInput.mTimeStamp,
+ aTouchInput.modifiers};
+ mouseInput.mHandledByAPZ = true;
+
+ TargetConfirmationFlags targetConfirmed{aHitInfo};
+ APZEventResult result;
+ result = mInputQueue->ReceiveInputEvent(mTouchBlockHitResult.mTargetApzc,
+ targetConfirmed, mouseInput);
+
+ // |aScrollThumbNode| is non-null iff. this is the event that starts the drag.
+ // If so, set up the drag.
+ if (aScrollThumbNode) {
+ SetupScrollbarDrag(mouseInput, aScrollThumbNode,
+ mTouchBlockHitResult.mTargetApzc.get());
+ }
+
+ // Since the input was targeted at a scrollbar:
+ // - The original touch event (which will be sent on to content) will
+ // not be untransformed.
+ // - We don't want to apply the callback transform in the main thread,
+ // so we remove the scrollid from the guid.
+ // Both of these match the behaviour of mouse events that target a scrollbar;
+ // see the code for handling mouse events in ReceiveInputEvent() for
+ // additional explanation.
+ result.mTargetGuid.mScrollId = ScrollableLayerGuid::NULL_SCROLL_ID;
+
+ return result;
+}
+
+void APZCTreeManager::SetupScrollbarDrag(
+ MouseInput& aMouseInput, const HitTestingTreeNodeAutoLock& aScrollThumbNode,
+ AsyncPanZoomController* aApzc) {
+ DragBlockState* dragBlock = mInputQueue->GetCurrentDragBlock();
+ if (!dragBlock) {
+ return;
+ }
+
+ const ScrollbarData& thumbData = aScrollThumbNode->GetScrollbarData();
+ MOZ_ASSERT(thumbData.mDirection.isSome());
+
+ // Record the thumb's position at the start of the drag.
+ // We snap back to this position if, during the drag, the mouse
+ // gets sufficiently far away from the scrollbar.
+ dragBlock->SetInitialThumbPos(thumbData.mThumbStart);
+
+ // Under some conditions, we can confirm the drag block right away.
+ // Otherwise, we have to wait for a main-thread confirmation.
+ if (StaticPrefs::apz_drag_initial_enabled() &&
+ // check that the scrollbar's target scroll frame is layerized
+ aScrollThumbNode->GetScrollTargetId() == aApzc->GetGuid().mScrollId &&
+ !aApzc->IsScrollInfoLayer()) {
+ uint64_t dragBlockId = dragBlock->GetBlockId();
+ // AsyncPanZoomController::HandleInputEvent() will call
+ // TransformToLocal() on the event, but we need its mLocalOrigin now
+ // to compute a drag start offset for the AsyncDragMetrics.
+ aMouseInput.TransformToLocal(aApzc->GetTransformToThis());
+ OuterCSSCoord dragStart =
+ aApzc->ConvertScrollbarPoint(aMouseInput.mLocalOrigin, thumbData);
+ // ConvertScrollbarPoint() got the drag start offset relative to
+ // the scroll track. Now get it relative to the thumb.
+ // ScrollThumbData::mThumbStart stores the offset of the thumb
+ // relative to the scroll track at the time of the last paint.
+ // Since that paint, the thumb may have acquired an async transform
+ // due to async scrolling, so look that up and apply it.
+ LayerToParentLayerMatrix4x4 thumbTransform;
+ {
+ RecursiveMutexAutoLock lock(mTreeLock);
+ thumbTransform = ComputeTransformForNode(aScrollThumbNode.Get(lock));
+ }
+ // Only consider the translation, since we do not support both
+ // zooming and scrollbar dragging on any platform.
+ OuterCSSCoord thumbStart =
+ thumbData.mThumbStart +
+ ((*thumbData.mDirection == ScrollDirection::eHorizontal)
+ ? thumbTransform._41
+ : thumbTransform._42);
+ dragStart -= thumbStart;
+
+ // Content can't prevent scrollbar dragging with preventDefault(),
+ // so we don't need to wait for a content response. It's important
+ // to do this before calling ConfirmDragBlock() since that can
+ // potentially process and consume the block.
+ dragBlock->SetContentResponse(false);
+
+ NotifyScrollbarDragInitiated(dragBlockId, aApzc->GetGuid(),
+ *thumbData.mDirection);
+
+ mInputQueue->ConfirmDragBlock(
+ dragBlockId, aApzc,
+ AsyncDragMetrics(aApzc->GetGuid().mScrollId,
+ aApzc->GetGuid().mPresShellId, dragBlockId, dragStart,
+ *thumbData.mDirection));
+ }
+}
+
+void APZCTreeManager::SynthesizePinchGestureFromMouseWheel(
+ const ScrollWheelInput& aWheelInput,
+ const RefPtr<AsyncPanZoomController>& aTarget) {
+ MOZ_ASSERT(aTarget);
+
+ ScreenPoint focusPoint = aWheelInput.mOrigin;
+
+ // Compute span values based on the wheel delta.
+ ScreenCoord oldSpan = 100;
+ ScreenCoord newSpan = oldSpan + aWheelInput.mDeltaY;
+
+ // There's no ambiguity as to the target for pinch gesture events.
+ TargetConfirmationFlags confFlags{true};
+
+ PinchGestureInput pinchStart{PinchGestureInput::PINCHGESTURE_START,
+ PinchGestureInput::MOUSEWHEEL,
+ aWheelInput.mTimeStamp,
+ ExternalPoint(0, 0),
+ focusPoint,
+ oldSpan,
+ oldSpan,
+ aWheelInput.modifiers};
+ PinchGestureInput pinchScale1{PinchGestureInput::PINCHGESTURE_SCALE,
+ PinchGestureInput::MOUSEWHEEL,
+ aWheelInput.mTimeStamp,
+ ExternalPoint(0, 0),
+ focusPoint,
+ oldSpan,
+ oldSpan,
+ aWheelInput.modifiers};
+ PinchGestureInput pinchScale2{PinchGestureInput::PINCHGESTURE_SCALE,
+ PinchGestureInput::MOUSEWHEEL,
+ aWheelInput.mTimeStamp,
+ ExternalPoint(0, 0),
+ focusPoint,
+ oldSpan,
+ newSpan,
+ aWheelInput.modifiers};
+ PinchGestureInput pinchEnd{PinchGestureInput::PINCHGESTURE_END,
+ PinchGestureInput::MOUSEWHEEL,
+ aWheelInput.mTimeStamp,
+ ExternalPoint(0, 0),
+ focusPoint,
+ newSpan,
+ newSpan,
+ aWheelInput.modifiers};
+
+ mInputQueue->ReceiveInputEvent(aTarget, confFlags, pinchStart);
+ mInputQueue->ReceiveInputEvent(aTarget, confFlags, pinchScale1);
+ mInputQueue->ReceiveInputEvent(aTarget, confFlags, pinchScale2);
+ mInputQueue->ReceiveInputEvent(aTarget, confFlags, pinchEnd);
+}
+
+void APZCTreeManager::UpdateWheelTransaction(
+ LayoutDeviceIntPoint aRefPoint, EventMessage aEventMessage,
+ const Maybe<ScrollableLayerGuid>& aTargetGuid) {
+ APZThreadUtils::AssertOnControllerThread();
+
+ WheelBlockState* txn = mInputQueue->GetActiveWheelTransaction();
+ if (!txn) {
+ return;
+ }
+
+ // If the transaction has simply timed out, we don't need to do anything
+ // else.
+ if (txn->MaybeTimeout(TimeStamp::Now())) {
+ return;
+ }
+
+ switch (aEventMessage) {
+ case eMouseMove:
+ case eDragOver: {
+ ScreenIntPoint point = ViewAs<ScreenPixel>(
+ aRefPoint,
+ PixelCastJustification::LayoutDeviceIsScreenForUntransformedEvent);
+
+ txn->OnMouseMove(point, aTargetGuid);
+
+ return;
+ }
+ case eKeyPress:
+ case eKeyUp:
+ case eKeyDown:
+ case eMouseUp:
+ case eMouseDown:
+ case eMouseDoubleClick:
+ case eMouseAuxClick:
+ case eMouseClick:
+ case eContextMenu:
+ case eDrop:
+ txn->EndTransaction();
+ return;
+ default:
+ break;
+ }
+}
+
+void APZCTreeManager::ProcessUnhandledEvent(LayoutDeviceIntPoint* aRefPoint,
+ ScrollableLayerGuid* aOutTargetGuid,
+ uint64_t* aOutFocusSequenceNumber,
+ LayersId* aOutLayersId) {
+ APZThreadUtils::AssertOnControllerThread();
+
+ // Transform the aRefPoint.
+ // If the event hits an overscrolled APZC, instruct the caller to ignore it.
+ PixelCastJustification LDIsScreen =
+ PixelCastJustification::LayoutDeviceIsScreenForUntransformedEvent;
+ ScreenIntPoint refPointAsScreen = ViewAs<ScreenPixel>(*aRefPoint, LDIsScreen);
+ HitTestResult hit = GetTargetAPZC(refPointAsScreen);
+ if (aOutLayersId) {
+ *aOutLayersId = hit.mLayersId;
+ }
+ if (hit.mTargetApzc) {
+ MOZ_ASSERT(hit.mHitResult != CompositorHitTestInvisibleToHit);
+ hit.mTargetApzc->GetGuid(aOutTargetGuid);
+ ScreenToParentLayerMatrix4x4 transformToApzc =
+ GetScreenToApzcTransform(hit.mTargetApzc);
+ ParentLayerToScreenMatrix4x4 transformToGecko =
+ GetApzcToGeckoTransformForHit(hit);
+ ScreenToScreenMatrix4x4 outTransform = transformToApzc * transformToGecko;
+ Maybe<ScreenIntPoint> untransformedRefPoint =
+ UntransformBy(outTransform, refPointAsScreen);
+ if (untransformedRefPoint) {
+ *aRefPoint =
+ ViewAs<LayoutDevicePixel>(*untransformedRefPoint, LDIsScreen);
+ }
+ }
+
+ // Update the focus sequence number and attach it to the event
+ mFocusState.ReceiveFocusChangingEvent();
+ *aOutFocusSequenceNumber = mFocusState.LastAPZProcessedEvent();
+}
+
+void APZCTreeManager::SetKeyboardMap(const KeyboardMap& aKeyboardMap) {
+ if (!APZThreadUtils::IsControllerThread()) {
+ APZThreadUtils::RunOnControllerThread(NewRunnableMethod<KeyboardMap>(
+ "layers::APZCTreeManager::SetKeyboardMap", this,
+ &APZCTreeManager::SetKeyboardMap, aKeyboardMap));
+ return;
+ }
+
+ APZThreadUtils::AssertOnControllerThread();
+
+ mKeyboardMap = aKeyboardMap;
+}
+
+void APZCTreeManager::ZoomToRect(const ScrollableLayerGuid& aGuid,
+ const ZoomTarget& aZoomTarget,
+ const uint32_t aFlags) {
+ if (!APZThreadUtils::IsControllerThread()) {
+ APZThreadUtils::RunOnControllerThread(
+ NewRunnableMethod<ScrollableLayerGuid, ZoomTarget, uint32_t>(
+ "layers::APZCTreeManager::ZoomToRect", this,
+ &APZCTreeManager::ZoomToRect, aGuid, aZoomTarget, aFlags));
+ return;
+ }
+
+ // We could probably move this to run on the updater thread if needed, but
+ // either way we should restrict it to a single thread. For now let's use the
+ // controller thread.
+ APZThreadUtils::AssertOnControllerThread();
+
+ RefPtr<AsyncPanZoomController> apzc = GetTargetAPZC(aGuid);
+ if (apzc) {
+ apzc->ZoomToRect(aZoomTarget, aFlags);
+ }
+}
+
+void APZCTreeManager::ContentReceivedInputBlock(uint64_t aInputBlockId,
+ bool aPreventDefault) {
+ if (!APZThreadUtils::IsControllerThread()) {
+ APZThreadUtils::RunOnControllerThread(NewRunnableMethod<uint64_t, bool>(
+ "layers::APZCTreeManager::ContentReceivedInputBlock", this,
+ &APZCTreeManager::ContentReceivedInputBlock, aInputBlockId,
+ aPreventDefault));
+ return;
+ }
+
+ APZThreadUtils::AssertOnControllerThread();
+
+ mInputQueue->ContentReceivedInputBlock(aInputBlockId, aPreventDefault);
+}
+
+void APZCTreeManager::SetTargetAPZC(
+ uint64_t aInputBlockId, const nsTArray<ScrollableLayerGuid>& aTargets) {
+ if (!APZThreadUtils::IsControllerThread()) {
+ APZThreadUtils::RunOnControllerThread(
+ NewRunnableMethod<uint64_t,
+ StoreCopyPassByRRef<nsTArray<ScrollableLayerGuid>>>(
+ "layers::APZCTreeManager::SetTargetAPZC", this,
+ &layers::APZCTreeManager::SetTargetAPZC, aInputBlockId,
+ aTargets.Clone()));
+ return;
+ }
+
+ RefPtr<AsyncPanZoomController> target = nullptr;
+ if (aTargets.Length() > 0) {
+ target = GetTargetAPZC(aTargets[0]);
+ }
+ for (size_t i = 1; i < aTargets.Length(); i++) {
+ RefPtr<AsyncPanZoomController> apzc = GetTargetAPZC(aTargets[i]);
+ target = GetZoomableTarget(target, apzc);
+ }
+ if (InputBlockState* block = mInputQueue->GetBlockForId(aInputBlockId)) {
+ if (block->AsPinchGestureBlock() && aTargets.Length() == 1) {
+ target = FindZoomableApzc(target);
+ }
+ }
+ mInputQueue->SetConfirmedTargetApzc(aInputBlockId, target);
+}
+
+void APZCTreeManager::UpdateZoomConstraints(
+ const ScrollableLayerGuid& aGuid,
+ const Maybe<ZoomConstraints>& aConstraints) {
+ if (!GetUpdater()->IsUpdaterThread()) {
+ // This can happen if we're in the UI process and got a call directly from
+ // nsBaseWidget or from a content process over PAPZCTreeManager. In that
+ // case we get this call on the compositor thread, which may be different
+ // from the updater thread. It can also happen in the GPU process if that is
+ // enabled, since the call will go over PAPZCTreeManager and arrive on the
+ // compositor thread in the GPU process.
+ GetUpdater()->RunOnUpdaterThread(
+ aGuid.mLayersId,
+ NewRunnableMethod<ScrollableLayerGuid, Maybe<ZoomConstraints>>(
+ "APZCTreeManager::UpdateZoomConstraints", this,
+ &APZCTreeManager::UpdateZoomConstraints, aGuid, aConstraints));
+ return;
+ }
+
+ AssertOnUpdaterThread();
+
+ // Propagate the zoom constraints down to the subtree, stopping at APZCs
+ // which have their own zoom constraints or are in a different layers id.
+ if (aConstraints) {
+ APZCTM_LOG("Recording constraints %s for guid %s\n",
+ ToString(aConstraints.value()).c_str(), ToString(aGuid).c_str());
+ mZoomConstraints[aGuid] = aConstraints.ref();
+ } else {
+ APZCTM_LOG("Removing constraints for guid %s\n", ToString(aGuid).c_str());
+ mZoomConstraints.erase(aGuid);
+ }
+
+ RecursiveMutexAutoLock lock(mTreeLock);
+ RefPtr<HitTestingTreeNode> node = DepthFirstSearchPostOrder<ReverseIterator>(
+ mRootNode.get(), [&aGuid](HitTestingTreeNode* aNode) {
+ bool matches = false;
+ if (auto zoomId = aNode->GetAsyncZoomContainerId()) {
+ matches = ScrollableLayerGuid::EqualsIgnoringPresShell(
+ aGuid, ScrollableLayerGuid(aNode->GetLayersId(), 0, *zoomId));
+ }
+ return matches;
+ });
+
+ // This does not hold because we can get zoom constraints updates before the
+ // layer tree update with the async zoom container (I assume).
+ // clang-format off
+ // MOZ_ASSERT(node || aConstraints.isNothing() ||
+ // (!aConstraints->mAllowZoom && !aConstraints->mAllowDoubleTapZoom));
+ // clang-format on
+
+ // If there is no async zoom container then the zoom constraints should not
+ // allow zooming and building the HTT should have handled clearing the zoom
+ // constraints from all nodes so we don't have to handle doing anything in
+ // case there is no async zoom container.
+
+ if (node && aConstraints) {
+ ForEachNode<ReverseIterator>(node.get(), [&aConstraints, &node, &aGuid,
+ this](HitTestingTreeNode* aNode) {
+ if (aNode != node) {
+ // don't go into other async zoom containers
+ if (auto zoomId = aNode->GetAsyncZoomContainerId()) {
+ MOZ_ASSERT(!ScrollableLayerGuid::EqualsIgnoringPresShell(
+ aGuid, ScrollableLayerGuid(aNode->GetLayersId(), 0, *zoomId)));
+ return TraversalFlag::Skip;
+ }
+ if (AsyncPanZoomController* childApzc = aNode->GetApzc()) {
+ if (!ScrollableLayerGuid::EqualsIgnoringPresShell(
+ aGuid, childApzc->GetGuid())) {
+ // We can have subtrees with their own zoom constraints - leave
+ // these alone.
+ if (this->mZoomConstraints.find(childApzc->GetGuid()) !=
+ this->mZoomConstraints.end()) {
+ return TraversalFlag::Skip;
+ }
+ }
+ }
+ }
+ if (aNode->IsPrimaryHolder()) {
+ MOZ_ASSERT(aNode->GetApzc());
+ aNode->GetApzc()->UpdateZoomConstraints(aConstraints.ref());
+ }
+ return TraversalFlag::Continue;
+ });
+ }
+}
+
+void APZCTreeManager::FlushRepaintsToClearScreenToGeckoTransform() {
+ // As the name implies, we flush repaint requests for the entire APZ tree in
+ // order to clear the screen-to-gecko transform (aka the "untransform" applied
+ // to incoming input events before they can be passed on to Gecko).
+ //
+ // The primary reason we do this is to avoid the problem where input events,
+ // after being untransformed, end up hit-testing differently in Gecko. This
+ // might happen in cases where the input event lands on content that is async-
+ // scrolled into view, but Gecko still thinks it is out of view given the
+ // visible area of a scrollframe.
+ //
+ // Another reason we want to clear the untransform is that if our APZ hit-test
+ // hits a dispatch-to-content region then that's an ambiguous result and we
+ // need to ask Gecko what actually got hit. In order to do this we need to
+ // untransform the input event into Gecko space - but to do that we need to
+ // know which APZC got hit! This leads to a circular dependency; the only way
+ // to get out of it is to make sure that the untransform for all the possible
+ // matched APZCs is the same. It is simplest to ensure that by flushing the
+ // pending repaint requests, which makes all of the untransforms empty (and
+ // therefore equal).
+ RecursiveMutexAutoLock lock(mTreeLock);
+
+ ForEachNode<ReverseIterator>(mRootNode.get(), [](HitTestingTreeNode* aNode) {
+ if (aNode->IsPrimaryHolder()) {
+ MOZ_ASSERT(aNode->GetApzc());
+ aNode->GetApzc()->FlushRepaintForNewInputBlock();
+ }
+ });
+}
+
+void APZCTreeManager::ClearTree() {
+ AssertOnUpdaterThread();
+
+ // Ensure that no references to APZCs are alive in any lingering input
+ // blocks. This breaks cycles from InputBlockState::mTargetApzc back to
+ // the InputQueue.
+ APZThreadUtils::RunOnControllerThread(NewRunnableMethod(
+ "layers::InputQueue::Clear", mInputQueue, &InputQueue::Clear));
+
+ RecursiveMutexAutoLock lock(mTreeLock);
+
+ // Collect the nodes into a list, and then destroy each one.
+ // We can't destroy them as we collect them, because ForEachNode()
+ // does a pre-order traversal of the tree, and Destroy() nulls out
+ // the fields needed to reach the children of the node.
+ nsTArray<RefPtr<HitTestingTreeNode>> nodesToDestroy;
+ ForEachNode<ReverseIterator>(mRootNode.get(),
+ [&nodesToDestroy](HitTestingTreeNode* aNode) {
+ nodesToDestroy.AppendElement(aNode);
+ });
+
+ for (size_t i = 0; i < nodesToDestroy.Length(); i++) {
+ nodesToDestroy[i]->Destroy();
+ }
+ mRootNode = nullptr;
+
+ {
+ // Also remove references to APZC instances in the map
+ MutexAutoLock lock(mMapLock);
+ mApzcMap.clear();
+ }
+
+ RefPtr<APZCTreeManager> self(this);
+ NS_DispatchToMainThread(
+ NS_NewRunnableFunction("layers::APZCTreeManager::ClearTree", [self] {
+ self->mFlushObserver->Unregister();
+ self->mFlushObserver = nullptr;
+ }));
+}
+
+RefPtr<HitTestingTreeNode> APZCTreeManager::GetRootNode() const {
+ RecursiveMutexAutoLock lock(mTreeLock);
+ return mRootNode;
+}
+
+/**
+ * Transform a displacement from the ParentLayer coordinates of a source APZC
+ * to the ParentLayer coordinates of a target APZC.
+ * @param aTreeManager the tree manager for the APZC tree containing |aSource|
+ * and |aTarget|
+ * @param aSource the source APZC
+ * @param aTarget the target APZC
+ * @param aStartPoint the start point of the displacement
+ * @param aEndPoint the end point of the displacement
+ * @return true on success, false if aStartPoint or aEndPoint cannot be
+ * transformed into target's coordinate space
+ */
+static bool TransformDisplacement(APZCTreeManager* aTreeManager,
+ AsyncPanZoomController* aSource,
+ AsyncPanZoomController* aTarget,
+ ParentLayerPoint& aStartPoint,
+ ParentLayerPoint& aEndPoint) {
+ if (aSource == aTarget) {
+ return true;
+ }
+
+ // Convert start and end points to Screen coordinates.
+ ParentLayerToScreenMatrix4x4 untransformToApzc =
+ aTreeManager->GetScreenToApzcTransform(aSource).Inverse();
+ ScreenPoint screenStart = TransformBy(untransformToApzc, aStartPoint);
+ ScreenPoint screenEnd = TransformBy(untransformToApzc, aEndPoint);
+
+ // Convert start and end points to aTarget's ParentLayer coordinates.
+ ScreenToParentLayerMatrix4x4 transformToApzc =
+ aTreeManager->GetScreenToApzcTransform(aTarget);
+ Maybe<ParentLayerPoint> startPoint =
+ UntransformBy(transformToApzc, screenStart);
+ Maybe<ParentLayerPoint> endPoint = UntransformBy(transformToApzc, screenEnd);
+ if (!startPoint || !endPoint) {
+ return false;
+ }
+ aEndPoint = *endPoint;
+ aStartPoint = *startPoint;
+
+ return true;
+}
+
+bool APZCTreeManager::DispatchScroll(
+ AsyncPanZoomController* aPrev, ParentLayerPoint& aStartPoint,
+ ParentLayerPoint& aEndPoint,
+ OverscrollHandoffState& aOverscrollHandoffState) {
+ const OverscrollHandoffChain& overscrollHandoffChain =
+ aOverscrollHandoffState.mChain;
+ uint32_t overscrollHandoffChainIndex = aOverscrollHandoffState.mChainIndex;
+ RefPtr<AsyncPanZoomController> next;
+ // If we have reached the end of the overscroll handoff chain, there is
+ // nothing more to scroll, so we ignore the rest of the pan gesture.
+ if (overscrollHandoffChainIndex >= overscrollHandoffChain.Length()) {
+ // Nothing more to scroll - ignore the rest of the pan gesture.
+ return false;
+ }
+
+ next = overscrollHandoffChain.GetApzcAtIndex(overscrollHandoffChainIndex);
+
+ if (next == nullptr || next->IsDestroyed()) {
+ return false;
+ }
+
+ // Convert the start and end points from |aPrev|'s coordinate space to
+ // |next|'s coordinate space.
+ if (!TransformDisplacement(this, aPrev, next, aStartPoint, aEndPoint)) {
+ return false;
+ }
+
+ // Scroll |next|. If this causes overscroll, it will call DispatchScroll()
+ // again with an incremented index.
+ if (!next->AttemptScroll(aStartPoint, aEndPoint, aOverscrollHandoffState)) {
+ // Transform |aStartPoint| and |aEndPoint| (which now represent the
+ // portion of the displacement that wasn't consumed by APZCs later
+ // in the handoff chain) back into |aPrev|'s coordinate space. This
+ // allows the caller (which is |aPrev|) to interpret the unconsumed
+ // displacement in its own coordinate space, and make use of it
+ // (e.g. by going into overscroll).
+ if (!TransformDisplacement(this, next, aPrev, aStartPoint, aEndPoint)) {
+ NS_WARNING("Failed to untransform scroll points during dispatch");
+ }
+ return false;
+ }
+
+ // Return true to indicate the scroll was consumed entirely.
+ return true;
+}
+
+ParentLayerPoint APZCTreeManager::DispatchFling(
+ AsyncPanZoomController* aPrev, const FlingHandoffState& aHandoffState) {
+ // If immediate handoff is disallowed, do not allow handoff beyond the
+ // single APZC that's scrolled by the input block that triggered this fling.
+ if (aHandoffState.mIsHandoff && !StaticPrefs::apz_allow_immediate_handoff() &&
+ aHandoffState.mScrolledApzc == aPrev) {
+ FLING_LOG("APZCTM dropping handoff due to disallowed immediate handoff\n");
+ return aHandoffState.mVelocity;
+ }
+
+ const OverscrollHandoffChain* chain = aHandoffState.mChain;
+ RefPtr<AsyncPanZoomController> current;
+ uint32_t overscrollHandoffChainLength = chain->Length();
+ uint32_t startIndex;
+
+ // The fling's velocity needs to be transformed from the screen coordinates
+ // of |aPrev| to the screen coordinates of |next|. To transform a velocity
+ // correctly, we need to convert it to a displacement. For now, we do this
+ // by anchoring it to a start point of (0, 0).
+ // TODO: For this to be correct in the presence of 3D transforms, we should
+ // use the end point of the touch that started the fling as the start point
+ // rather than (0, 0).
+ ParentLayerPoint startPoint; // (0, 0)
+ ParentLayerPoint endPoint;
+
+ if (aHandoffState.mIsHandoff) {
+ startIndex = chain->IndexOf(aPrev) + 1;
+
+ // IndexOf will return aOverscrollHandoffChain->Length() if
+ // |aPrev| is not found.
+ if (startIndex >= overscrollHandoffChainLength) {
+ return aHandoffState.mVelocity;
+ }
+ } else {
+ startIndex = 0;
+ }
+
+ // This will store any velocity left over after the entire handoff.
+ ParentLayerPoint finalResidualVelocity = aHandoffState.mVelocity;
+
+ ParentLayerPoint currentVelocity = aHandoffState.mVelocity;
+ for (; startIndex < overscrollHandoffChainLength; startIndex++) {
+ current = chain->GetApzcAtIndex(startIndex);
+
+ // Make sure the apzc about to be handled can be handled
+ if (current == nullptr || current->IsDestroyed()) {
+ break;
+ }
+
+ endPoint = startPoint + currentVelocity;
+
+ RefPtr<AsyncPanZoomController> prevApzc =
+ (startIndex > 0) ? chain->GetApzcAtIndex(startIndex - 1) : nullptr;
+
+ // Only transform when current apzc can be transformed with previous
+ if (prevApzc) {
+ if (!TransformDisplacement(this, prevApzc, current, startPoint,
+ endPoint)) {
+ break;
+ }
+ }
+
+ ParentLayerPoint availableVelocity = (endPoint - startPoint);
+ ParentLayerPoint residualVelocity;
+
+ FlingHandoffState transformedHandoffState = aHandoffState;
+ transformedHandoffState.mVelocity = availableVelocity;
+
+ // Obey overscroll-behavior.
+ if (prevApzc) {
+ residualVelocity += prevApzc->AdjustHandoffVelocityForOverscrollBehavior(
+ transformedHandoffState.mVelocity);
+ }
+
+ residualVelocity += current->AttemptFling(transformedHandoffState);
+
+ // If there's no residual velocity, there's nothing more to hand off.
+ if (current->IsZero(residualVelocity)) {
+ return ParentLayerPoint();
+ }
+
+ // If any of the velocity available to be handed off was consumed,
+ // subtract the proportion of consumed velocity from finalResidualVelocity.
+ // Note: it's important to compare |residualVelocity| to |availableVelocity|
+ // here and not to |transformedHandoffState.mVelocity|, since the latter
+ // may have been modified by AdjustHandoffVelocityForOverscrollBehavior().
+ if (!current->IsZero(availableVelocity.x - residualVelocity.x)) {
+ finalResidualVelocity.x *= (residualVelocity.x / availableVelocity.x);
+ }
+ if (!current->IsZero(availableVelocity.y - residualVelocity.y)) {
+ finalResidualVelocity.y *= (residualVelocity.y / availableVelocity.y);
+ }
+
+ currentVelocity = residualVelocity;
+ }
+
+ // Return any residual velocity left over after the entire handoff process.
+ return finalResidualVelocity;
+}
+
+already_AddRefed<AsyncPanZoomController> APZCTreeManager::GetTargetAPZC(
+ const ScrollableLayerGuid& aGuid) {
+ RecursiveMutexAutoLock lock(mTreeLock);
+ RefPtr<HitTestingTreeNode> node = GetTargetNode(aGuid, nullptr);
+ MOZ_ASSERT(!node || node->GetApzc()); // any node returned must have an APZC
+ RefPtr<AsyncPanZoomController> apzc = node ? node->GetApzc() : nullptr;
+ return apzc.forget();
+}
+
+already_AddRefed<AsyncPanZoomController> APZCTreeManager::GetTargetAPZC(
+ const LayersId& aLayersId,
+ const ScrollableLayerGuid::ViewID& aScrollId) const {
+ MutexAutoLock lock(mMapLock);
+ return GetTargetAPZC(aLayersId, aScrollId, lock);
+}
+
+already_AddRefed<AsyncPanZoomController> APZCTreeManager::GetTargetAPZC(
+ const LayersId& aLayersId, const ScrollableLayerGuid::ViewID& aScrollId,
+ const MutexAutoLock& aProofOfMapLock) const {
+ ScrollableLayerGuid guid(aLayersId, 0, aScrollId);
+ auto it = mApzcMap.find(guid);
+ RefPtr<AsyncPanZoomController> apzc =
+ (it != mApzcMap.end() ? it->second.apzc : nullptr);
+ return apzc.forget();
+}
+
+already_AddRefed<HitTestingTreeNode> APZCTreeManager::GetTargetNode(
+ const ScrollableLayerGuid& aGuid, GuidComparator aComparator) const {
+ mTreeLock.AssertCurrentThreadIn();
+ RefPtr<HitTestingTreeNode> target =
+ DepthFirstSearchPostOrder<ReverseIterator>(
+ mRootNode.get(), [&aGuid, &aComparator](HitTestingTreeNode* node) {
+ bool matches = false;
+ if (node->GetApzc()) {
+ if (aComparator) {
+ matches = aComparator(aGuid, node->GetApzc()->GetGuid());
+ } else {
+ matches = node->GetApzc()->Matches(aGuid);
+ }
+ }
+ return matches;
+ });
+ return target.forget();
+}
+
+APZCTreeManager::HitTestResult APZCTreeManager::GetTargetAPZC(
+ const ScreenPoint& aPoint) {
+ RecursiveMutexAutoLock lock(mTreeLock);
+ MOZ_ASSERT(mHitTester);
+ return mHitTester->GetAPZCAtPoint(aPoint, lock);
+}
+
+APZCTreeManager::TargetApzcForNodeResult APZCTreeManager::FindHandoffParent(
+ const AsyncPanZoomController* aApzc) {
+ RefPtr<HitTestingTreeNode> node = GetTargetNode(aApzc->GetGuid(), nullptr);
+ while (node) {
+ auto result = GetTargetApzcForNode(node->GetParent());
+ if (result.mApzc) {
+ // avoid infinite recursion in the overscroll handoff chain.
+ if (result.mApzc != aApzc) {
+ return result;
+ }
+ }
+ node = node->GetParent();
+ }
+
+ return {nullptr, false};
+}
+
+RefPtr<const OverscrollHandoffChain>
+APZCTreeManager::BuildOverscrollHandoffChain(
+ const RefPtr<AsyncPanZoomController>& aInitialTarget) {
+ // Scroll grabbing is a mechanism that allows content to specify that
+ // the initial target of a pan should be not the innermost scrollable
+ // frame at the touch point (which is what GetTargetAPZC finds), but
+ // something higher up in the tree.
+ // It's not sufficient to just find the initial target, however, as
+ // overscroll can be handed off to another APZC. Without scroll grabbing,
+ // handoff just occurs from child to parent. With scroll grabbing, the
+ // handoff order can be different, so we build a chain of APZCs in the
+ // order in which scroll will be handed off to them.
+
+ // Grab tree lock since we'll be walking the APZC tree.
+ RecursiveMutexAutoLock lock(mTreeLock);
+
+ // Build the chain. If there is a scroll parent link, we use that. This is
+ // needed to deal with scroll info layers, because they participate in handoff
+ // but do not follow the expected layer tree structure. If there are no
+ // scroll parent links we just walk up the tree to find the scroll parent.
+ OverscrollHandoffChain* result = new OverscrollHandoffChain;
+ AsyncPanZoomController* apzc = aInitialTarget;
+ while (apzc != nullptr) {
+ result->Add(apzc);
+
+ APZCTreeManager::TargetApzcForNodeResult handoffResult =
+ FindHandoffParent(apzc);
+
+ if (!handoffResult.mIsFixed && !apzc->IsRootForLayersId() &&
+ apzc->GetScrollHandoffParentId() ==
+ ScrollableLayerGuid::NULL_SCROLL_ID) {
+ // This probably indicates a bug or missed case in layout code
+ NS_WARNING("Found a non-root APZ with no handoff parent");
+ }
+
+ // If `apzc` is inside fixed content, we want to hand off to the document's
+ // root APZC next. The scroll parent id wouldn't give us this because it's
+ // based on ASRs.
+ if (handoffResult.mIsFixed || apzc->GetScrollHandoffParentId() ==
+ ScrollableLayerGuid::NULL_SCROLL_ID) {
+ apzc = handoffResult.mApzc;
+ continue;
+ }
+
+ // Guard against a possible infinite-loop condition. If we hit this, the
+ // layout code that generates the handoff parents did something wrong.
+ MOZ_ASSERT(apzc->GetScrollHandoffParentId() != apzc->GetGuid().mScrollId);
+ RefPtr<AsyncPanZoomController> scrollParent = GetTargetAPZC(
+ apzc->GetGuid().mLayersId, apzc->GetScrollHandoffParentId());
+ apzc = scrollParent.get();
+ }
+
+ // Now adjust the chain to account for scroll grabbing. Sorting is a bit
+ // of an overkill here, but scroll grabbing will likely be generalized
+ // to scroll priorities, so we might as well do it this way.
+ result->SortByScrollPriority();
+
+ // Print the overscroll chain for debugging.
+ for (uint32_t i = 0; i < result->Length(); ++i) {
+ APZCTM_LOG("OverscrollHandoffChain[%d] = %p\n", i,
+ result->GetApzcAtIndex(i).get());
+ }
+
+ return result;
+}
+
+void APZCTreeManager::SetLongTapEnabled(bool aLongTapEnabled) {
+ if (!APZThreadUtils::IsControllerThread()) {
+ APZThreadUtils::RunOnControllerThread(NewRunnableMethod<bool>(
+ "layers::APZCTreeManager::SetLongTapEnabled", this,
+ &APZCTreeManager::SetLongTapEnabled, aLongTapEnabled));
+ return;
+ }
+
+ APZThreadUtils::AssertOnControllerThread();
+ GestureEventListener::SetLongTapEnabled(aLongTapEnabled);
+}
+
+void APZCTreeManager::AddInputBlockCallback(
+ uint64_t aInputBlockId, InputBlockCallbackInfo&& aCallbackInfo) {
+ APZThreadUtils::AssertOnControllerThread();
+ mInputQueue->AddInputBlockCallback(aInputBlockId, std::move(aCallbackInfo));
+}
+
+void APZCTreeManager::FindScrollThumbNode(
+ const AsyncDragMetrics& aDragMetrics, LayersId aLayersId,
+ HitTestingTreeNodeAutoLock& aOutThumbNode) {
+ if (!aDragMetrics.mDirection) {
+ // The AsyncDragMetrics has not been initialized yet - there will be
+ // no matching node, so don't bother searching the tree.
+ return;
+ }
+
+ RecursiveMutexAutoLock lock(mTreeLock);
+
+ RefPtr<HitTestingTreeNode> result = DepthFirstSearch<ReverseIterator>(
+ mRootNode.get(), [&aDragMetrics, &aLayersId](HitTestingTreeNode* aNode) {
+ return aNode->MatchesScrollDragMetrics(aDragMetrics, aLayersId);
+ });
+ if (result) {
+ aOutThumbNode.Initialize(lock, result.forget(), mTreeLock);
+ }
+}
+
+APZCTreeManager::TargetApzcForNodeResult APZCTreeManager::GetTargetApzcForNode(
+ const HitTestingTreeNode* aNode) {
+ for (const HitTestingTreeNode* n = aNode;
+ n && n->GetLayersId() == aNode->GetLayersId(); n = n->GetParent()) {
+ // For a fixed node, GetApzc() may return an APZC for content in the
+ // enclosing document, so we need to check GetFixedPosTarget() before
+ // GetApzc().
+ if (n->GetFixedPosTarget() != ScrollableLayerGuid::NULL_SCROLL_ID) {
+ RefPtr<AsyncPanZoomController> fpTarget =
+ GetTargetAPZC(n->GetLayersId(), n->GetFixedPosTarget());
+ APZCTM_LOG("Found target APZC %p using fixed-pos lookup on %" PRIu64 "\n",
+ fpTarget.get(), n->GetFixedPosTarget());
+ return {fpTarget.get(), true};
+ }
+ if (n->GetApzc()) {
+ APZCTM_LOG("Found target %p using ancestor lookup\n", n->GetApzc());
+ return {n->GetApzc(), false};
+ }
+ }
+ return {nullptr, false};
+}
+
+HitTestingTreeNode* APZCTreeManager::FindRootNodeForLayersId(
+ LayersId aLayersId) const {
+ mTreeLock.AssertCurrentThreadIn();
+
+ HitTestingTreeNode* resultNode = BreadthFirstSearch<ReverseIterator>(
+ mRootNode.get(), [aLayersId](HitTestingTreeNode* aNode) {
+ AsyncPanZoomController* apzc = aNode->GetApzc();
+ return apzc && apzc->GetLayersId() == aLayersId &&
+ apzc->IsRootForLayersId();
+ });
+ return resultNode;
+}
+
+already_AddRefed<AsyncPanZoomController> APZCTreeManager::FindZoomableApzc(
+ AsyncPanZoomController* aStart) const {
+ return GetZoomableTarget(aStart, aStart);
+}
+
+ScreenMargin APZCTreeManager::GetCompositorFixedLayerMargins() const {
+ RecursiveMutexAutoLock lock(mTreeLock);
+ return mCompositorFixedLayerMargins;
+}
+
+AsyncPanZoomController* APZCTreeManager::FindRootContentApzcForLayersId(
+ LayersId aLayersId) const {
+ mTreeLock.AssertCurrentThreadIn();
+
+ HitTestingTreeNode* resultNode = BreadthFirstSearch<ReverseIterator>(
+ mRootNode.get(), [aLayersId](HitTestingTreeNode* aNode) {
+ AsyncPanZoomController* apzc = aNode->GetApzc();
+ return apzc && apzc->GetLayersId() == aLayersId &&
+ apzc->IsRootContent();
+ });
+ return resultNode ? resultNode->GetApzc() : nullptr;
+}
+
+// clang-format off
+/* The methods GetScreenToApzcTransform() and GetApzcToGeckoTransform() return
+ some useful transformations that input events may need applied. This is best
+ illustrated with an example. Consider a chain of layers, L, M, N, O, P, Q, R. Layer L
+ is the layer that corresponds to the argument |aApzc|, and layer R is the root
+ of the layer tree. Layer M is the parent of L, N is the parent of M, and so on.
+ When layer L is displayed to the screen by the compositor, the set of transforms that
+ are applied to L are (in order from top to bottom):
+
+ L's CSS transform (hereafter referred to as transform matrix LC)
+ L's nontransient async transform (hereafter referred to as transform matrix LN)
+ L's transient async transform (hereafter referred to as transform matrix LT)
+ M's CSS transform (hereafter referred to as transform matrix MC)
+ M's nontransient async transform (hereafter referred to as transform matrix MN)
+ M's transient async transform (hereafter referred to as transform matrix MT)
+ ...
+ R's CSS transform (hereafter referred to as transform matrix RC)
+ R's nontransient async transform (hereafter referred to as transform matrix RN)
+ R's transient async transform (hereafter referred to as transform matrix RT)
+
+ Also, for any layer, the async transform is the combination of its transient and non-transient
+ parts. That is, for any layer L:
+ LA === LN * LT
+ LA.Inverse() === LT.Inverse() * LN.Inverse()
+
+ If we want user input to modify L's transient async transform, we have to first convert
+ user input from screen space to the coordinate space of L's transient async transform. Doing
+ this involves applying the following transforms (in order from top to bottom):
+ RT.Inverse()
+ RN.Inverse()
+ RC.Inverse()
+ ...
+ MT.Inverse()
+ MN.Inverse()
+ MC.Inverse()
+ This combined transformation is returned by GetScreenToApzcTransform().
+
+ Next, if we want user inputs sent to gecko for event-dispatching, we will need to strip
+ out all of the async transforms that are involved in this chain. This is because async
+ transforms are stored only in the compositor and gecko does not account for them when
+ doing display-list-based hit-testing for event dispatching.
+ Furthermore, because these input events are processed by Gecko in a FIFO queue that
+ includes other things (specifically paint requests), it is possible that by time the
+ input event reaches gecko, it will have painted something else. Therefore, we need to
+ apply another transform to the input events to account for the possible disparity between
+ what we know gecko last painted and the last paint request we sent to gecko. Let this
+ transform be represented by LD, MD, ... RD.
+ Therefore, given a user input in screen space, the following transforms need to be applied
+ (in order from top to bottom):
+ RT.Inverse()
+ RN.Inverse()
+ RC.Inverse()
+ ...
+ MT.Inverse()
+ MN.Inverse()
+ MC.Inverse()
+ LT.Inverse()
+ LN.Inverse()
+ LC.Inverse()
+ LC
+ LD
+ MC
+ MD
+ ...
+ RC
+ RD
+ This sequence can be simplified and refactored to the following:
+ GetScreenToApzcTransform()
+ LA.Inverse()
+ LD
+ MC
+ MD
+ ...
+ RC
+ RD
+ Since GetScreenToApzcTransform() can be obtained by calling that function, GetApzcToGeckoTransform()
+ returns the remaining transforms (LA.Inverse() * LD * ... * RD), so that the caller code can
+ combine it with GetScreenToApzcTransform() to get the final transform required in this case.
+
+ Note that for many of these layers, there will be no AsyncPanZoomController attached, and
+ so the async transform will be the identity transform. So, in the example above, if layers
+ L and P have APZC instances attached, MT, MN, MD, NT, NN, ND, OT, ON, OD, QT, QN, QD, RT,
+ RN and RD will be identity transforms.
+ Additionally, for space-saving purposes, each APZC instance stores its layer's individual
+ CSS transform and the accumulation of CSS transforms to its parent APZC. So the APZC for
+ layer L would store LC and (MC * NC * OC), and the layer P would store PC and (QC * RC).
+ The APZC instances track the last dispatched paint request and so are able to calculate LD and
+ PD using those internally stored values.
+ The APZCs also obviously have LT, LN, PT, and PN, so all of the above transformation combinations
+ required can be generated.
+ */
+// clang-format on
+
+/*
+ * See the long comment above for a detailed explanation of this function.
+ */
+ScreenToParentLayerMatrix4x4 APZCTreeManager::GetScreenToApzcTransform(
+ const AsyncPanZoomController* aApzc) const {
+ Matrix4x4 result;
+ RecursiveMutexAutoLock lock(mTreeLock);
+
+ // The comments below assume there is a chain of layers L..R with L and P
+ // having APZC instances as explained in the comment above. This function is
+ // called with aApzc at L, and the loop below performs one iteration, where
+ // parent is at P. The comments explain what values are stored in the
+ // variables at these two levels. All the comments use standard matrix
+ // notation where the leftmost matrix in a multiplication is applied first.
+
+ // ancestorUntransform is PC.Inverse() * OC.Inverse() * NC.Inverse() *
+ // MC.Inverse()
+ Matrix4x4 ancestorUntransform = aApzc->GetAncestorTransform().Inverse();
+
+ // result is initialized to PC.Inverse() * OC.Inverse() * NC.Inverse() *
+ // MC.Inverse()
+ result = ancestorUntransform;
+
+ for (AsyncPanZoomController* parent = aApzc->GetParent(); parent;
+ parent = parent->GetParent()) {
+ // ancestorUntransform is updated to RC.Inverse() * QC.Inverse() when parent
+ // == P
+ ancestorUntransform = parent->GetAncestorTransform().Inverse();
+ // asyncUntransform is updated to PA.Inverse() when parent == P
+ Matrix4x4 asyncUntransform = parent
+ ->GetCurrentAsyncTransformWithOverscroll(
+ AsyncPanZoomController::eForHitTesting)
+ .Inverse()
+ .ToUnknownMatrix();
+ // untransformSinceLastApzc is RC.Inverse() * QC.Inverse() * PA.Inverse()
+ Matrix4x4 untransformSinceLastApzc = ancestorUntransform * asyncUntransform;
+
+ // result is RC.Inverse() * QC.Inverse() * PA.Inverse() * PC.Inverse() *
+ // OC.Inverse() * NC.Inverse() * MC.Inverse()
+ result = untransformSinceLastApzc * result;
+
+ // The above value for result when parent == P matches the required output
+ // as explained in the comment above this method. Note that any missing
+ // terms are guaranteed to be identity transforms.
+ }
+
+ return ViewAs<ScreenToParentLayerMatrix4x4>(result);
+}
+
+/*
+ * See the long comment above GetScreenToApzcTransform() for a detailed
+ * explanation of this function.
+ */
+ParentLayerToScreenMatrix4x4 APZCTreeManager::GetApzcToGeckoTransform(
+ const AsyncPanZoomController* aApzc,
+ const AsyncTransformComponents& aComponents) const {
+ Matrix4x4 result;
+ RecursiveMutexAutoLock lock(mTreeLock);
+
+ // The comments below assume there is a chain of layers L..R with L and P
+ // having APZC instances as explained in the comment above. This function is
+ // called with aApzc at L, and the loop below performs one iteration, where
+ // parent is at P. The comments explain what values are stored in the
+ // variables at these two levels. All the comments use standard matrix
+ // notation where the leftmost matrix in a multiplication is applied first.
+
+ // asyncUntransform is LA.Inverse()
+ Matrix4x4 asyncUntransform =
+ aApzc
+ ->GetCurrentAsyncTransformWithOverscroll(
+ AsyncPanZoomController::eForHitTesting, aComponents)
+ .Inverse()
+ .ToUnknownMatrix();
+
+ // aTransformToGeckoOut is initialized to LA.Inverse() * LD * MC * NC * OC *
+ // PC
+ result = asyncUntransform *
+ aApzc->GetTransformToLastDispatchedPaint(aComponents) *
+ aApzc->GetAncestorTransform();
+
+ for (AsyncPanZoomController* parent = aApzc->GetParent(); parent;
+ parent = parent->GetParent()) {
+ // aTransformToGeckoOut is LA.Inverse() * LD * MC * NC * OC * PC * PD * QC *
+ // RC
+ //
+ // Note: Do not pass the async transform components for the current target
+ // to the parent.
+ result = result *
+ parent->GetTransformToLastDispatchedPaint(LayoutAndVisual) *
+ parent->GetAncestorTransform();
+
+ // The above value for result when parent == P matches the required output
+ // as explained in the comment above this method. Note that any missing
+ // terms are guaranteed to be identity transforms.
+ }
+
+ return ViewAs<ParentLayerToScreenMatrix4x4>(result);
+}
+
+ParentLayerToScreenMatrix4x4 APZCTreeManager::GetApzcToGeckoTransformForHit(
+ HitTestResult& aHitResult) const {
+ // Fixed content is only subject to the visual component of the async
+ // transform.
+ AsyncTransformComponents components =
+ aHitResult.mFixedPosSides == SideBits::eNone
+ ? LayoutAndVisual
+ : AsyncTransformComponents{AsyncTransformComponent::eVisual};
+ return GetApzcToGeckoTransform(aHitResult.mTargetApzc, components);
+}
+
+ScreenPoint APZCTreeManager::GetCurrentMousePosition() const {
+ auto pos = mCurrentMousePosition.Lock();
+ return pos.ref();
+}
+
+void APZCTreeManager::SetCurrentMousePosition(const ScreenPoint& aNewPos) {
+ auto pos = mCurrentMousePosition.Lock();
+ pos.ref() = aNewPos;
+}
+
+static AsyncPanZoomController* GetApzcWithDifferentLayersIdByWalkingParents(
+ AsyncPanZoomController* aApzc) {
+ if (!aApzc) {
+ return nullptr;
+ }
+ AsyncPanZoomController* parent = aApzc->GetParent();
+ while (parent && (parent->GetLayersId() == aApzc->GetLayersId())) {
+ parent = parent->GetParent();
+ }
+ return parent;
+}
+
+already_AddRefed<AsyncPanZoomController> APZCTreeManager::GetZoomableTarget(
+ AsyncPanZoomController* aApzc1, AsyncPanZoomController* aApzc2) const {
+ RecursiveMutexAutoLock lock(mTreeLock);
+ RefPtr<AsyncPanZoomController> apzc;
+ // For now, we only ever want to do pinching on the root-content APZC for
+ // a given layers id.
+ if (aApzc1 && aApzc2 && aApzc1->GetLayersId() == aApzc2->GetLayersId()) {
+ // If the two APZCs have the same layers id, find the root-content APZC
+ // for that layers id. Don't call CommonAncestor() because there may not
+ // be a common ancestor for the layers id (e.g. if one APZCs is inside a
+ // fixed-position element).
+ apzc = FindRootContentApzcForLayersId(aApzc1->GetLayersId());
+ if (apzc) {
+ return apzc.forget();
+ }
+ }
+
+ // Otherwise, find the common ancestor (to reach a common layers id), and then
+ // walk up the apzc tree until we find a root-content APZC.
+ apzc = CommonAncestor(aApzc1, aApzc2);
+ RefPtr<AsyncPanZoomController> zoomable;
+ while (apzc && !zoomable) {
+ zoomable = FindRootContentApzcForLayersId(apzc->GetLayersId());
+ apzc = GetApzcWithDifferentLayersIdByWalkingParents(apzc);
+ }
+
+ return zoomable.forget();
+}
+
+Maybe<ScreenIntPoint> APZCTreeManager::ConvertToGecko(
+ const ScreenIntPoint& aPoint, AsyncPanZoomController* aApzc) {
+ RecursiveMutexAutoLock lock(mTreeLock);
+ // TODO: The current check assumes that a touch gesture and a touchpad tap
+ // gesture can't both be active at the same time. If we turn on double-tap-
+ // to-zoom on a touchscreen platform like Windows or Linux, this assumption
+ // would no longer be valid, and we'd have to instead have TapGestureInput
+ // track and inform this function whether it was created from touch events.
+ const HitTestResult& hit = mInputQueue->GetCurrentTouchBlock()
+ ? mTouchBlockHitResult
+ : mTapGestureHitResult;
+ AsyncTransformComponents components =
+ hit.mFixedPosSides == SideBits::eNone
+ ? LayoutAndVisual
+ : AsyncTransformComponents{AsyncTransformComponent::eVisual};
+ ScreenToScreenMatrix4x4 transformScreenToGecko =
+ GetScreenToApzcTransform(aApzc) *
+ GetApzcToGeckoTransform(aApzc, components);
+ Maybe<ScreenIntPoint> geckoPoint =
+ UntransformBy(transformScreenToGecko, aPoint);
+ if (geckoPoint) {
+ AdjustEventPointForDynamicToolbar(*geckoPoint, hit);
+ }
+ return geckoPoint;
+}
+
+already_AddRefed<AsyncPanZoomController> APZCTreeManager::CommonAncestor(
+ AsyncPanZoomController* aApzc1, AsyncPanZoomController* aApzc2) const {
+ mTreeLock.AssertCurrentThreadIn();
+ RefPtr<AsyncPanZoomController> ancestor;
+
+ // If either aApzc1 or aApzc2 is null, min(depth1, depth2) will be 0 and this
+ // function will return null.
+
+ // Calculate depth of the APZCs in the tree
+ int depth1 = 0, depth2 = 0;
+ for (AsyncPanZoomController* parent = aApzc1; parent;
+ parent = parent->GetParent()) {
+ depth1++;
+ }
+ for (AsyncPanZoomController* parent = aApzc2; parent;
+ parent = parent->GetParent()) {
+ depth2++;
+ }
+
+ // At most one of the following two loops will be executed; the deeper APZC
+ // pointer will get walked up to the depth of the shallower one.
+ int minDepth = depth1 < depth2 ? depth1 : depth2;
+ while (depth1 > minDepth) {
+ depth1--;
+ aApzc1 = aApzc1->GetParent();
+ }
+ while (depth2 > minDepth) {
+ depth2--;
+ aApzc2 = aApzc2->GetParent();
+ }
+
+ // Walk up the ancestor chains of both APZCs, always staying at the same depth
+ // for either APZC, and return the the first common ancestor encountered.
+ while (true) {
+ if (aApzc1 == aApzc2) {
+ ancestor = aApzc1;
+ break;
+ }
+ if (depth1 <= 0) {
+ break;
+ }
+ aApzc1 = aApzc1->GetParent();
+ aApzc2 = aApzc2->GetParent();
+ }
+ return ancestor.forget();
+}
+
+bool APZCTreeManager::IsFixedToRootContent(
+ const HitTestingTreeNode* aNode) const {
+ MutexAutoLock lock(mMapLock);
+ return IsFixedToRootContent(FixedPositionInfo(aNode), lock);
+}
+
+bool APZCTreeManager::IsFixedToRootContent(
+ const FixedPositionInfo& aFixedInfo,
+ const MutexAutoLock& aProofOfMapLock) const {
+ ScrollableLayerGuid::ViewID fixedTarget = aFixedInfo.mFixedPosTarget;
+ if (fixedTarget == ScrollableLayerGuid::NULL_SCROLL_ID) {
+ return false;
+ }
+ auto it =
+ mApzcMap.find(ScrollableLayerGuid(aFixedInfo.mLayersId, 0, fixedTarget));
+ if (it == mApzcMap.end()) {
+ return false;
+ }
+ RefPtr<AsyncPanZoomController> targetApzc = it->second.apzc;
+ return targetApzc && targetApzc->IsRootContent();
+}
+
+SideBits APZCTreeManager::SidesStuckToRootContent(
+ const HitTestingTreeNode* aNode) const {
+ MutexAutoLock lock(mMapLock);
+ return SidesStuckToRootContent(StickyPositionInfo(aNode), lock);
+}
+
+SideBits APZCTreeManager::SidesStuckToRootContent(
+ const StickyPositionInfo& aStickyInfo,
+ const MutexAutoLock& aProofOfMapLock) const {
+ SideBits result = SideBits::eNone;
+
+ ScrollableLayerGuid::ViewID stickyTarget = aStickyInfo.mStickyPosTarget;
+ if (stickyTarget == ScrollableLayerGuid::NULL_SCROLL_ID) {
+ return result;
+ }
+
+ // We support the dynamic toolbar at top and bottom.
+ if ((aStickyInfo.mFixedPosSides & SideBits::eTopBottom) == SideBits::eNone) {
+ return result;
+ }
+
+ auto it = mApzcMap.find(
+ ScrollableLayerGuid(aStickyInfo.mLayersId, 0, stickyTarget));
+ if (it == mApzcMap.end()) {
+ return result;
+ }
+ RefPtr<AsyncPanZoomController> stickyTargetApzc = it->second.apzc;
+ if (!stickyTargetApzc || !stickyTargetApzc->IsRootContent()) {
+ return result;
+ }
+
+ ParentLayerPoint translation =
+ stickyTargetApzc
+ ->GetCurrentAsyncTransform(
+ AsyncPanZoomController::eForHitTesting,
+ AsyncTransformComponents{AsyncTransformComponent::eLayout})
+ .mTranslation;
+
+ if (apz::IsStuckAtTop(translation.y, aStickyInfo.mStickyScrollRangeInner,
+ aStickyInfo.mStickyScrollRangeOuter)) {
+ result |= SideBits::eTop;
+ }
+ if (apz::IsStuckAtBottom(translation.y, aStickyInfo.mStickyScrollRangeInner,
+ aStickyInfo.mStickyScrollRangeOuter)) {
+ result |= SideBits::eBottom;
+ }
+ return result;
+}
+
+LayerToParentLayerMatrix4x4 APZCTreeManager::ComputeTransformForNode(
+ const HitTestingTreeNode* aNode) const {
+ mTreeLock.AssertCurrentThreadIn();
+ // The async transforms applied here for hit-testing purposes, are intended
+ // to match the ones AsyncCompositionManager (or equivalent WebRender code)
+ // applies for rendering purposes.
+ // Note that with containerless scrolling, the layer structure looks like
+ // this:
+ //
+ // root container layer
+ // async zoom container layer
+ // scrollable content layers (with scroll metadata)
+ // fixed content layers (no scroll metadta, annotated isFixedPosition)
+ // scrollbar layers
+ //
+ // The intended async transforms in this case are:
+ // * On the async zoom container layer, the "visual" portion of the root
+ // content APZC's async transform (which includes the zoom, and async
+ // scrolling of the visual viewport relative to the layout viewport).
+ // * On the scrollable layers bearing the root content APZC's scroll
+ // metadata, the "layout" portion of the root content APZC's async
+ // transform (which includes async scrolling of the layout viewport
+ // relative to the scrollable rect origin).
+ if (AsyncPanZoomController* apzc = aNode->GetApzc()) {
+ // If the node represents scrollable content, apply the async transform
+ // from its APZC.
+ bool visualTransformIsInheritedFromAncestor =
+ /* we're the APZC whose visual transform might be on the async
+ zoom container */
+ apzc->IsRootContent() &&
+ /* there is an async zoom container on this subtree */
+ mAsyncZoomContainerSubtree == Some(aNode->GetLayersId()) &&
+ /* it's not us */
+ !aNode->GetAsyncZoomContainerId();
+ AsyncTransformComponents components =
+ visualTransformIsInheritedFromAncestor
+ ? AsyncTransformComponents{AsyncTransformComponent::eLayout}
+ : LayoutAndVisual;
+ return aNode->GetTransform() *
+ CompleteAsyncTransform(apzc->GetCurrentAsyncTransformWithOverscroll(
+ AsyncPanZoomController::eForHitTesting, components));
+ } else if (aNode->GetAsyncZoomContainerId()) {
+ if (AsyncPanZoomController* rootContent =
+ FindRootContentApzcForLayersId(aNode->GetLayersId())) {
+ return aNode->GetTransform() *
+ CompleteAsyncTransform(
+ rootContent->GetCurrentAsyncTransformWithOverscroll(
+ AsyncPanZoomController::eForHitTesting,
+ {AsyncTransformComponent::eVisual}));
+ }
+ } else if (aNode->IsScrollThumbNode()) {
+ // If the node represents a scrollbar thumb, compute and apply the
+ // transformation that will be applied to the thumb in
+ // AsyncCompositionManager.
+ ScrollableLayerGuid guid{aNode->GetLayersId(), 0,
+ aNode->GetScrollTargetId()};
+ if (RefPtr<HitTestingTreeNode> scrollTargetNode = GetTargetNode(
+ guid, &ScrollableLayerGuid::EqualsIgnoringPresShell)) {
+ AsyncPanZoomController* scrollTargetApzc = scrollTargetNode->GetApzc();
+ MOZ_ASSERT(scrollTargetApzc);
+ return scrollTargetApzc->CallWithLastContentPaintMetrics(
+ [&](const FrameMetrics& aMetrics) {
+ return ComputeTransformForScrollThumb(
+ aNode->GetTransform() * AsyncTransformMatrix(),
+ scrollTargetNode->GetTransform().ToUnknownMatrix(),
+ scrollTargetApzc, aMetrics, aNode->GetScrollbarData(),
+ scrollTargetNode->IsAncestorOf(aNode));
+ });
+ }
+ } else if (IsFixedToRootContent(aNode)) {
+ ParentLayerPoint translation;
+ {
+ MutexAutoLock mapLock(mMapLock);
+ translation = ViewAs<ParentLayerPixel>(
+ apz::ComputeFixedMarginsOffset(
+ GetCompositorFixedLayerMargins(mapLock),
+ aNode->GetFixedPosSides(), mGeckoFixedLayerMargins),
+ PixelCastJustification::ScreenIsParentLayerForRoot);
+ }
+ return aNode->GetTransform() *
+ CompleteAsyncTransform(
+ AsyncTransformComponentMatrix::Translation(translation));
+ }
+ SideBits sides = SidesStuckToRootContent(aNode);
+ if (sides != SideBits::eNone) {
+ ParentLayerPoint translation;
+ {
+ MutexAutoLock mapLock(mMapLock);
+ translation = ViewAs<ParentLayerPixel>(
+ apz::ComputeFixedMarginsOffset(
+ GetCompositorFixedLayerMargins(mapLock), sides,
+ // For sticky layers, we don't need to factor
+ // mGeckoFixedLayerMargins because Gecko doesn't shift the
+ // position of sticky elements for dynamic toolbar movements.
+ ScreenMargin()),
+ PixelCastJustification::ScreenIsParentLayerForRoot);
+ }
+ return aNode->GetTransform() *
+ CompleteAsyncTransform(
+ AsyncTransformComponentMatrix::Translation(translation));
+ }
+ // Otherwise, the node does not have an async transform.
+ return aNode->GetTransform() * AsyncTransformMatrix();
+}
+
+already_AddRefed<wr::WebRenderAPI> APZCTreeManager::GetWebRenderAPI() const {
+ RefPtr<wr::WebRenderAPI> api;
+ CompositorBridgeParent::CallWithIndirectShadowTree(
+ mRootLayersId, [&](LayerTreeState& aState) -> void {
+ if (aState.mWrBridge) {
+ api = aState.mWrBridge->GetWebRenderAPI();
+ }
+ });
+ return api.forget();
+}
+
+/*static*/
+already_AddRefed<GeckoContentController> APZCTreeManager::GetContentController(
+ LayersId aLayersId) {
+ RefPtr<GeckoContentController> controller;
+ CompositorBridgeParent::CallWithIndirectShadowTree(
+ aLayersId,
+ [&](LayerTreeState& aState) -> void { controller = aState.mController; });
+ return controller.forget();
+}
+
+ScreenMargin APZCTreeManager::GetCompositorFixedLayerMargins(
+ const MutexAutoLock& aProofOfMapLock) const {
+ ScreenMargin result = mCompositorFixedLayerMargins;
+ if (StaticPrefs::apz_fixed_margin_override_enabled()) {
+ result.top = StaticPrefs::apz_fixed_margin_override_top();
+ result.bottom = StaticPrefs::apz_fixed_margin_override_bottom();
+ }
+ return result;
+}
+
+bool APZCTreeManager::GetAPZTestData(LayersId aLayersId,
+ APZTestData* aOutData) {
+ AssertOnUpdaterThread();
+
+ { // copy the relevant test data into aOutData while holding the
+ // mTestDataLock
+ MutexAutoLock lock(mTestDataLock);
+ auto it = mTestData.find(aLayersId);
+ if (it == mTestData.end()) {
+ return false;
+ }
+ *aOutData = *(it->second);
+ }
+
+ { // add some additional "current state" into the returned APZTestData
+ MutexAutoLock mapLock(mMapLock);
+
+ ClippedCompositionBoundsMap clippedCompBounds;
+ for (const auto& mapping : mApzcMap) {
+ if (mapping.first.mLayersId != aLayersId) {
+ continue;
+ }
+
+ ParentLayerRect clippedBounds = ComputeClippedCompositionBounds(
+ mapLock, clippedCompBounds, mapping.first);
+ AsyncPanZoomController* apzc = mapping.second.apzc;
+ std::string viewId = std::to_string(mapping.first.mScrollId);
+ std::string apzcState;
+ if (apzc->GetCheckerboardMagnitude(clippedBounds)) {
+ apzcState += "checkerboarding,";
+ }
+ if (apzc->IsOverscrolled()) {
+ apzcState += "overscrolled,";
+ }
+ aOutData->RecordAdditionalData(viewId, apzcState);
+ }
+ }
+ return true;
+}
+
+void APZCTreeManager::SendSubtreeTransformsToChromeMainThread(
+ const AsyncPanZoomController* aAncestor) {
+ RefPtr<GeckoContentController> controller =
+ GetContentController(mRootLayersId);
+ if (!controller) {
+ return;
+ }
+ nsTArray<MatrixMessage> messages;
+ bool underAncestor = (aAncestor == nullptr);
+ bool shouldNotify = false;
+ {
+ RecursiveMutexAutoLock lock(mTreeLock);
+ if (!mRootNode) {
+ // Event dispatched during shutdown, after ClearTree().
+ // Note, mRootNode needs to be checked with mTreeLock held.
+ return;
+ }
+ // This formulation duplicates matrix multiplications closer
+ // to the root of the tree. For now, aiming for separation
+ // of concerns rather than minimum number of multiplications.
+ ForEachNode<ReverseIterator>(
+ mRootNode.get(),
+ [&](HitTestingTreeNode* aNode) {
+ mTreeLock.AssertCurrentThreadIn();
+ bool atAncestor = (aAncestor && aNode->GetApzc() == aAncestor);
+ MOZ_ASSERT(!(underAncestor && atAncestor));
+ underAncestor |= atAncestor;
+ if (!underAncestor) {
+ return;
+ }
+ LayersId layersId = aNode->GetLayersId();
+ HitTestingTreeNode* parent = aNode->GetParent();
+ if (!parent) {
+ messages.AppendElement(MatrixMessage(Some(LayerToScreenMatrix4x4()),
+ ScreenRect(), layersId));
+ } else if (layersId != parent->GetLayersId()) {
+ if (mDetachedLayersIds.find(layersId) != mDetachedLayersIds.end()) {
+ messages.AppendElement(
+ MatrixMessage(Nothing(), ScreenRect(), layersId));
+ } else {
+ messages.AppendElement(MatrixMessage(
+ Some(parent->GetTransformToGecko()),
+ parent->GetRemoteDocumentScreenRect(), layersId));
+ }
+ }
+ },
+ [&](HitTestingTreeNode* aNode) {
+ bool atAncestor = (aAncestor && aNode->GetApzc() == aAncestor);
+ if (atAncestor) {
+ MOZ_ASSERT(underAncestor);
+ underAncestor = false;
+ }
+ });
+ if (messages != mLastMessages) {
+ mLastMessages = messages;
+ shouldNotify = true;
+ }
+ }
+ if (shouldNotify) {
+ controller->NotifyLayerTransforms(std::move(messages));
+ }
+}
+
+void APZCTreeManager::SetFixedLayerMargins(ScreenIntCoord aTop,
+ ScreenIntCoord aBottom) {
+ MutexAutoLock lock(mMapLock);
+ mCompositorFixedLayerMargins.top = aTop;
+ mCompositorFixedLayerMargins.bottom = aBottom;
+}
+
+/*static*/
+LayerToParentLayerMatrix4x4 APZCTreeManager::ComputeTransformForScrollThumb(
+ const LayerToParentLayerMatrix4x4& aCurrentTransform,
+ const Matrix4x4& aScrollableContentTransform, AsyncPanZoomController* aApzc,
+ const FrameMetrics& aMetrics, const ScrollbarData& aScrollbarData,
+ bool aScrollbarIsDescendant) {
+ return apz::ComputeTransformForScrollThumb(
+ aCurrentTransform, aScrollableContentTransform, aApzc, aMetrics,
+ aScrollbarData, aScrollbarIsDescendant);
+}
+
+APZSampler* APZCTreeManager::GetSampler() const {
+ // We should always have a sampler here, since in practice the sampler
+ // is destroyed at the same time that this APZCTreeMAnager instance is.
+ MOZ_ASSERT(mSampler);
+ return mSampler;
+}
+
+void APZCTreeManager::AssertOnSamplerThread() {
+ GetSampler()->AssertOnSamplerThread();
+}
+
+APZUpdater* APZCTreeManager::GetUpdater() const {
+ // We should always have an updater here, since in practice the updater
+ // is destroyed at the same time that this APZCTreeManager instance is.
+ MOZ_ASSERT(mUpdater);
+ return mUpdater;
+}
+
+void APZCTreeManager::AssertOnUpdaterThread() {
+ GetUpdater()->AssertOnUpdaterThread();
+}
+
+MOZ_PUSH_IGNORE_THREAD_SAFETY
+void APZCTreeManager::LockTree() {
+ AssertOnUpdaterThread();
+ mTreeLock.Lock();
+}
+
+void APZCTreeManager::UnlockTree() {
+ AssertOnUpdaterThread();
+ mTreeLock.Unlock();
+}
+MOZ_POP_THREAD_SAFETY
+
+void APZCTreeManager::SetDPI(float aDpiValue) {
+ if (!APZThreadUtils::IsControllerThread()) {
+ APZThreadUtils::RunOnControllerThread(
+ NewRunnableMethod<float>("layers::APZCTreeManager::SetDPI", this,
+ &APZCTreeManager::SetDPI, aDpiValue));
+ return;
+ }
+
+ APZThreadUtils::AssertOnControllerThread();
+ mDPI = aDpiValue;
+}
+
+float APZCTreeManager::GetDPI() const {
+ APZThreadUtils::AssertOnControllerThread();
+ return mDPI;
+}
+
+APZCTreeManager::FixedPositionInfo::FixedPositionInfo(
+ const HitTestingTreeNode* aNode) {
+ mFixedPositionAnimationId = aNode->GetFixedPositionAnimationId();
+ mFixedPosSides = aNode->GetFixedPosSides();
+ mFixedPosTarget = aNode->GetFixedPosTarget();
+ mLayersId = aNode->GetLayersId();
+}
+
+APZCTreeManager::StickyPositionInfo::StickyPositionInfo(
+ const HitTestingTreeNode* aNode) {
+ mStickyPositionAnimationId = aNode->GetStickyPositionAnimationId();
+ mFixedPosSides = aNode->GetFixedPosSides();
+ mStickyPosTarget = aNode->GetStickyPosTarget();
+ mLayersId = aNode->GetLayersId();
+ mStickyScrollRangeInner = aNode->GetStickyScrollRangeInner();
+ mStickyScrollRangeOuter = aNode->GetStickyScrollRangeOuter();
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/apz/src/APZCTreeManager.h b/gfx/layers/apz/src/APZCTreeManager.h
new file mode 100644
index 0000000000..fb0e98e350
--- /dev/null
+++ b/gfx/layers/apz/src/APZCTreeManager.h
@@ -0,0 +1,1064 @@
+/* -*- 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_APZCTreeManager_h
+#define mozilla_layers_APZCTreeManager_h
+
+#include <unordered_map> // for std::unordered_map
+
+#include "FocusState.h" // for FocusState
+#include "HitTestingTreeNode.h" // for HitTestingTreeNodeAutoLock
+#include "IAPZHitTester.h" // for IAPZHitTester::HitTestResult
+#include "gfxPoint.h" // for gfxPoint
+#include "mozilla/Assertions.h" // for MOZ_ASSERT_HELPER2
+#include "mozilla/DataMutex.h" // for DataMutex
+#include "mozilla/gfx/CompositorHitTestInfo.h"
+#include "mozilla/gfx/Logging.h" // for gfx::TreeLog
+#include "mozilla/gfx/Matrix.h" // for Matrix4x4
+#include "mozilla/layers/APZInputBridge.h" // for APZInputBridge
+#include "mozilla/layers/APZTestData.h" // for APZTestData
+#include "mozilla/layers/APZUtils.h" // for GeckoViewMetrics
+#include "mozilla/layers/IAPZCTreeManager.h" // for IAPZCTreeManager
+#include "mozilla/layers/ScrollbarData.h"
+#include "mozilla/layers/LayersTypes.h"
+#include "mozilla/layers/KeyboardMap.h" // for KeyboardMap
+#include "mozilla/layers/TouchCounter.h" // for TouchCounter
+#include "mozilla/layers/ZoomConstraints.h" // for ZoomConstraints
+#include "mozilla/webrender/webrender_ffi.h"
+#include "mozilla/RecursiveMutex.h" // for RecursiveMutex
+#include "mozilla/RefPtr.h" // for RefPtr
+#include "mozilla/TimeStamp.h" // for mozilla::TimeStamp
+#include "mozilla/UniquePtr.h" // for UniquePtr
+#include "nsCOMPtr.h" // for already_AddRefed
+#include "nsTArray.h"
+
+namespace mozilla {
+class MultiTouchInput;
+
+namespace wr {
+class TransactionWrapper;
+class WebRenderAPI;
+} // namespace wr
+
+namespace layers {
+
+class Layer;
+class AsyncPanZoomController;
+class APZCTreeManagerParent;
+class APZSampler;
+class APZUpdater;
+class CompositorBridgeParent;
+class OverscrollHandoffChain;
+struct OverscrollHandoffState;
+class FocusTarget;
+struct FlingHandoffState;
+class InputQueue;
+struct InputBlockCallbackInfo;
+class GeckoContentController;
+class HitTestingTreeNode;
+class SampleTime;
+class WebRenderScrollDataWrapper;
+struct AncestorTransform;
+struct ScrollThumbData;
+struct ZoomTarget;
+
+/**
+ * ****************** NOTE ON LOCK ORDERING IN APZ **************************
+ *
+ * To avoid deadlock, APZ imposes and respects a global ordering on threads
+ * and locks relevant to APZ.
+ *
+ * Please see the "Threading / Locking Overview" section of
+ * gfx/docs/AsyncPanZoom.rst (hosted in rendered form at
+ * https://firefox-source-docs.mozilla.org/gfx/gfx/AsyncPanZoom.html#threading-locking-overview)
+ * for what the ordering is, and what are the rules for respecting it.
+ * **************************************************************************
+ */
+
+/**
+ * This class manages the tree of AsyncPanZoomController instances. There is one
+ * instance of this class owned by each CompositorBridgeParent, and it contains
+ * as many AsyncPanZoomController instances as there are scrollable container
+ * layers. This class generally lives on the updater thread, although some
+ * functions may be called from other threads as noted; thread safety is ensured
+ * internally.
+ *
+ * The bulk of the work of this class happens as part of the
+ * UpdateHitTestingTree function, which is when a layer tree update is received
+ * by the compositor. This function walks through the layer tree and creates a
+ * tree of HitTestingTreeNode instances to match the layer tree and for use in
+ * hit-testing on the controller thread. APZC instances may be preserved across
+ * calls to this function if the corresponding layers are still present in the
+ * layer tree.
+ *
+ * The other functions on this class are used by various pieces of client code
+ * to notify the APZC instances of events relevant to them. This includes, for
+ * example, user input events that drive panning and zooming, changes to the
+ * scroll viewport area, and changes to pan/zoom constraints.
+ *
+ * Note that the ClearTree function MUST be called when this class is no longer
+ * needed; see the method documentation for details.
+ *
+ * Behaviour of APZ is controlled by a number of preferences shown
+ * \ref APZCPrefs "here".
+ */
+class APZCTreeManager : public IAPZCTreeManager, public APZInputBridge {
+ typedef mozilla::layers::AllowedTouchBehavior AllowedTouchBehavior;
+ typedef mozilla::layers::AsyncDragMetrics AsyncDragMetrics;
+ using HitTestResult = IAPZHitTester::HitTestResult;
+
+ /**
+ * A result from APZCTreeManager::FindHandoffParent.
+ */
+ struct TargetApzcForNodeResult {
+ // The APZC to handoff overscroll to.
+ AsyncPanZoomController* mApzc;
+ // Targeting a document's root APZC from content fixed to the document.
+ bool mIsFixed;
+ };
+
+ // Helper struct to hold some state while we build the hit-testing tree. The
+ // sole purpose of this struct is to shorten the argument list to
+ // UpdateHitTestingTree. All the state that we don't need to
+ // push on the stack during recursion and pop on unwind is stored here.
+ struct TreeBuildingState;
+
+ public:
+ explicit APZCTreeManager(LayersId aRootLayersId,
+ UniquePtr<IAPZHitTester> aHitTester = nullptr);
+
+ static mozilla::LazyLogModule sLog;
+
+ void SetSampler(APZSampler* aSampler);
+ void SetUpdater(APZUpdater* aUpdater);
+
+ /**
+ * Notifies this APZCTreeManager that the associated compositor is now
+ * responsible for managing another layers id, which got moved over from
+ * some other compositor. That other compositor's APZCTreeManager is also
+ * provided. This allows APZCTreeManager to transfer any necessary state
+ * from the old APZCTreeManager related to that layers id.
+ * This function must be called on the updater thread.
+ */
+ void NotifyLayerTreeAdopted(LayersId aLayersId,
+ const RefPtr<APZCTreeManager>& aOldTreeManager);
+
+ /**
+ * Notifies this APZCTreeManager that a layer tree being managed by the
+ * associated compositor has been removed/destroyed. Note that this does
+ * NOT get called during shutdown situations, when the root layer tree is
+ * also getting destroyed.
+ * This function must be called on the updater thread.
+ */
+ void NotifyLayerTreeRemoved(LayersId aLayersId);
+
+ /**
+ * Rebuild the focus state based on the focus target from the layer tree
+ * update that just occurred. This must be called on the updater thread.
+ *
+ * @param aRootLayerTreeId The layer tree ID of the root layer corresponding
+ * to this APZCTreeManager
+ * @param aOriginatingLayersId The layer tree ID of the layer corresponding to
+ * this layer tree update.
+ */
+ void UpdateFocusState(LayersId aRootLayerTreeId,
+ LayersId aOriginatingLayersId,
+ const FocusTarget& aFocusTarget);
+
+ /**
+ * Rebuild the hit-testing tree based on an incoming WebRender transaction.
+ * Preserve nodes and APZC instances where possible, but retire those whose
+ * layers are no longer in the layer tree.
+ * (Note: "layer tree" here refers to the tree of WebRenderLayerScrollData
+ * nodes sent as part of a WebRender transaction.)
+ *
+ * This must be called on the updater thread.
+ *
+ * @param aRoot The root of the (full) layer tree
+ * @param aOriginatingLayersId The layers id of the subtree that triggered
+ * this repaint, and to which aIsFirstPaint
+ * applies.
+ * @param aIsFirstPaint True if the transaction that this is called in
+ * response to included a first-paint. If this is true,
+ * the part of the tree that is affected by the
+ * first-paint flag is indicated by the
+ * aOriginatingLayersId parameter.
+ * @param aPaintSequenceNumber The sequence number of the paint that triggered
+ * this layer update. Note that every child
+ * process' layer subtree has its own sequence
+ * numbers.
+ */
+ void UpdateHitTestingTree(const WebRenderScrollDataWrapper& aRoot,
+ bool aIsFirstPaint, LayersId aOriginatingLayersId,
+ uint32_t aPaintSequenceNumber);
+
+ /**
+ * Called when webrender is enabled, from the sampler thread. This function
+ * populates the provided transaction with any async scroll offsets needed.
+ * It also advances APZ animations to the specified sample time, and requests
+ * another composite if there are still active animations.
+ * In effect it is the webrender equivalent of (part of) the code in
+ * AsyncCompositionManager.
+ */
+ void SampleForWebRender(const Maybe<VsyncId>& aVsyncId,
+ wr::TransactionWrapper& aTxn,
+ const SampleTime& aSampleTime);
+
+ /**
+ * Refer to the documentation of APZInputBridge::ReceiveInputEvent() and
+ * APZEventResult.
+ */
+ APZEventResult ReceiveInputEvent(
+ InputData& aEvent,
+ InputBlockCallback&& aCallback = InputBlockCallback()) override;
+
+ /**
+ * Set the keyboard shortcuts to use for translating keyboard events.
+ */
+ void SetKeyboardMap(const KeyboardMap& aKeyboardMap) override;
+
+ /**
+ * Kicks an animation to zoom to a rect. This may be either a zoom out or zoom
+ * in. The actual animation is done on the sampler thread after being set
+ * up. |aRect| must be given in CSS pixels, relative to the document.
+ * |aFlags| is a combination of the ZoomToRectBehavior enum values.
+ */
+ void ZoomToRect(const ScrollableLayerGuid& aGuid,
+ const ZoomTarget& aZoomTarget,
+ const uint32_t aFlags = DEFAULT_BEHAVIOR) override;
+
+ /**
+ * If we have touch listeners, this should always be called when we know
+ * definitively whether or not content has preventDefaulted any touch events
+ * that have come in. If |aPreventDefault| is true, any touch events in the
+ * queue will be discarded. This function must be called on the controller
+ * thread.
+ */
+ void ContentReceivedInputBlock(uint64_t aInputBlockId,
+ bool aPreventDefault) override;
+
+ /**
+ * When the event regions code is enabled, this function should be invoked to
+ * to confirm the target of the input block. This is only needed in cases
+ * where the initial input event of the block hit a dispatch-to-content region
+ * but is safe to call for all input blocks.
+ * The different elements in the array of targets correspond to the targets
+ * for the different touch points. In the case where the touch point has no
+ * target, or the target is not a scrollable frame, the target's |mScrollId|
+ * should be set to ScrollableLayerGuid::NULL_SCROLL_ID.
+ * Note: For mouse events that start a scrollbar drag, both SetTargetAPZC()
+ * and StartScrollbarDrag() will be called, and the calls may happen
+ * in either order. That's fine - whichever arrives first will confirm
+ * the block, and StartScrollbarDrag() will fill in the drag metrics.
+ * If the block is confirmed before we have drag metrics, some events
+ * in the drag block may be handled as no-ops until the drag metrics
+ * arrive.
+ */
+ void SetTargetAPZC(uint64_t aInputBlockId,
+ const nsTArray<ScrollableLayerGuid>& aTargets) override;
+
+ /**
+ * Updates any zoom constraints contained in the <meta name="viewport"> tag.
+ * If the |aConstraints| is Nothing() then previously-provided constraints for
+ * the given |aGuid| are cleared.
+ */
+ void UpdateZoomConstraints(
+ const ScrollableLayerGuid& aGuid,
+ const Maybe<ZoomConstraints>& aConstraints) override;
+
+ /**
+ * Calls Destroy() on all APZC instances attached to the tree, and resets the
+ * tree back to empty. This function must be called exactly once during the
+ * lifetime of this APZCTreeManager, when this APZCTreeManager is no longer
+ * needed. Failing to call this function may prevent objects from being freed
+ * properly.
+ * This must be called on the updater thread.
+ */
+ void ClearTree();
+
+ /**
+ * Sets the dpi value used by all AsyncPanZoomControllers attached to this
+ * tree manager.
+ * DPI defaults to 160 if not set using SetDPI() at any point.
+ */
+ void SetDPI(float aDpiValue) override;
+
+ /**
+ * Returns the current dpi value in use.
+ */
+ float GetDPI() const;
+
+ /**
+ * Find the hit testing node for the scrollbar thumb that matches these
+ * drag metrics. Initializes aOutThumbNode with the node, if there is one.
+ */
+ void FindScrollThumbNode(const AsyncDragMetrics& aDragMetrics,
+ LayersId aLayersId,
+ HitTestingTreeNodeAutoLock& aOutThumbNode);
+
+ /**
+ * Sets allowed touch behavior values for current touch-session for specific
+ * input block (determined by aInputBlock).
+ * Should be invoked by the widget. Each value of the aValues arrays
+ * corresponds to the different touch point that is currently active.
+ * Must be called after receiving the TOUCH_START event that starts the
+ * touch-session.
+ */
+ void SetAllowedTouchBehavior(
+ uint64_t aInputBlockId,
+ const nsTArray<TouchBehaviorFlags>& aValues) override;
+
+ void SetBrowserGestureResponse(uint64_t aInputBlockId,
+ BrowserGestureResponse aResponse) override;
+
+ /**
+ * This is a callback for AsyncPanZoomController to call when it wants to
+ * scroll in response to a touch-move event, or when it needs to hand off
+ * overscroll to the next APZC. Note that because of scroll grabbing, the
+ * first APZC to scroll may not be the one that is receiving the touch events.
+ *
+ * |aPrev| is the APZC that received the touch events triggering the scroll
+ * (in the case of an initial scroll), or the last APZC to scroll (in the
+ * case of overscroll)
+ * |aStartPoint| and |aEndPoint| are in |aPrev|'s transformed screen
+ * coordinates (i.e. the same coordinates in which touch points are given to
+ * APZCs). The amount of (over)scroll is represented by two points rather
+ * than a displacement because with certain 3D transforms, the same
+ * displacement between different points in transformed coordinates can
+ * represent different displacements in untransformed coordinates.
+ * |aOverscrollHandoffChain| is the overscroll handoff chain used for
+ * determining the order in which scroll should be handed off between
+ * APZCs
+ * |aOverscrollHandoffChainIndex| is the next position in the overscroll
+ * handoff chain that should be scrolled.
+ *
+ * aStartPoint and aEndPoint will be modified depending on how much of the
+ * scroll each APZC consumes. This is to allow the sending APZC to go into
+ * an overscrolled state if no APZC further up in the handoff chain accepted
+ * the entire scroll.
+ *
+ * The function will return true if the entire scroll was consumed, and
+ * false otherwise. As this function also modifies aStartPoint and aEndPoint,
+ * when scroll is consumed, it should always the case that this function
+ * returns true if and only if IsZero(aStartPoint - aEndPoint), using the
+ * modified aStartPoint and aEndPoint after the function returns.
+ *
+ * The way this method works is best illustrated with an example.
+ * Consider three nested APZCs, A, B, and C, with C being the innermost one.
+ * Say B is scroll-grabbing.
+ * The touch events go to C because it's the innermost one (so e.g. taps
+ * should go through C), but the overscroll handoff chain is B -> C -> A
+ * because B is scroll-grabbing.
+ * For convenience I'll refer to the three APZC objects as A, B, and C, and
+ * to the tree manager object as TM.
+ * Here's what happens when C receives a touch-move event:
+ * - C.TrackTouch() calls TM.DispatchScroll() with index = 0.
+ * - TM.DispatchScroll() calls B.AttemptScroll() (since B is at index 0 in
+ * the chain).
+ * - B.AttemptScroll() scrolls B. If there is overscroll, it calls
+ * TM.DispatchScroll() with index = 1.
+ * - TM.DispatchScroll() calls C.AttemptScroll() (since C is at index 1 in
+ * the chain)
+ * - C.AttemptScroll() scrolls C. If there is overscroll, it calls
+ * TM.DispatchScroll() with index = 2.
+ * - TM.DispatchScroll() calls A.AttemptScroll() (since A is at index 2 in
+ * the chain)
+ * - A.AttemptScroll() scrolls A. If there is overscroll, it calls
+ * TM.DispatchScroll() with index = 3.
+ * - TM.DispatchScroll() discards the rest of the scroll as there are no
+ * more elements in the chain.
+ *
+ * Note: this should be used for panning only. For handing off overscroll for
+ * a fling, use DispatchFling().
+ */
+ bool DispatchScroll(AsyncPanZoomController* aPrev,
+ ParentLayerPoint& aStartPoint,
+ ParentLayerPoint& aEndPoint,
+ OverscrollHandoffState& aOverscrollHandoffState);
+
+ /**
+ * This is a callback for AsyncPanZoomController to call when it wants to
+ * start a fling in response to a touch-end event, or when it needs to hand
+ * off a fling to the next APZC. Note that because of scroll grabbing, the
+ * first APZC to fling may not be the one that is receiving the touch events.
+ *
+ * @param aApzc the APZC that wants to start or hand off the fling
+ * @param aHandoffState a collection of state about the operation,
+ * which contains the following:
+ *
+ * mVelocity the current velocity of the fling, in |aApzc|'s screen
+ * pixels per millisecond
+ * mChain the chain of APZCs along which the fling
+ * should be handed off
+ * mIsHandoff is true if |aApzc| is handing off an existing fling (in
+ * this case the fling is given to the next APZC in the
+ * handoff chain after |aApzc|), and false is |aApzc| wants
+ * start a fling (in this case the fling is given to the
+ * first APZC in the chain)
+ *
+ * The return value is the "residual velocity", the portion of
+ * |aHandoffState.mVelocity| that was not consumed by APZCs in the
+ * handoff chain doing flings.
+ * The caller can use this value to determine whether it should consume
+ * the excess velocity by going into overscroll.
+ */
+ ParentLayerPoint DispatchFling(AsyncPanZoomController* aApzc,
+ const FlingHandoffState& aHandoffState);
+
+ void StartScrollbarDrag(const ScrollableLayerGuid& aGuid,
+ const AsyncDragMetrics& aDragMetrics) override;
+
+ bool StartAutoscroll(const ScrollableLayerGuid& aGuid,
+ const ScreenPoint& aAnchorLocation) override;
+
+ void StopAutoscroll(const ScrollableLayerGuid& aGuid) override;
+
+ /*
+ * Build the chain of APZCs that will handle overscroll for a pan starting at
+ * |aInitialTarget|.
+ */
+ RefPtr<const OverscrollHandoffChain> BuildOverscrollHandoffChain(
+ const RefPtr<AsyncPanZoomController>& aInitialTarget);
+
+ /**
+ * Function used to disable LongTap gestures.
+ *
+ * On slow running tests, drags and touch events can be misinterpreted
+ * as a long tap. This allows tests to disable long tap gesture detection.
+ */
+ void SetLongTapEnabled(bool aTapGestureEnabled) override;
+
+ APZInputBridge* InputBridge() override { return this; }
+
+ /**
+ * Add a callback to be invoked when |aInputBlockId| is ready for handling.
+ *
+ * Should only be used for input blocks that are not yet ready for handling
+ * at the time this is called. If the input block was already handled,
+ * the callback will never be called.
+ *
+ * Only one callback can be registered for an input block at a time.
+ * Subsequent attempts to register a callback for an input block will be
+ * ignored until the existing callback is triggered.
+ */
+ void AddInputBlockCallback(uint64_t aInputBlockId,
+ InputBlockCallbackInfo&& aCallbackInfo);
+
+ // Methods to help process WidgetInputEvents (or manage conversion to/from
+ // InputData)
+
+ void ProcessUnhandledEvent(LayoutDeviceIntPoint* aRefPoint,
+ ScrollableLayerGuid* aOutTargetGuid,
+ uint64_t* aOutFocusSequenceNumber,
+ LayersId* aOutLayersId) override;
+
+ void UpdateWheelTransaction(
+ LayoutDeviceIntPoint aRefPoint, EventMessage aEventMessage,
+ const Maybe<ScrollableLayerGuid>& aTargetGuid) override;
+
+ bool GetAPZTestData(LayersId aLayersId, APZTestData* aOutData);
+
+ /**
+ * Iterates over the hit testing tree, collects LayersIds and associated
+ * transforms from layer coordinate space to root coordinate space, and
+ * sends these over to the main thread of the chrome process. If the provided
+ * |aAncestor| argument is non-null, then only the transforms for layer
+ * subtrees scrolled by the aAncestor (i.e. descendants of aAncestor) will be
+ * sent.
+ */
+ void SendSubtreeTransformsToChromeMainThread(
+ const AsyncPanZoomController* aAncestor);
+
+ /**
+ * Set fixed layer margins for dynamic toolbar.
+ */
+ void SetFixedLayerMargins(ScreenIntCoord aTop, ScreenIntCoord aBottom);
+
+ /**
+ * Refer to apz::ComputeTransformForScrollThumb() for a description
+ * of the parameters.
+ */
+ static LayerToParentLayerMatrix4x4 ComputeTransformForScrollThumb(
+ const LayerToParentLayerMatrix4x4& aCurrentTransform,
+ const gfx::Matrix4x4& aScrollableContentTransform,
+ AsyncPanZoomController* aApzc, const FrameMetrics& aMetrics,
+ const ScrollbarData& aScrollbarData, bool aScrollbarIsDescendant);
+
+ /**
+ * Dispatch a flush complete notification from the repaint thread of the
+ * content controller for the given layers id.
+ */
+ static void FlushApzRepaints(LayersId aLayersId);
+
+ /**
+ * Mark |aLayersId| as having been moved from the compositor that owns this
+ * tree manager to a compositor that doesn't use APZ.
+ * See |mDetachedLayersIds| for more details.
+ */
+ void MarkAsDetached(LayersId aLayersId);
+
+ // Assert that the current thread is the sampler thread for this APZCTM.
+ void AssertOnSamplerThread();
+ // Assert that the current thread is the updater thread for this APZCTM.
+ void AssertOnUpdaterThread();
+
+ // Returns a pointer to the WebRenderAPI this APZCTreeManager is for.
+ // This might be null (for example, if WebRender is not enabled).
+ already_AddRefed<wr::WebRenderAPI> GetWebRenderAPI() const;
+
+ protected:
+ // Protected destructor, to discourage deletion outside of Release():
+ virtual ~APZCTreeManager();
+
+ APZSampler* GetSampler() const;
+ APZUpdater* GetUpdater() const;
+
+ // We need to allow APZUpdater to lock and unlock this tree during a WR
+ // scene swap. We do this using private helpers to avoid exposing these
+ // functions to the world.
+ private:
+ friend class APZUpdater;
+ void LockTree() MOZ_CAPABILITY_ACQUIRE(mTreeLock);
+ void UnlockTree() MOZ_CAPABILITY_RELEASE(mTreeLock);
+
+ // Protected hooks for gtests subclass
+ virtual AsyncPanZoomController* NewAPZCInstance(
+ LayersId aLayersId, GeckoContentController* aController);
+
+ public:
+ // Public hook for gtests subclass
+ virtual SampleTime GetFrameTime();
+
+ // Also used for controlling time during tests
+ void SetTestSampleTime(const Maybe<TimeStamp>& aTime);
+
+ private:
+ mutable DataMutex<Maybe<TimeStamp>> mTestSampleTime;
+ CopyableTArray<MatrixMessage> mLastMessages;
+
+ public:
+ /* Some helper functions to find an APZC given some identifying input. These
+ functions lock the tree of APZCs while they find the right one, and then
+ return an addref'd pointer to it. This allows caller code to just use the
+ target APZC without worrying about it going away. These are public for
+ testing code and generally should not be used by other production code.
+ */
+ RefPtr<HitTestingTreeNode> GetRootNode() const;
+ HitTestResult GetTargetAPZC(const ScreenPoint& aPoint);
+ already_AddRefed<AsyncPanZoomController> GetTargetAPZC(
+ const LayersId& aLayersId,
+ const ScrollableLayerGuid::ViewID& aScrollId) const;
+ already_AddRefed<AsyncPanZoomController> GetTargetAPZC(
+ const LayersId& aLayersId, const ScrollableLayerGuid::ViewID& aScrollId,
+ const MutexAutoLock& aProofOfMapLock) const;
+ ScreenToParentLayerMatrix4x4 GetScreenToApzcTransform(
+ const AsyncPanZoomController* aApzc) const;
+ ParentLayerToScreenMatrix4x4 GetApzcToGeckoTransformForHit(
+ HitTestResult& aHitResult) const;
+ ParentLayerToScreenMatrix4x4 GetApzcToGeckoTransform(
+ const AsyncPanZoomController* aApzc,
+ const AsyncTransformComponents& aComponents) const;
+ ScreenPoint GetCurrentMousePosition() const;
+ void SetCurrentMousePosition(const ScreenPoint& aNewPos);
+
+ /**
+ * Convert a screen point of an event targeting |aApzc| to Gecko
+ * coordinates.
+ */
+ Maybe<ScreenIntPoint> ConvertToGecko(const ScreenIntPoint& aPoint,
+ AsyncPanZoomController* aApzc);
+
+ /**
+ * Find the zoomable APZC in the same layer subtree (i.e. with the same
+ * layers id) as the given APZC.
+ */
+ already_AddRefed<AsyncPanZoomController> FindZoomableApzc(
+ AsyncPanZoomController* aStart) const;
+
+ ScreenMargin GetCompositorFixedLayerMargins() const;
+
+ void AdjustEventPointForDynamicToolbar(ScreenIntPoint& aEventPoint,
+ const HitTestResult& aHit);
+
+ APZScrollGeneration NewAPZScrollGeneration() {
+ // In the production code this function gets only called from the sampler
+ // thread but in tests using nsIDOMWindowUtils.setAsyncScrollOffset this
+ // function gets called from the controller thread so we need to lock the
+ // mutex for this counter.
+ MutexAutoLock lock(mScrollGenerationLock);
+ return mScrollGenerationCounter.NewAPZGeneration();
+ }
+
+ template <typename Callback>
+ void CallWithMapLock(Callback& aCallback) {
+ MutexAutoLock lock(mMapLock);
+ aCallback(lock);
+ }
+
+ private:
+ using GuidComparator = ScrollableLayerGuid::Comparator;
+ using ScrollNode = WebRenderScrollDataWrapper;
+
+ /* Helpers */
+
+ void AttachNodeToTree(HitTestingTreeNode* aNode, HitTestingTreeNode* aParent,
+ HitTestingTreeNode* aNextSibling)
+ MOZ_REQUIRES(mTreeLock);
+ already_AddRefed<AsyncPanZoomController> GetTargetAPZC(
+ const ScrollableLayerGuid& aGuid);
+ already_AddRefed<HitTestingTreeNode> GetTargetNode(
+ const ScrollableLayerGuid& aGuid, GuidComparator aComparator) const;
+ HitTestingTreeNode* FindTargetNode(HitTestingTreeNode* aNode,
+ const ScrollableLayerGuid& aGuid,
+ GuidComparator aComparator);
+ TargetApzcForNodeResult GetTargetApzcForNode(const HitTestingTreeNode* aNode);
+ TargetApzcForNodeResult FindHandoffParent(
+ const AsyncPanZoomController* aApzc);
+ HitTestingTreeNode* FindRootNodeForLayersId(LayersId aLayersId) const;
+ AsyncPanZoomController* FindRootContentApzcForLayersId(
+ LayersId aLayersId) const;
+ already_AddRefed<AsyncPanZoomController> GetZoomableTarget(
+ AsyncPanZoomController* aApzc1, AsyncPanZoomController* aApzc2) const;
+ already_AddRefed<AsyncPanZoomController> CommonAncestor(
+ AsyncPanZoomController* aApzc1, AsyncPanZoomController* aApzc2) const;
+
+ struct FixedPositionInfo;
+ struct StickyPositionInfo;
+
+ // Returns true if |aNode| is a fixed layer that is fixed to the root content
+ // APZC.
+ // The map lock is required within these functions; if the map lock is already
+ // being held by the caller, the second overload should be used. If the map
+ // lock is not being held at the call site, the first overload should be used.
+ bool IsFixedToRootContent(const HitTestingTreeNode* aNode) const;
+ bool IsFixedToRootContent(const FixedPositionInfo& aFixedInfo,
+ const MutexAutoLock& aProofOfMapLock) const;
+
+ // Returns the vertical sides of |aNode| that are stuck to the root content.
+ // The map lock is required within these functions; if the map lock is already
+ // being held by the caller, the second overload should be used. If the map
+ // lock is not being held at the call site, the first overload should be used.
+ SideBits SidesStuckToRootContent(const HitTestingTreeNode* aNode) const;
+ SideBits SidesStuckToRootContent(const StickyPositionInfo& aStickyInfo,
+ const MutexAutoLock& aProofOfMapLock) const;
+
+ /**
+ * Perform hit testing for a touch-start event.
+ *
+ * @param aEvent The touch-start event.
+ *
+ * The remaining parameters are out-parameter used to communicate additional
+ * return values:
+ *
+ * @param aOutTouchBehaviors
+ * The touch behaviours that should be allowed for this touch block.
+
+ * @return The results of the hit test, including the APZC that was hit.
+ */
+ HitTestResult GetTouchInputBlockAPZC(
+ const MultiTouchInput& aEvent,
+ nsTArray<TouchBehaviorFlags>* aOutTouchBehaviors);
+
+ /**
+ * A helper structure for use by ReceiveInputEvent() and its helpers.
+ */
+ struct InputHandlingState {
+ // A reference to the event being handled.
+ InputData& mEvent;
+
+ // The value that will be returned by ReceiveInputEvent().
+ APZEventResult mResult;
+
+ // If we performed a hit-test while handling this input event, or
+ // reused the result of a previous hit-test in the input block,
+ // this is populated with the result of the hit test.
+ HitTestResult mHit;
+
+ // Called at the end of ReceiveInputEvent() to perform any final
+ // computations, and then return mResult.
+ // If the event will have a delayed result then this takes care of adding
+ // the specified callback to the APZCTreeManager.
+ APZEventResult Finish(APZCTreeManager& aTreeManager,
+ InputBlockCallback&& aCallback);
+ };
+
+ void ProcessTouchInput(InputHandlingState& aState, MultiTouchInput& aInput);
+ /**
+ * Given a mouse-down event that hit a scroll thumb node, set up APZ
+ * dragging of the scroll thumb.
+ *
+ * Must be called after the mouse event has been sent to InputQueue.
+ *
+ * @param aMouseInput The mouse-down event.
+ * @param aScrollThumbNode Tthe scroll thumb node that was hit.
+ * @param aApzc
+ * The APZC for the scroll frame scrolled by the scroll thumb, if that
+ * scroll frame is layerized. (A thumb can be layerized without its
+ * target scroll frame being layerized.) Otherwise, an enclosing APZC.
+ */
+ void SetupScrollbarDrag(MouseInput& aMouseInput,
+ const HitTestingTreeNodeAutoLock& aScrollThumbNode,
+ AsyncPanZoomController* aApzc);
+ /**
+ * Process a touch event that's part of a scrollbar touch-drag gesture.
+ *
+ * @param aInput The touch event.
+ * @param aScrollThumbNode
+ * If this is the touch-start event, the node representing the scroll
+ * thumb we are starting to drag. Otherwise nullptr.
+ * @param aHitInfo
+ * The hit-test flags for the touch input.
+ * @return See ReceiveInputEvent() for what the return value means.
+ */
+ APZEventResult ProcessTouchInputForScrollbarDrag(
+ MultiTouchInput& aInput,
+ const HitTestingTreeNodeAutoLock& aScrollThumbNode,
+ const gfx::CompositorHitTestInfo& aHitInfo);
+ void FlushRepaintsToClearScreenToGeckoTransform();
+
+ void SynthesizePinchGestureFromMouseWheel(
+ const ScrollWheelInput& aWheelInput,
+ const RefPtr<AsyncPanZoomController>& aTarget);
+
+ already_AddRefed<HitTestingTreeNode> RecycleOrCreateNode(
+ const RecursiveMutexAutoLock& aProofOfTreeLock, TreeBuildingState& aState,
+ AsyncPanZoomController* aApzc, LayersId aLayersId);
+ HitTestingTreeNode* PrepareNodeForLayer(
+ const RecursiveMutexAutoLock& aProofOfTreeLock, const ScrollNode& aLayer,
+ const FrameMetrics& aMetrics, LayersId aLayersId,
+ const Maybe<ZoomConstraints>& aZoomConstraints,
+ const AncestorTransform& aAncestorTransform, HitTestingTreeNode* aParent,
+ HitTestingTreeNode* aNextSibling, TreeBuildingState& aState);
+
+ void PrintLayerInfo(const ScrollNode& aLayer);
+
+ void NotifyScrollbarDragInitiated(uint64_t aDragBlockId,
+ const ScrollableLayerGuid& aGuid,
+ ScrollDirection aDirection) const;
+ void NotifyScrollbarDragRejected(const ScrollableLayerGuid& aGuid) const;
+ void NotifyAutoscrollRejected(const ScrollableLayerGuid& aGuid) const;
+
+ // Returns the transform that converts from |aNode|'s coordinates to
+ // the coordinates of |aNode|'s parent in the hit-testing tree.
+ // Requires the caller to hold mTreeLock.
+ LayerToParentLayerMatrix4x4 ComputeTransformForNode(
+ const HitTestingTreeNode* aNode) const MOZ_REQUIRES(mTreeLock);
+
+ // Look up the GeckoContentController for the given layers id.
+ static already_AddRefed<GeckoContentController> GetContentController(
+ LayersId aLayersId);
+
+ bool AdvanceAnimationsInternal(const MutexAutoLock& aProofOfMapLock,
+ const SampleTime& aSampleTime);
+
+ using ClippedCompositionBoundsMap =
+ std::unordered_map<ScrollableLayerGuid, ParentLayerRect,
+ ScrollableLayerGuid::HashIgnoringPresShellFn,
+ ScrollableLayerGuid::EqualIgnoringPresShellFn>;
+ // This is a recursive function that populates `aDestMap` with the clipped
+ // composition bounds for the APZC corresponding to `aGuid` and returns those
+ // bounds as a convenience. It recurses to also populate `aDestMap` with that
+ // APZC's ancestors. In order to do this it needs to access mApzcMap
+ // and therefore requires the caller to hold the map lock.
+ ParentLayerRect ComputeClippedCompositionBounds(
+ const MutexAutoLock& aProofOfMapLock,
+ ClippedCompositionBoundsMap& aDestMap, ScrollableLayerGuid aGuid);
+
+ ScreenMargin GetCompositorFixedLayerMargins(
+ const MutexAutoLock& aProofOfMapLock) const;
+
+ protected:
+ /* The input queue where input events are held until we know enough to
+ * figure out where they're going. Protected so gtests can access it.
+ */
+ RefPtr<InputQueue> mInputQueue;
+
+ private:
+ /* Layers id for the root CompositorBridgeParent that owns this
+ * APZCTreeManager. */
+ LayersId mRootLayersId;
+
+ /* Pointer to the APZSampler instance that is bound to this APZCTreeManager.
+ * The sampler has a RefPtr to this class, and this non-owning raw pointer
+ * back to the APZSampler is nulled out in the sampler's destructor, so this
+ * pointer should always be valid.
+ */
+ APZSampler* MOZ_NON_OWNING_REF mSampler;
+ /* Pointer to the APZUpdater instance that is bound to this APZCTreeManager.
+ * The updater has a RefPtr to this class, and this non-owning raw pointer
+ * back to the APZUpdater is nulled out in the updater's destructor, so this
+ * pointer should always be valid.
+ */
+ APZUpdater* MOZ_NON_OWNING_REF mUpdater;
+
+ /* Whenever walking or mutating the tree rooted at mRootNode, mTreeLock must
+ * be held. This lock does not need to be held while manipulating a single
+ * APZC instance in isolation (that is, if its tree pointers are not being
+ * accessed or mutated). The lock also needs to be held when accessing the
+ * mRootNode instance variable, as that is considered part of the APZC tree
+ * management state.
+ * IMPORTANT: See the note about lock ordering at the top of this file. */
+ mutable mozilla::RecursiveMutex mTreeLock;
+ RefPtr<HitTestingTreeNode> mRootNode MOZ_GUARDED_BY(mTreeLock);
+
+ /*
+ * A set of LayersIds for which APZCTM should only send empty
+ * MatrixMessages via NotifyLayerTransform().
+ * This is used in cases where a tab has been transferred to a non-APZ
+ * compositor (and thus will not receive MatrixMessages reflecting its new
+ * transforms) and we need to make sure it doesn't get stuck with transforms
+ * from its old tree manager (us).
+ * Acquire mTreeLock before accessing this.
+ */
+ std::unordered_set<LayersId, LayersId::HashFn> mDetachedLayersIds
+ MOZ_GUARDED_BY(mTreeLock);
+
+ /* If the current hit-testing tree contains an async zoom container
+ * node, this is set to the layers id of subtree that has the node.
+ */
+ Maybe<LayersId> mAsyncZoomContainerSubtree;
+
+ /** A lock that protects mApzcMap, mScrollThumbInfo, mRootScrollbarInfo,
+ * mFixedPositionInfo, and mStickyPositionInfo.
+ */
+ mutable mozilla::Mutex mMapLock;
+
+ /**
+ * Helper structure to store a bunch of things in mApzcMap so that they can
+ * be used from the sampler thread.
+ */
+ struct ApzcMapData {
+ // A pointer to the APZC itself
+ RefPtr<AsyncPanZoomController> apzc;
+ // The parent APZC's guid, or Nothing() if there is no parent
+ Maybe<ScrollableLayerGuid> parent;
+ };
+
+ /**
+ * A map for quick access to get some APZC data by guid, without having to
+ * acquire the tree lock. mMapLock must be acquired while accessing or
+ * modifying mApzcMap.
+ */
+ std::unordered_map<ScrollableLayerGuid, ApzcMapData,
+ ScrollableLayerGuid::HashIgnoringPresShellFn,
+ ScrollableLayerGuid::EqualIgnoringPresShellFn>
+ mApzcMap;
+ /**
+ * A helper structure to store all the information needed to compute the
+ * async transform for a scrollthumb on the sampler thread.
+ */
+ struct ScrollThumbInfo {
+ uint64_t mThumbAnimationId;
+ CSSTransformMatrix mThumbTransform;
+ ScrollbarData mThumbData;
+ ScrollableLayerGuid mTargetGuid;
+ CSSTransformMatrix mTargetTransform;
+ bool mTargetIsAncestor;
+
+ ScrollThumbInfo(const uint64_t& aThumbAnimationId,
+ const CSSTransformMatrix& aThumbTransform,
+ const ScrollbarData& aThumbData,
+ const ScrollableLayerGuid& aTargetGuid,
+ const CSSTransformMatrix& aTargetTransform,
+ bool aTargetIsAncestor)
+ : mThumbAnimationId(aThumbAnimationId),
+ mThumbTransform(aThumbTransform),
+ mThumbData(aThumbData),
+ mTargetGuid(aTargetGuid),
+ mTargetTransform(aTargetTransform),
+ mTargetIsAncestor(aTargetIsAncestor) {
+ MOZ_ASSERT(mTargetGuid.mScrollId == mThumbData.mTargetViewId);
+ }
+ };
+ /**
+ * If this APZCTreeManager is being used with WebRender, this vector gets
+ * populated during a layers update. It holds a package of information needed
+ * to compute and set the async transforms on scroll thumbs. This information
+ * is extracted from the HitTestingTreeNodes for the WebRender case because
+ * accessing the HitTestingTreeNodes requires holding the tree lock which
+ * we cannot do on the WR sampler thread. mScrollThumbInfo, however, can
+ * be accessed while just holding the mMapLock which is safe to do on the
+ * sampler thread.
+ * mMapLock must be acquired while accessing or modifying mScrollThumbInfo.
+ */
+ std::vector<ScrollThumbInfo> mScrollThumbInfo;
+
+ /**
+ * A helper structure to store all the information needed to compute the
+ * async transform for a scrollthumb on the sampler thread.
+ */
+ struct RootScrollbarInfo {
+ uint64_t mScrollbarAnimationId;
+ ScrollDirection mScrollDirection;
+
+ RootScrollbarInfo(const uint64_t& aScrollbarAnimationId,
+ const ScrollDirection aScrollDirection)
+ : mScrollbarAnimationId(aScrollbarAnimationId),
+ mScrollDirection(aScrollDirection) {}
+ };
+ /**
+ * If this APZCTreeManager is being used with WebRender, this vector gets
+ * populated during a layers update. It holds a package of information needed
+ * to compute and set the async transforms on root scrollbars. This
+ * information is extracted from the HitTestingTreeNodes for the WebRender
+ * case because accessing the HitTestingTreeNodes requires holding the tree
+ * lock which we cannot do on the WR sampler thread. mRootScrollbarInfo,
+ * however, can be accessed while just holding the mMapLock which is safe to
+ * do on the sampler thread.
+ * mMapLock must be acquired while accessing or modifying mRootScrollbarInfo.
+ */
+ std::vector<RootScrollbarInfo> mRootScrollbarInfo;
+
+ /**
+ * A helper structure to store all the information needed to compute the
+ * async transform for a fixed position element on the sampler thread.
+ */
+ struct FixedPositionInfo {
+ Maybe<uint64_t> mFixedPositionAnimationId;
+ SideBits mFixedPosSides;
+ ScrollableLayerGuid::ViewID mFixedPosTarget;
+ LayersId mLayersId;
+
+ explicit FixedPositionInfo(const HitTestingTreeNode* aNode);
+ };
+ /**
+ * If this APZCTreeManager is being used with WebRender, this vector gets
+ * populated during a layers update. It holds a package of information needed
+ * to compute and set the async transforms on fixed position content. This
+ * information is extracted from the HitTestingTreeNodes for the WebRender
+ * case because accessing the HitTestingTreeNodes requires holding the tree
+ * lock which we cannot do on the WR sampler thread. mFixedPositionInfo,
+ * however, can be accessed while just holding the mMapLock which is safe to
+ * do on the sampler thread. mMapLock must be acquired while accessing or
+ * modifying mFixedPositionInfo.
+ */
+ std::vector<FixedPositionInfo> mFixedPositionInfo;
+
+ /**
+ * A helper structure to store all the information needed to compute the
+ * async transform for a sticky position element on the sampler thread.
+ */
+ struct StickyPositionInfo {
+ Maybe<uint64_t> mStickyPositionAnimationId;
+ SideBits mFixedPosSides;
+ ScrollableLayerGuid::ViewID mStickyPosTarget;
+ LayersId mLayersId;
+ LayerRectAbsolute mStickyScrollRangeInner;
+ LayerRectAbsolute mStickyScrollRangeOuter;
+
+ explicit StickyPositionInfo(const HitTestingTreeNode* aNode);
+ };
+ /**
+ * If this APZCTreeManager is being used with WebRender, this vector gets
+ * populated during a layers update. It holds a package of information needed
+ * to compute and set the async transforms on sticky position content. This
+ * information is extracted from the HitTestingTreeNodes for the WebRender
+ * case because accessing the HitTestingTreeNodes requires holding the tree
+ * lock which we cannot do on the WR sampler thread. mStickyPositionInfo,
+ * however, can be accessed while just holding the mMapLock which is safe to
+ * do on the sampler thread. mMapLock must be acquired while accessing or
+ * modifying mStickyPositionInfo.
+ */
+ std::vector<StickyPositionInfo> mStickyPositionInfo;
+
+ /* Holds the zoom constraints for scrollable layers, as determined by the
+ * the main-thread gecko code. This can only be accessed on the updater
+ * thread. */
+ std::unordered_map<ScrollableLayerGuid, ZoomConstraints,
+ ScrollableLayerGuid::HashIgnoringPresShellFn,
+ ScrollableLayerGuid::EqualIgnoringPresShellFn>
+ mZoomConstraints;
+ /* A list of keyboard shortcuts to use for translating keyboard inputs into
+ * keyboard actions. This is gathered on the main thread from XBL bindings.
+ * This must only be accessed on the controller thread.
+ */
+ KeyboardMap mKeyboardMap;
+ /* This tracks the focus targets of chrome and content and whether we have
+ * a current focus target or whether we are waiting for a new confirmation.
+ */
+ FocusState mFocusState;
+ /* This tracks the hit test result info for the current touch input block.
+ * In particular, it tracks the target APZC, the hit test flags, and the
+ * fixed pos sides. This is populated at the start of a touch block based
+ * on the hit-test result, and used for subsequent touch events in the block.
+ * This allows touch points to move outside the thing they started on, but
+ * still have the touch events delivered to the same initial APZC. This will
+ * only ever be touched on the input delivery thread, and so does not require
+ * locking.
+ */
+ HitTestResult mTouchBlockHitResult;
+ /* Sometimes we want to ignore all touches except one. In such cases, this
+ * is set to the identifier of the touch we are not ignoring; in other cases,
+ * this is set to -1.
+ */
+ int32_t mRetainedTouchIdentifier;
+ /* This tracks whether the current input block represents a touch-drag of
+ * a scrollbar. In this state, touch events are forwarded to content as touch
+ * events, but converted to mouse events before going into InputQueue and
+ * being handled by an APZC (to reuse the APZ code for scrollbar dragging
+ * with a mouse).
+ */
+ bool mInScrollbarTouchDrag;
+ /* Tracks the number of touch points we are tracking that are currently on
+ * the screen. */
+ TouchCounter mTouchCounter;
+ /* If a tap gesture event sent directly by widget code (rather than gesture
+ * detected from touch events by APZ) is being processed, this stores the
+ * result of hit testing for that tap gesture event.
+ */
+ HitTestResult mTapGestureHitResult;
+ /* Stores the current mouse position in screen coordinates.
+ */
+ mutable DataMutex<ScreenPoint> mCurrentMousePosition;
+ /* Extra margins that should be applied to content that fixed wrt. the
+ * RCD-RSF, to account for the dynamic toolbar.
+ * Acquire mMapLock before accessing this.
+ */
+ ScreenMargin mCompositorFixedLayerMargins;
+ /* Similar to above |mCompositorFixedLayerMargins|. But this value is the
+ * margins on the main-thread at the last time position:fixed elements were
+ * updated during the dynamic toolbar transitions.
+ * Acquire mMapLock before accessing this.
+ */
+ ScreenMargin mGeckoFixedLayerMargins;
+ /* For logging the APZC tree for debugging (enabled by the apz.printtree
+ * pref). The purpose of using LOG_CRITICAL is so that you don't also need to
+ * change the gfx.logging.level pref to see the output. */
+ gfx::TreeLog<gfx::LOG_CRITICAL> mApzcTreeLog;
+
+ class CheckerboardFlushObserver;
+ friend class CheckerboardFlushObserver;
+ RefPtr<CheckerboardFlushObserver> mFlushObserver;
+
+ // Map from layers id to APZTestData. Accesses and mutations must be
+ // protected by the mTestDataLock.
+ std::unordered_map<LayersId, UniquePtr<APZTestData>, LayersId::HashFn>
+ mTestData;
+ mutable mozilla::Mutex mTestDataLock;
+
+ // This must only be touched on the controller thread.
+ float mDPI;
+
+ friend class IAPZHitTester;
+ UniquePtr<IAPZHitTester> mHitTester;
+
+ // NOTE: This ScrollGenerationCounter needs to be per APZCTreeManager since
+ // the generation is bumped up on the sampler theread which is per
+ // APZCTreeManager.
+ ScrollGenerationCounter mScrollGenerationCounter;
+ mozilla::Mutex mScrollGenerationLock;
+
+#if defined(MOZ_WIDGET_ANDROID)
+ private:
+ // Last Frame metrics sent to java through UIController.
+ GeckoViewMetrics mLastRootMetrics;
+#endif // defined(MOZ_WIDGET_ANDROID)
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif // mozilla_layers_PanZoomController_h
diff --git a/gfx/layers/apz/src/APZInputBridge.cpp b/gfx/layers/apz/src/APZInputBridge.cpp
new file mode 100644
index 0000000000..bce801a6d2
--- /dev/null
+++ b/gfx/layers/apz/src/APZInputBridge.cpp
@@ -0,0 +1,435 @@
+/* -*- 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 "mozilla/layers/APZInputBridge.h"
+
+#include "AsyncPanZoomController.h"
+#include "InputData.h" // for MouseInput, etc
+#include "InputBlockState.h" // for InputBlockState
+#include "OverscrollHandoffState.h" // for OverscrollHandoffState
+#include "mozilla/EventForwards.h"
+#include "mozilla/dom/WheelEventBinding.h" // for WheelEvent constants
+#include "mozilla/EventStateManager.h" // for EventStateManager
+#include "mozilla/layers/APZThreadUtils.h" // for AssertOnControllerThread, etc
+#include "mozilla/MouseEvents.h" // for WidgetMouseEvent
+#include "mozilla/StaticPrefs_apz.h"
+#include "mozilla/StaticPrefs_general.h"
+#include "mozilla/StaticPrefs_test.h"
+#include "mozilla/TextEvents.h" // for WidgetKeyboardEvent
+#include "mozilla/TouchEvents.h" // for WidgetTouchEvent
+#include "mozilla/WheelHandlingHelper.h" // for WheelDeltaHorizontalizer,
+ // WheelDeltaAdjustmentStrategy
+
+namespace mozilla {
+namespace layers {
+
+APZEventResult::APZEventResult()
+ : mStatus(nsEventStatus_eIgnore),
+ mInputBlockId(InputBlockState::NO_BLOCK_ID) {}
+
+APZEventResult::APZEventResult(
+ const RefPtr<AsyncPanZoomController>& aInitialTarget,
+ TargetConfirmationFlags aFlags)
+ : APZEventResult() {
+ mHandledResult = [&]() -> Maybe<APZHandledResult> {
+ if (!aInitialTarget->IsRootContent()) {
+ // If the initial target is not the root, this will definitely not be
+ // handled by the root. (The confirmed target is either the initial
+ // target, or a descendant.)
+ return Some(
+ APZHandledResult{APZHandledPlace::HandledByContent, aInitialTarget});
+ }
+
+ if (!aFlags.mDispatchToContent) {
+ // If the initial target is the root and we don't need to dispatch to
+ // content, the event will definitely be handled by the root.
+ return Some(
+ APZHandledResult{APZHandledPlace::HandledByRoot, aInitialTarget});
+ }
+
+ // Otherwise, we're not sure.
+ return Nothing();
+ }();
+ aInitialTarget->GetGuid(&mTargetGuid);
+}
+
+void APZEventResult::SetStatusAsConsumeDoDefault(
+ const InputBlockState& aBlock) {
+ SetStatusAsConsumeDoDefault(aBlock.GetTargetApzc());
+}
+
+void APZEventResult::SetStatusAsConsumeDoDefault(
+ const RefPtr<AsyncPanZoomController>& aTarget) {
+ mStatus = nsEventStatus_eConsumeDoDefault;
+ mHandledResult =
+ Some(aTarget && aTarget->IsRootContent()
+ ? APZHandledResult{APZHandledPlace::HandledByRoot, aTarget}
+ : APZHandledResult{APZHandledPlace::HandledByContent, aTarget});
+}
+
+void APZEventResult::SetStatusForTouchEvent(
+ const InputBlockState& aBlock, TargetConfirmationFlags aFlags,
+ PointerEventsConsumableFlags aConsumableFlags,
+ const AsyncPanZoomController* aTarget) {
+ // Note, we need to continue setting mStatus to eIgnore in the {mHasRoom=true,
+ // mAllowedByTouchAction=false} case because this is the behaviour expected by
+ // APZEventState::ProcessTouchEvent() when it determines when to send a
+ // `pointercancel` event. TODO: Use something more descriptive than
+ // nsEventStatus for this purpose.
+ mStatus = aConsumableFlags.IsConsumable() ? nsEventStatus_eConsumeDoDefault
+ : nsEventStatus_eIgnore;
+
+ UpdateHandledResult(aBlock, aConsumableFlags, aTarget,
+ aFlags.mDispatchToContent);
+}
+
+void APZEventResult::UpdateHandledResult(
+ const InputBlockState& aBlock,
+ PointerEventsConsumableFlags aConsumableFlags,
+ const AsyncPanZoomController* aTarget, bool aDispatchToContent) {
+ // If the touch event's effect is disallowed by touch-action, treat it as if
+ // a touch event listener had preventDefault()-ed it (i.e. return
+ // HandledByContent, except we can do it eagerly rather than having to wait
+ // for the listener to run).
+ if (!aConsumableFlags.mAllowedByTouchAction) {
+ mHandledResult =
+ Some(APZHandledResult{APZHandledPlace::HandledByContent, aTarget});
+ return;
+ }
+
+ if (mHandledResult && !aDispatchToContent && !aConsumableFlags.mHasRoom) {
+ // Set result to Unhandled if we have no room to scroll, unless it
+ // was HandledByContent because we're over a dispatch-to-content region,
+ // in which case it should remain HandledByContent.
+ mHandledResult->mPlace = APZHandledPlace::Unhandled;
+ }
+
+ if (aTarget && !aTarget->IsRootContent()) {
+ auto [result, rootApzc] =
+ aBlock.GetOverscrollHandoffChain()->ScrollingDownWillMoveDynamicToolbar(
+ aTarget);
+ if (result) {
+ MOZ_ASSERT(rootApzc && rootApzc->IsRootContent());
+ // The event is actually consumed by a non-root APZC but scroll
+ // positions in all relevant APZCs are at the bottom edge, so if there's
+ // still contents covered by the dynamic toolbar we need to move the
+ // dynamic toolbar to make the covered contents visible, thus we need
+ // to tell it to GeckoView so we handle it as if it's consumed in the
+ // root APZC.
+ // IMPORTANT NOTE: If the incoming TargetConfirmationFlags has
+ // mDispatchToContent, we need to change it to Nothing() so that
+ // GeckoView can properly wait for results from the content on the
+ // main-thread.
+ mHandledResult =
+ aDispatchToContent
+ ? Nothing()
+ : Some(APZHandledResult{aConsumableFlags.IsConsumable()
+ ? APZHandledPlace::HandledByRoot
+ : APZHandledPlace::Unhandled,
+ rootApzc});
+ }
+ }
+}
+
+void APZEventResult::SetStatusForFastFling(
+ const TouchBlockState& aBlock, TargetConfirmationFlags aFlags,
+ PointerEventsConsumableFlags aConsumableFlags,
+ const AsyncPanZoomController* aTarget) {
+ MOZ_ASSERT(aBlock.IsDuringFastFling());
+
+ // Set eConsumeNoDefault for fast fling since we don't want to send the event
+ // to content at all.
+ mStatus = nsEventStatus_eConsumeNoDefault;
+
+ // In the case of fast fling, the event will never be sent to content, so we
+ // want a result where `aDispatchToContent` is false whatever the original
+ // `aFlags.mDispatchToContent` is.
+ UpdateHandledResult(aBlock, aConsumableFlags, aTarget, false /*
+ aDispatchToContent */);
+}
+
+static bool WillHandleMouseEvent(const WidgetMouseEventBase& aEvent) {
+ return aEvent.mMessage == eMouseMove || aEvent.mMessage == eMouseDown ||
+ aEvent.mMessage == eMouseUp || aEvent.mMessage == eDragEnd ||
+ (StaticPrefs::test_events_async_enabled() &&
+ aEvent.mMessage == eMouseHitTest);
+}
+
+/* static */
+Maybe<APZWheelAction> APZInputBridge::ActionForWheelEvent(
+ WidgetWheelEvent* aEvent) {
+ if (!(aEvent->mDeltaMode == dom::WheelEvent_Binding::DOM_DELTA_LINE ||
+ aEvent->mDeltaMode == dom::WheelEvent_Binding::DOM_DELTA_PIXEL ||
+ aEvent->mDeltaMode == dom::WheelEvent_Binding::DOM_DELTA_PAGE)) {
+ return Nothing();
+ }
+ return EventStateManager::APZWheelActionFor(aEvent);
+}
+
+APZEventResult APZInputBridge::ReceiveInputEvent(
+ WidgetInputEvent& aEvent, InputBlockCallback&& aCallback) {
+ APZThreadUtils::AssertOnControllerThread();
+
+ APZEventResult result;
+
+ switch (aEvent.mClass) {
+ case eMouseEventClass:
+ case eDragEventClass: {
+ WidgetMouseEvent& mouseEvent = *aEvent.AsMouseEvent();
+ if (WillHandleMouseEvent(mouseEvent)) {
+ MouseInput input(mouseEvent);
+ input.mOrigin =
+ ScreenPoint(mouseEvent.mRefPoint.x, mouseEvent.mRefPoint.y);
+
+ result = ReceiveInputEvent(input, std::move(aCallback));
+
+ mouseEvent.mRefPoint = TruncatedToInt(ViewAs<LayoutDevicePixel>(
+ input.mOrigin,
+ PixelCastJustification::LayoutDeviceIsScreenForUntransformedEvent));
+ mouseEvent.mFlags.mHandledByAPZ = input.mHandledByAPZ;
+ mouseEvent.mFocusSequenceNumber = input.mFocusSequenceNumber;
+#ifdef XP_MACOSX
+ // It's not assumed that the click event has already been prevented,
+ // except mousedown event with ctrl key is pressed where we prevent
+ // click event from widget on Mac platform.
+ MOZ_ASSERT_IF(!mouseEvent.IsControl() ||
+ mouseEvent.mMessage != eMouseDown ||
+ mouseEvent.mButton != MouseButton::ePrimary,
+ !mouseEvent.mClickEventPrevented);
+#else
+ MOZ_ASSERT(
+ !mouseEvent.mClickEventPrevented,
+ "It's not assumed that the click event has already been prevented");
+#endif
+ mouseEvent.mClickEventPrevented |= input.mPreventClickEvent;
+ MOZ_ASSERT_IF(mouseEvent.mClickEventPrevented,
+ mouseEvent.mMessage == eMouseDown ||
+ mouseEvent.mMessage == eMouseUp);
+ aEvent.mLayersId = input.mLayersId;
+
+ if (mouseEvent.IsReal()) {
+ UpdateWheelTransaction(mouseEvent.mRefPoint, mouseEvent.mMessage,
+ Some(result.mTargetGuid));
+ }
+
+ return result;
+ }
+
+ if (mouseEvent.IsReal()) {
+ UpdateWheelTransaction(mouseEvent.mRefPoint, mouseEvent.mMessage,
+ Nothing());
+ }
+
+ ProcessUnhandledEvent(&mouseEvent.mRefPoint, &result.mTargetGuid,
+ &aEvent.mFocusSequenceNumber, &aEvent.mLayersId);
+ return result;
+ }
+ case eTouchEventClass: {
+ WidgetTouchEvent& touchEvent = *aEvent.AsTouchEvent();
+ MultiTouchInput touchInput(touchEvent);
+ result = ReceiveInputEvent(touchInput, std::move(aCallback));
+ // touchInput was modified in-place to possibly remove some
+ // touch points (if we are overscrolled), and the coordinates were
+ // modified using the APZ untransform. We need to copy these changes
+ // back into the WidgetInputEvent.
+ touchEvent.mTouches.Clear();
+ touchEvent.mTouches.SetCapacity(touchInput.mTouches.Length());
+ for (size_t i = 0; i < touchInput.mTouches.Length(); i++) {
+ *touchEvent.mTouches.AppendElement() =
+ touchInput.mTouches[i].ToNewDOMTouch();
+ }
+ touchEvent.mFlags.mHandledByAPZ = touchInput.mHandledByAPZ;
+ touchEvent.mFocusSequenceNumber = touchInput.mFocusSequenceNumber;
+ aEvent.mLayersId = touchInput.mLayersId;
+ return result;
+ }
+ case eWheelEventClass: {
+ WidgetWheelEvent& wheelEvent = *aEvent.AsWheelEvent();
+
+ if (Maybe<APZWheelAction> action = ActionForWheelEvent(&wheelEvent)) {
+ ScrollWheelInput::ScrollMode scrollMode =
+ ScrollWheelInput::SCROLLMODE_INSTANT;
+ if (StaticPrefs::general_smoothScroll() &&
+ ((wheelEvent.mDeltaMode ==
+ dom::WheelEvent_Binding::DOM_DELTA_LINE &&
+ StaticPrefs::general_smoothScroll_mouseWheel()) ||
+ (wheelEvent.mDeltaMode ==
+ dom::WheelEvent_Binding::DOM_DELTA_PAGE &&
+ StaticPrefs::general_smoothScroll_pages()))) {
+ scrollMode = ScrollWheelInput::SCROLLMODE_SMOOTH;
+ }
+
+ WheelDeltaAdjustmentStrategy strategy =
+ EventStateManager::GetWheelDeltaAdjustmentStrategy(wheelEvent);
+ // Adjust the delta values of the wheel event if the current default
+ // action is to horizontalize scrolling. I.e., deltaY values are set to
+ // deltaX and deltaY and deltaZ values are set to 0.
+ // If horizontalized, the delta values will be restored and its overflow
+ // deltaX will become 0 when the WheelDeltaHorizontalizer instance is
+ // being destroyed.
+ WheelDeltaHorizontalizer horizontalizer(wheelEvent);
+ if (WheelDeltaAdjustmentStrategy::eHorizontalize == strategy) {
+ horizontalizer.Horizontalize();
+ }
+
+ // If the wheel event becomes no-op event, don't handle it as scroll.
+ if (wheelEvent.mDeltaX || wheelEvent.mDeltaY) {
+ ScreenPoint origin(wheelEvent.mRefPoint.x, wheelEvent.mRefPoint.y);
+ ScrollWheelInput input(
+ wheelEvent.mTimeStamp, 0, scrollMode,
+ ScrollWheelInput::DeltaTypeForDeltaMode(wheelEvent.mDeltaMode),
+ origin, wheelEvent.mDeltaX, wheelEvent.mDeltaY,
+ wheelEvent.mAllowToOverrideSystemScrollSpeed, strategy);
+ input.mAPZAction = action.value();
+
+ // We add the user multiplier as a separate field, rather than
+ // premultiplying it, because if the input is converted back to a
+ // WidgetWheelEvent, then EventStateManager would apply the delta a
+ // second time. We could in theory work around this by asking ESM to
+ // customize the event much sooner, and then save the
+ // "mCustomizedByUserPrefs" bit on ScrollWheelInput - but for now,
+ // this seems easier.
+ EventStateManager::GetUserPrefsForWheelEvent(
+ &wheelEvent, &input.mUserDeltaMultiplierX,
+ &input.mUserDeltaMultiplierY);
+
+ result = ReceiveInputEvent(input, std::move(aCallback));
+ wheelEvent.mRefPoint = TruncatedToInt(ViewAs<LayoutDevicePixel>(
+ input.mOrigin, PixelCastJustification::
+ LayoutDeviceIsScreenForUntransformedEvent));
+ wheelEvent.mFlags.mHandledByAPZ = input.mHandledByAPZ;
+ wheelEvent.mFocusSequenceNumber = input.mFocusSequenceNumber;
+ aEvent.mLayersId = input.mLayersId;
+
+ return result;
+ }
+ }
+
+ UpdateWheelTransaction(aEvent.mRefPoint, aEvent.mMessage, Nothing());
+ ProcessUnhandledEvent(&aEvent.mRefPoint, &result.mTargetGuid,
+ &aEvent.mFocusSequenceNumber, &aEvent.mLayersId);
+ MOZ_ASSERT(result.GetStatus() == nsEventStatus_eIgnore);
+ return result;
+ }
+ case eKeyboardEventClass: {
+ WidgetKeyboardEvent& keyboardEvent = *aEvent.AsKeyboardEvent();
+
+ KeyboardInput input(keyboardEvent);
+
+ result = ReceiveInputEvent(input, std::move(aCallback));
+
+ keyboardEvent.mFlags.mHandledByAPZ = input.mHandledByAPZ;
+ keyboardEvent.mFocusSequenceNumber = input.mFocusSequenceNumber;
+ return result;
+ }
+ default: {
+ UpdateWheelTransaction(aEvent.mRefPoint, aEvent.mMessage, Nothing());
+ ProcessUnhandledEvent(&aEvent.mRefPoint, &result.mTargetGuid,
+ &aEvent.mFocusSequenceNumber, &aEvent.mLayersId);
+ return result;
+ }
+ }
+
+ MOZ_ASSERT_UNREACHABLE("Invalid WidgetInputEvent type.");
+ result.SetStatusAsConsumeNoDefault();
+ return result;
+}
+
+APZHandledResult::APZHandledResult(APZHandledPlace aPlace,
+ const AsyncPanZoomController* aTarget)
+ : mPlace(aPlace) {
+ MOZ_ASSERT(aTarget);
+ switch (aPlace) {
+ case APZHandledPlace::Unhandled:
+ break;
+ case APZHandledPlace::HandledByContent:
+ if (aTarget) {
+ mScrollableDirections = aTarget->ScrollableDirections();
+ mOverscrollDirections = aTarget->GetAllowedHandoffDirections();
+ }
+ break;
+ case APZHandledPlace::HandledByRoot: {
+ MOZ_ASSERT(aTarget->IsRootContent());
+ if (aTarget) {
+ mScrollableDirections = aTarget->ScrollableDirections();
+ mOverscrollDirections = aTarget->GetAllowedHandoffDirections();
+ }
+ break;
+ }
+ default:
+ MOZ_ASSERT_UNREACHABLE("Invalid APZHandledPlace");
+ break;
+ }
+}
+
+std::ostream& operator<<(std::ostream& aOut, const SideBits& aSideBits) {
+ if ((aSideBits & SideBits::eAll) == SideBits::eAll) {
+ aOut << "all";
+ } else {
+ AutoTArray<nsCString, 4> strings;
+ if (aSideBits & SideBits::eTop) {
+ strings.AppendElement("top"_ns);
+ }
+ if (aSideBits & SideBits::eRight) {
+ strings.AppendElement("right"_ns);
+ }
+ if (aSideBits & SideBits::eBottom) {
+ strings.AppendElement("bottom"_ns);
+ }
+ if (aSideBits & SideBits::eLeft) {
+ strings.AppendElement("left"_ns);
+ }
+ aOut << strings;
+ }
+ return aOut;
+}
+
+std::ostream& operator<<(std::ostream& aOut,
+ const ScrollDirections& aScrollDirections) {
+ if (aScrollDirections.contains(EitherScrollDirection)) {
+ aOut << "either";
+ } else if (aScrollDirections.contains(HorizontalScrollDirection)) {
+ aOut << "horizontal";
+ } else if (aScrollDirections.contains(VerticalScrollDirection)) {
+ aOut << "vertical";
+ } else {
+ aOut << "none";
+ }
+ return aOut;
+}
+
+std::ostream& operator<<(std::ostream& aOut,
+ const APZHandledPlace& aHandledPlace) {
+ switch (aHandledPlace) {
+ case APZHandledPlace::Unhandled:
+ aOut << "unhandled";
+ break;
+ case APZHandledPlace::HandledByRoot: {
+ aOut << "handled-by-root";
+ break;
+ }
+ case APZHandledPlace::HandledByContent: {
+ aOut << "handled-by-content";
+ break;
+ }
+ case APZHandledPlace::Invalid: {
+ aOut << "INVALID";
+ break;
+ }
+ }
+ return aOut;
+}
+
+std::ostream& operator<<(std::ostream& aOut,
+ const APZHandledResult& aHandledResult) {
+ aOut << "handled: " << aHandledResult.mPlace << ", ";
+ aOut << "scrollable: " << aHandledResult.mScrollableDirections << ", ";
+ aOut << "overscroll: " << aHandledResult.mOverscrollDirections << std::endl;
+ return aOut;
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/apz/src/APZPublicUtils.cpp b/gfx/layers/apz/src/APZPublicUtils.cpp
new file mode 100644
index 0000000000..6902e0738c
--- /dev/null
+++ b/gfx/layers/apz/src/APZPublicUtils.cpp
@@ -0,0 +1,111 @@
+/* -*- 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 "mozilla/layers/APZPublicUtils.h"
+
+#include "AsyncPanZoomController.h"
+#include "mozilla/HelperMacros.h"
+#include "mozilla/StaticPrefs_general.h"
+
+namespace mozilla {
+namespace layers {
+
+namespace apz {
+
+/*static*/ void InitializeGlobalState() {
+ MOZ_ASSERT(NS_IsMainThread());
+ AsyncPanZoomController::InitializeGlobalState();
+}
+
+/*static*/ const ScreenMargin CalculatePendingDisplayPort(
+ const FrameMetrics& aFrameMetrics, const ParentLayerPoint& aVelocity) {
+ return AsyncPanZoomController::CalculatePendingDisplayPort(
+ aFrameMetrics, aVelocity, AsyncPanZoomController::ZoomInProgress::No);
+}
+
+/*static*/ gfx::IntSize GetDisplayportAlignmentMultiplier(
+ const ScreenSize& aBaseSize) {
+ return AsyncPanZoomController::GetDisplayportAlignmentMultiplier(aBaseSize);
+}
+
+ScrollAnimationBezierPhysicsSettings ComputeBezierAnimationSettingsForOrigin(
+ ScrollOrigin aOrigin) {
+ int32_t minMS = 0;
+ int32_t maxMS = 0;
+ bool isOriginSmoothnessEnabled = false;
+
+#define READ_DURATIONS(prefbase) \
+ isOriginSmoothnessEnabled = StaticPrefs::general_smoothScroll() && \
+ StaticPrefs::general_smoothScroll_##prefbase(); \
+ if (isOriginSmoothnessEnabled) { \
+ minMS = StaticPrefs::general_smoothScroll_##prefbase##_durationMinMS(); \
+ maxMS = StaticPrefs::general_smoothScroll_##prefbase##_durationMaxMS(); \
+ }
+
+ switch (aOrigin) {
+ case ScrollOrigin::Pixels:
+ READ_DURATIONS(pixels)
+ break;
+ case ScrollOrigin::Lines:
+ READ_DURATIONS(lines)
+ break;
+ case ScrollOrigin::Pages:
+ READ_DURATIONS(pages)
+ break;
+ case ScrollOrigin::MouseWheel:
+ READ_DURATIONS(mouseWheel)
+ break;
+ case ScrollOrigin::Scrollbars:
+ READ_DURATIONS(scrollbars)
+ break;
+ default:
+ READ_DURATIONS(other)
+ break;
+ }
+
+#undef READ_DURATIONS
+
+ if (isOriginSmoothnessEnabled) {
+ static const int32_t kSmoothScrollMaxAllowedAnimationDurationMS = 10000;
+ maxMS = clamped(maxMS, 0, kSmoothScrollMaxAllowedAnimationDurationMS);
+ minMS = clamped(minMS, 0, maxMS);
+ }
+
+ // Keep the animation duration longer than the average event intervals
+ // (to "connect" consecutive scroll animations before the scroll comes to a
+ // stop).
+ double intervalRatio =
+ ((double)StaticPrefs::general_smoothScroll_durationToIntervalRatio()) /
+ 100.0;
+
+ // Duration should be at least as long as the intervals -> ratio is at least 1
+ intervalRatio = std::max(1.0, intervalRatio);
+
+ return ScrollAnimationBezierPhysicsSettings{minMS, maxMS, intervalRatio};
+}
+
+ScrollMode GetScrollModeForOrigin(ScrollOrigin origin) {
+ if (!StaticPrefs::general_smoothScroll()) return ScrollMode::Instant;
+ switch (origin) {
+ case ScrollOrigin::Lines:
+ return StaticPrefs::general_smoothScroll_lines() ? ScrollMode::Smooth
+ : ScrollMode::Instant;
+ case ScrollOrigin::Pages:
+ return StaticPrefs::general_smoothScroll_pages() ? ScrollMode::Smooth
+ : ScrollMode::Instant;
+ case ScrollOrigin::Other:
+ return StaticPrefs::general_smoothScroll_other() ? ScrollMode::Smooth
+ : ScrollMode::Instant;
+ default:
+ MOZ_ASSERT(false, "Unknown keyboard scroll origin");
+ return StaticPrefs::general_smoothScroll() ? ScrollMode::Smooth
+ : ScrollMode::Instant;
+ }
+}
+
+} // namespace apz
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/apz/src/APZSampler.cpp b/gfx/layers/apz/src/APZSampler.cpp
new file mode 100644
index 0000000000..088fb6f7a0
--- /dev/null
+++ b/gfx/layers/apz/src/APZSampler.cpp
@@ -0,0 +1,216 @@
+/* -*- 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 "mozilla/layers/APZSampler.h"
+
+#include "AsyncPanZoomController.h"
+#include "mozilla/ClearOnShutdown.h"
+#include "mozilla/layers/APZThreadUtils.h"
+#include "mozilla/layers/APZUtils.h"
+#include "mozilla/layers/CompositorThread.h"
+#include "mozilla/layers/SynchronousTask.h"
+#include "TreeTraversal.h"
+#include "mozilla/webrender/WebRenderAPI.h"
+
+namespace mozilla {
+namespace layers {
+
+StaticMutex APZSampler::sWindowIdLock;
+StaticAutoPtr<std::unordered_map<uint64_t, RefPtr<APZSampler>>>
+ APZSampler::sWindowIdMap;
+
+APZSampler::APZSampler(const RefPtr<APZCTreeManager>& aApz,
+ bool aIsUsingWebRender)
+ : mApz(aApz),
+ mIsUsingWebRender(aIsUsingWebRender),
+ mThreadIdLock("APZSampler::mThreadIdLock"),
+ mSampleTimeLock("APZSampler::mSampleTimeLock") {
+ MOZ_ASSERT(aApz);
+ mApz->SetSampler(this);
+}
+
+APZSampler::~APZSampler() { mApz->SetSampler(nullptr); }
+
+void APZSampler::Destroy() {
+ StaticMutexAutoLock lock(sWindowIdLock);
+ if (mWindowId) {
+ MOZ_ASSERT(sWindowIdMap);
+ sWindowIdMap->erase(wr::AsUint64(*mWindowId));
+ }
+}
+
+void APZSampler::SetWebRenderWindowId(const wr::WindowId& aWindowId) {
+ StaticMutexAutoLock lock(sWindowIdLock);
+ MOZ_ASSERT(!mWindowId);
+ mWindowId = Some(aWindowId);
+ if (!sWindowIdMap) {
+ sWindowIdMap = new std::unordered_map<uint64_t, RefPtr<APZSampler>>();
+ NS_DispatchToMainThread(NS_NewRunnableFunction(
+ "APZSampler::ClearOnShutdown", [] { ClearOnShutdown(&sWindowIdMap); }));
+ }
+ (*sWindowIdMap)[wr::AsUint64(aWindowId)] = this;
+}
+
+/*static*/
+void APZSampler::SetSamplerThread(const wr::WrWindowId& aWindowId) {
+ if (RefPtr<APZSampler> sampler = GetSampler(aWindowId)) {
+ MutexAutoLock lock(sampler->mThreadIdLock);
+ sampler->mSamplerThreadId = Some(PlatformThread::CurrentId());
+ }
+}
+
+/*static*/
+void APZSampler::SampleForWebRender(const wr::WrWindowId& aWindowId,
+ const uint64_t* aGeneratedFrameId,
+ wr::Transaction* aTransaction) {
+ if (RefPtr<APZSampler> sampler = GetSampler(aWindowId)) {
+ wr::TransactionWrapper txn(aTransaction);
+ Maybe<VsyncId> vsyncId =
+ aGeneratedFrameId ? Some(VsyncId{*aGeneratedFrameId}) : Nothing();
+ sampler->SampleForWebRender(vsyncId, txn);
+ }
+}
+
+void APZSampler::SetSampleTime(const SampleTime& aSampleTime) {
+ MOZ_ASSERT(CompositorThreadHolder::IsInCompositorThread());
+ MutexAutoLock lock(mSampleTimeLock);
+ // This only gets called with WR, and the time provided is going to be
+ // the time at which the current vsync interval ends. i.e. it is the timestamp
+ // for the next vsync that will occur.
+ mSampleTime = aSampleTime;
+}
+
+void APZSampler::SampleForWebRender(const Maybe<VsyncId>& aVsyncId,
+ wr::TransactionWrapper& aTxn) {
+ AssertOnSamplerThread();
+ SampleTime sampleTime;
+ { // scope lock
+ MutexAutoLock lock(mSampleTimeLock);
+
+ // If mSampleTime is null we're in a startup phase where the
+ // WebRenderBridgeParent hasn't yet provided us with a sample time.
+ // If we're that early there probably aren't any APZ animations happening
+ // anyway, so using Timestamp::Now() should be fine.
+ SampleTime now = SampleTime::FromNow();
+ sampleTime = mSampleTime.IsNull() ? now : mSampleTime;
+ }
+ mApz->SampleForWebRender(aVsyncId, aTxn, sampleTime);
+}
+
+AsyncTransform APZSampler::GetCurrentAsyncTransform(
+ const LayersId& aLayersId, const ScrollableLayerGuid::ViewID& aScrollId,
+ AsyncTransformComponents aComponents,
+ const MutexAutoLock& aProofOfMapLock) const {
+ MOZ_ASSERT(!CompositorThreadHolder::IsInCompositorThread());
+ AssertOnSamplerThread();
+
+ RefPtr<AsyncPanZoomController> apzc =
+ mApz->GetTargetAPZC(aLayersId, aScrollId, aProofOfMapLock);
+ if (!apzc) {
+ // It's possible that this function can get called even after the target
+ // APZC has been already destroyed because destroying the animation which
+ // triggers this function call is basically processed later than the APZC,
+ // i.e. queue mCompositorAnimationsToDelete in WebRenderBridgeParent and
+ // then remove in WebRenderBridgeParent::RemoveEpochDataPriorTo.
+ return AsyncTransform{};
+ }
+
+ return apzc->GetCurrentAsyncTransform(AsyncPanZoomController::eForCompositing,
+ aComponents);
+}
+
+ParentLayerRect APZSampler::GetCompositionBounds(
+ const LayersId& aLayersId, const ScrollableLayerGuid::ViewID& aScrollId,
+ const MutexAutoLock& aProofOfMapLock) const {
+ // This function can get called on the compositor in case of non WebRender
+ // get called on the sampler thread in case of WebRender.
+ AssertOnSamplerThread();
+
+ RefPtr<AsyncPanZoomController> apzc =
+ mApz->GetTargetAPZC(aLayersId, aScrollId, aProofOfMapLock);
+ if (!apzc) {
+ // On WebRender it's possible that this function can get called even after
+ // the target APZC has been already destroyed because destroying the
+ // animation which triggers this function call is basically processed later
+ // than the APZC one, i.e. queue mCompositorAnimationsToDelete in
+ // WebRenderBridgeParent and then remove them in
+ // WebRenderBridgeParent::RemoveEpochDataPriorTo.
+ return ParentLayerRect();
+ }
+
+ return apzc->GetCompositionBounds();
+}
+
+Maybe<APZSampler::ScrollOffsetAndRange>
+APZSampler::GetCurrentScrollOffsetAndRange(
+ const LayersId& aLayersId, const ScrollableLayerGuid::ViewID& aScrollId,
+ const MutexAutoLock& aProofOfMapLock) const {
+ // Note: This is called from OMTA Sampler thread, or Compositor thread for
+ // testing.
+
+ RefPtr<AsyncPanZoomController> apzc =
+ mApz->GetTargetAPZC(aLayersId, aScrollId, aProofOfMapLock);
+ if (!apzc) {
+ return Nothing();
+ }
+
+ return Some(ScrollOffsetAndRange{
+ // FIXME: Use the one-frame delayed offset now. This doesn't take
+ // scroll-linked effets into accounts, so we have to fix this in the
+ // future.
+ apzc->GetCurrentAsyncVisualViewport(
+ AsyncPanZoomController::AsyncTransformConsumer::eForCompositing)
+ .TopLeft(),
+ apzc->GetCurrentScrollRangeInCssPixels()});
+}
+
+void APZSampler::AssertOnSamplerThread() const {
+ if (APZThreadUtils::GetThreadAssertionsEnabled()) {
+ MOZ_ASSERT(IsSamplerThread());
+ }
+}
+
+bool APZSampler::IsSamplerThread() const {
+ if (mIsUsingWebRender) {
+ // If the sampler thread id isn't set yet then we cannot be running on the
+ // sampler thread (because we will have the thread id before we run any
+ // other C++ code on it, and this function is only ever invoked from C++
+ // code), so return false in that scenario.
+ MutexAutoLock lock(mThreadIdLock);
+ return mSamplerThreadId && PlatformThread::CurrentId() == *mSamplerThreadId;
+ }
+ return CompositorThreadHolder::IsInCompositorThread();
+}
+
+/*static*/
+already_AddRefed<APZSampler> APZSampler::GetSampler(
+ const wr::WrWindowId& aWindowId) {
+ RefPtr<APZSampler> sampler;
+ StaticMutexAutoLock lock(sWindowIdLock);
+ if (sWindowIdMap) {
+ auto it = sWindowIdMap->find(wr::AsUint64(aWindowId));
+ if (it != sWindowIdMap->end()) {
+ sampler = it->second;
+ }
+ }
+ return sampler.forget();
+}
+
+} // namespace layers
+} // namespace mozilla
+
+void apz_register_sampler(mozilla::wr::WrWindowId aWindowId) {
+ mozilla::layers::APZSampler::SetSamplerThread(aWindowId);
+}
+
+void apz_sample_transforms(mozilla::wr::WrWindowId aWindowId,
+ const uint64_t* aGeneratedFrameId,
+ mozilla::wr::Transaction* aTransaction) {
+ mozilla::layers::APZSampler::SampleForWebRender(aWindowId, aGeneratedFrameId,
+ aTransaction);
+}
+
+void apz_deregister_sampler(mozilla::wr::WrWindowId aWindowId) {}
diff --git a/gfx/layers/apz/src/APZUpdater.cpp b/gfx/layers/apz/src/APZUpdater.cpp
new file mode 100644
index 0000000000..2bbad6e1a7
--- /dev/null
+++ b/gfx/layers/apz/src/APZUpdater.cpp
@@ -0,0 +1,546 @@
+/* -*- 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 "mozilla/layers/APZUpdater.h"
+
+#include "APZCTreeManager.h"
+#include "AsyncPanZoomController.h"
+#include "base/task.h"
+#include "mozilla/ClearOnShutdown.h"
+#include "mozilla/layers/APZThreadUtils.h"
+#include "mozilla/layers/CompositorThread.h"
+#include "mozilla/layers/SynchronousTask.h"
+#include "mozilla/layers/WebRenderScrollDataWrapper.h"
+#include "mozilla/webrender/WebRenderAPI.h"
+
+namespace mozilla {
+namespace layers {
+
+StaticMutex APZUpdater::sWindowIdLock;
+StaticAutoPtr<std::unordered_map<uint64_t, APZUpdater*>>
+ APZUpdater::sWindowIdMap;
+
+APZUpdater::APZUpdater(const RefPtr<APZCTreeManager>& aApz,
+ bool aConnectedToWebRender)
+ : mApz(aApz),
+ mDestroyed(false),
+ mConnectedToWebRender(aConnectedToWebRender),
+ mThreadIdLock("APZUpdater::ThreadIdLock"),
+ mQueueLock("APZUpdater::QueueLock") {
+ MOZ_ASSERT(aApz);
+ mApz->SetUpdater(this);
+}
+
+APZUpdater::~APZUpdater() {
+ mApz->SetUpdater(nullptr);
+
+ StaticMutexAutoLock lock(sWindowIdLock);
+ if (mWindowId) {
+ MOZ_ASSERT(sWindowIdMap);
+ // Ensure that ClearTree was called and the task got run
+ MOZ_ASSERT(sWindowIdMap->find(wr::AsUint64(*mWindowId)) ==
+ sWindowIdMap->end());
+ }
+}
+
+bool APZUpdater::HasTreeManager(const RefPtr<APZCTreeManager>& aApz) {
+ return aApz.get() == mApz.get();
+}
+
+void APZUpdater::SetWebRenderWindowId(const wr::WindowId& aWindowId) {
+ StaticMutexAutoLock lock(sWindowIdLock);
+ MOZ_ASSERT(!mWindowId);
+ mWindowId = Some(aWindowId);
+ if (!sWindowIdMap) {
+ sWindowIdMap = new std::unordered_map<uint64_t, APZUpdater*>();
+ NS_DispatchToMainThread(NS_NewRunnableFunction(
+ "APZUpdater::ClearOnShutdown", [] { ClearOnShutdown(&sWindowIdMap); }));
+ }
+ (*sWindowIdMap)[wr::AsUint64(aWindowId)] = this;
+}
+
+/*static*/
+void APZUpdater::SetUpdaterThread(const wr::WrWindowId& aWindowId) {
+ if (RefPtr<APZUpdater> updater = GetUpdater(aWindowId)) {
+ MutexAutoLock lock(updater->mThreadIdLock);
+ updater->mUpdaterThreadId = Some(PlatformThread::CurrentId());
+ }
+}
+
+// Takes a conditional lock!
+/*static*/
+void APZUpdater::PrepareForSceneSwap(const wr::WrWindowId& aWindowId)
+ MOZ_NO_THREAD_SAFETY_ANALYSIS {
+ if (RefPtr<APZUpdater> updater = GetUpdater(aWindowId)) {
+ updater->mApz->LockTree();
+ }
+}
+
+// Assumes we took a conditional lock!
+/*static*/
+void APZUpdater::CompleteSceneSwap(const wr::WrWindowId& aWindowId,
+ const wr::WrPipelineInfo& aInfo) {
+ RefPtr<APZUpdater> updater = GetUpdater(aWindowId);
+ if (!updater) {
+ // This should only happen in cases where PrepareForSceneSwap also got a
+ // null updater. No updater-thread tasks get run between PrepareForSceneSwap
+ // and this function, so there is no opportunity for the updater mapping
+ // to have gotten removed from sWindowIdMap in between the two calls.
+ return;
+ }
+ updater->mApz->mTreeLock.AssertCurrentThreadIn();
+
+ for (const auto& removedPipeline : aInfo.removed_pipelines) {
+ LayersId layersId = wr::AsLayersId(removedPipeline.pipeline_id);
+ updater->mEpochData.erase(layersId);
+ }
+ // Reset the built info for all pipelines, then put it back for the ones
+ // that got built in this scene swap.
+ for (auto& i : updater->mEpochData) {
+ i.second.mBuilt = Nothing();
+ }
+ for (const auto& epoch : aInfo.epochs) {
+ LayersId layersId = wr::AsLayersId(epoch.pipeline_id);
+ updater->mEpochData[layersId].mBuilt = Some(epoch.epoch);
+ }
+
+ // Run any tasks that got unblocked, then unlock the tree. The order is
+ // important because we want to run all the tasks up to and including the
+ // UpdateHitTestingTree calls corresponding to the built epochs, and we
+ // want to run those before we release the lock (i.e. atomically with the
+ // scene swap). This ensures that any hit-tests always encounter a consistent
+ // state between the APZ tree and the built scene in WR.
+ //
+ // While we could add additional information to the queued tasks to figure
+ // out the minimal set of tasks we want to run here, it's easier and harmless
+ // to just run all the queued and now-unblocked tasks inside the lock.
+ //
+ // Note that the ProcessQueue here might remove the window id -> APZUpdater
+ // mapping from sWindowIdMap, but we still unlock the tree successfully to
+ // leave things in a good state.
+ updater->ProcessQueue();
+
+ updater->mApz->UnlockTree();
+}
+
+/*static*/
+void APZUpdater::ProcessPendingTasks(const wr::WrWindowId& aWindowId) {
+ if (RefPtr<APZUpdater> updater = GetUpdater(aWindowId)) {
+ updater->ProcessQueue();
+ }
+}
+
+void APZUpdater::ClearTree(LayersId aRootLayersId) {
+ MOZ_ASSERT(CompositorThreadHolder::IsInCompositorThread());
+ RefPtr<APZUpdater> self = this;
+ RunOnUpdaterThread(aRootLayersId,
+ NS_NewRunnableFunction("APZUpdater::ClearTree", [=]() {
+ self->mApz->ClearTree();
+ self->mDestroyed = true;
+
+ // Once ClearTree is called on the APZCTreeManager, we
+ // are in a shutdown phase. After this point it's ok if
+ // WebRender cannot get a hold of the updater via the
+ // window id, and it's a good point to remove the mapping
+ // and avoid leaving a dangling pointer to this object.
+ StaticMutexAutoLock lock(sWindowIdLock);
+ if (self->mWindowId) {
+ MOZ_ASSERT(sWindowIdMap);
+ sWindowIdMap->erase(wr::AsUint64(*(self->mWindowId)));
+ }
+ }));
+}
+
+void APZUpdater::UpdateFocusState(LayersId aRootLayerTreeId,
+ LayersId aOriginatingLayersId,
+ const FocusTarget& aFocusTarget) {
+ MOZ_ASSERT(CompositorThreadHolder::IsInCompositorThread());
+ RunOnUpdaterThread(aOriginatingLayersId,
+ NewRunnableMethod<LayersId, LayersId, FocusTarget>(
+ "APZUpdater::UpdateFocusState", mApz,
+ &APZCTreeManager::UpdateFocusState, aRootLayerTreeId,
+ aOriginatingLayersId, aFocusTarget));
+}
+
+void APZUpdater::UpdateScrollDataAndTreeState(
+ LayersId aRootLayerTreeId, LayersId aOriginatingLayersId,
+ const wr::Epoch& aEpoch, WebRenderScrollData&& aScrollData) {
+ MOZ_ASSERT(CompositorThreadHolder::IsInCompositorThread());
+ RefPtr<APZUpdater> self = this;
+ // Insert an epoch requirement update into the queue, so that
+ // tasks inserted into the queue after this point only get executed
+ // once the epoch requirement is satisfied. In particular, the
+ // UpdateHitTestingTree call below needs to wait until the epoch requirement
+ // is satisfied, which is why it is a separate task in the queue.
+ RunOnUpdaterThread(
+ aOriginatingLayersId,
+ NS_NewRunnableFunction("APZUpdater::UpdateEpochRequirement", [=]() {
+ if (aRootLayerTreeId == aOriginatingLayersId) {
+ self->mEpochData[aOriginatingLayersId].mIsRoot = true;
+ }
+ self->mEpochData[aOriginatingLayersId].mRequired = aEpoch;
+ }));
+ RunOnUpdaterThread(
+ aOriginatingLayersId,
+ NS_NewRunnableFunction(
+ "APZUpdater::UpdateHitTestingTree",
+ [=, aScrollData = std::move(aScrollData)]() mutable {
+ auto isFirstPaint = aScrollData.IsFirstPaint();
+ auto paintSequenceNumber = aScrollData.GetPaintSequenceNumber();
+
+ self->mScrollData[aOriginatingLayersId] = std::move(aScrollData);
+ auto root = self->mScrollData.find(aRootLayerTreeId);
+ if (root == self->mScrollData.end()) {
+ return;
+ }
+ self->mApz->UpdateHitTestingTree(
+ WebRenderScrollDataWrapper(*self, &(root->second)),
+ isFirstPaint, aOriginatingLayersId, paintSequenceNumber);
+ }));
+}
+
+void APZUpdater::UpdateScrollOffsets(LayersId aRootLayerTreeId,
+ LayersId aOriginatingLayersId,
+ ScrollUpdatesMap&& aUpdates,
+ uint32_t aPaintSequenceNumber) {
+ MOZ_ASSERT(CompositorThreadHolder::IsInCompositorThread());
+ RefPtr<APZUpdater> self = this;
+ RunOnUpdaterThread(
+ aOriginatingLayersId,
+ NS_NewRunnableFunction(
+ "APZUpdater::UpdateScrollOffsets",
+ [=, updates = std::move(aUpdates)]() mutable {
+ self->mScrollData[aOriginatingLayersId].ApplyUpdates(
+ std::move(updates), aPaintSequenceNumber);
+ auto root = self->mScrollData.find(aRootLayerTreeId);
+ if (root == self->mScrollData.end()) {
+ return;
+ }
+ self->mApz->UpdateHitTestingTree(
+ WebRenderScrollDataWrapper(*self, &(root->second)),
+ /*isFirstPaint*/ false, aOriginatingLayersId,
+ aPaintSequenceNumber);
+ }));
+}
+
+void APZUpdater::NotifyLayerTreeAdopted(LayersId aLayersId,
+ const RefPtr<APZUpdater>& aOldUpdater) {
+ MOZ_ASSERT(CompositorThreadHolder::IsInCompositorThread());
+ RunOnUpdaterThread(aLayersId,
+ NewRunnableMethod<LayersId, RefPtr<APZCTreeManager>>(
+ "APZUpdater::NotifyLayerTreeAdopted", mApz,
+ &APZCTreeManager::NotifyLayerTreeAdopted, aLayersId,
+ aOldUpdater ? aOldUpdater->mApz : nullptr));
+}
+
+void APZUpdater::NotifyLayerTreeRemoved(LayersId aLayersId) {
+ MOZ_ASSERT(CompositorThreadHolder::IsInCompositorThread());
+ RefPtr<APZUpdater> self = this;
+ RunOnUpdaterThread(
+ aLayersId,
+ NS_NewRunnableFunction("APZUpdater::NotifyLayerTreeRemoved", [=]() {
+ self->mEpochData.erase(aLayersId);
+ self->mScrollData.erase(aLayersId);
+ self->mApz->NotifyLayerTreeRemoved(aLayersId);
+ }));
+}
+
+bool APZUpdater::GetAPZTestData(LayersId aLayersId, APZTestData* aOutData) {
+ MOZ_ASSERT(CompositorThreadHolder::IsInCompositorThread());
+
+ RefPtr<APZCTreeManager> apz = mApz;
+ bool ret = false;
+ SynchronousTask waiter("APZUpdater::GetAPZTestData");
+ RunOnUpdaterThread(
+ aLayersId, NS_NewRunnableFunction("APZUpdater::GetAPZTestData", [&]() {
+ AutoCompleteTask notifier(&waiter);
+ ret = apz->GetAPZTestData(aLayersId, aOutData);
+ }));
+
+ // Wait until the task posted above has run and populated aOutData and ret
+ waiter.Wait();
+
+ return ret;
+}
+
+void APZUpdater::SetTestAsyncScrollOffset(
+ LayersId aLayersId, const ScrollableLayerGuid::ViewID& aScrollId,
+ const CSSPoint& aOffset) {
+ MOZ_ASSERT(CompositorThreadHolder::IsInCompositorThread());
+ RefPtr<APZCTreeManager> apz = mApz;
+ RunOnUpdaterThread(
+ aLayersId,
+ NS_NewRunnableFunction("APZUpdater::SetTestAsyncScrollOffset", [=]() {
+ RefPtr<AsyncPanZoomController> apzc =
+ apz->GetTargetAPZC(aLayersId, aScrollId);
+ if (apzc) {
+ apzc->SetTestAsyncScrollOffset(aOffset);
+ } else {
+ NS_WARNING("Unable to find APZC in SetTestAsyncScrollOffset");
+ }
+ }));
+}
+
+void APZUpdater::SetTestAsyncZoom(LayersId aLayersId,
+ const ScrollableLayerGuid::ViewID& aScrollId,
+ const LayerToParentLayerScale& aZoom) {
+ MOZ_ASSERT(CompositorThreadHolder::IsInCompositorThread());
+ RefPtr<APZCTreeManager> apz = mApz;
+ RunOnUpdaterThread(
+ aLayersId, NS_NewRunnableFunction("APZUpdater::SetTestAsyncZoom", [=]() {
+ RefPtr<AsyncPanZoomController> apzc =
+ apz->GetTargetAPZC(aLayersId, aScrollId);
+ if (apzc) {
+ apzc->SetTestAsyncZoom(aZoom);
+ } else {
+ NS_WARNING("Unable to find APZC in SetTestAsyncZoom");
+ }
+ }));
+}
+
+const WebRenderScrollData* APZUpdater::GetScrollData(LayersId aLayersId) const {
+ AssertOnUpdaterThread();
+ auto it = mScrollData.find(aLayersId);
+ return (it == mScrollData.end() ? nullptr : &(it->second));
+}
+
+void APZUpdater::AssertOnUpdaterThread() const {
+ if (APZThreadUtils::GetThreadAssertionsEnabled()) {
+ MOZ_ASSERT(IsUpdaterThread());
+ }
+}
+
+void APZUpdater::RunOnUpdaterThread(LayersId aLayersId,
+ already_AddRefed<Runnable> aTask) {
+ RefPtr<Runnable> task = aTask;
+
+ // In the scenario where IsConnectedToWebRender() is true, this function
+ // might get called early (before mUpdaterThreadId is set). In that case
+ // IsUpdaterThread() will return false and we'll queue the task onto
+ // mUpdaterQueue. This is fine; the task is still guaranteed to run (barring
+ // catastrophic failure) because the WakeSceneBuilder call will still trigger
+ // the callback to run tasks.
+
+ if (IsUpdaterThread()) {
+ // This function should only be called from the updater thread in test
+ // scenarios where we are not connected to WebRender. If it were called from
+ // the updater thread when we are connected to WebRender, running the task
+ // right away would be incorrect (we'd need to check that |aLayersId|
+ // isn't blocked, and if it is then enqueue the task instead).
+ MOZ_ASSERT(!IsConnectedToWebRender());
+ task->Run();
+ return;
+ }
+
+ if (IsConnectedToWebRender()) {
+ // If the updater thread is a WebRender thread, and we're not on it
+ // right now, save the task in the queue. We will run tasks from the queue
+ // during the callback from the updater thread, which we trigger by the
+ // call to WakeSceneBuilder.
+
+ bool sendWakeMessage = true;
+ { // scope lock
+ MutexAutoLock lock(mQueueLock);
+ for (const auto& queuedTask : mUpdaterQueue) {
+ if (queuedTask.mLayersId == aLayersId) {
+ // If there's already a task in the queue with this layers id, then
+ // we must have previously sent a WakeSceneBuilder message (when
+ // adding the first task with this layers id to the queue). Either
+ // that hasn't been fully processed yet, or the layers id is blocked
+ // waiting for an epoch - in either case there's no point in sending
+ // another WakeSceneBuilder message.
+ sendWakeMessage = false;
+ break;
+ }
+ }
+ mUpdaterQueue.push_back(QueuedTask{aLayersId, task});
+ }
+ if (sendWakeMessage) {
+ RefPtr<wr::WebRenderAPI> api = mApz->GetWebRenderAPI();
+ if (api) {
+ api->WakeSceneBuilder();
+ } else {
+ // Not sure if this can happen, but it might be possible. If it does,
+ // the task is in the queue, but if we didn't get a WebRenderAPI it
+ // might never run, or it might run later if we manage to get a
+ // WebRenderAPI later. For now let's just emit a warning, this can
+ // probably be upgraded to an assert later.
+ NS_WARNING("Possibly dropping task posted to updater thread");
+ }
+ }
+ return;
+ }
+
+ if (CompositorThread()) {
+ CompositorThread()->Dispatch(task.forget());
+ } else {
+ // Could happen during startup
+ NS_WARNING("Dropping task posted to updater thread");
+ }
+}
+
+bool APZUpdater::IsUpdaterThread() const {
+ if (IsConnectedToWebRender()) {
+ // If the updater thread id isn't set yet then we cannot be running on the
+ // updater thread (because we will have the thread id before we run any
+ // C++ code on it, and this function is only ever invoked from C++ code),
+ // so return false in that scenario.
+ MutexAutoLock lock(mThreadIdLock);
+ return mUpdaterThreadId && PlatformThread::CurrentId() == *mUpdaterThreadId;
+ }
+ return CompositorThreadHolder::IsInCompositorThread();
+}
+
+void APZUpdater::RunOnControllerThread(LayersId aLayersId,
+ already_AddRefed<Runnable> aTask) {
+ MOZ_ASSERT(CompositorThreadHolder::IsInCompositorThread());
+
+ RefPtr<Runnable> task = aTask;
+
+ RunOnUpdaterThread(
+ aLayersId,
+ NewRunnableFunction("APZUpdater::RunOnControllerThread",
+ &APZThreadUtils::RunOnControllerThread,
+ std::move(task), nsIThread::DISPATCH_NORMAL));
+}
+
+bool APZUpdater::IsConnectedToWebRender() const {
+ return mConnectedToWebRender;
+}
+
+/*static*/
+already_AddRefed<APZUpdater> APZUpdater::GetUpdater(
+ const wr::WrWindowId& aWindowId) {
+ RefPtr<APZUpdater> updater;
+ StaticMutexAutoLock lock(sWindowIdLock);
+ if (sWindowIdMap) {
+ auto it = sWindowIdMap->find(wr::AsUint64(aWindowId));
+ if (it != sWindowIdMap->end()) {
+ updater = it->second;
+ }
+ }
+ return updater.forget();
+}
+
+void APZUpdater::ProcessQueue() {
+ MOZ_ASSERT(!mDestroyed);
+
+ { // scope lock to check for emptiness
+ MutexAutoLock lock(mQueueLock);
+ if (mUpdaterQueue.empty()) {
+ return;
+ }
+ }
+
+ std::deque<QueuedTask> blockedTasks;
+ while (true) {
+ QueuedTask task;
+
+ { // scope lock to extract a task
+ MutexAutoLock lock(mQueueLock);
+ if (mUpdaterQueue.empty()) {
+ // If we're done processing mUpdaterQueue, swap the tasks that are
+ // still blocked back in and finish
+ std::swap(mUpdaterQueue, blockedTasks);
+ break;
+ }
+ task = mUpdaterQueue.front();
+ mUpdaterQueue.pop_front();
+ }
+
+ // We check the task to see if it is blocked. Note that while this
+ // ProcessQueue function is executing, a particular layers id cannot go
+ // from blocked to unblocked, because only CompleteSceneSwap can unblock
+ // a layers id, and that also runs on the updater thread. If somehow
+ // a layers id gets unblocked while we're processing the queue, then it
+ // might result in tasks getting executed out of order.
+
+ auto it = mEpochData.find(task.mLayersId);
+ if (it != mEpochData.end() && it->second.IsBlocked()) {
+ // If this task is blocked, put it into the blockedTasks queue that
+ // we will replace mUpdaterQueue with
+ blockedTasks.push_back(task);
+ } else {
+ // Run and discard the task
+ task.mRunnable->Run();
+ }
+ }
+
+ if (mDestroyed) {
+ // If we get here, then we must have just run the ClearTree task for
+ // this updater. There might be tasks in the queue from content subtrees
+ // of this window that are blocked due to stale epochs. This can happen
+ // if the tasks were queued after the root pipeline was removed in
+ // WebRender, which prevents scene builds (and therefore prevents us
+ // from getting updated epochs via CompleteSceneSwap). See bug 1465658
+ // comment 43 for some more context.
+ // To avoid leaking these tasks, we discard the contents of the queue.
+ // This happens during window shutdown so if we don't run the tasks it's
+ // not going to matter much.
+ MutexAutoLock lock(mQueueLock);
+ if (!mUpdaterQueue.empty()) {
+ mUpdaterQueue.clear();
+ }
+ }
+}
+
+void APZUpdater::MarkAsDetached(LayersId aLayersId) {
+ mApz->MarkAsDetached(aLayersId);
+}
+
+APZUpdater::EpochState::EpochState() : mRequired{0}, mIsRoot(false) {}
+
+bool APZUpdater::EpochState::IsBlocked() const {
+ // The root is a special case because we basically assume it is "visible"
+ // even before it is built for the first time. This is because building the
+ // scene automatically makes it visible, and we need to make sure the APZ
+ // scroll data gets applied atomically with that happening.
+ //
+ // Layer subtrees on the other hand do not automatically become visible upon
+ // being built, because there must be a another layer tree update to change
+ // the visibility (i.e. an ancestor layer tree update that adds the necessary
+ // reflayer to complete the chain of reflayers).
+ //
+ // So in the case of non-visible subtrees, we know that no hit-test will
+ // actually end up hitting that subtree either before or after the scene swap,
+ // because the subtree will remain non-visible. That in turns means that we
+ // can apply the APZ scroll data for that subtree epoch before the scene is
+ // built, because it's not going to get used anyway. And that means we don't
+ // need to block the queue for non-visible subtrees. Which is a good thing,
+ // because in practice it seems like we often have non-visible subtrees sent
+ // to the compositor from content.
+ if (mIsRoot && !mBuilt) {
+ return true;
+ }
+ return mBuilt && (*mBuilt < mRequired);
+}
+
+} // namespace layers
+} // namespace mozilla
+
+// Rust callback implementations
+
+void apz_register_updater(mozilla::wr::WrWindowId aWindowId) {
+ mozilla::layers::APZUpdater::SetUpdaterThread(aWindowId);
+}
+
+void apz_pre_scene_swap(mozilla::wr::WrWindowId aWindowId) {
+ mozilla::layers::APZUpdater::PrepareForSceneSwap(aWindowId);
+}
+
+void apz_post_scene_swap(mozilla::wr::WrWindowId aWindowId,
+ const mozilla::wr::WrPipelineInfo* aInfo) {
+ mozilla::layers::APZUpdater::CompleteSceneSwap(aWindowId, *aInfo);
+}
+
+void apz_run_updater(mozilla::wr::WrWindowId aWindowId) {
+ mozilla::layers::APZUpdater::ProcessPendingTasks(aWindowId);
+}
+
+void apz_deregister_updater(mozilla::wr::WrWindowId aWindowId) {
+ // Run anything that's still left.
+ mozilla::layers::APZUpdater::ProcessPendingTasks(aWindowId);
+}
diff --git a/gfx/layers/apz/src/APZUtils.cpp b/gfx/layers/apz/src/APZUtils.cpp
new file mode 100644
index 0000000000..843046c34a
--- /dev/null
+++ b/gfx/layers/apz/src/APZUtils.cpp
@@ -0,0 +1,118 @@
+/* -*- 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 "mozilla/layers/APZUtils.h"
+
+#include "mozilla/StaticPrefs_apz.h"
+#include "mozilla/StaticPrefs_layers.h"
+
+namespace mozilla {
+namespace layers {
+
+namespace apz {
+
+bool IsCloseToHorizontal(float aAngle, float aThreshold) {
+ return (aAngle < aThreshold || aAngle > (M_PI - aThreshold));
+}
+
+bool IsCloseToVertical(float aAngle, float aThreshold) {
+ return (fabs(aAngle - (M_PI / 2)) < aThreshold);
+}
+
+bool IsStuckAtBottom(gfxFloat aTranslation,
+ const LayerRectAbsolute& aInnerRange,
+ const LayerRectAbsolute& aOuterRange) {
+ // The item will be stuck at the bottom if the async scroll delta is in
+ // the range [aOuterRange.Y(), aInnerRange.Y()]. Since the translation
+ // is negated with repect to the async scroll delta (i.e. scrolling down
+ // produces a positive scroll delta and negative translation), we invert it
+ // and check to see if it falls in the specified range.
+ return aOuterRange.Y() <= -aTranslation && -aTranslation <= aInnerRange.Y();
+}
+
+bool IsStuckAtTop(gfxFloat aTranslation, const LayerRectAbsolute& aInnerRange,
+ const LayerRectAbsolute& aOuterRange) {
+ // Same as IsStuckAtBottom, except we want to check for the range
+ // [aInnerRange.YMost(), aOuterRange.YMost()].
+ return aInnerRange.YMost() <= -aTranslation &&
+ -aTranslation <= aOuterRange.YMost();
+}
+
+ScreenPoint ComputeFixedMarginsOffset(
+ const ScreenMargin& aCompositorFixedLayerMargins, SideBits aFixedSides,
+ const ScreenMargin& aGeckoFixedLayerMargins) {
+ // Work out the necessary translation, in screen space.
+ ScreenPoint translation;
+
+ ScreenMargin effectiveMargin =
+ aCompositorFixedLayerMargins - aGeckoFixedLayerMargins;
+ if ((aFixedSides & SideBits::eLeftRight) == SideBits::eLeftRight) {
+ translation.x += (effectiveMargin.left - effectiveMargin.right) / 2;
+ } else if (aFixedSides & SideBits::eRight) {
+ translation.x -= effectiveMargin.right;
+ } else if (aFixedSides & SideBits::eLeft) {
+ translation.x += effectiveMargin.left;
+ }
+
+ if ((aFixedSides & SideBits::eTopBottom) == SideBits::eTopBottom) {
+ translation.y += (effectiveMargin.top - effectiveMargin.bottom) / 2;
+ } else if (aFixedSides & SideBits::eBottom) {
+ translation.y -= effectiveMargin.bottom;
+ } else if (aFixedSides & SideBits::eTop) {
+ translation.y += effectiveMargin.top;
+ }
+
+ return translation;
+}
+
+bool AboutToCheckerboard(const FrameMetrics& aPaintedMetrics,
+ const FrameMetrics& aCompositorMetrics) {
+ // The main-thread code to compute the painted area can introduce some
+ // rounding error due to multiple unit conversions, so we inflate the rect by
+ // one app unit to account for that.
+ CSSRect painted = aPaintedMetrics.GetDisplayPort() +
+ aPaintedMetrics.GetLayoutScrollOffset();
+ painted.Inflate(CSSMargin::FromAppUnits(nsMargin(1, 1, 1, 1)));
+
+ // Inflate the rect by the danger zone. See the description of the danger zone
+ // prefs in AsyncPanZoomController.cpp for an explanation of this.
+ CSSRect visible =
+ CSSRect(aCompositorMetrics.GetVisualScrollOffset(),
+ aCompositorMetrics.CalculateBoundedCompositedSizeInCssPixels());
+ visible.Inflate(ScreenSize(StaticPrefs::apz_danger_zone_x(),
+ StaticPrefs::apz_danger_zone_y()) /
+ aCompositorMetrics.DisplayportPixelsPerCSSPixel());
+
+ // Clamp both rects to the scrollable rect, because having either of those
+ // exceed the scrollable rect doesn't make sense, and could lead to false
+ // positives.
+ painted = painted.Intersect(aPaintedMetrics.GetScrollableRect());
+ visible = visible.Intersect(aPaintedMetrics.GetScrollableRect());
+
+ return !painted.Contains(visible);
+}
+
+SideBits GetOverscrollSideBits(const ParentLayerPoint& aOverscrollAmount) {
+ SideBits sides = SideBits::eNone;
+
+ if (aOverscrollAmount.x < 0) {
+ sides |= SideBits::eLeft;
+ } else if (aOverscrollAmount.x > 0) {
+ sides |= SideBits::eRight;
+ }
+
+ if (aOverscrollAmount.y < 0) {
+ sides |= SideBits::eTop;
+ } else if (aOverscrollAmount.y > 0) {
+ sides |= SideBits::eBottom;
+ }
+
+ return sides;
+}
+
+} // namespace apz
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/apz/src/APZUtils.h b/gfx/layers/apz/src/APZUtils.h
new file mode 100644
index 0000000000..8adaa71339
--- /dev/null
+++ b/gfx/layers/apz/src/APZUtils.h
@@ -0,0 +1,220 @@
+/* -*- 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_APZUtils_h
+#define mozilla_layers_APZUtils_h
+
+// This file is for APZ-related utilities that are used by code in gfx/layers
+// only. For APZ-related utilities used by the Rest of the World (widget/,
+// layout/, dom/, IPDL protocols, etc.), use APZPublicUtils.h.
+// Do not include this header from source files outside of gfx/layers.
+
+#include <stdint.h> // for uint32_t
+#include <type_traits>
+#include "gfxTypes.h"
+#include "FrameMetrics.h"
+#include "LayersTypes.h"
+#include "UnitTransforms.h"
+#include "mozilla/gfx/CompositorHitTestInfo.h"
+#include "mozilla/gfx/Point.h"
+#include "mozilla/DefineEnum.h"
+#include "mozilla/EnumSet.h"
+#include "mozilla/FloatingPoint.h"
+
+namespace mozilla {
+
+namespace layers {
+
+enum CancelAnimationFlags : uint32_t {
+ Default = 0x0, /* Cancel all animations */
+ ExcludeOverscroll = 0x1, /* Don't clear overscroll */
+ ScrollSnap = 0x2, /* Snap to snap points */
+ ExcludeWheel = 0x4, /* Don't stop wheel smooth-scroll animations */
+ TriggeredExternally = 0x8, /* Cancellation was not triggered by APZ in
+ response to an input event */
+};
+
+inline CancelAnimationFlags operator|(CancelAnimationFlags a,
+ CancelAnimationFlags b) {
+ return static_cast<CancelAnimationFlags>(static_cast<int>(a) |
+ static_cast<int>(b));
+}
+
+// clang-format off
+enum class ScrollSource {
+ // Touch-screen.
+ Touchscreen,
+
+ // Touchpad with gesture support.
+ Touchpad,
+
+ // Mouse wheel.
+ Wheel,
+
+ // Keyboard
+ Keyboard,
+};
+// clang-format on
+
+inline bool ScrollSourceRespectsDisregardedDirections(ScrollSource aSource) {
+ return aSource == ScrollSource::Wheel || aSource == ScrollSource::Touchpad;
+}
+
+inline bool ScrollSourceAllowsOverscroll(ScrollSource aSource) {
+ return aSource == ScrollSource::Touchpad ||
+ aSource == ScrollSource::Touchscreen;
+}
+
+// Epsilon to be used when comparing 'float' coordinate values
+// with FuzzyEqualsAdditive. The rationale is that 'float' has 7 decimal
+// digits of precision, and coordinate values should be no larger than in the
+// ten thousands. Note also that the smallest legitimate difference in page
+// coordinates is 1 app unit, which is 1/60 of a (CSS pixel), so this epsilon
+// isn't too large.
+const CSSCoord COORDINATE_EPSILON = 0.01f;
+
+inline bool IsZero(const CSSPoint& aPoint) {
+ return FuzzyEqualsAdditive(aPoint.x, CSSCoord(), COORDINATE_EPSILON) &&
+ FuzzyEqualsAdditive(aPoint.y, CSSCoord(), COORDINATE_EPSILON);
+}
+
+// Represents async transforms consisting of a scale and a translation.
+struct AsyncTransform {
+ explicit AsyncTransform(
+ LayerToParentLayerScale aScale = LayerToParentLayerScale(),
+ ParentLayerPoint aTranslation = ParentLayerPoint())
+ : mScale(aScale), mTranslation(aTranslation) {}
+
+ operator AsyncTransformComponentMatrix() const {
+ return AsyncTransformComponentMatrix::Scaling(mScale.scale, mScale.scale, 1)
+ .PostTranslate(mTranslation.x, mTranslation.y, 0);
+ }
+
+ bool operator==(const AsyncTransform& rhs) const {
+ return mTranslation == rhs.mTranslation && mScale == rhs.mScale;
+ }
+
+ bool operator!=(const AsyncTransform& rhs) const { return !(*this == rhs); }
+
+ LayerToParentLayerScale mScale;
+ ParentLayerPoint mTranslation;
+};
+
+// Deem an AsyncTransformComponentMatrix (obtained by multiplying together
+// one or more AsyncTransformComponentMatrix objects) as constituting a
+// complete async transform.
+inline AsyncTransformMatrix CompleteAsyncTransform(
+ const AsyncTransformComponentMatrix& aMatrix) {
+ return ViewAs<AsyncTransformMatrix>(
+ aMatrix, PixelCastJustification::MultipleAsyncTransforms);
+}
+
+struct TargetConfirmationFlags final {
+ explicit TargetConfirmationFlags(bool aTargetConfirmed)
+ : mTargetConfirmed(aTargetConfirmed),
+ mRequiresTargetConfirmation(false),
+ mHitScrollbar(false),
+ mHitScrollThumb(false),
+ mDispatchToContent(false) {}
+
+ explicit TargetConfirmationFlags(
+ const gfx::CompositorHitTestInfo& aHitTestInfo)
+ : mTargetConfirmed(
+ (aHitTestInfo != gfx::CompositorHitTestInvisibleToHit) &&
+ (aHitTestInfo & gfx::CompositorHitTestDispatchToContent).isEmpty()),
+ mRequiresTargetConfirmation(aHitTestInfo.contains(
+ gfx::CompositorHitTestFlags::eRequiresTargetConfirmation)),
+ mHitScrollbar(
+ aHitTestInfo.contains(gfx::CompositorHitTestFlags::eScrollbar)),
+ mHitScrollThumb(aHitTestInfo.contains(
+ gfx::CompositorHitTestFlags::eScrollbarThumb)),
+ mDispatchToContent(
+ !(aHitTestInfo & gfx::CompositorHitTestDispatchToContent)
+ .isEmpty()) {}
+
+ bool mTargetConfirmed : 1;
+ bool mRequiresTargetConfirmation : 1;
+ bool mHitScrollbar : 1;
+ bool mHitScrollThumb : 1;
+ bool mDispatchToContent : 1;
+};
+
+enum class AsyncTransformComponent { eLayout, eVisual };
+
+using AsyncTransformComponents = EnumSet<AsyncTransformComponent>;
+
+constexpr AsyncTransformComponents LayoutAndVisual(
+ AsyncTransformComponent::eLayout, AsyncTransformComponent::eVisual);
+
+/**
+ * Metrics that GeckoView wants to know at every composite.
+ * These are the effective visual scroll offset and zoom level of
+ * the root content APZC at composition time.
+ */
+struct GeckoViewMetrics {
+ CSSPoint mVisualScrollOffset;
+ CSSToParentLayerScale mZoom;
+};
+
+namespace apz {
+
+/**
+ * Is aAngle within the given threshold of the horizontal axis?
+ * @param aAngle an angle in radians in the range [0, pi]
+ * @param aThreshold an angle in radians in the range [0, pi/2]
+ */
+bool IsCloseToHorizontal(float aAngle, float aThreshold);
+
+// As above, but for the vertical axis.
+bool IsCloseToVertical(float aAngle, float aThreshold);
+
+// Returns true if a sticky layer with async translation |aTranslation| is
+// stuck with a bottom margin. The inner/outer ranges are produced by the main
+// thread at the last paint, and so |aTranslation| only needs to be the
+// async translation from the last paint.
+bool IsStuckAtBottom(gfxFloat aTranslation,
+ const LayerRectAbsolute& aInnerRange,
+ const LayerRectAbsolute& aOuterRange);
+
+// Returns true if a sticky layer with async translation |aTranslation| is
+// stuck with a top margin.
+bool IsStuckAtTop(gfxFloat aTranslation, const LayerRectAbsolute& aInnerRange,
+ const LayerRectAbsolute& aOuterRange);
+
+/**
+ * Compute the translation that should be applied to a layer that's fixed
+ * at |eFixedSides|, to respect the fixed layer margins |aFixedMargins|.
+ */
+ScreenPoint ComputeFixedMarginsOffset(
+ const ScreenMargin& aCompositorFixedLayerMargins, SideBits aFixedSides,
+ const ScreenMargin& aGeckoFixedLayerMargins);
+
+/**
+ * Takes the visible rect from the compositor metrics, adds a pref-based
+ * margin around it, and checks to see if it is contained inside the painted
+ * rect from the painted metrics. Returns true if it is contained, or false
+ * if not. Returning false means that a (relatively) small amount of async
+ * scrolling/zooming can result in the visible area going outside the painted
+ * area and resulting in visual checkerboarding.
+ * Note that this may return false positives for cases where the scrollframe
+ * in question is nested inside other scrollframes, as the composition bounds
+ * used to determine the visible rect may in fact be clipped by enclosing
+ * scrollframes, but that is not accounted for in this function.
+ */
+bool AboutToCheckerboard(const FrameMetrics& aPaintedMetrics,
+ const FrameMetrics& aCompositorMetrics);
+
+/**
+ * Returns SideBits where the given |aOverscrollAmount| overscrolls.
+ */
+SideBits GetOverscrollSideBits(const ParentLayerPoint& aOverscrollAmount);
+
+} // namespace apz
+
+} // namespace layers
+} // namespace mozilla
+
+#endif // mozilla_layers_APZUtils_h
diff --git a/gfx/layers/apz/src/AndroidAPZ.cpp b/gfx/layers/apz/src/AndroidAPZ.cpp
new file mode 100644
index 0000000000..4895c893de
--- /dev/null
+++ b/gfx/layers/apz/src/AndroidAPZ.cpp
@@ -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/. */
+
+#include "AndroidAPZ.h"
+
+#include "AndroidFlingPhysics.h"
+#include "AndroidVelocityTracker.h"
+#include "AsyncPanZoomController.h"
+#include "GenericFlingAnimation.h"
+#include "OverscrollHandoffState.h"
+
+namespace mozilla {
+namespace layers {
+
+AsyncPanZoomAnimation* AndroidSpecificState::CreateFlingAnimation(
+ AsyncPanZoomController& aApzc, const FlingHandoffState& aHandoffState,
+ float aPLPPI) {
+ return new GenericFlingAnimation<AndroidFlingPhysics>(aApzc, aHandoffState,
+ aPLPPI);
+}
+
+UniquePtr<VelocityTracker> AndroidSpecificState::CreateVelocityTracker(
+ Axis* aAxis) {
+ return MakeUnique<AndroidVelocityTracker>();
+}
+
+/* static */
+void AndroidSpecificState::InitializeGlobalState() {
+ AndroidFlingPhysics::InitializeGlobalState();
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/apz/src/AndroidAPZ.h b/gfx/layers/apz/src/AndroidAPZ.h
new file mode 100644
index 0000000000..ab30b4e612
--- /dev/null
+++ b/gfx/layers/apz/src/AndroidAPZ.h
@@ -0,0 +1,34 @@
+/* -*- 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_AndroidAPZ_h_
+#define mozilla_layers_AndroidAPZ_h_
+
+#include "AsyncPanZoomAnimation.h"
+#include "AsyncPanZoomController.h"
+
+namespace mozilla {
+namespace layers {
+
+class AndroidSpecificState : public PlatformSpecificStateBase {
+ public:
+ virtual AndroidSpecificState* AsAndroidSpecificState() override {
+ return this;
+ }
+
+ virtual AsyncPanZoomAnimation* CreateFlingAnimation(
+ AsyncPanZoomController& aApzc, const FlingHandoffState& aHandoffState,
+ float aPLPPI) override;
+ virtual UniquePtr<VelocityTracker> CreateVelocityTracker(
+ Axis* aAxis) override;
+
+ static void InitializeGlobalState();
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif // mozilla_layers_AndroidAPZ_h_
diff --git a/gfx/layers/apz/src/AndroidFlingPhysics.cpp b/gfx/layers/apz/src/AndroidFlingPhysics.cpp
new file mode 100644
index 0000000000..d18f4be4d4
--- /dev/null
+++ b/gfx/layers/apz/src/AndroidFlingPhysics.cpp
@@ -0,0 +1,218 @@
+/* -*- 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 "AndroidFlingPhysics.h"
+
+#include <cmath>
+
+#include "mozilla/ClearOnShutdown.h"
+#include "mozilla/StaticPrefs_apz.h"
+#include "mozilla/StaticPtr.h"
+
+namespace mozilla {
+namespace layers {
+
+// The fling physics calculations implemented here are adapted from
+// Chrome's implementation of fling physics on Android:
+// https://cs.chromium.org/chromium/src/ui/events/android/scroller.cc?rcl=3ae3aaff927038a5c644926842cb0c31dea60c79
+
+static double ComputeDeceleration(float aDPI) {
+ const float kFriction = 0.84f;
+ const float kGravityEarth = 9.80665f;
+ return kGravityEarth // g (m/s^2)
+ * 39.37f // inch/meter
+ * aDPI // pixels/inch
+ * kFriction;
+}
+
+// == std::log(0.78f) / std::log(0.9f)
+const float kDecelerationRate = 2.3582018f;
+
+// Default friction constant in android.view.ViewConfiguration.
+static float GetFlingFriction() {
+ return StaticPrefs::apz_android_chrome_fling_physics_friction();
+}
+
+// Tension lines cross at (GetInflexion(), 1).
+static float GetInflexion() {
+ // Clamp the inflexion to the range [0,1]. Values outside of this range
+ // do not make sense in the physics model, and for negative values the
+ // approximation used to compute the spline curve does not converge.
+ const float inflexion =
+ StaticPrefs::apz_android_chrome_fling_physics_inflexion();
+ if (inflexion < 0.0f) {
+ return 0.0f;
+ }
+ if (inflexion > 1.0f) {
+ return 1.0f;
+ }
+ return inflexion;
+}
+
+// Fling scroll is stopped when the scroll position is |kThresholdForFlingEnd|
+// pixels or closer from the end.
+static float GetThresholdForFlingEnd() {
+ return StaticPrefs::apz_android_chrome_fling_physics_stop_threshold();
+}
+
+static double ComputeSplineDeceleration(ParentLayerCoord aVelocity,
+ double aTuningCoeff) {
+ float velocityPerSec = aVelocity * 1000.0f;
+ return std::log(GetInflexion() * velocityPerSec /
+ (GetFlingFriction() * aTuningCoeff));
+}
+
+static TimeDuration ComputeFlingDuration(ParentLayerCoord aVelocity,
+ double aTuningCoeff) {
+ const double splineDecel = ComputeSplineDeceleration(aVelocity, aTuningCoeff);
+ const double timeSeconds = std::exp(splineDecel / (kDecelerationRate - 1.0));
+ return TimeDuration::FromSeconds(timeSeconds);
+}
+
+static ParentLayerCoord ComputeFlingDistance(ParentLayerCoord aVelocity,
+ double aTuningCoeff) {
+ const double splineDecel = ComputeSplineDeceleration(aVelocity, aTuningCoeff);
+ return GetFlingFriction() * aTuningCoeff *
+ std::exp(kDecelerationRate / (kDecelerationRate - 1.0) * splineDecel);
+}
+
+struct SplineConstants {
+ public:
+ SplineConstants() {
+ const float kStartTension = 0.5f;
+ const float kEndTension = 1.0f;
+ const float kP1 = kStartTension * GetInflexion();
+ const float kP2 = 1.0f - kEndTension * (1.0f - GetInflexion());
+
+ float xMin = 0.0f;
+ for (int i = 0; i < kNumSamples; i++) {
+ const float alpha = static_cast<float>(i) / kNumSamples;
+
+ float xMax = 1.0f;
+ float x, tx, coef;
+ // While the inflexion can be overridden by the user, it's clamped to
+ // [0,1]. For values in this range, the approximation algorithm below
+ // should converge in < 20 iterations. For good measure, we impose an
+ // iteration limit as well.
+ static const int sIterationLimit = 100;
+ int iterations = 0;
+ while (iterations++ < sIterationLimit) {
+ x = xMin + (xMax - xMin) / 2.0f;
+ coef = 3.0f * x * (1.0f - x);
+ tx = coef * ((1.0f - x) * kP1 + x * kP2) + x * x * x;
+ if (FuzzyEqualsAdditive(tx, alpha)) {
+ break;
+ }
+ if (tx > alpha) {
+ xMax = x;
+ } else {
+ xMin = x;
+ }
+ }
+ mSplinePositions[i] = coef * ((1.0f - x) * kStartTension + x) + x * x * x;
+ }
+ mSplinePositions[kNumSamples] = 1.0f;
+ }
+
+ void CalculateCoefficients(float aTime, float* aOutDistanceCoef,
+ float* aOutVelocityCoef) {
+ *aOutDistanceCoef = 1.0f;
+ *aOutVelocityCoef = 0.0f;
+ const int index = static_cast<int>(kNumSamples * aTime);
+ if (index < kNumSamples) {
+ const float tInf = static_cast<float>(index) / kNumSamples;
+ const float dInf = mSplinePositions[index];
+ const float tSup = static_cast<float>(index + 1) / kNumSamples;
+ const float dSup = mSplinePositions[index + 1];
+ *aOutVelocityCoef = (dSup - dInf) / (tSup - tInf);
+ *aOutDistanceCoef = dInf + (aTime - tInf) * *aOutVelocityCoef;
+ }
+ }
+
+ private:
+ static const int kNumSamples = 100;
+ float mSplinePositions[kNumSamples + 1];
+};
+
+StaticAutoPtr<SplineConstants> gSplineConstants;
+
+/* static */
+void AndroidFlingPhysics::InitializeGlobalState() {
+ gSplineConstants = new SplineConstants();
+ ClearOnShutdown(&gSplineConstants);
+}
+
+void AndroidFlingPhysics::Init(const ParentLayerPoint& aStartingVelocity,
+ float aPLPPI) {
+ mVelocity = aStartingVelocity.Length();
+ // We should not have created a fling animation if there is no velocity.
+ MOZ_ASSERT(mVelocity != 0.0f);
+ const double tuningCoeff = ComputeDeceleration(aPLPPI);
+ mTargetDuration = ComputeFlingDuration(mVelocity, tuningCoeff);
+ MOZ_ASSERT(!mTargetDuration.IsZero());
+ mDurationSoFar = TimeDuration();
+ mLastPos = ParentLayerPoint();
+ mCurrentPos = ParentLayerPoint();
+ float coeffX =
+ mVelocity == 0 ? 1.0f : aStartingVelocity.x.value / mVelocity.value;
+ float coeffY =
+ mVelocity == 0 ? 1.0f : aStartingVelocity.y.value / mVelocity.value;
+ mTargetDistance = ComputeFlingDistance(mVelocity, tuningCoeff);
+ mTargetPos =
+ ParentLayerPoint(mTargetDistance * coeffX, mTargetDistance * coeffY);
+ const float hyp = mTargetPos.Length();
+ if (FuzzyEqualsAdditive(hyp, 0.0f)) {
+ mDeltaNorm = ParentLayerPoint(1, 1);
+ } else {
+ mDeltaNorm = ParentLayerPoint(mTargetPos.x / hyp, mTargetPos.y / hyp);
+ }
+}
+void AndroidFlingPhysics::Sample(const TimeDuration& aDelta,
+ ParentLayerPoint* aOutVelocity,
+ ParentLayerPoint* aOutOffset) {
+ float newVelocity;
+ if (SampleImpl(aDelta, &newVelocity)) {
+ *aOutOffset = (mCurrentPos - mLastPos);
+ *aOutVelocity = ParentLayerPoint(mDeltaNorm.x * newVelocity,
+ mDeltaNorm.y * newVelocity);
+ mLastPos = mCurrentPos;
+ } else {
+ *aOutOffset = (mTargetPos - mLastPos);
+ *aOutVelocity = ParentLayerPoint();
+ }
+}
+
+bool AndroidFlingPhysics::SampleImpl(const TimeDuration& aDelta,
+ float* aOutVelocity) {
+ mDurationSoFar += aDelta;
+ if (mDurationSoFar >= mTargetDuration) {
+ return false;
+ }
+
+ const float timeRatio =
+ mDurationSoFar.ToSeconds() / mTargetDuration.ToSeconds();
+ float distanceCoef = 1.0f;
+ float velocityCoef = 0.0f;
+ gSplineConstants->CalculateCoefficients(timeRatio, &distanceCoef,
+ &velocityCoef);
+
+ // The caller expects the velocity in pixels per _millisecond_.
+ *aOutVelocity =
+ velocityCoef * mTargetDistance / mTargetDuration.ToMilliseconds();
+
+ mCurrentPos = mTargetPos * distanceCoef;
+
+ ParentLayerPoint remainder = mTargetPos - mCurrentPos;
+ const float threshold = GetThresholdForFlingEnd();
+ if (fabsf(remainder.x) < threshold && fabsf(remainder.y) < threshold) {
+ return false;
+ }
+
+ return true;
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/apz/src/AndroidFlingPhysics.h b/gfx/layers/apz/src/AndroidFlingPhysics.h
new file mode 100644
index 0000000000..68fb53e804
--- /dev/null
+++ b/gfx/layers/apz/src/AndroidFlingPhysics.h
@@ -0,0 +1,45 @@
+/* -*- 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_AndroidFlingPhysics_h_
+#define mozilla_layers_AndroidFlingPhysics_h_
+
+#include "AsyncPanZoomController.h"
+#include "Units.h"
+#include "mozilla/Assertions.h"
+
+namespace mozilla {
+namespace layers {
+
+class AndroidFlingPhysics {
+ public:
+ void Init(const ParentLayerPoint& aVelocity, float aPLPPI);
+ void Sample(const TimeDuration& aDelta, ParentLayerPoint* aOutVelocity,
+ ParentLayerPoint* aOutOffset);
+
+ static void InitializeGlobalState();
+
+ private:
+ // Returns false if the animation should end.
+ bool SampleImpl(const TimeDuration& aDelta, float* aOutVelocity);
+
+ // Information pertaining to the current fling.
+ // This is initialized on each call to Init().
+ ParentLayerCoord mVelocity; // diagonal velocity (length of velocity vector)
+ TimeDuration mTargetDuration;
+ TimeDuration mDurationSoFar;
+ ParentLayerPoint mLastPos;
+ ParentLayerPoint mCurrentPos;
+ ParentLayerCoord mTargetDistance; // diagonal distance
+ ParentLayerPoint mTargetPos; // really a target *offset* relative to the
+ // start position, which we don't track
+ ParentLayerPoint mDeltaNorm; // mTargetPos with length normalized to 1
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif // mozilla_layers_AndroidFlingPhysics_h_
diff --git a/gfx/layers/apz/src/AndroidVelocityTracker.cpp b/gfx/layers/apz/src/AndroidVelocityTracker.cpp
new file mode 100644
index 0000000000..a355811a00
--- /dev/null
+++ b/gfx/layers/apz/src/AndroidVelocityTracker.cpp
@@ -0,0 +1,288 @@
+/* -*- 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 "AndroidVelocityTracker.h"
+
+#include "mozilla/StaticPrefs_apz.h"
+
+namespace mozilla {
+namespace layers {
+
+// This velocity tracker implementation was adapted from Chromium's
+// second-order unweighted least-squares velocity tracker strategy
+// (https://cs.chromium.org/chromium/src/ui/events/gesture_detection/velocity_tracker.cc?l=101&rcl=9ea9a086d4f54c702ec9a38e55fb3eb8bbc2401b).
+
+// Threshold between position updates for determining that a pointer has
+// stopped moving. Some input devices do not send move events in the
+// case where a pointer has stopped. We need to detect this case so that we can
+// accurately predict the velocity after the pointer starts moving again.
+static const TimeDuration kAssumePointerMoveStoppedTime =
+ TimeDuration::FromMilliseconds(40);
+
+// The degree of the approximation.
+static const uint8_t kDegree = 2;
+
+// The degree of the polynomial used in SolveLeastSquares().
+// This should be the degree of the approximation plus one.
+static const uint8_t kPolyDegree = kDegree + 1;
+
+// Maximum size of position history.
+static const uint8_t kHistorySize = 20;
+
+AndroidVelocityTracker::AndroidVelocityTracker() {}
+
+void AndroidVelocityTracker::StartTracking(ParentLayerCoord aPos,
+ TimeStamp aTimestamp) {
+ Clear();
+ mHistory.AppendElement(std::make_pair(aTimestamp, aPos));
+ mLastEventTime = aTimestamp;
+}
+
+Maybe<float> AndroidVelocityTracker::AddPosition(ParentLayerCoord aPos,
+ TimeStamp aTimestamp) {
+ if ((aTimestamp - mLastEventTime) >= kAssumePointerMoveStoppedTime) {
+ Clear();
+ }
+
+ if ((aTimestamp - mLastEventTime).ToMilliseconds() < 1.0) {
+ // If we get a sample within a millisecond of the previous one,
+ // just update its position. Two samples in the history with the
+ // same timestamp can lead to things like infinite velocities.
+ if (mHistory.Length() > 0) {
+ mHistory[mHistory.Length() - 1].second = aPos;
+ }
+ } else {
+ mHistory.AppendElement(std::make_pair(aTimestamp, aPos));
+ if (mHistory.Length() > kHistorySize) {
+ mHistory.RemoveElementAt(0);
+ }
+ }
+
+ mLastEventTime = aTimestamp;
+
+ if (mHistory.Length() < 2) {
+ return Nothing();
+ }
+
+ auto start = mHistory[mHistory.Length() - 2];
+ auto end = mHistory[mHistory.Length() - 1];
+ auto velocity =
+ (end.second - start.second) / (end.first - start.first).ToMilliseconds();
+ // The velocity needs to be negated because the positions represent
+ // touch positions, and the direction of scrolling is opposite to the
+ // direction of the finger's movement.
+ return Some(-velocity);
+}
+
+static float VectorDot(const float* a, const float* b, uint32_t m) {
+ float r = 0;
+ while (m--) {
+ r += *(a++) * *(b++);
+ }
+ return r;
+}
+
+static float VectorNorm(const float* a, uint32_t m) {
+ float r = 0;
+ while (m--) {
+ float t = *(a++);
+ r += t * t;
+ }
+ return sqrtf(r);
+}
+
+/**
+ * Solves a linear least squares problem to obtain a N degree polynomial that
+ * fits the specified input data as nearly as possible.
+ *
+ * Returns true if a solution is found, false otherwise.
+ *
+ * The input consists of two vectors of data points X and Y with indices 0..m-1
+ * along with a weight vector W of the same size.
+ *
+ * The output is a vector B with indices 0..n that describes a polynomial
+ * that fits the data, such the sum of W[i] * W[i] * abs(Y[i] - (B[0] + B[1]
+ * X[i] * + B[2] X[i]^2 ... B[n] X[i]^n)) for all i between 0 and m-1 is
+ * minimized.
+ *
+ * Accordingly, the weight vector W should be initialized by the caller with the
+ * reciprocal square root of the variance of the error in each input data point.
+ * In other words, an ideal choice for W would be W[i] = 1 / var(Y[i]) = 1 /
+ * stddev(Y[i]).
+ * The weights express the relative importance of each data point. If the
+ * weights are* all 1, then the data points are considered to be of equal
+ * importance when fitting the polynomial. It is a good idea to choose weights
+ * that diminish the importance of data points that may have higher than usual
+ * error margins.
+ *
+ * Errors among data points are assumed to be independent. W is represented
+ * here as a vector although in the literature it is typically taken to be a
+ * diagonal matrix.
+ *
+ * That is to say, the function that generated the input data can be
+ * approximated by y(x) ~= B[0] + B[1] x + B[2] x^2 + ... + B[n] x^n.
+ *
+ * The coefficient of determination (R^2) is also returned to describe the
+ * goodness of fit of the model for the given data. It is a value between 0
+ * and 1, where 1 indicates perfect correspondence.
+ *
+ * This function first expands the X vector to a m by n matrix A such that
+ * A[i][0] = 1, A[i][1] = X[i], A[i][2] = X[i]^2, ..., A[i][n] = X[i]^n, then
+ * multiplies it by w[i].
+ *
+ * Then it calculates the QR decomposition of A yielding an m by m orthonormal
+ * matrix Q and an m by n upper triangular matrix R. Because R is upper
+ * triangular (lower part is all zeroes), we can simplify the decomposition into
+ * an m by n matrix Q1 and a n by n matrix R1 such that A = Q1 R1.
+ *
+ * Finally we solve the system of linear equations given by
+ * R1 B = (Qtranspose W Y) to find B.
+ *
+ * For efficiency, we lay out A and Q column-wise in memory because we
+ * frequently operate on the column vectors. Conversely, we lay out R row-wise.
+ *
+ * http://en.wikipedia.org/wiki/Numerical_methods_for_linear_least_squares
+ * http://en.wikipedia.org/wiki/Gram-Schmidt
+ */
+static bool SolveLeastSquares(const float* x, const float* y, const float* w,
+ uint32_t m, uint32_t n, float* out_b) {
+ // MSVC does not support variable-length arrays (used by the original Android
+ // implementation of this function).
+#if defined(COMPILER_MSVC)
+ const uint32_t M_ARRAY_LENGTH = VelocityTracker::kHistorySize;
+ const uint32_t N_ARRAY_LENGTH = VelocityTracker::kPolyDegree;
+ DCHECK_LE(m, M_ARRAY_LENGTH);
+ DCHECK_LE(n, N_ARRAY_LENGTH);
+#else
+ const uint32_t M_ARRAY_LENGTH = m;
+ const uint32_t N_ARRAY_LENGTH = n;
+#endif
+
+ // Expand the X vector to a matrix A, pre-multiplied by the weights.
+ float a[N_ARRAY_LENGTH][M_ARRAY_LENGTH]; // column-major order
+ for (uint32_t h = 0; h < m; h++) {
+ a[0][h] = w[h];
+ for (uint32_t i = 1; i < n; i++) {
+ a[i][h] = a[i - 1][h] * x[h];
+ }
+ }
+
+ // Apply the Gram-Schmidt process to A to obtain its QR decomposition.
+
+ // Orthonormal basis, column-major order.
+ float q[N_ARRAY_LENGTH][M_ARRAY_LENGTH];
+ // Upper triangular matrix, row-major order.
+ float r[N_ARRAY_LENGTH][N_ARRAY_LENGTH];
+ for (uint32_t j = 0; j < n; j++) {
+ for (uint32_t h = 0; h < m; h++) {
+ q[j][h] = a[j][h];
+ }
+ for (uint32_t i = 0; i < j; i++) {
+ float dot = VectorDot(&q[j][0], &q[i][0], m);
+ for (uint32_t h = 0; h < m; h++) {
+ q[j][h] -= dot * q[i][h];
+ }
+ }
+
+ float norm = VectorNorm(&q[j][0], m);
+ if (norm < 0.000001f) {
+ // vectors are linearly dependent or zero so no solution
+ return false;
+ }
+
+ float invNorm = 1.0f / norm;
+ for (uint32_t h = 0; h < m; h++) {
+ q[j][h] *= invNorm;
+ }
+ for (uint32_t i = 0; i < n; i++) {
+ r[j][i] = i < j ? 0 : VectorDot(&q[j][0], &a[i][0], m);
+ }
+ }
+
+ // Solve R B = Qt W Y to find B. This is easy because R is upper triangular.
+ // We just work from bottom-right to top-left calculating B's coefficients.
+ float wy[M_ARRAY_LENGTH];
+ for (uint32_t h = 0; h < m; h++) {
+ wy[h] = y[h] * w[h];
+ }
+ for (uint32_t i = n; i-- != 0;) {
+ out_b[i] = VectorDot(&q[i][0], wy, m);
+ for (uint32_t j = n - 1; j > i; j--) {
+ out_b[i] -= r[i][j] * out_b[j];
+ }
+ out_b[i] /= r[i][i];
+ }
+
+ return true;
+}
+
+Maybe<float> AndroidVelocityTracker::ComputeVelocity(TimeStamp aTimestamp) {
+ if (mHistory.IsEmpty()) {
+ return Nothing{};
+ }
+
+ // Polynomial coefficients describing motion along the axis.
+ float xcoeff[kPolyDegree + 1];
+ for (size_t i = 0; i <= kPolyDegree; i++) {
+ xcoeff[i] = 0;
+ }
+
+ // Iterate over movement samples in reverse time order and collect samples.
+ float pos[kHistorySize];
+ float w[kHistorySize];
+ float time[kHistorySize];
+ uint32_t m = 0;
+ int index = mHistory.Length() - 1;
+ const TimeDuration horizon = TimeDuration::FromMilliseconds(
+ StaticPrefs::apz_velocity_relevance_time_ms());
+ const auto& newest_movement = mHistory[index];
+
+ do {
+ const auto& movement = mHistory[index];
+ TimeDuration age = newest_movement.first - movement.first;
+ if (age > horizon) break;
+
+ ParentLayerCoord position = movement.second;
+ pos[m] = position;
+ w[m] = 1.0f;
+ time[m] =
+ -static_cast<float>(age.ToMilliseconds()) / 1000.0f; // in seconds
+ index--;
+ m++;
+ } while (index >= 0);
+
+ if (m == 0) {
+ return Nothing{}; // no data
+ }
+
+ // Calculate a least squares polynomial fit.
+
+ // Polynomial degree (number of coefficients), or zero if no information is
+ // available.
+ uint32_t degree = kDegree;
+ if (degree > m - 1) {
+ degree = m - 1;
+ }
+
+ if (degree >= 1) { // otherwise, no velocity data available
+ uint32_t n = degree + 1;
+ if (SolveLeastSquares(time, pos, w, m, n, xcoeff)) {
+ float velocity = xcoeff[1];
+
+ // The velocity needs to be negated because the positions represent
+ // touch positions, and the direction of scrolling is opposite to the
+ // direction of the finger's movement.
+ return Some(-velocity / 1000.0f); // convert to pixels per millisecond
+ }
+ }
+
+ return Nothing{};
+}
+
+void AndroidVelocityTracker::Clear() { mHistory.Clear(); }
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/apz/src/AndroidVelocityTracker.h b/gfx/layers/apz/src/AndroidVelocityTracker.h
new file mode 100644
index 0000000000..40e346a9ea
--- /dev/null
+++ b/gfx/layers/apz/src/AndroidVelocityTracker.h
@@ -0,0 +1,42 @@
+/* -*- 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_AndroidVelocityTracker_h
+#define mozilla_layers_AndroidVelocityTracker_h
+
+#include <utility>
+#include <cstdint>
+
+#include "Axis.h"
+#include "mozilla/Attributes.h"
+#include "mozilla/Maybe.h"
+#include "nsTArray.h"
+
+namespace mozilla {
+namespace layers {
+
+class AndroidVelocityTracker : public VelocityTracker {
+ public:
+ explicit AndroidVelocityTracker();
+ void StartTracking(ParentLayerCoord aPos, TimeStamp aTimestamp) override;
+ Maybe<float> AddPosition(ParentLayerCoord aPos,
+ TimeStamp aTimestamp) override;
+ Maybe<float> ComputeVelocity(TimeStamp aTimestamp) override;
+ void Clear() override;
+
+ private:
+ // A queue of (timestamp, position) pairs; these are the historical
+ // positions at the given timestamps.
+ nsTArray<std::pair<TimeStamp, ParentLayerCoord>> mHistory;
+ // The last time an event was added to the tracker, or the null moment if no
+ // events have been added.
+ TimeStamp mLastEventTime;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif
diff --git a/gfx/layers/apz/src/AsyncDragMetrics.h b/gfx/layers/apz/src/AsyncDragMetrics.h
new file mode 100644
index 0000000000..5374818a06
--- /dev/null
+++ b/gfx/layers/apz/src/AsyncDragMetrics.h
@@ -0,0 +1,54 @@
+/* -*- 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_DragMetrics_h
+#define mozilla_layers_DragMetrics_h
+
+#include "mozilla/layers/ScrollableLayerGuid.h"
+#include "LayersTypes.h"
+#include "mozilla/Maybe.h"
+
+namespace IPC {
+template <typename T>
+struct ParamTraits;
+} // namespace IPC
+
+namespace mozilla {
+
+namespace layers {
+
+class AsyncDragMetrics {
+ friend struct IPC::ParamTraits<mozilla::layers::AsyncDragMetrics>;
+
+ public:
+ // IPC constructor
+ AsyncDragMetrics()
+ : mViewId(0),
+ mPresShellId(0),
+ mDragStartSequenceNumber(0),
+ mScrollbarDragOffset(0) {}
+
+ AsyncDragMetrics(const ScrollableLayerGuid::ViewID& aViewId,
+ uint32_t aPresShellId, uint64_t aDragStartSequenceNumber,
+ OuterCSSCoord aScrollbarDragOffset,
+ ScrollDirection aDirection)
+ : mViewId(aViewId),
+ mPresShellId(aPresShellId),
+ mDragStartSequenceNumber(aDragStartSequenceNumber),
+ mScrollbarDragOffset(aScrollbarDragOffset),
+ mDirection(Some(aDirection)) {}
+
+ ScrollableLayerGuid::ViewID mViewId;
+ uint32_t mPresShellId;
+ uint64_t mDragStartSequenceNumber;
+ OuterCSSCoord mScrollbarDragOffset; // relative to the thumb's start offset
+ Maybe<ScrollDirection> mDirection;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif
diff --git a/gfx/layers/apz/src/AsyncPanZoomAnimation.h b/gfx/layers/apz/src/AsyncPanZoomAnimation.h
new file mode 100644
index 0000000000..127667afd9
--- /dev/null
+++ b/gfx/layers/apz/src/AsyncPanZoomAnimation.h
@@ -0,0 +1,101 @@
+/* -*- 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_AsyncPanZoomAnimation_h_
+#define mozilla_layers_AsyncPanZoomAnimation_h_
+
+#include "APZUtils.h"
+#include "mozilla/RefPtr.h"
+#include "mozilla/TimeStamp.h"
+#include "nsISupportsImpl.h"
+#include "nsTArray.h"
+#include "nsThreadUtils.h"
+
+namespace mozilla {
+namespace layers {
+
+struct FrameMetrics;
+
+class WheelScrollAnimation;
+class OverscrollAnimation;
+class SmoothMsdScrollAnimation;
+class SmoothScrollAnimation;
+
+class AsyncPanZoomAnimation {
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(AsyncPanZoomAnimation)
+
+ public:
+ explicit AsyncPanZoomAnimation() = default;
+
+ virtual bool DoSample(FrameMetrics& aFrameMetrics,
+ const TimeDuration& aDelta) = 0;
+
+ /**
+ * Attempt to handle a main-thread scroll offset update without cancelling
+ * the animation. This may or may not make sense depending on the type of
+ * the animation and whether the scroll update is relative or absolute.
+ *
+ * If the scroll update is relative, |aRelativeDelta| will contain the
+ * delta of the relative update. If the scroll update is absolute,
+ * |aRelativeDelta| will be Nothing() (the animation can check the APZC's
+ * FrameMetrics for the new absolute scroll offset if it wants to handle
+ * and absolute update).
+ *
+ * Returns whether the animation could handle the scroll update. If the
+ * return value is false, the animation will be cancelled.
+ */
+ virtual bool HandleScrollOffsetUpdate(const Maybe<CSSPoint>& aRelativeDelta) {
+ return false;
+ }
+
+ bool Sample(FrameMetrics& aFrameMetrics, const TimeDuration& aDelta) {
+ // In some situations, particularly when handoff is involved, it's possible
+ // for |aDelta| to be negative on the first call to sample. Ignore such a
+ // sample here, to avoid each derived class having to deal with this case.
+ if (aDelta.ToMilliseconds() <= 0) {
+ return true;
+ }
+
+ return DoSample(aFrameMetrics, aDelta);
+ }
+
+ /**
+ * Get the deferred tasks in |mDeferredTasks| and place them in |aTasks|. See
+ * |mDeferredTasks| for more information. Clears |mDeferredTasks|.
+ */
+ nsTArray<RefPtr<Runnable>> TakeDeferredTasks() {
+ return std::move(mDeferredTasks);
+ }
+
+ virtual WheelScrollAnimation* AsWheelScrollAnimation() { return nullptr; }
+ virtual SmoothMsdScrollAnimation* AsSmoothMsdScrollAnimation() {
+ return nullptr;
+ }
+ virtual SmoothScrollAnimation* AsSmoothScrollAnimation() { return nullptr; }
+ virtual OverscrollAnimation* AsOverscrollAnimation() { return nullptr; }
+
+ virtual bool WantsRepaints() { return true; }
+
+ virtual void Cancel(CancelAnimationFlags aFlags) {}
+
+ virtual bool WasTriggeredByScript() const { return false; }
+
+ protected:
+ // Protected destructor, to discourage deletion outside of Release():
+ virtual ~AsyncPanZoomAnimation() = default;
+
+ /**
+ * Tasks scheduled for execution after the APZC's mMonitor is released.
+ * Derived classes can add tasks here in Sample(), and the APZC can call
+ * ExecuteDeferredTasks() to execute them.
+ */
+ nsTArray<RefPtr<Runnable>> mDeferredTasks;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif // mozilla_layers_AsyncPanZoomAnimation_h_
diff --git a/gfx/layers/apz/src/AsyncPanZoomController.cpp b/gfx/layers/apz/src/AsyncPanZoomController.cpp
new file mode 100644
index 0000000000..802d5b9dbf
--- /dev/null
+++ b/gfx/layers/apz/src/AsyncPanZoomController.cpp
@@ -0,0 +1,6654 @@
+/* -*- 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 "AsyncPanZoomController.h" // for AsyncPanZoomController, etc
+
+#include <math.h> // for fabsf, fabs, atan2
+#include <stdint.h> // for uint32_t, uint64_t
+#include <sys/types.h> // for int32_t
+#include <algorithm> // for max, min
+#include <utility> // for std::make_pair
+
+#include "APZCTreeManager.h" // for APZCTreeManager
+#include "AsyncPanZoomAnimation.h" // for AsyncPanZoomAnimation
+#include "AutoDirWheelDeltaAdjuster.h" // for APZAutoDirWheelDeltaAdjuster
+#include "AutoscrollAnimation.h" // for AutoscrollAnimation
+#include "Axis.h" // for AxisX, AxisY, Axis, etc
+#include "CheckerboardEvent.h" // for CheckerboardEvent
+#include "Compositor.h" // for Compositor
+#include "DesktopFlingPhysics.h" // for DesktopFlingPhysics
+#include "FrameMetrics.h" // for FrameMetrics, etc
+#include "GenericFlingAnimation.h" // for GenericFlingAnimation
+#include "GestureEventListener.h" // for GestureEventListener
+#include "HitTestingTreeNode.h" // for HitTestingTreeNode
+#include "InputData.h" // for MultiTouchInput, etc
+#include "InputBlockState.h" // for InputBlockState, TouchBlockState
+#include "InputQueue.h" // for InputQueue
+#include "Overscroll.h" // for OverscrollAnimation
+#include "OverscrollHandoffState.h" // for OverscrollHandoffState
+#include "SimpleVelocityTracker.h" // for SimpleVelocityTracker
+#include "Units.h" // for CSSRect, CSSPoint, etc
+#include "UnitTransforms.h" // for TransformTo
+#include "base/message_loop.h" // for MessageLoop
+#include "base/task.h" // for NewRunnableMethod, etc
+#include "gfxTypes.h" // for gfxFloat
+#include "mozilla/Assertions.h" // for MOZ_ASSERT, etc
+#include "mozilla/BasicEvents.h" // for Modifiers, MODIFIER_*
+#include "mozilla/ClearOnShutdown.h" // for ClearOnShutdown
+#include "mozilla/ServoStyleConsts.h" // for StyleComputedTimingFunction
+#include "mozilla/EventForwards.h" // for nsEventStatus_*
+#include "mozilla/EventStateManager.h" // for EventStateManager
+#include "mozilla/MouseEvents.h" // for WidgetWheelEvent
+#include "mozilla/Preferences.h" // for Preferences
+#include "mozilla/RecursiveMutex.h" // for RecursiveMutexAutoLock, etc
+#include "mozilla/RefPtr.h" // for RefPtr
+#include "mozilla/ScrollTypes.h"
+#include "mozilla/StaticPrefs_apz.h"
+#include "mozilla/StaticPrefs_general.h"
+#include "mozilla/StaticPrefs_gfx.h"
+#include "mozilla/StaticPrefs_mousewheel.h"
+#include "mozilla/StaticPrefs_layers.h"
+#include "mozilla/StaticPrefs_layout.h"
+#include "mozilla/StaticPrefs_slider.h"
+#include "mozilla/StaticPrefs_test.h"
+#include "mozilla/StaticPrefs_toolkit.h"
+#include "mozilla/Telemetry.h" // for Telemetry
+#include "mozilla/TimeStamp.h" // for TimeDuration, TimeStamp
+#include "mozilla/dom/CheckerboardReportService.h" // for CheckerboardEventStorage
+// note: CheckerboardReportService.h actually lives in gfx/layers/apz/util/
+#include "mozilla/dom/Touch.h" // for Touch
+#include "mozilla/gfx/gfxVars.h" // for gfxVars
+#include "mozilla/gfx/BasePoint.h" // for BasePoint
+#include "mozilla/gfx/BaseRect.h" // for BaseRect
+#include "mozilla/gfx/Point.h" // for Point, RoundedToInt, etc
+#include "mozilla/gfx/Rect.h" // for RoundedIn
+#include "mozilla/gfx/ScaleFactor.h" // for ScaleFactor
+#include "mozilla/layers/APZThreadUtils.h" // for AssertOnControllerThread, etc
+#include "mozilla/layers/APZUtils.h" // for AsyncTransform
+#include "mozilla/layers/CompositorController.h" // for CompositorController
+#include "mozilla/layers/DirectionUtils.h" // for GetAxis{Start,End,Length,Scale}
+#include "mozilla/layers/APZPublicUtils.h" // for GetScrollMode
+#include "mozilla/mozalloc.h" // for operator new, etc
+#include "mozilla/Unused.h" // for unused
+#include "nsAlgorithm.h" // for clamped
+#include "nsCOMPtr.h" // for already_AddRefed
+#include "nsDebug.h" // for NS_WARNING
+#include "nsLayoutUtils.h"
+#include "nsMathUtils.h" // for NS_hypot
+#include "nsPoint.h" // for nsIntPoint
+#include "nsStyleConsts.h"
+#include "nsTArray.h" // for nsTArray, nsTArray_Impl, etc
+#include "nsThreadUtils.h" // for NS_IsMainThread
+#include "nsViewportInfo.h" // for ViewportMinScale(), ViewportMaxScale()
+#include "prsystem.h" // for PR_GetPhysicalMemorySize
+#include "mozilla/ipc/SharedMemoryBasic.h" // for SharedMemoryBasic
+#include "ScrollSnap.h" // for ScrollSnapUtils
+#include "ScrollAnimationPhysics.h" // for ComputeAcceleratedWheelDelta
+#include "SmoothMsdScrollAnimation.h"
+#include "SmoothScrollAnimation.h"
+#include "WheelScrollAnimation.h"
+#if defined(MOZ_WIDGET_ANDROID)
+# include "AndroidAPZ.h"
+#endif // defined(MOZ_WIDGET_ANDROID)
+
+static mozilla::LazyLogModule sApzCtlLog("apz.controller");
+#define APZC_LOG(...) MOZ_LOG(sApzCtlLog, LogLevel::Debug, (__VA_ARGS__))
+#define APZC_LOGV(...) MOZ_LOG(sApzCtlLog, LogLevel::Verbose, (__VA_ARGS__))
+
+// Log to the apz.controller log with additional info from the APZC
+#define APZC_LOG_DETAIL(fmt, apzc, ...) \
+ APZC_LOG("%p(%s scrollId=%" PRIu64 "): " fmt, (apzc), \
+ (apzc)->IsRootContent() ? "root" : "subframe", \
+ (apzc)->GetScrollId(), ##__VA_ARGS__)
+
+#define APZC_LOG_FM_COMMON(fm, prefix, level, ...) \
+ if (MOZ_LOG_TEST(sApzCtlLog, level)) { \
+ std::stringstream ss; \
+ ss << nsPrintfCString(prefix, __VA_ARGS__).get() << ":" << fm; \
+ MOZ_LOG(sApzCtlLog, level, ("%s\n", ss.str().c_str())); \
+ }
+#define APZC_LOG_FM(fm, prefix, ...) \
+ APZC_LOG_FM_COMMON(fm, prefix, LogLevel::Debug, __VA_ARGS__)
+#define APZC_LOGV_FM(fm, prefix, ...) \
+ APZC_LOG_FM_COMMON(fm, prefix, LogLevel::Verbose, __VA_ARGS__)
+
+namespace mozilla {
+namespace layers {
+
+typedef mozilla::layers::AllowedTouchBehavior AllowedTouchBehavior;
+typedef GeckoContentController::APZStateChange APZStateChange;
+typedef GeckoContentController::TapType TapType;
+typedef mozilla::gfx::Point Point;
+typedef mozilla::gfx::Matrix4x4 Matrix4x4;
+
+// Choose between platform-specific implementations.
+#ifdef MOZ_WIDGET_ANDROID
+typedef WidgetOverscrollEffect OverscrollEffect;
+typedef AndroidSpecificState PlatformSpecificState;
+#else
+typedef GenericOverscrollEffect OverscrollEffect;
+typedef PlatformSpecificStateBase
+ PlatformSpecificState; // no extra state, just use the base class
+#endif
+
+/**
+ * \page APZCPrefs APZ preferences
+ *
+ * The following prefs are used to control the behaviour of the APZC.
+ * The default values are provided in StaticPrefList.yaml.
+ *
+ * \li\b apz.allow_double_tap_zooming
+ * Pref that allows or disallows double tap to zoom
+ *
+ * \li\b apz.allow_immediate_handoff
+ * If set to true, scroll can be handed off from one APZC to another within
+ * a single input block. If set to false, a single input block can only
+ * scroll one APZC.
+ *
+ * \li\b apz.allow_zooming_out
+ * If set to true, APZ will allow zooming out past the initial scale on
+ * desktop. This is false by default to match Chrome's behaviour.
+ *
+ * \li\b apz.android.chrome_fling_physics.friction
+ * A tunable parameter for Chrome fling physics on Android that governs
+ * how quickly a fling animation slows down due to friction (and therefore
+ * also how far it reaches). Should be in the range [0-1].
+ *
+ * \li\b apz.android.chrome_fling_physics.inflexion
+ * A tunable parameter for Chrome fling physics on Android that governs
+ * the shape of the fling curve. Should be in the range [0-1].
+ *
+ * \li\b apz.android.chrome_fling_physics.stop_threshold
+ * A tunable parameter for Chrome fling physics on Android that governs
+ * how close the fling animation has to get to its target destination
+ * before it stops.
+ * Units: ParentLayer pixels
+ *
+ * \li\b apz.autoscroll.enabled
+ * If set to true, autoscrolling is driven by APZ rather than the content
+ * process main thread.
+ *
+ * \li\b apz.axis_lock.mode
+ * The preferred axis locking style. See AxisLockMode for possible values.
+ *
+ * \li\b apz.axis_lock.lock_angle
+ * Angle from axis within which we stay axis-locked.\n
+ * Units: radians
+ *
+ * \li\b apz.axis_lock.breakout_threshold
+ * Distance in inches the user must pan before axis lock can be broken.\n
+ * Units: (real-world, i.e. screen) inches
+ *
+ * \li\b apz.axis_lock.breakout_angle
+ * Angle at which axis lock can be broken.\n
+ * Units: radians
+ *
+ * \li\b apz.axis_lock.direct_pan_angle
+ * If the angle from an axis to the line drawn by a pan move is less than
+ * this value, we can assume that panning can be done in the allowed direction
+ * (horizontal or vertical).\n
+ * Currently used only for touch-action css property stuff and was addded to
+ * keep behaviour consistent with IE.\n
+ * Units: radians
+ *
+ * \li\b apz.content_response_timeout
+ * Amount of time before we timeout response from content. For example, if
+ * content is being unruly/slow and we don't get a response back within this
+ * time, we will just pretend that content did not preventDefault any touch
+ * events we dispatched to it.\n
+ * Units: milliseconds
+ *
+ * \li\b apz.danger_zone_x
+ * \li\b apz.danger_zone_y
+ * When drawing high-res tiles, we drop down to drawing low-res tiles
+ * when we know we can't keep up with the scrolling. The way we determine
+ * this is by checking if we are entering the "danger zone", which is the
+ * boundary of the painted content. For example, if the painted content
+ * goes from y=0...1000 and the visible portion is y=250...750 then
+ * we're far from checkerboarding. If we get to y=490...990 though then we're
+ * only 10 pixels away from showing checkerboarding so we are probably in
+ * a state where we can't keep up with scrolling. The danger zone prefs specify
+ * how wide this margin is; in the above example a y-axis danger zone of 10
+ * pixels would make us drop to low-res at y=490...990.\n
+ * This value is in screen pixels.
+ *
+ * \li\b apz.disable_for_scroll_linked_effects
+ * Setting this pref to true will disable APZ scrolling on documents where
+ * scroll-linked effects are detected. A scroll linked effect is detected if
+ * positioning or transform properties are updated inside a scroll event
+ * dispatch; we assume that such an update is in response to the scroll event
+ * and is therefore a scroll-linked effect which will be laggy with APZ
+ * scrolling.
+ *
+ * \li\b apz.displayport_expiry_ms
+ * While a scrollable frame is scrolling async, we set a displayport on it
+ * to make sure it is layerized. However this takes up memory, so once the
+ * scrolling stops we want to remove the displayport. This pref controls how
+ * long after scrolling stops the displayport is removed. A value of 0 will
+ * disable the expiry behavior entirely.
+ * Units: milliseconds
+ *
+ * \li\b apz.drag.enabled
+ * Setting this pref to true will cause APZ to handle mouse-dragging of
+ * scrollbar thumbs.
+ *
+ * \li\b apz.drag.initial.enabled
+ * Setting this pref to true will cause APZ to try to handle mouse-dragging
+ * of scrollbar thumbs without an initial round-trip to content to start it
+ * if possible. Only has an effect if apz.drag.enabled is also true.
+ *
+ * \li\b apz.drag.touch.enabled
+ * Setting this pref to true will cause APZ to handle touch-dragging of
+ * scrollbar thumbs. Only has an effect if apz.drag.enabled is also true.
+ *
+ * \li\b apz.enlarge_displayport_when_clipped
+ * Pref that enables enlarging of the displayport along one axis when the
+ * generated displayport's size is beyond that of the scrollable rect on the
+ * opposite axis.
+ *
+ * \li\b apz.fling_accel_min_fling_velocity
+ * The minimum velocity of the second fling, and the minimum velocity of the
+ * previous fling animation at the point of interruption, for the new fling to
+ * be considered for fling acceleration.
+ * Units: screen pixels per milliseconds
+ *
+ * \li\b apz.fling_accel_min_pan_velocity
+ * The minimum velocity during the pan gesture that causes a fling for that
+ * fling to be considered for fling acceleration.
+ * Units: screen pixels per milliseconds
+ *
+ * \li\b apz.fling_accel_max_pause_interval_ms
+ * The maximum time that is allowed to elapse between the touch start event that
+ * interrupts the previous fling, and the touch move that initiates panning for
+ * the current fling, for that fling to be considered for fling acceleration.
+ * Units: milliseconds
+ *
+ * \li\b apz.fling_accel_base_mult
+ * \li\b apz.fling_accel_supplemental_mult
+ * When applying an acceleration on a fling, the new computed velocity is
+ * (new_fling_velocity * base_mult) + (old_velocity * supplemental_mult).
+ * The base_mult and supplemental_mult multiplier values are controlled by
+ * these prefs. Note that "old_velocity" here is the initial velocity of the
+ * previous fling _after_ acceleration was applied to it (if applicable).
+ *
+ * \li\b apz.fling_curve_function_x1
+ * \li\b apz.fling_curve_function_y1
+ * \li\b apz.fling_curve_function_x2
+ * \li\b apz.fling_curve_function_y2
+ * \li\b apz.fling_curve_threshold_inches_per_ms
+ * These five parameters define a Bezier curve function and threshold used to
+ * increase the actual velocity relative to the user's finger velocity. When the
+ * finger velocity is below the threshold (or if the threshold is not positive),
+ * the velocity is used as-is. If the finger velocity exceeds the threshold
+ * velocity, then the function defined by the curve is applied on the part of
+ * the velocity that exceeds the threshold. Note that the upper bound of the
+ * velocity is still specified by the \b apz.max_velocity_inches_per_ms pref,
+ * and the function will smoothly curve the velocity from the threshold to the
+ * max. In general the function parameters chosen should define an ease-out
+ * curve in order to increase the velocity in this range, or an ease-in curve to
+ * decrease the velocity. A straight-line curve is equivalent to disabling the
+ * curve entirely by setting the threshold to -1. The max velocity pref must
+ * also be set in order for the curving to take effect, as it defines the upper
+ * bound of the velocity curve.\n
+ * The points (x1, y1) and (x2, y2) used as the two intermediate control points
+ * in the cubic bezier curve; the first and last points are (0,0) and (1,1).\n
+ * Some example values for these prefs can be found at\n
+ * https://searchfox.org/mozilla-central/rev/f82d5c549f046cb64ce5602bfd894b7ae807c8f8/dom/animation/ComputedTimingFunction.cpp#27-33
+ *
+ * \li\b apz.fling_friction
+ * Amount of friction applied during flings. This is used in the following
+ * formula: v(t1) = v(t0) * (1 - f)^(t1 - t0), where v(t1) is the velocity
+ * for a new sample, v(t0) is the velocity at the previous sample, f is the
+ * value of this pref, and (t1 - t0) is the amount of time, in milliseconds,
+ * that has elapsed between the two samples.\n
+ * NOTE: Not currently used in Android fling calculations.
+ *
+ * \li\b apz.fling_min_velocity_threshold
+ * Minimum velocity for a fling to actually kick off. If the user pans and lifts
+ * their finger such that the velocity is smaller than or equal to this amount,
+ * no fling is initiated.\n
+ * Units: screen pixels per millisecond
+ *
+ * \li\b apz.fling_stop_on_tap_threshold
+ * When flinging, if the velocity is above this number, then a tap on the
+ * screen will stop the fling without dispatching a tap to content. If the
+ * velocity is below this threshold a tap will also be dispatched.
+ * Note: when modifying this pref be sure to run the APZC gtests as some of
+ * them depend on the value of this pref.\n
+ * Units: screen pixels per millisecond
+ *
+ * \li\b apz.fling_stopped_threshold
+ * When flinging, if the velocity goes below this number, we just stop the
+ * animation completely. This is to prevent asymptotically approaching 0
+ * velocity and rerendering unnecessarily.\n
+ * Units: screen pixels per millisecond.\n
+ * NOTE: Should not be set to anything
+ * other than 0.0 for Android except for tests to disable flings.
+ *
+ * \li\b apz.keyboard.enabled
+ * Determines whether scrolling with the keyboard will be allowed to be handled
+ * by APZ.
+ *
+ * \li\b apz.keyboard.passive-listeners
+ * When enabled, APZ will interpret the passive event listener flag to mean
+ * that the event listener won't change the focused element or selection of
+ * the page. With this, web content can use passive key listeners and not have
+ * keyboard APZ disabled.
+ *
+ * \li\b apz.max_tap_time
+ * Maximum time for a touch on the screen and corresponding lift of the finger
+ * to be considered a tap. This also applies to double taps, except that it is
+ * used both for the interval between the first touchdown and first touchup,
+ * and for the interval between the first touchup and the second touchdown.\n
+ * Units: milliseconds.
+ *
+ * \li\b apz.max_velocity_inches_per_ms
+ * Maximum velocity. Velocity will be capped at this value if a faster fling
+ * occurs. Negative values indicate unlimited velocity.\n
+ * Units: (real-world, i.e. screen) inches per millisecond
+ *
+ * \li\b apz.max_velocity_queue_size
+ * Maximum size of velocity queue. The queue contains last N velocity records.
+ * On touch end we calculate the average velocity in order to compensate
+ * touch/mouse drivers misbehaviour.
+ *
+ * \li\b apz.min_skate_speed
+ * Minimum amount of speed along an axis before we switch to "skate" multipliers
+ * rather than using the "stationary" multipliers.\n
+ * Units: CSS pixels per millisecond
+ *
+ * \li\b apz.one_touch_pinch.enabled
+ * Whether or not the "one-touch-pinch" gesture (for zooming with one finger)
+ * is enabled or not.
+ *
+ * \li\b apz.overscroll.enabled
+ * Pref that enables overscrolling. If this is disabled, excess scroll that
+ * cannot be handed off is discarded.
+ *
+ * \li\b apz.overscroll.min_pan_distance_ratio
+ * The minimum ratio of the pan distance along one axis to the pan distance
+ * along the other axis needed to initiate overscroll along the first axis
+ * during panning.
+ *
+ * \li\b apz.overscroll.stretch_factor
+ * How much overscrolling can stretch content along an axis.
+ * The maximum stretch along an axis is a factor of (1 + kStretchFactor).
+ * (So if kStretchFactor is 0, you can't stretch at all; if kStretchFactor
+ * is 1, you can stretch at most by a factor of 2).
+ *
+ * \li\b apz.overscroll.stop_distance_threshold
+ * \li\b apz.overscroll.stop_velocity_threshold
+ * Thresholds for stopping the overscroll animation. When both the distance
+ * and the velocity fall below their thresholds, we stop oscillating.\n
+ * Units: screen pixels (for distance)
+ * screen pixels per millisecond (for velocity)
+ *
+ * \li\b apz.overscroll.spring_stiffness
+ * The spring stiffness constant for the overscroll mass-spring-damper model.
+ *
+ * \li\b apz.overscroll.damping
+ * The damping constant for the overscroll mass-spring-damper model.
+ *
+ * \li\b apz.overscroll.max_velocity
+ * The maximum velocity (in ParentLayerPixels per millisecond) allowed when
+ * initiating the overscroll snap-back animation.
+ *
+ * \li\b apz.paint_skipping.enabled
+ * When APZ is scrolling and sending repaint requests to the main thread, often
+ * the main thread doesn't actually need to do a repaint. This pref allows the
+ * main thread to skip doing those repaints in cases where it doesn't need to.
+ *
+ * \li\b apz.pinch_lock.mode
+ * The preferred pinch locking style. See PinchLockMode for possible values.
+ *
+ * \li\b apz.pinch_lock.scroll_lock_threshold
+ * Pinch locking is triggered if the user scrolls more than this distance
+ * and pinches less than apz.pinch_lock.span_lock_threshold.\n
+ * Units: (real-world, i.e. screen) inches
+ *
+ * \li\b apz.pinch_lock.span_breakout_threshold
+ * Distance in inches the user must pinch before lock can be broken.\n
+ * Units: (real-world, i.e. screen) inches measured between two touch points
+ *
+ * \li\b apz.pinch_lock.span_lock_threshold
+ * Pinch locking is triggered if the user pinches less than this distance
+ * and scrolls more than apz.pinch_lock.scroll_lock_threshold.\n
+ * Units: (real-world, i.e. screen) inches measured between two touch points
+ *
+ * \li\b apz.pinch_lock.buffer_max_age
+ * To ensure that pinch locking threshold calculations are not affected by
+ * variations in touch screen sensitivity, calculations draw from a buffer of
+ * recent events. This preference specifies the maximum time that events are
+ * held in this buffer.
+ * Units: milliseconds
+ *
+ * \li\b apz.popups.enabled
+ * Determines whether APZ is used for XUL popup widgets with remote content.
+ * Ideally, this should always be true, but it is currently not well tested, and
+ * has known issues, so needs to be prefable.
+ *
+ * \li\b apz.record_checkerboarding
+ * Whether or not to record detailed info on checkerboarding events.
+ *
+ * \li\b apz.second_tap_tolerance
+ * Constant describing the tolerance in distance we use, multiplied by the
+ * device DPI, within which a second tap is counted as part of a gesture
+ * continuing from the first tap. Making this larger allows the user more
+ * distance between the first and second taps in a "double tap" or "one touch
+ * pinch" gesture.\n
+ * Units: (real-world, i.e. screen) inches
+ *
+ * \li\b apz.test.logging_enabled
+ * Enable logging of APZ test data (see bug 961289).
+ *
+ * \li\b apz.touch_move_tolerance
+ * See the description for apz.touch_start_tolerance below. This is a similar
+ * threshold, except it is used to suppress touchmove events from being
+ * delivered to content for NON-scrollable frames (or more precisely, for APZCs
+ * where ArePointerEventsConsumable returns false).\n Units: (real-world, i.e.
+ * screen) inches
+ *
+ * \li\b apz.touch_start_tolerance
+ * Constant describing the tolerance in distance we use, multiplied by the
+ * device DPI, before we start panning the screen. This is to prevent us from
+ * accidentally processing taps as touch moves, and from very short/accidental
+ * touches moving the screen. touchmove events are also not delivered to content
+ * within this distance on scrollable frames.\n
+ * Units: (real-world, i.e. screen) inches
+ *
+ * \li\b apz.velocity_bias
+ * How much to adjust the displayport in the direction of scrolling. This value
+ * is multiplied by the velocity and added to the displayport offset.
+ *
+ * \li\b apz.velocity_relevance_time_ms
+ * When computing a fling velocity from the most recently stored velocity
+ * information, only velocities within the most X milliseconds are used.
+ * This pref controls the value of X.\n
+ * Units: ms
+ *
+ * \li\b apz.x_skate_size_multiplier
+ * \li\b apz.y_skate_size_multiplier
+ * The multiplier we apply to the displayport size if it is skating (current
+ * velocity is above \b apz.min_skate_speed). We prefer to increase the size of
+ * the Y axis because it is more natural in the case that a user is reading a
+ * page page that scrolls up/down. Note that one, both or neither of these may
+ * be used at any instant.\n In general we want \b
+ * apz.[xy]_skate_size_multiplier to be smaller than the corresponding
+ * stationary size multiplier because when panning fast we would like to paint
+ * less and get faster, more predictable paint times. When panning slowly we
+ * can afford to paint more even though it's slower.
+ *
+ * \li\b apz.x_stationary_size_multiplier
+ * \li\b apz.y_stationary_size_multiplier
+ * The multiplier we apply to the displayport size if it is not skating (see
+ * documentation for the skate size multipliers above).
+ *
+ * \li\b apz.x_skate_highmem_adjust
+ * \li\b apz.y_skate_highmem_adjust
+ * On high memory systems, we adjust the displayport during skating
+ * to be larger so we can reduce checkerboarding.
+ *
+ * \li\b apz.zoom_animation_duration_ms
+ * This controls how long the zoom-to-rect animation takes.\n
+ * Units: ms
+ *
+ * \li\b apz.scale_repaint_delay_ms
+ * How long to delay between repaint requests during a scale.
+ * A negative number prevents repaint requests during a scale.\n
+ * Units: ms
+ */
+
+/**
+ * Computed time function used for sampling frames of a zoom to animation.
+ */
+StaticAutoPtr<StyleComputedTimingFunction> gZoomAnimationFunction;
+
+/**
+ * Computed time function used for curving up velocity when it gets high.
+ */
+StaticAutoPtr<StyleComputedTimingFunction> gVelocityCurveFunction;
+
+/**
+ * The estimated duration of a paint for the purposes of calculating a new
+ * displayport, in milliseconds.
+ */
+static const double kDefaultEstimatedPaintDurationMs = 50;
+
+/**
+ * Returns true if this is a high memory system and we can use
+ * extra memory for a larger displayport to reduce checkerboarding.
+ */
+static bool gIsHighMemSystem = false;
+static bool IsHighMemSystem() { return gIsHighMemSystem; }
+
+AsyncPanZoomAnimation* PlatformSpecificStateBase::CreateFlingAnimation(
+ AsyncPanZoomController& aApzc, const FlingHandoffState& aHandoffState,
+ float aPLPPI) {
+ return new GenericFlingAnimation<DesktopFlingPhysics>(aApzc, aHandoffState,
+ aPLPPI);
+}
+
+UniquePtr<VelocityTracker> PlatformSpecificStateBase::CreateVelocityTracker(
+ Axis* aAxis) {
+ return MakeUnique<SimpleVelocityTracker>(aAxis);
+}
+
+SampleTime AsyncPanZoomController::GetFrameTime() const {
+ APZCTreeManager* treeManagerLocal = GetApzcTreeManager();
+ return treeManagerLocal ? treeManagerLocal->GetFrameTime()
+ : SampleTime::FromNow();
+}
+
+bool AsyncPanZoomController::IsZero(const ParentLayerPoint& aPoint) const {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+
+ const auto zoom = Metrics().GetZoom();
+
+ if (zoom == CSSToParentLayerScale(0)) {
+ return true;
+ }
+
+ return layers::IsZero(aPoint / zoom);
+}
+
+bool AsyncPanZoomController::IsZero(ParentLayerCoord aCoord) const {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+
+ const auto zoom = Metrics().GetZoom();
+
+ if (zoom == CSSToParentLayerScale(0)) {
+ return true;
+ }
+
+ return FuzzyEqualsAdditive((aCoord / zoom), CSSCoord(), COORDINATE_EPSILON);
+}
+
+bool AsyncPanZoomController::FuzzyGreater(ParentLayerCoord aCoord1,
+ ParentLayerCoord aCoord2) const {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+
+ const auto zoom = Metrics().GetZoom();
+
+ if (zoom == CSSToParentLayerScale(0)) {
+ return false;
+ }
+
+ return (aCoord1 - aCoord2) / zoom > COORDINATE_EPSILON;
+}
+
+class MOZ_STACK_CLASS StateChangeNotificationBlocker final {
+ public:
+ explicit StateChangeNotificationBlocker(AsyncPanZoomController* aApzc)
+ : mApzc(aApzc) {
+ RecursiveMutexAutoLock lock(mApzc->mRecursiveMutex);
+ mInitialState = mApzc->mState;
+ mApzc->mNotificationBlockers++;
+ }
+
+ ~StateChangeNotificationBlocker() {
+ AsyncPanZoomController::PanZoomState newState;
+ {
+ RecursiveMutexAutoLock lock(mApzc->mRecursiveMutex);
+ mApzc->mNotificationBlockers--;
+ newState = mApzc->mState;
+ }
+ mApzc->DispatchStateChangeNotification(mInitialState, newState);
+ }
+
+ private:
+ AsyncPanZoomController* mApzc;
+ AsyncPanZoomController::PanZoomState mInitialState;
+};
+
+/**
+ * An RAII class to temporarily apply async test attributes to the provided
+ * AsyncPanZoomController.
+ *
+ * This class should be used in the implementation of any AsyncPanZoomController
+ * method that queries the async scroll offset or async zoom (this includes
+ * the async layout viewport offset, since modifying the async scroll offset
+ * may result in the layout viewport moving as well).
+ */
+class MOZ_RAII AutoApplyAsyncTestAttributes final {
+ public:
+ explicit AutoApplyAsyncTestAttributes(
+ const AsyncPanZoomController*,
+ const RecursiveMutexAutoLock& aProofOfLock);
+ ~AutoApplyAsyncTestAttributes();
+
+ private:
+ AsyncPanZoomController* mApzc;
+ FrameMetrics mPrevFrameMetrics;
+ ParentLayerPoint mPrevOverscroll;
+ const RecursiveMutexAutoLock& mProofOfLock;
+};
+
+AutoApplyAsyncTestAttributes::AutoApplyAsyncTestAttributes(
+ const AsyncPanZoomController* aApzc,
+ const RecursiveMutexAutoLock& aProofOfLock)
+ // Having to use const_cast here seems less ugly than the alternatives
+ // of making several members of AsyncPanZoomController that
+ // ApplyAsyncTestAttributes() modifies |mutable|, or several methods that
+ // query the async transforms non-const.
+ : mApzc(const_cast<AsyncPanZoomController*>(aApzc)),
+ mPrevFrameMetrics(aApzc->Metrics()),
+ mPrevOverscroll(aApzc->GetOverscrollAmountInternal()),
+ mProofOfLock(aProofOfLock) {
+ mApzc->ApplyAsyncTestAttributes(aProofOfLock);
+}
+
+AutoApplyAsyncTestAttributes::~AutoApplyAsyncTestAttributes() {
+ mApzc->UnapplyAsyncTestAttributes(mProofOfLock, mPrevFrameMetrics,
+ mPrevOverscroll);
+}
+
+class ZoomAnimation : public AsyncPanZoomAnimation {
+ public:
+ ZoomAnimation(AsyncPanZoomController& aApzc, const CSSPoint& aStartOffset,
+ const CSSToParentLayerScale& aStartZoom,
+ const CSSPoint& aEndOffset,
+ const CSSToParentLayerScale& aEndZoom)
+ : mApzc(aApzc),
+ mTotalDuration(TimeDuration::FromMilliseconds(
+ StaticPrefs::apz_zoom_animation_duration_ms())),
+ mStartOffset(aStartOffset),
+ mStartZoom(aStartZoom),
+ mEndOffset(aEndOffset),
+ mEndZoom(aEndZoom) {}
+
+ virtual bool DoSample(FrameMetrics& aFrameMetrics,
+ const TimeDuration& aDelta) override {
+ mDuration += aDelta;
+ double animPosition = mDuration / mTotalDuration;
+
+ if (animPosition >= 1.0) {
+ aFrameMetrics.SetZoom(mEndZoom);
+ mApzc.SetVisualScrollOffset(mEndOffset);
+ return false;
+ }
+
+ // Sample the zoom at the current time point. The sampled zoom
+ // will affect the final computed resolution.
+ float sampledPosition =
+ gZoomAnimationFunction->At(animPosition, /* aBeforeFlag = */ false);
+
+ // We scale the scrollOffset linearly with sampledPosition, so the zoom
+ // needs to scale inversely to match.
+ if (mStartZoom == CSSToParentLayerScale(0) ||
+ mEndZoom == CSSToParentLayerScale(0)) {
+ return false;
+ }
+
+ aFrameMetrics.SetZoom(
+ CSSToParentLayerScale(1 / (sampledPosition / mEndZoom.scale +
+ (1 - sampledPosition) / mStartZoom.scale)));
+
+ mApzc.SetVisualScrollOffset(CSSPoint::FromUnknownPoint(gfx::Point(
+ mEndOffset.x * sampledPosition + mStartOffset.x * (1 - sampledPosition),
+ mEndOffset.y * sampledPosition +
+ mStartOffset.y * (1 - sampledPosition))));
+ return true;
+ }
+
+ virtual bool WantsRepaints() override { return true; }
+
+ private:
+ AsyncPanZoomController& mApzc;
+
+ TimeDuration mDuration;
+ const TimeDuration mTotalDuration;
+
+ // Old metrics from before we started a zoom animation. This is only valid
+ // when we are in the "ANIMATED_ZOOM" state. This is used so that we can
+ // interpolate between the start and end frames. We only use the
+ // |mViewportScrollOffset| and |mResolution| fields on this.
+ CSSPoint mStartOffset;
+ CSSToParentLayerScale mStartZoom;
+
+ // Target metrics for a zoom to animation. This is only valid when we are in
+ // the "ANIMATED_ZOOM" state. We only use the |mViewportScrollOffset| and
+ // |mResolution| fields on this.
+ CSSPoint mEndOffset;
+ CSSToParentLayerScale mEndZoom;
+};
+
+/*static*/
+void AsyncPanZoomController::InitializeGlobalState() {
+ static bool sInitialized = false;
+ if (sInitialized) return;
+ sInitialized = true;
+
+ MOZ_ASSERT(NS_IsMainThread());
+
+ gZoomAnimationFunction = new StyleComputedTimingFunction(
+ StyleComputedTimingFunction::Keyword(StyleTimingKeyword::Ease));
+ ClearOnShutdown(&gZoomAnimationFunction);
+ gVelocityCurveFunction =
+ new StyleComputedTimingFunction(StyleComputedTimingFunction::CubicBezier(
+ StaticPrefs::apz_fling_curve_function_x1_AtStartup(),
+ StaticPrefs::apz_fling_curve_function_y1_AtStartup(),
+ StaticPrefs::apz_fling_curve_function_x2_AtStartup(),
+ StaticPrefs::apz_fling_curve_function_y2_AtStartup()));
+ ClearOnShutdown(&gVelocityCurveFunction);
+
+ uint64_t sysmem = PR_GetPhysicalMemorySize();
+ uint64_t threshold = 1LL << 32; // 4 GB in bytes
+ gIsHighMemSystem = sysmem >= threshold;
+
+ PlatformSpecificState::InitializeGlobalState();
+}
+
+AsyncPanZoomController::AsyncPanZoomController(
+ LayersId aLayersId, APZCTreeManager* aTreeManager,
+ const RefPtr<InputQueue>& aInputQueue,
+ GeckoContentController* aGeckoContentController, GestureBehavior aGestures)
+ : mLayersId(aLayersId),
+ mGeckoContentController(aGeckoContentController),
+ mRefPtrMonitor("RefPtrMonitor"),
+ // mTreeManager must be initialized before GetFrameTime() is called
+ mTreeManager(aTreeManager),
+ mRecursiveMutex("AsyncPanZoomController"),
+ mLastContentPaintMetrics(mLastContentPaintMetadata.GetMetrics()),
+ mPanDirRestricted(false),
+ mPinchLocked(false),
+ mPinchEventBuffer(TimeDuration::FromMilliseconds(
+ StaticPrefs::apz_pinch_lock_buffer_max_age_AtStartup())),
+ mZoomConstraints(false, false,
+ mScrollMetadata.GetMetrics().GetDevPixelsPerCSSPixel() *
+ ViewportMinScale() / ParentLayerToScreenScale(1),
+ mScrollMetadata.GetMetrics().GetDevPixelsPerCSSPixel() *
+ ViewportMaxScale() / ParentLayerToScreenScale(1)),
+ mLastSampleTime(GetFrameTime()),
+ mLastCheckerboardReport(GetFrameTime()),
+ mLastNotifiedZoom(),
+ mOverscrollEffect(MakeUnique<OverscrollEffect>(*this)),
+ mState(NOTHING),
+ mX(this),
+ mY(this),
+ mNotificationBlockers(0),
+ mInputQueue(aInputQueue),
+ mPinchPaintTimerSet(false),
+ mDelayedTransformEnd(false),
+ mTestAttributeAppliers(0),
+ mTestHasAsyncKeyScrolled(false),
+ mCheckerboardEventLock("APZCBELock") {
+ if (aGestures == USE_GESTURE_DETECTOR) {
+ mGestureEventListener = new GestureEventListener(this);
+ }
+ // Put one default-constructed sampled state in the queue.
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ mSampledState.emplace_back();
+}
+
+AsyncPanZoomController::~AsyncPanZoomController() { MOZ_ASSERT(IsDestroyed()); }
+
+PlatformSpecificStateBase* AsyncPanZoomController::GetPlatformSpecificState() {
+ if (!mPlatformSpecificState) {
+ mPlatformSpecificState = MakeUnique<PlatformSpecificState>();
+ }
+ return mPlatformSpecificState.get();
+}
+
+already_AddRefed<GeckoContentController>
+AsyncPanZoomController::GetGeckoContentController() const {
+ MonitorAutoLock lock(mRefPtrMonitor);
+ RefPtr<GeckoContentController> controller = mGeckoContentController;
+ return controller.forget();
+}
+
+already_AddRefed<GestureEventListener>
+AsyncPanZoomController::GetGestureEventListener() const {
+ MonitorAutoLock lock(mRefPtrMonitor);
+ RefPtr<GestureEventListener> listener = mGestureEventListener;
+ return listener.forget();
+}
+
+const RefPtr<InputQueue>& AsyncPanZoomController::GetInputQueue() const {
+ return mInputQueue;
+}
+
+void AsyncPanZoomController::Destroy() {
+ AssertOnUpdaterThread();
+
+ CancelAnimation(CancelAnimationFlags::ScrollSnap);
+
+ { // scope the lock
+ MonitorAutoLock lock(mRefPtrMonitor);
+ mGeckoContentController = nullptr;
+ mGestureEventListener = nullptr;
+ }
+ mParent = nullptr;
+ mTreeManager = nullptr;
+}
+
+bool AsyncPanZoomController::IsDestroyed() const {
+ return mTreeManager == nullptr;
+}
+
+float AsyncPanZoomController::GetDPI() const {
+ if (APZCTreeManager* localPtr = mTreeManager) {
+ return localPtr->GetDPI();
+ }
+ // If this APZC has been destroyed then this value is not going to be
+ // used for anything that the user will end up seeing, so we can just
+ // return 0.
+ return 0.0;
+}
+
+ScreenCoord AsyncPanZoomController::GetTouchStartTolerance() const {
+ return (StaticPrefs::apz_touch_start_tolerance() * GetDPI());
+}
+
+ScreenCoord AsyncPanZoomController::GetTouchMoveTolerance() const {
+ return (StaticPrefs::apz_touch_move_tolerance() * GetDPI());
+}
+
+ScreenCoord AsyncPanZoomController::GetSecondTapTolerance() const {
+ return (StaticPrefs::apz_second_tap_tolerance() * GetDPI());
+}
+
+/* static */ AsyncPanZoomController::AxisLockMode
+AsyncPanZoomController::GetAxisLockMode() {
+ return static_cast<AxisLockMode>(StaticPrefs::apz_axis_lock_mode());
+}
+
+bool AsyncPanZoomController::UsingStatefulAxisLock() const {
+ return (GetAxisLockMode() == STANDARD || GetAxisLockMode() == STICKY);
+}
+
+/* static */ AsyncPanZoomController::PinchLockMode
+AsyncPanZoomController::GetPinchLockMode() {
+ return static_cast<PinchLockMode>(StaticPrefs::apz_pinch_lock_mode());
+}
+
+PointerEventsConsumableFlags AsyncPanZoomController::ArePointerEventsConsumable(
+ TouchBlockState* aBlock, const MultiTouchInput& aInput) {
+ uint32_t touchPoints = aInput.mTouches.Length();
+ if (touchPoints == 0) {
+ // Cant' do anything with zero touch points
+ return {false, false};
+ }
+
+ // This logic is simplified, erring on the side of returning true if we're
+ // not sure. It's safer to pretend that we can consume the event and then
+ // not be able to than vice-versa. But at the same time, we should try hard
+ // to return an accurate result, because returning true can trigger a
+ // pointercancel event to web content, which can break certain features
+ // that are using touch-action and handling the pointermove events.
+ //
+ // Note that in particular this function can return true if APZ is waiting on
+ // the main thread for touch-action information. In this scenario, the
+ // APZEventState::MainThreadAgreesEventsAreConsumableByAPZ() function tries
+ // to use the main-thread touch-action information to filter out false
+ // positives.
+ //
+ // We could probably enhance this logic to determine things like "we're
+ // not pannable, so we can only zoom in, and the zoom is already maxed
+ // out, so we're not zoomable either" but no need for that at this point.
+
+ bool pannableX = aBlock->GetOverscrollHandoffChain()->CanScrollInDirection(
+ this, ScrollDirection::eHorizontal);
+ bool touchActionAllowsX = aBlock->TouchActionAllowsPanningX();
+ bool pannableY = (aBlock->GetOverscrollHandoffChain()->CanScrollInDirection(
+ this, ScrollDirection::eVertical) ||
+ // In the case of the root APZC with any dynamic toolbar, it
+ // shoule be pannable if there is room moving the dynamic
+ // toolbar.
+ (IsRootContent() && CanVerticalScrollWithDynamicToolbar()));
+ bool touchActionAllowsY = aBlock->TouchActionAllowsPanningY();
+
+ bool pannable;
+ bool touchActionAllowsPanning;
+
+ Maybe<ScrollDirection> panDirection =
+ aBlock->GetBestGuessPanDirection(aInput);
+ if (panDirection == Some(ScrollDirection::eVertical)) {
+ pannable = pannableY;
+ touchActionAllowsPanning = touchActionAllowsY;
+ } else if (panDirection == Some(ScrollDirection::eHorizontal)) {
+ pannable = pannableX;
+ touchActionAllowsPanning = touchActionAllowsX;
+ } else {
+ // If we don't have a guessed pan direction, err on the side of returning
+ // true.
+ pannable = pannableX || pannableY;
+ touchActionAllowsPanning = touchActionAllowsX || touchActionAllowsY;
+ }
+
+ if (touchPoints == 1) {
+ return {pannable, touchActionAllowsPanning};
+ }
+
+ bool zoomable = ZoomConstraintsAllowZoom();
+ bool touchActionAllowsZoom = aBlock->TouchActionAllowsPinchZoom();
+
+ return {pannable || zoomable,
+ touchActionAllowsPanning || touchActionAllowsZoom};
+}
+
+nsEventStatus AsyncPanZoomController::HandleDragEvent(
+ const MouseInput& aEvent, const AsyncDragMetrics& aDragMetrics,
+ OuterCSSCoord aInitialThumbPos) {
+ // RDM is a special case where touch events will be synthesized in response
+ // to mouse events, and APZ will receive both even though RDM prevent-defaults
+ // the mouse events. This is because mouse events don't opt into APZ waiting
+ // to check if the event has been prevent-defaulted and are still processed
+ // as a result. To handle this, have APZ ignore mouse events when RDM and
+ // touch simulation are active.
+ bool isRDMTouchSimulationActive = false;
+ {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ isRDMTouchSimulationActive =
+ mScrollMetadata.GetIsRDMTouchSimulationActive();
+ }
+
+ if (!StaticPrefs::apz_drag_enabled() || isRDMTouchSimulationActive) {
+ return nsEventStatus_eIgnore;
+ }
+
+ if (!GetApzcTreeManager()) {
+ return nsEventStatus_eConsumeNoDefault;
+ }
+
+ {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+
+ if (aEvent.mType == MouseInput::MouseType::MOUSE_UP) {
+ if (mState == SCROLLBAR_DRAG) {
+ APZC_LOG("%p ending drag\n", this);
+ SetState(NOTHING);
+ }
+
+ SnapBackIfOverscrolled();
+
+ return nsEventStatus_eConsumeNoDefault;
+ }
+ }
+
+ HitTestingTreeNodeAutoLock node;
+ GetApzcTreeManager()->FindScrollThumbNode(aDragMetrics, mLayersId, node);
+ if (!node) {
+ APZC_LOG("%p unable to find scrollthumb node with viewid %" PRIu64 "\n",
+ this, aDragMetrics.mViewId);
+ return nsEventStatus_eConsumeNoDefault;
+ }
+
+ if (aEvent.mType == MouseInput::MouseType::MOUSE_DOWN) {
+ APZC_LOG("%p starting scrollbar drag\n", this);
+ SetState(SCROLLBAR_DRAG);
+ }
+
+ if (aEvent.mType != MouseInput::MouseType::MOUSE_MOVE) {
+ APZC_LOG("%p discarding event of type %d\n", this, aEvent.mType);
+ return nsEventStatus_eConsumeNoDefault;
+ }
+
+ const ScrollbarData& scrollbarData = node->GetScrollbarData();
+ MOZ_ASSERT(scrollbarData.mScrollbarLayerType ==
+ layers::ScrollbarLayerType::Thumb);
+ MOZ_ASSERT(scrollbarData.mDirection.isSome());
+ ScrollDirection direction = *scrollbarData.mDirection;
+
+ bool isMouseAwayFromThumb = false;
+ if (int snapMultiplier = StaticPrefs::slider_snapMultiplier_AtStartup()) {
+ // It's fine to ignore the async component of the thumb's transform,
+ // because any async transform of the thumb will be in the direction of
+ // scrolling, but here we're interested in the other direction.
+ ParentLayerRect thumbRect =
+ (node->GetTransform() * AsyncTransformMatrix())
+ .TransformBounds(LayerRect(node->GetVisibleRegion().GetBounds()));
+ ScrollDirection otherDirection = GetPerpendicularDirection(direction);
+ ParentLayerCoord distance =
+ GetAxisStart(otherDirection, thumbRect.DistanceTo(aEvent.mLocalOrigin));
+ ParentLayerCoord thumbWidth = GetAxisLength(otherDirection, thumbRect);
+ // Avoid triggering this condition spuriously when the thumb is
+ // offscreen and its visible region is therefore empty.
+ if (thumbWidth > 0 && thumbWidth * snapMultiplier < distance) {
+ isMouseAwayFromThumb = true;
+ APZC_LOG("%p determined mouse is away from thumb, will snap\n", this);
+ }
+ }
+
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ OuterCSSCoord thumbPosition;
+ if (isMouseAwayFromThumb) {
+ thumbPosition = aInitialThumbPos;
+ } else {
+ thumbPosition = ConvertScrollbarPoint(aEvent.mLocalOrigin, scrollbarData) -
+ aDragMetrics.mScrollbarDragOffset;
+ }
+
+ OuterCSSCoord maxThumbPos = scrollbarData.mScrollTrackLength;
+ maxThumbPos -= scrollbarData.mThumbLength;
+
+ float scrollPercent =
+ maxThumbPos.value == 0.0f ? 0.0f : (float)(thumbPosition / maxThumbPos);
+ APZC_LOG("%p scrollbar dragged to %f percent\n", this, scrollPercent);
+
+ CSSCoord minScrollPosition =
+ GetAxisStart(direction, Metrics().GetScrollableRect().TopLeft());
+ CSSCoord maxScrollPosition =
+ GetAxisStart(direction, Metrics().GetScrollableRect().BottomRight()) -
+ GetAxisLength(direction, Metrics().CalculateCompositedSizeInCssPixels());
+ CSSCoord scrollPosition =
+ minScrollPosition +
+ (scrollPercent * (maxScrollPosition - minScrollPosition));
+
+ scrollPosition = std::max(scrollPosition, minScrollPosition);
+ scrollPosition = std::min(scrollPosition, maxScrollPosition);
+
+ CSSPoint scrollOffset = Metrics().GetVisualScrollOffset();
+ if (direction == ScrollDirection::eHorizontal) {
+ scrollOffset.x = scrollPosition;
+ } else {
+ scrollOffset.y = scrollPosition;
+ }
+ APZC_LOG("%p set scroll offset to %s from scrollbar drag\n", this,
+ ToString(scrollOffset).c_str());
+ SetVisualScrollOffset(scrollOffset);
+ ScheduleCompositeAndMaybeRepaint();
+
+ return nsEventStatus_eConsumeNoDefault;
+}
+
+nsEventStatus AsyncPanZoomController::HandleInputEvent(
+ const InputData& aEvent,
+ const ScreenToParentLayerMatrix4x4& aTransformToApzc) {
+ APZThreadUtils::AssertOnControllerThread();
+
+ nsEventStatus rv = nsEventStatus_eIgnore;
+
+ switch (aEvent.mInputType) {
+ case MULTITOUCH_INPUT: {
+ MultiTouchInput multiTouchInput = aEvent.AsMultiTouchInput();
+ RefPtr<GestureEventListener> listener = GetGestureEventListener();
+ if (listener) {
+ // We only care about screen coordinates in the gesture listener,
+ // so we don't bother transforming the event to parent layer coordinates
+ rv = listener->HandleInputEvent(multiTouchInput);
+ if (rv == nsEventStatus_eConsumeNoDefault) {
+ return rv;
+ }
+ }
+
+ if (!multiTouchInput.TransformToLocal(aTransformToApzc)) {
+ return rv;
+ }
+
+ switch (multiTouchInput.mType) {
+ case MultiTouchInput::MULTITOUCH_START:
+ rv = OnTouchStart(multiTouchInput);
+ break;
+ case MultiTouchInput::MULTITOUCH_MOVE:
+ rv = OnTouchMove(multiTouchInput);
+ break;
+ case MultiTouchInput::MULTITOUCH_END:
+ rv = OnTouchEnd(multiTouchInput);
+ break;
+ case MultiTouchInput::MULTITOUCH_CANCEL:
+ rv = OnTouchCancel(multiTouchInput);
+ break;
+ }
+ break;
+ }
+ case PANGESTURE_INPUT: {
+ PanGestureInput panGestureInput = aEvent.AsPanGestureInput();
+ if (!panGestureInput.TransformToLocal(aTransformToApzc)) {
+ return rv;
+ }
+
+ switch (panGestureInput.mType) {
+ case PanGestureInput::PANGESTURE_MAYSTART:
+ rv = OnPanMayBegin(panGestureInput);
+ break;
+ case PanGestureInput::PANGESTURE_CANCELLED:
+ rv = OnPanCancelled(panGestureInput);
+ break;
+ case PanGestureInput::PANGESTURE_START:
+ rv = OnPanBegin(panGestureInput);
+ break;
+ case PanGestureInput::PANGESTURE_PAN:
+ rv = OnPan(panGestureInput, FingersOnTouchpad::Yes);
+ break;
+ case PanGestureInput::PANGESTURE_END:
+ rv = OnPanEnd(panGestureInput);
+ break;
+ case PanGestureInput::PANGESTURE_MOMENTUMSTART:
+ rv = OnPanMomentumStart(panGestureInput);
+ break;
+ case PanGestureInput::PANGESTURE_MOMENTUMPAN:
+ rv = OnPan(panGestureInput, FingersOnTouchpad::No);
+ break;
+ case PanGestureInput::PANGESTURE_MOMENTUMEND:
+ rv = OnPanMomentumEnd(panGestureInput);
+ break;
+ case PanGestureInput::PANGESTURE_INTERRUPTED:
+ rv = OnPanInterrupted(panGestureInput);
+ break;
+ }
+ break;
+ }
+ case MOUSE_INPUT: {
+ MouseInput mouseInput = aEvent.AsMouseInput();
+ if (!mouseInput.TransformToLocal(aTransformToApzc)) {
+ return rv;
+ }
+ break;
+ }
+ case SCROLLWHEEL_INPUT: {
+ ScrollWheelInput scrollInput = aEvent.AsScrollWheelInput();
+ if (!scrollInput.TransformToLocal(aTransformToApzc)) {
+ return rv;
+ }
+
+ rv = OnScrollWheel(scrollInput);
+ break;
+ }
+ case PINCHGESTURE_INPUT: {
+ // The APZCTreeManager should take care of ensuring that only root-content
+ // APZCs get pinch inputs.
+ MOZ_ASSERT(IsRootContent());
+ PinchGestureInput pinchInput = aEvent.AsPinchGestureInput();
+ if (!pinchInput.TransformToLocal(aTransformToApzc)) {
+ return rv;
+ }
+
+ rv = HandleGestureEvent(pinchInput);
+ break;
+ }
+ case TAPGESTURE_INPUT: {
+ TapGestureInput tapInput = aEvent.AsTapGestureInput();
+ if (!tapInput.TransformToLocal(aTransformToApzc)) {
+ return rv;
+ }
+
+ rv = HandleGestureEvent(tapInput);
+ break;
+ }
+ case KEYBOARD_INPUT: {
+ const KeyboardInput& keyInput = aEvent.AsKeyboardInput();
+ rv = OnKeyboard(keyInput);
+ break;
+ }
+ }
+
+ return rv;
+}
+
+nsEventStatus AsyncPanZoomController::HandleGestureEvent(
+ const InputData& aEvent) {
+ APZThreadUtils::AssertOnControllerThread();
+
+ nsEventStatus rv = nsEventStatus_eIgnore;
+
+ switch (aEvent.mInputType) {
+ case PINCHGESTURE_INPUT: {
+ // This may be invoked via a one-touch-pinch gesture from
+ // GestureEventListener. In that case we want redirect it to the enclosing
+ // root-content APZC.
+ if (!IsRootContent()) {
+ if (APZCTreeManager* treeManagerLocal = GetApzcTreeManager()) {
+ if (RefPtr<AsyncPanZoomController> root =
+ treeManagerLocal->FindZoomableApzc(this)) {
+ rv = root->HandleGestureEvent(aEvent);
+ }
+ }
+ break;
+ }
+ PinchGestureInput pinchGestureInput = aEvent.AsPinchGestureInput();
+ pinchGestureInput.TransformToLocal(GetTransformToThis());
+ switch (pinchGestureInput.mType) {
+ case PinchGestureInput::PINCHGESTURE_START:
+ rv = OnScaleBegin(pinchGestureInput);
+ break;
+ case PinchGestureInput::PINCHGESTURE_SCALE:
+ rv = OnScale(pinchGestureInput);
+ break;
+ case PinchGestureInput::PINCHGESTURE_FINGERLIFTED:
+ case PinchGestureInput::PINCHGESTURE_END:
+ rv = OnScaleEnd(pinchGestureInput);
+ break;
+ }
+ break;
+ }
+ case TAPGESTURE_INPUT: {
+ TapGestureInput tapGestureInput = aEvent.AsTapGestureInput();
+ tapGestureInput.TransformToLocal(GetTransformToThis());
+ switch (tapGestureInput.mType) {
+ case TapGestureInput::TAPGESTURE_LONG:
+ rv = OnLongPress(tapGestureInput);
+ break;
+ case TapGestureInput::TAPGESTURE_LONG_UP:
+ rv = OnLongPressUp(tapGestureInput);
+ break;
+ case TapGestureInput::TAPGESTURE_UP:
+ rv = OnSingleTapUp(tapGestureInput);
+ break;
+ case TapGestureInput::TAPGESTURE_CONFIRMED:
+ rv = OnSingleTapConfirmed(tapGestureInput);
+ break;
+ case TapGestureInput::TAPGESTURE_DOUBLE:
+ // This means that double tapping on an oop iframe "works" in that we
+ // don't try (and fail) to zoom the oop iframe. But it also means it
+ // is impossible to zoom to some content inside that oop iframe.
+ // Instead the best we can do is zoom to the oop iframe itself. This
+ // is consistent with what Chrome and Safari currently do. Allowing
+ // zooming to content inside an oop iframe would be decently
+ // complicated and it doesn't seem worth it. Bug 1715179 is on file
+ // for this.
+ if (!IsRootContent()) {
+ if (APZCTreeManager* treeManagerLocal = GetApzcTreeManager()) {
+ if (RefPtr<AsyncPanZoomController> root =
+ treeManagerLocal->FindZoomableApzc(this)) {
+ rv = root->OnDoubleTap(tapGestureInput);
+ }
+ }
+ break;
+ }
+ rv = OnDoubleTap(tapGestureInput);
+ break;
+ case TapGestureInput::TAPGESTURE_SECOND:
+ rv = OnSecondTap(tapGestureInput);
+ break;
+ case TapGestureInput::TAPGESTURE_CANCEL:
+ rv = OnCancelTap(tapGestureInput);
+ break;
+ }
+ break;
+ }
+ default:
+ MOZ_ASSERT_UNREACHABLE("Unhandled input event");
+ break;
+ }
+
+ return rv;
+}
+
+void AsyncPanZoomController::StartAutoscroll(const ScreenPoint& aPoint) {
+ // Cancel any existing animation.
+ CancelAnimation();
+
+ SetState(AUTOSCROLL);
+ StartAnimation(new AutoscrollAnimation(*this, aPoint));
+}
+
+void AsyncPanZoomController::StopAutoscroll() {
+ if (mState == AUTOSCROLL) {
+ CancelAnimation(TriggeredExternally);
+ }
+}
+
+nsEventStatus AsyncPanZoomController::OnTouchStart(
+ const MultiTouchInput& aEvent) {
+ APZC_LOG_DETAIL("got a touch-start in state %s\n", this,
+ ToString(mState).c_str());
+ mPanDirRestricted = false;
+
+ switch (mState) {
+ case FLING:
+ case ANIMATING_ZOOM:
+ case SMOOTH_SCROLL:
+ case SMOOTHMSD_SCROLL:
+ case OVERSCROLL_ANIMATION:
+ case WHEEL_SCROLL:
+ case KEYBOARD_SCROLL:
+ case PAN_MOMENTUM:
+ case AUTOSCROLL:
+ MOZ_ASSERT(GetCurrentTouchBlock());
+ GetCurrentTouchBlock()->GetOverscrollHandoffChain()->CancelAnimations(
+ ExcludeOverscroll);
+ [[fallthrough]];
+ case SCROLLBAR_DRAG:
+ case NOTHING: {
+ ParentLayerPoint point = GetFirstTouchPoint(aEvent);
+ mLastTouch.mPosition = mStartTouch = GetFirstExternalTouchPoint(aEvent);
+ StartTouch(point, aEvent.mTimeStamp);
+ if (RefPtr<GeckoContentController> controller =
+ GetGeckoContentController()) {
+ MOZ_ASSERT(GetCurrentTouchBlock());
+ controller->NotifyAPZStateChange(
+ GetGuid(), APZStateChange::eStartTouch,
+ GetCurrentTouchBlock()->GetOverscrollHandoffChain()->CanBePanned(
+ this),
+ Some(GetCurrentTouchBlock()->GetBlockId()));
+ }
+ mLastTouch.mTimeStamp = mTouchStartTime = aEvent.mTimeStamp;
+ SetState(TOUCHING);
+ break;
+ }
+ case TOUCHING:
+ case PANNING:
+ case PANNING_LOCKED_X:
+ case PANNING_LOCKED_Y:
+ case PINCHING:
+ NS_WARNING("Received impossible touch in OnTouchStart");
+ break;
+ }
+
+ return nsEventStatus_eConsumeNoDefault;
+}
+
+nsEventStatus AsyncPanZoomController::OnTouchMove(
+ const MultiTouchInput& aEvent) {
+ APZC_LOG_DETAIL("got a touch-move in state %s\n", this,
+ ToString(mState).c_str());
+ switch (mState) {
+ case FLING:
+ case SMOOTHMSD_SCROLL:
+ case NOTHING:
+ case ANIMATING_ZOOM:
+ // May happen if the user double-taps and drags without lifting after the
+ // second tap. Ignore the move if this happens.
+ return nsEventStatus_eIgnore;
+
+ case TOUCHING: {
+ ScreenCoord panThreshold = GetTouchStartTolerance();
+ ExternalPoint extPoint = GetFirstExternalTouchPoint(aEvent);
+ Maybe<std::pair<MultiTouchInput, MultiTouchInput>> splitEvent;
+
+ // We intentionally skip the UpdateWithTouchAtDevicePoint call when the
+ // panThreshold is zero. This ensures more deterministic behaviour during
+ // testing. If we call that, Axis::mPos gets updated to the point of this
+ // touchmove event, but we "consume" the move to overcome the
+ // panThreshold, so it's hard to pan a specific amount reliably from a
+ // mochitest.
+ if (panThreshold > 0.0f) {
+ const float vectorLength = PanVector(extPoint).Length();
+
+ if (vectorLength < panThreshold) {
+ UpdateWithTouchAtDevicePoint(aEvent);
+ mLastTouch = {extPoint, aEvent.mTimeStamp};
+
+ return nsEventStatus_eIgnore;
+ }
+
+ splitEvent = MaybeSplitTouchMoveEvent(aEvent, panThreshold,
+ vectorLength, extPoint);
+
+ UpdateWithTouchAtDevicePoint(splitEvent ? splitEvent->first : aEvent);
+ }
+
+ nsEventStatus result;
+ const MultiTouchInput& firstEvent =
+ splitEvent ? splitEvent->first : aEvent;
+
+ MOZ_ASSERT(GetCurrentTouchBlock());
+ if (GetCurrentTouchBlock()->TouchActionAllowsPanningXY()) {
+ // In the calls to StartPanning() below, the first argument needs to be
+ // the External position of |firstEvent|.
+ // However, instead of computing that using
+ // GetFirstExternalTouchPoint(firstEvent), we pass |extPoint| which
+ // has been modified by MaybeSplitTouchMoveEvent() to the desired
+ // value. This is a workaround for the fact that recomputing the
+ // External point would require a round-trip through |mScreenPoint|
+ // which is an integer.
+
+ // User tries to trigger a touch behavior. If allowed touch behavior is
+ // vertical pan + horizontal pan (touch-action value is equal to AUTO)
+ // we can return ConsumeNoDefault status immediately to trigger cancel
+ // event further.
+ // It should happen independent of the parent type (whether it is
+ // scrolling or not).
+ StartPanning(extPoint, firstEvent.mTimeStamp);
+ result = nsEventStatus_eConsumeNoDefault;
+ } else {
+ result = StartPanning(extPoint, firstEvent.mTimeStamp);
+ }
+
+ if (splitEvent && IsInPanningState()) {
+ TrackTouch(splitEvent->second);
+ return nsEventStatus_eConsumeNoDefault;
+ }
+
+ return result;
+ }
+
+ case PANNING:
+ case PANNING_LOCKED_X:
+ case PANNING_LOCKED_Y:
+ case PAN_MOMENTUM:
+ TrackTouch(aEvent);
+ return nsEventStatus_eConsumeNoDefault;
+
+ case PINCHING:
+ // The scale gesture listener should have handled this.
+ NS_WARNING(
+ "Gesture listener should have handled pinching in OnTouchMove.");
+ return nsEventStatus_eIgnore;
+
+ case SMOOTH_SCROLL:
+ case WHEEL_SCROLL:
+ case KEYBOARD_SCROLL:
+ case OVERSCROLL_ANIMATION:
+ case AUTOSCROLL:
+ case SCROLLBAR_DRAG:
+ // Should not receive a touch-move in the OVERSCROLL_ANIMATION state
+ // as touch blocks that begin in an overscrolled state cancel the
+ // animation. The same is true for wheel scroll animations.
+ NS_WARNING("Received impossible touch in OnTouchMove");
+ break;
+ }
+
+ return nsEventStatus_eConsumeNoDefault;
+}
+
+nsEventStatus AsyncPanZoomController::OnTouchEnd(
+ const MultiTouchInput& aEvent) {
+ APZC_LOG_DETAIL("got a touch-end in state %s\n", this,
+ ToString(mState).c_str());
+ OnTouchEndOrCancel();
+
+ // In case no touch behavior triggered previously we can avoid sending
+ // scroll events or requesting content repaint. This condition is added
+ // to make tests consistent - in case touch-action is NONE (and therefore
+ // no pans/zooms can be performed) we expected neither scroll or repaint
+ // events.
+ if (mState != NOTHING) {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ }
+
+ switch (mState) {
+ case FLING:
+ // Should never happen.
+ NS_WARNING("Received impossible touch end in OnTouchEnd.");
+ [[fallthrough]];
+ case ANIMATING_ZOOM:
+ case SMOOTHMSD_SCROLL:
+ case NOTHING:
+ // May happen if the user double-taps and drags without lifting after the
+ // second tap. Ignore if this happens.
+ return nsEventStatus_eIgnore;
+
+ case TOUCHING:
+ // We may have some velocity stored on the axis from move events
+ // that were not big enough to trigger scrolling. Clear that out.
+ SetVelocityVector(ParentLayerPoint(0, 0));
+ MOZ_ASSERT(GetCurrentTouchBlock());
+ APZC_LOG("%p still has %u touch points active\n", this,
+ GetCurrentTouchBlock()->GetActiveTouchCount());
+ // In cases where the user is panning, then taps the second finger without
+ // entering a pinch, we will arrive here when the second finger is lifted.
+ // However the first finger is still down so we want to remain in state
+ // TOUCHING.
+ if (GetCurrentTouchBlock()->GetActiveTouchCount() == 0) {
+ // It's possible we may be overscrolled if the user tapped during a
+ // previous overscroll pan. Make sure to snap back in this situation.
+ // An ancestor APZC could be overscrolled instead of this APZC, so
+ // walk the handoff chain as well.
+ GetCurrentTouchBlock()
+ ->GetOverscrollHandoffChain()
+ ->SnapBackOverscrolledApzc(this);
+ mFlingAccelerator.Reset();
+ // SnapBackOverscrolledApzc() will put any APZC it causes to snap back
+ // into the OVERSCROLL_ANIMATION state. If that's not us, since we're
+ // done TOUCHING enter the NOTHING state.
+ if (mState != OVERSCROLL_ANIMATION) {
+ SetState(NOTHING);
+ }
+ }
+ return nsEventStatus_eIgnore;
+
+ case PANNING:
+ case PANNING_LOCKED_X:
+ case PANNING_LOCKED_Y:
+ case PAN_MOMENTUM: {
+ MOZ_ASSERT(GetCurrentTouchBlock());
+ EndTouch(aEvent.mTimeStamp, Axis::ClearAxisLock::Yes);
+ return HandleEndOfPan();
+ }
+ case PINCHING:
+ SetState(NOTHING);
+ // Scale gesture listener should have handled this.
+ NS_WARNING(
+ "Gesture listener should have handled pinching in OnTouchEnd.");
+ return nsEventStatus_eIgnore;
+
+ case SMOOTH_SCROLL:
+ case WHEEL_SCROLL:
+ case KEYBOARD_SCROLL:
+ case OVERSCROLL_ANIMATION:
+ case AUTOSCROLL:
+ case SCROLLBAR_DRAG:
+ // Should not receive a touch-end in the OVERSCROLL_ANIMATION state
+ // as touch blocks that begin in an overscrolled state cancel the
+ // animation. The same is true for WHEEL_SCROLL.
+ NS_WARNING("Received impossible touch in OnTouchEnd");
+ break;
+ }
+
+ return nsEventStatus_eConsumeNoDefault;
+}
+
+nsEventStatus AsyncPanZoomController::OnTouchCancel(
+ const MultiTouchInput& aEvent) {
+ APZC_LOG_DETAIL("got a touch-cancel in state %s\n", this,
+ ToString(mState).c_str());
+ OnTouchEndOrCancel();
+ CancelAnimationAndGestureState();
+ return nsEventStatus_eConsumeNoDefault;
+}
+
+nsEventStatus AsyncPanZoomController::OnScaleBegin(
+ const PinchGestureInput& aEvent) {
+ APZC_LOG_DETAIL("got a scale-begin in state %s\n", this,
+ ToString(mState).c_str());
+
+ mPinchLocked = false;
+ mPinchPaintTimerSet = false;
+ // Note that there may not be a touch block at this point, if we received the
+ // PinchGestureEvent directly from widget code without any touch events.
+ if (HasReadyTouchBlock() &&
+ !GetCurrentTouchBlock()->TouchActionAllowsPinchZoom()) {
+ return nsEventStatus_eIgnore;
+ }
+
+ // For platforms that don't support APZ zooming, dispatch a message to the
+ // content controller, it may want to do something else with this gesture.
+ // FIXME: bug 1525793 -- this may need to handle zooming or not on a
+ // per-document basis.
+ if (!StaticPrefs::apz_allow_zooming()) {
+ if (RefPtr<GeckoContentController> controller =
+ GetGeckoContentController()) {
+ APZC_LOG("%p notifying controller of pinch gesture start\n", this);
+ controller->NotifyPinchGesture(
+ aEvent.mType, GetGuid(),
+ ViewAs<LayoutDevicePixel>(
+ aEvent.mFocusPoint,
+ PixelCastJustification::
+ LayoutDeviceIsScreenForUntransformedEvent),
+ 0, aEvent.modifiers);
+ }
+ }
+
+ SetState(PINCHING);
+ Telemetry::Accumulate(Telemetry::APZ_ZOOM_PINCHSOURCE, (int)aEvent.mSource);
+ SetVelocityVector(ParentLayerPoint(0, 0));
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ mLastZoomFocus =
+ aEvent.mLocalFocusPoint - Metrics().GetCompositionBounds().TopLeft();
+
+ mPinchEventBuffer.push(aEvent);
+
+ return nsEventStatus_eConsumeNoDefault;
+}
+
+nsEventStatus AsyncPanZoomController::OnScale(const PinchGestureInput& aEvent) {
+ APZC_LOG_DETAIL("got a scale in state %s\n", this, ToString(mState).c_str());
+
+ if (HasReadyTouchBlock() &&
+ !GetCurrentTouchBlock()->TouchActionAllowsPinchZoom()) {
+ return nsEventStatus_eIgnore;
+ }
+
+ if (mState != PINCHING) {
+ return nsEventStatus_eConsumeNoDefault;
+ }
+
+ mPinchEventBuffer.push(aEvent);
+ HandlePinchLocking(aEvent);
+ bool allowZoom = ZoomConstraintsAllowZoom() && !mPinchLocked;
+
+ // If we are pinch-locked, this is a two-finger pan.
+ // Tracking panning distance and velocity.
+ // UpdateWithTouchAtDevicePoint() acquires the tree lock, so
+ // it cannot be called while the mRecursiveMutex lock is held.
+ if (mPinchLocked) {
+ mX.UpdateWithTouchAtDevicePoint(aEvent.mLocalFocusPoint.x,
+ aEvent.mTimeStamp);
+ mY.UpdateWithTouchAtDevicePoint(aEvent.mLocalFocusPoint.y,
+ aEvent.mTimeStamp);
+ }
+
+ // FIXME: bug 1525793 -- this may need to handle zooming or not on a
+ // per-document basis.
+ if (!StaticPrefs::apz_allow_zooming()) {
+ if (RefPtr<GeckoContentController> controller =
+ GetGeckoContentController()) {
+ APZC_LOG("%p notifying controller of pinch gesture\n", this);
+ controller->NotifyPinchGesture(
+ aEvent.mType, GetGuid(),
+ ViewAs<LayoutDevicePixel>(
+ aEvent.mFocusPoint,
+ PixelCastJustification::
+ LayoutDeviceIsScreenForUntransformedEvent),
+ ViewAs<LayoutDevicePixel>(
+ aEvent.mCurrentSpan - aEvent.mPreviousSpan,
+ PixelCastJustification::
+ LayoutDeviceIsScreenForUntransformedEvent),
+ aEvent.modifiers);
+ }
+ }
+
+ {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ // Only the root APZC is zoomable, and the root APZC is not allowed to have
+ // different x and y scales. If it did, the calculations in this function
+ // would have to be adjusted (as e.g. it would no longer be valid to take
+ // the minimum or maximum of the ratios of the widths and heights of the
+ // page rect and the composition bounds).
+ MOZ_ASSERT(Metrics().IsRootContent());
+
+ CSSToParentLayerScale userZoom = Metrics().GetZoom();
+ ParentLayerPoint focusPoint =
+ aEvent.mLocalFocusPoint - Metrics().GetCompositionBounds().TopLeft();
+ CSSPoint cssFocusPoint;
+ if (Metrics().GetZoom() != CSSToParentLayerScale(0)) {
+ cssFocusPoint = focusPoint / Metrics().GetZoom();
+ }
+
+ ParentLayerPoint focusChange = mLastZoomFocus - focusPoint;
+ mLastZoomFocus = focusPoint;
+ // If displacing by the change in focus point will take us off page bounds,
+ // then reduce the displacement such that it doesn't.
+ focusChange.x -= mX.DisplacementWillOverscrollAmount(focusChange.x);
+ focusChange.y -= mY.DisplacementWillOverscrollAmount(focusChange.y);
+ if (userZoom != CSSToParentLayerScale(0)) {
+ ScrollBy(focusChange / userZoom);
+ }
+
+ // If the span is zero or close to it, we don't want to process this zoom
+ // change because we're going to get wonky numbers for the spanRatio. So
+ // let's bail out here. Note that we do this after the focus-change-scroll
+ // above, so that if we have a pinch with zero span but changing focus,
+ // such as generated by some Synaptics touchpads on Windows, we still
+ // scroll properly.
+ float prevSpan = aEvent.mPreviousSpan;
+ if (fabsf(prevSpan) <= EPSILON || fabsf(aEvent.mCurrentSpan) <= EPSILON) {
+ // We might have done a nonzero ScrollBy above, so update metrics and
+ // repaint/recomposite
+ ScheduleCompositeAndMaybeRepaint();
+ return nsEventStatus_eConsumeNoDefault;
+ }
+ float spanRatio = aEvent.mCurrentSpan / aEvent.mPreviousSpan;
+
+ // When we zoom in with focus, we can zoom too much towards the boundaries
+ // that we actually go over them. These are the needed displacements along
+ // either axis such that we don't overscroll the boundaries when zooming.
+ CSSPoint neededDisplacement;
+
+ CSSToParentLayerScale realMinZoom = mZoomConstraints.mMinZoom;
+ CSSToParentLayerScale realMaxZoom = mZoomConstraints.mMaxZoom;
+ realMinZoom.scale =
+ std::max(realMinZoom.scale, Metrics().GetCompositionBounds().Width() /
+ Metrics().GetScrollableRect().Width());
+ realMinZoom.scale =
+ std::max(realMinZoom.scale, Metrics().GetCompositionBounds().Height() /
+ Metrics().GetScrollableRect().Height());
+ if (realMaxZoom < realMinZoom) {
+ realMaxZoom = realMinZoom;
+ }
+
+ bool doScale = allowZoom && ((spanRatio > 1.0 && userZoom < realMaxZoom) ||
+ (spanRatio < 1.0 && userZoom > realMinZoom));
+
+ if (doScale) {
+ spanRatio = clamped(spanRatio, realMinZoom.scale / userZoom.scale,
+ realMaxZoom.scale / userZoom.scale);
+
+ // Note that the spanRatio here should never put us into OVERSCROLL_BOTH
+ // because up above we clamped it.
+ neededDisplacement.x =
+ -mX.ScaleWillOverscrollAmount(spanRatio, cssFocusPoint.x);
+ neededDisplacement.y =
+ -mY.ScaleWillOverscrollAmount(spanRatio, cssFocusPoint.y);
+
+ ScaleWithFocus(spanRatio, cssFocusPoint);
+
+ if (neededDisplacement != CSSPoint()) {
+ ScrollBy(neededDisplacement);
+ }
+
+ // We don't want to redraw on every scale, so throttle it.
+ if (!mPinchPaintTimerSet) {
+ const int delay = StaticPrefs::apz_scale_repaint_delay_ms();
+ if (delay >= 0) {
+ if (RefPtr<GeckoContentController> controller =
+ GetGeckoContentController()) {
+ mPinchPaintTimerSet = true;
+ controller->PostDelayedTask(
+ NewRunnableMethod(
+ "layers::AsyncPanZoomController::"
+ "DoDelayedRequestContentRepaint",
+ this,
+ &AsyncPanZoomController::DoDelayedRequestContentRepaint),
+ delay);
+ }
+ }
+ } else if (apz::AboutToCheckerboard(mLastContentPaintMetrics,
+ Metrics())) {
+ // If we already scheduled a throttled repaint request but are also
+ // in danger of checkerboarding soon, trigger the repaint request to
+ // go out immediately. This should reduce the amount of time we spend
+ // checkerboarding.
+ //
+ // Note that if we remain in this "about to
+ // checkerboard" state over a period of time with multiple pinch input
+ // events (which is quite likely), then we will flip-flop between taking
+ // the above branch (!mPinchPaintTimerSet) and this branch (which will
+ // flush the repaint request and reset mPinchPaintTimerSet to false).
+ // This is sort of desirable because it halves the number of repaint
+ // requests we send, and therefore reduces IPC traffic.
+ // Keep in mind that many of these repaint requests will be ignored on
+ // the main-thread anyway due to the resolution mismatch - the first
+ // repaint request will be honored because APZ's notion of the painted
+ // resolution matches the actual main thread resolution, but that first
+ // repaint request will change the resolution on the main thread.
+ // Subsequent repaint requests will be ignored in APZCCallbackHelper, at
+ // https://searchfox.org/mozilla-central/rev/e0eb861a187f0bb6d994228f2e0e49b2c9ee455e/gfx/layers/apz/util/APZCCallbackHelper.cpp#331-338,
+ // until we receive a NotifyLayersUpdated call that re-syncs APZ's
+ // notion of the painted resolution to the main thread. These ignored
+ // repaint requests are contributing to IPC traffic needlessly, and so
+ // halving the number of repaint requests (as mentioned above) seems
+ // desirable.
+ DoDelayedRequestContentRepaint();
+ }
+ } else {
+ // Trigger a repaint request after scrolling.
+ RequestContentRepaint();
+ }
+
+ // We did a ScrollBy call above even if we didn't do a scale, so we
+ // should composite for that.
+ ScheduleComposite();
+ }
+
+ return nsEventStatus_eConsumeNoDefault;
+}
+
+nsEventStatus AsyncPanZoomController::OnScaleEnd(
+ const PinchGestureInput& aEvent) {
+ APZC_LOG_DETAIL("got a scale-end in state %s\n", this,
+ ToString(mState).c_str());
+
+ mPinchPaintTimerSet = false;
+
+ if (HasReadyTouchBlock() &&
+ !GetCurrentTouchBlock()->TouchActionAllowsPinchZoom()) {
+ return nsEventStatus_eIgnore;
+ }
+
+ // FIXME: bug 1525793 -- this may need to handle zooming or not on a
+ // per-document basis.
+ if (!StaticPrefs::apz_allow_zooming()) {
+ if (RefPtr<GeckoContentController> controller =
+ GetGeckoContentController()) {
+ controller->NotifyPinchGesture(
+ aEvent.mType, GetGuid(),
+ ViewAs<LayoutDevicePixel>(
+ aEvent.mFocusPoint,
+ PixelCastJustification::
+ LayoutDeviceIsScreenForUntransformedEvent),
+ 0, aEvent.modifiers);
+ }
+ }
+
+ {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ ScheduleComposite();
+ RequestContentRepaint();
+ }
+
+ mPinchEventBuffer.clear();
+
+ if (aEvent.mType == PinchGestureInput::PINCHGESTURE_FINGERLIFTED) {
+ // One finger is still down, so transition to a TOUCHING state
+ if (!mPinchLocked) {
+ mPanDirRestricted = false;
+ mLastTouch.mPosition = mStartTouch =
+ ToExternalPoint(aEvent.mScreenOffset, aEvent.mFocusPoint);
+ mLastTouch.mTimeStamp = mTouchStartTime = aEvent.mTimeStamp;
+ StartTouch(aEvent.mLocalFocusPoint, aEvent.mTimeStamp);
+ SetState(TOUCHING);
+ } else {
+ // If we are pinch locked, StartTouch() was already called
+ // when we entered the pinch lock.
+ StartPanning(ToExternalPoint(aEvent.mScreenOffset, aEvent.mFocusPoint),
+ aEvent.mTimeStamp);
+ }
+ } else {
+ // Otherwise, handle the gesture being completely done.
+
+ // Some of the code paths below, like ScrollSnap() or HandleEndOfPan(),
+ // may start an animation, but otherwise we want to end up in the NOTHING
+ // state. To avoid state change notification churn, we use a
+ // notification blocker.
+ bool stateWasPinching = (mState == PINCHING);
+ StateChangeNotificationBlocker blocker(this);
+ SetState(NOTHING);
+
+ if (ZoomConstraintsAllowZoom()) {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+
+ // We can get into a situation where we are overscrolled at the end of a
+ // pinch if we go into overscroll with a two-finger pan, and then turn
+ // that into a pinch by increasing the span sufficiently. In such a case,
+ // there is no snap-back animation to get us out of overscroll, so we need
+ // to get out of it somehow.
+ // Moreover, in cases of scroll handoff, the overscroll can be on an APZC
+ // further up in the handoff chain rather than on the current APZC, so
+ // we need to clear overscroll along the entire handoff chain.
+ if (HasReadyTouchBlock()) {
+ GetCurrentTouchBlock()->GetOverscrollHandoffChain()->ClearOverscroll();
+ } else {
+ ClearOverscroll();
+ }
+ // Along with clearing the overscroll, we also want to snap to the nearest
+ // snap point as appropriate.
+ ScrollSnap(ScrollSnapFlags::IntendedEndPosition);
+ } else {
+ // when zoom is not allowed
+ EndTouch(aEvent.mTimeStamp, Axis::ClearAxisLock::Yes);
+ if (stateWasPinching) {
+ // still pinching
+ if (HasReadyTouchBlock()) {
+ return HandleEndOfPan();
+ }
+ }
+ }
+ }
+ return nsEventStatus_eConsumeNoDefault;
+}
+
+nsEventStatus AsyncPanZoomController::HandleEndOfPan() {
+ MOZ_ASSERT(!mAnimation);
+ MOZ_ASSERT(GetCurrentTouchBlock() || GetCurrentPanGestureBlock());
+ GetCurrentInputBlock()->GetOverscrollHandoffChain()->FlushRepaints();
+ ParentLayerPoint flingVelocity = GetVelocityVector();
+
+ // Clear our velocities; if DispatchFling() gives the fling to us,
+ // the fling velocity gets *added* to our existing velocity in
+ // AcceptFling().
+ SetVelocityVector(ParentLayerPoint(0, 0));
+ // Clear our state so that we don't stay in the PANNING state
+ // if DispatchFling() gives the fling to somone else. However,
+ // don't send the state change notification until we've determined
+ // what our final state is to avoid notification churn.
+ StateChangeNotificationBlocker blocker(this);
+ SetState(NOTHING);
+
+ APZC_LOG("%p starting a fling animation if %f > %f\n", this,
+ flingVelocity.Length().value,
+ StaticPrefs::apz_fling_min_velocity_threshold());
+
+ if (flingVelocity.Length() <=
+ StaticPrefs::apz_fling_min_velocity_threshold()) {
+ // Relieve overscroll now if needed, since we will not transition to a fling
+ // animation and then an overscroll animation, and relieve it then.
+ GetCurrentInputBlock()
+ ->GetOverscrollHandoffChain()
+ ->SnapBackOverscrolledApzc(this);
+ mFlingAccelerator.Reset();
+ return nsEventStatus_eConsumeNoDefault;
+ }
+
+ // Make a local copy of the tree manager pointer and check that it's not
+ // null before calling DispatchFling(). This is necessary because Destroy(),
+ // which nulls out mTreeManager, could be called concurrently.
+ if (APZCTreeManager* treeManagerLocal = GetApzcTreeManager()) {
+ const FlingHandoffState handoffState{
+ flingVelocity,
+ GetCurrentInputBlock()->GetOverscrollHandoffChain(),
+ Some(mTouchStartRestingTimeBeforePan),
+ mMinimumVelocityDuringPan.valueOr(0),
+ false /* not handoff */,
+ GetCurrentInputBlock()->GetScrolledApzc()};
+ treeManagerLocal->DispatchFling(this, handoffState);
+ }
+ return nsEventStatus_eConsumeNoDefault;
+}
+
+Maybe<LayoutDevicePoint> AsyncPanZoomController::ConvertToGecko(
+ const ScreenIntPoint& aPoint) {
+ if (APZCTreeManager* treeManagerLocal = GetApzcTreeManager()) {
+ if (Maybe<ScreenIntPoint> layoutPoint =
+ treeManagerLocal->ConvertToGecko(aPoint, this)) {
+ return Some(LayoutDevicePoint(ViewAs<LayoutDevicePixel>(
+ *layoutPoint,
+ PixelCastJustification::LayoutDeviceIsScreenForUntransformedEvent)));
+ }
+ }
+ return Nothing();
+}
+
+OuterCSSCoord AsyncPanZoomController::ConvertScrollbarPoint(
+ const ParentLayerPoint& aScrollbarPoint,
+ const ScrollbarData& aThumbData) const {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+
+ CSSPoint scrollbarPoint;
+ if (Metrics().GetZoom() != CSSToParentLayerScale(0)) {
+ // First, get it into the right coordinate space.
+ scrollbarPoint = aScrollbarPoint / Metrics().GetZoom();
+ }
+
+ // The scrollbar can be transformed with the frame but the pres shell
+ // resolution is only applied to the scroll frame.
+ OuterCSSPoint outerScrollbarPoint =
+ scrollbarPoint * Metrics().GetCSSToOuterCSSScale();
+
+ // Now, get it to be relative to the beginning of the scroll track.
+ OuterCSSRect cssCompositionBound =
+ Metrics().CalculateCompositionBoundsInOuterCssPixels();
+ return GetAxisStart(*aThumbData.mDirection, outerScrollbarPoint) -
+ GetAxisStart(*aThumbData.mDirection, cssCompositionBound) -
+ aThumbData.mScrollTrackStart;
+}
+
+static bool AllowsScrollingMoreThanOnePage(double aMultiplier) {
+ return Abs(aMultiplier) >=
+ EventStateManager::MIN_MULTIPLIER_VALUE_ALLOWING_OVER_ONE_PAGE_SCROLL;
+}
+
+ParentLayerPoint AsyncPanZoomController::GetScrollWheelDelta(
+ const ScrollWheelInput& aEvent) const {
+ return GetScrollWheelDelta(aEvent, aEvent.mDeltaX, aEvent.mDeltaY,
+ aEvent.mUserDeltaMultiplierX,
+ aEvent.mUserDeltaMultiplierY);
+}
+
+ParentLayerPoint AsyncPanZoomController::GetScrollWheelDelta(
+ const ScrollWheelInput& aEvent, double aDeltaX, double aDeltaY,
+ double aMultiplierX, double aMultiplierY) const {
+ ParentLayerSize scrollAmount;
+ ParentLayerSize pageScrollSize;
+
+ {
+ // Grab the lock to access the frame metrics.
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ LayoutDeviceIntSize scrollAmountLD = mScrollMetadata.GetLineScrollAmount();
+ LayoutDeviceIntSize pageScrollSizeLD =
+ mScrollMetadata.GetPageScrollAmount();
+ scrollAmount = scrollAmountLD / Metrics().GetDevPixelsPerCSSPixel() *
+ Metrics().GetZoom();
+ pageScrollSize = pageScrollSizeLD / Metrics().GetDevPixelsPerCSSPixel() *
+ Metrics().GetZoom();
+ }
+
+ ParentLayerPoint delta;
+ switch (aEvent.mDeltaType) {
+ case ScrollWheelInput::SCROLLDELTA_LINE: {
+ delta.x = aDeltaX * scrollAmount.width;
+ delta.y = aDeltaY * scrollAmount.height;
+ break;
+ }
+ case ScrollWheelInput::SCROLLDELTA_PAGE: {
+ delta.x = aDeltaX * pageScrollSize.width;
+ delta.y = aDeltaY * pageScrollSize.height;
+ break;
+ }
+ case ScrollWheelInput::SCROLLDELTA_PIXEL: {
+ delta = ToParentLayerCoordinates(ScreenPoint(aDeltaX, aDeltaY),
+ aEvent.mOrigin);
+ break;
+ }
+ }
+
+ // Apply user-set multipliers.
+ delta.x *= aMultiplierX;
+ delta.y *= aMultiplierY;
+
+ // For the conditions under which we allow system scroll overrides, see
+ // WidgetWheelEvent::OverriddenDelta{X,Y}.
+ // Note that we do *not* restrict this to the root content, see bug 1217715
+ // for discussion on this.
+ if (StaticPrefs::mousewheel_system_scroll_override_enabled() &&
+ !aEvent.IsCustomizedByUserPrefs() &&
+ aEvent.mDeltaType == ScrollWheelInput::SCROLLDELTA_LINE &&
+ aEvent.mAllowToOverrideSystemScrollSpeed) {
+ delta.x = WidgetWheelEvent::ComputeOverriddenDelta(delta.x, false);
+ delta.y = WidgetWheelEvent::ComputeOverriddenDelta(delta.y, true);
+ }
+
+ // If this is a line scroll, and this event was part of a scroll series, then
+ // it might need extra acceleration. See WheelHandlingHelper.cpp.
+ if (aEvent.mDeltaType == ScrollWheelInput::SCROLLDELTA_LINE &&
+ aEvent.mScrollSeriesNumber > 0) {
+ int32_t start = StaticPrefs::mousewheel_acceleration_start();
+ if (start >= 0 && aEvent.mScrollSeriesNumber >= uint32_t(start)) {
+ int32_t factor = StaticPrefs::mousewheel_acceleration_factor();
+ if (factor > 0) {
+ delta.x = ComputeAcceleratedWheelDelta(
+ delta.x, aEvent.mScrollSeriesNumber, factor);
+ delta.y = ComputeAcceleratedWheelDelta(
+ delta.y, aEvent.mScrollSeriesNumber, factor);
+ }
+ }
+ }
+
+ // We shouldn't scroll more than one page at once except when the
+ // user preference is large.
+ if (!AllowsScrollingMoreThanOnePage(aMultiplierX) &&
+ Abs(delta.x) > pageScrollSize.width) {
+ delta.x = (delta.x >= 0) ? pageScrollSize.width : -pageScrollSize.width;
+ }
+ if (!AllowsScrollingMoreThanOnePage(aMultiplierY) &&
+ Abs(delta.y) > pageScrollSize.height) {
+ delta.y = (delta.y >= 0) ? pageScrollSize.height : -pageScrollSize.height;
+ }
+
+ return delta;
+}
+
+nsEventStatus AsyncPanZoomController::OnKeyboard(const KeyboardInput& aEvent) {
+ // Mark that this APZC has async key scrolled
+ mTestHasAsyncKeyScrolled = true;
+
+ // Calculate the destination for this keyboard scroll action
+ CSSPoint destination = GetKeyboardDestination(aEvent.mAction);
+ ScrollOrigin scrollOrigin =
+ SmoothScrollAnimation::GetScrollOriginForAction(aEvent.mAction.mType);
+ Maybe<CSSSnapTarget> snapTarget = MaybeAdjustDestinationForScrollSnapping(
+ aEvent, destination, GetScrollSnapFlagsForKeyboardAction(aEvent.mAction));
+ ScrollMode scrollMode = apz::GetScrollModeForOrigin(scrollOrigin);
+
+ RecordScrollPayload(aEvent.mTimeStamp);
+ // If the scrolling is instant, then scroll immediately to the destination
+ if (scrollMode == ScrollMode::Instant) {
+ CancelAnimation();
+
+ ParentLayerPoint startPoint, endPoint;
+
+ {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+
+ // CallDispatchScroll interprets the start and end points as the start and
+ // end of a touch scroll so they need to be reversed.
+ startPoint = destination * Metrics().GetZoom();
+ endPoint = Metrics().GetVisualScrollOffset() * Metrics().GetZoom();
+ }
+
+ ParentLayerPoint delta = endPoint - startPoint;
+
+ ScreenPoint distance = ToScreenCoordinates(
+ ParentLayerPoint(fabs(delta.x), fabs(delta.y)), startPoint);
+
+ OverscrollHandoffState handoffState(
+ *mInputQueue->GetCurrentKeyboardBlock()->GetOverscrollHandoffChain(),
+ distance, ScrollSource::Keyboard);
+
+ CallDispatchScroll(startPoint, endPoint, handoffState);
+ ParentLayerPoint remainingDelta = endPoint - startPoint;
+ if (remainingDelta != delta) {
+ // If any scrolling happened, set KEYBOARD_SCROLL explicitly so that it
+ // will trigger a TransformEnd notification.
+ SetState(KEYBOARD_SCROLL);
+ }
+
+ if (snapTarget) {
+ {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ mLastSnapTargetIds = std::move(snapTarget->mTargetIds);
+ }
+ }
+ SetState(NOTHING);
+
+ return nsEventStatus_eConsumeDoDefault;
+ }
+
+ // The lock must be held across the entire update operation, so the
+ // compositor doesn't end the animation before we get a chance to
+ // update it.
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+
+ if (snapTarget) {
+ // If we're scroll snapping, use a smooth scroll animation to get
+ // the desired physics. Note that SmoothMsdScrollTo() will re-use an
+ // existing smooth scroll animation if there is one.
+ APZC_LOG("%p keyboard scrolling to snap point %s\n", this,
+ ToString(destination).c_str());
+ SmoothMsdScrollTo(std::move(*snapTarget), ScrollTriggeredByScript::No);
+ return nsEventStatus_eConsumeDoDefault;
+ }
+
+ // Use a keyboard scroll animation to scroll, reusing an existing one if it
+ // exists
+ if (mState != KEYBOARD_SCROLL) {
+ CancelAnimation();
+ SetState(KEYBOARD_SCROLL);
+
+ nsPoint initialPosition =
+ CSSPoint::ToAppUnits(Metrics().GetVisualScrollOffset());
+ StartAnimation(
+ new SmoothScrollAnimation(*this, initialPosition, scrollOrigin));
+ }
+
+ // Convert velocity from ParentLayerPoints/ms to ParentLayerPoints/s and then
+ // to appunits/second.
+ nsPoint velocity;
+ if (Metrics().GetZoom() != CSSToParentLayerScale(0)) {
+ velocity =
+ CSSPoint::ToAppUnits(ParentLayerPoint(mX.GetVelocity() * 1000.0f,
+ mY.GetVelocity() * 1000.0f) /
+ Metrics().GetZoom());
+ }
+
+ SmoothScrollAnimation* animation = mAnimation->AsSmoothScrollAnimation();
+ MOZ_ASSERT(animation);
+
+ animation->UpdateDestination(aEvent.mTimeStamp,
+ CSSPixel::ToAppUnits(destination),
+ nsSize(velocity.x, velocity.y));
+
+ return nsEventStatus_eConsumeDoDefault;
+}
+
+CSSPoint AsyncPanZoomController::GetKeyboardDestination(
+ const KeyboardScrollAction& aAction) const {
+ CSSSize lineScrollSize;
+ CSSSize pageScrollSize;
+ CSSPoint scrollOffset;
+ CSSRect scrollRect;
+ ParentLayerRect compositionBounds;
+
+ {
+ // Grab the lock to access the frame metrics.
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+
+ lineScrollSize = mScrollMetadata.GetLineScrollAmount() /
+ Metrics().GetDevPixelsPerCSSPixel();
+ pageScrollSize = mScrollMetadata.GetPageScrollAmount() /
+ Metrics().GetDevPixelsPerCSSPixel();
+
+ scrollOffset = GetCurrentAnimationDestination(lock).valueOr(
+ Metrics().GetVisualScrollOffset());
+
+ scrollRect = Metrics().GetScrollableRect();
+ compositionBounds = Metrics().GetCompositionBounds();
+ }
+
+ // Calculate the scroll destination based off of the scroll type and direction
+ CSSPoint scrollDestination = scrollOffset;
+
+ switch (aAction.mType) {
+ case KeyboardScrollAction::eScrollCharacter: {
+ int32_t scrollDistance =
+ StaticPrefs::toolkit_scrollbox_horizontalScrollDistance();
+
+ if (aAction.mForward) {
+ scrollDestination.x += scrollDistance * lineScrollSize.width;
+ } else {
+ scrollDestination.x -= scrollDistance * lineScrollSize.width;
+ }
+ break;
+ }
+ case KeyboardScrollAction::eScrollLine: {
+ int32_t scrollDistance =
+ StaticPrefs::toolkit_scrollbox_verticalScrollDistance();
+ if (scrollDistance * lineScrollSize.height <=
+ compositionBounds.Height()) {
+ if (aAction.mForward) {
+ scrollDestination.y += scrollDistance * lineScrollSize.height;
+ } else {
+ scrollDestination.y -= scrollDistance * lineScrollSize.height;
+ }
+ break;
+ }
+ [[fallthrough]];
+ }
+ case KeyboardScrollAction::eScrollPage: {
+ if (aAction.mForward) {
+ scrollDestination.y += pageScrollSize.height;
+ } else {
+ scrollDestination.y -= pageScrollSize.height;
+ }
+ break;
+ }
+ case KeyboardScrollAction::eScrollComplete: {
+ if (aAction.mForward) {
+ scrollDestination.y = scrollRect.YMost();
+ } else {
+ scrollDestination.y = scrollRect.Y();
+ }
+ break;
+ }
+ }
+
+ return scrollDestination;
+}
+
+ScrollSnapFlags AsyncPanZoomController::GetScrollSnapFlagsForKeyboardAction(
+ const KeyboardScrollAction& aAction) const {
+ switch (aAction.mType) {
+ case KeyboardScrollAction::eScrollCharacter:
+ case KeyboardScrollAction::eScrollLine:
+ return ScrollSnapFlags::IntendedDirection;
+ case KeyboardScrollAction::eScrollPage:
+ return ScrollSnapFlags::IntendedDirection |
+ ScrollSnapFlags::IntendedEndPosition;
+ case KeyboardScrollAction::eScrollComplete:
+ return ScrollSnapFlags::IntendedEndPosition;
+ }
+ return ScrollSnapFlags::Disabled;
+}
+
+ParentLayerPoint AsyncPanZoomController::GetDeltaForEvent(
+ const InputData& aEvent) const {
+ ParentLayerPoint delta;
+ if (aEvent.mInputType == SCROLLWHEEL_INPUT) {
+ delta = GetScrollWheelDelta(aEvent.AsScrollWheelInput());
+ } else if (aEvent.mInputType == PANGESTURE_INPUT) {
+ const PanGestureInput& panInput = aEvent.AsPanGestureInput();
+ delta = ToParentLayerCoordinates(panInput.UserMultipliedPanDisplacement(),
+ panInput.mPanStartPoint);
+ }
+ return delta;
+}
+
+CSSRect AsyncPanZoomController::GetCurrentScrollRangeInCssPixels() const {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ return Metrics().CalculateScrollRange();
+}
+
+// Return whether or not the underlying layer can be scrolled on either axis.
+bool AsyncPanZoomController::CanScroll(const InputData& aEvent) const {
+ ParentLayerPoint delta = GetDeltaForEvent(aEvent);
+ if (!delta.x && !delta.y) {
+ return false;
+ }
+
+ if (SCROLLWHEEL_INPUT == aEvent.mInputType) {
+ const ScrollWheelInput& scrollWheelInput = aEvent.AsScrollWheelInput();
+ // If it's a wheel scroll, we first check if it is an auto-dir scroll.
+ // 1. For an auto-dir scroll, check if it's delta should be adjusted, if it
+ // is, then we can conclude it must be scrollable; otherwise, fall back
+ // to checking if it is scrollable without adjusting its delta.
+ // 2. For a non-auto-dir scroll, simply check if it is scrollable without
+ // adjusting its delta.
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ if (scrollWheelInput.IsAutoDir(mScrollMetadata.ForceMousewheelAutodir())) {
+ auto deltaX = scrollWheelInput.mDeltaX;
+ auto deltaY = scrollWheelInput.mDeltaY;
+ bool isRTL =
+ IsContentOfHonouredTargetRightToLeft(scrollWheelInput.HonoursRoot(
+ mScrollMetadata.ForceMousewheelAutodirHonourRoot()));
+ APZAutoDirWheelDeltaAdjuster adjuster(deltaX, deltaY, mX, mY, isRTL);
+ if (adjuster.ShouldBeAdjusted()) {
+ // If we detect that the delta values should be adjusted for an auto-dir
+ // wheel scroll, then it is impossible to be an unscrollable scroll.
+ return true;
+ }
+ }
+ return CanScrollWithWheel(delta);
+ }
+ return CanScroll(delta);
+}
+
+ScrollDirections AsyncPanZoomController::GetAllowedHandoffDirections() const {
+ ScrollDirections result;
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+
+ // In Fission there can be non-scrollable APZCs. It's unclear whether
+ // overscroll-behavior should be respected for these
+ // (see https://github.com/w3c/csswg-drafts/issues/6523) but
+ // we currently don't, to match existing practice.
+ const bool isScrollable = mX.CanScroll() || mY.CanScroll();
+ const bool isRoot = IsRootContent();
+ if ((!isScrollable && !isRoot) || mX.OverscrollBehaviorAllowsHandoff()) {
+ result += ScrollDirection::eHorizontal;
+ }
+ if ((!isScrollable && !isRoot) || mY.OverscrollBehaviorAllowsHandoff()) {
+ result += ScrollDirection::eVertical;
+ }
+ return result;
+}
+
+bool AsyncPanZoomController::CanScroll(const ParentLayerPoint& aDelta) const {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ return mX.CanScroll(ParentLayerCoord(aDelta.x)) ||
+ mY.CanScroll(ParentLayerCoord(aDelta.y));
+}
+
+bool AsyncPanZoomController::CanScrollWithWheel(
+ const ParentLayerPoint& aDelta) const {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+
+ // For more details about the concept of a disregarded direction, refer to the
+ // code in struct ScrollMetadata which defines mDisregardedDirection.
+ Maybe<ScrollDirection> disregardedDirection =
+ mScrollMetadata.GetDisregardedDirection();
+ if (mX.CanScroll(ParentLayerCoord(aDelta.x)) &&
+ disregardedDirection != Some(ScrollDirection::eHorizontal)) {
+ return true;
+ }
+ if (mY.CanScroll(ParentLayerCoord(aDelta.y)) &&
+ disregardedDirection != Some(ScrollDirection::eVertical)) {
+ return true;
+ }
+ return false;
+}
+
+bool AsyncPanZoomController::CanScroll(ScrollDirection aDirection) const {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ switch (aDirection) {
+ case ScrollDirection::eHorizontal:
+ return mX.CanScroll();
+ case ScrollDirection::eVertical:
+ return mY.CanScroll();
+ }
+ MOZ_ASSERT_UNREACHABLE("Invalid value");
+ return false;
+}
+
+bool AsyncPanZoomController::CanVerticalScrollWithDynamicToolbar() const {
+ MOZ_ASSERT(IsRootContent());
+
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ return mY.CanVerticalScrollWithDynamicToolbar();
+}
+
+bool AsyncPanZoomController::CanScrollDownwards() const {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ return mY.CanScrollTo(eSideBottom);
+}
+
+SideBits AsyncPanZoomController::ScrollableDirections() const {
+ SideBits result;
+ { // scope lock to respect lock ordering with APZCTreeManager::mTreeLock
+ // which will be acquired in the `GetCompositorFixedLayerMargins` below.
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ result = mX.ScrollableDirections() | mY.ScrollableDirections();
+ }
+
+ if (IsRootContent()) {
+ if (APZCTreeManager* treeManagerLocal = GetApzcTreeManager()) {
+ ScreenMargin fixedLayerMargins =
+ treeManagerLocal->GetCompositorFixedLayerMargins();
+ {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ result |= mY.ScrollableDirectionsWithDynamicToolbar(fixedLayerMargins);
+ }
+ }
+ }
+
+ return result;
+}
+
+bool AsyncPanZoomController::IsContentOfHonouredTargetRightToLeft(
+ bool aHonoursRoot) const {
+ if (aHonoursRoot) {
+ return mScrollMetadata.IsAutoDirRootContentRTL();
+ }
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ return Metrics().IsHorizontalContentRightToLeft();
+}
+
+bool AsyncPanZoomController::AllowScrollHandoffInCurrentBlock() const {
+ bool result = mInputQueue->AllowScrollHandoff();
+ if (!StaticPrefs::apz_allow_immediate_handoff()) {
+ if (InputBlockState* currentBlock = GetCurrentInputBlock()) {
+ // Do not allow handoff beyond the first APZC to scroll.
+ if (currentBlock->GetScrolledApzc() == this) {
+ result = false;
+ APZC_LOG("%p dropping handoff; AllowImmediateHandoff=false\n", this);
+ }
+ }
+ }
+ return result;
+}
+
+void AsyncPanZoomController::DoDelayedRequestContentRepaint() {
+ if (!IsDestroyed() && mPinchPaintTimerSet) {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ RequestContentRepaint();
+ }
+ mPinchPaintTimerSet = false;
+}
+
+void AsyncPanZoomController::DoDelayedTransformEndNotification(
+ PanZoomState aOldState) {
+ if (!IsDestroyed() && IsDelayedTransformEndSet()) {
+ DispatchStateChangeNotification(aOldState, NOTHING);
+ }
+ SetDelayedTransformEnd(false);
+}
+
+static void AdjustDeltaForAllowedScrollDirections(
+ ParentLayerPoint& aDelta,
+ const ScrollDirections& aAllowedScrollDirections) {
+ if (!aAllowedScrollDirections.contains(ScrollDirection::eHorizontal)) {
+ aDelta.x = 0;
+ }
+ if (!aAllowedScrollDirections.contains(ScrollDirection::eVertical)) {
+ aDelta.y = 0;
+ }
+}
+
+nsEventStatus AsyncPanZoomController::OnScrollWheel(
+ const ScrollWheelInput& aEvent) {
+ // Get the scroll wheel's delta values in parent-layer pixels. But before
+ // getting the values, we need to check if it is an auto-dir scroll and if it
+ // should be adjusted, if both answers are yes, let's adjust X and Y values
+ // first, and then get the delta values in parent-layer pixels based on the
+ // adjusted values.
+ bool adjustedByAutoDir = false;
+ auto deltaX = aEvent.mDeltaX;
+ auto deltaY = aEvent.mDeltaY;
+ ParentLayerPoint delta;
+ {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ if (aEvent.IsAutoDir(mScrollMetadata.ForceMousewheelAutodir())) {
+ // It's an auto-dir scroll, so check if its delta should be adjusted, if
+ // so, adjust it.
+ bool isRTL = IsContentOfHonouredTargetRightToLeft(aEvent.HonoursRoot(
+ mScrollMetadata.ForceMousewheelAutodirHonourRoot()));
+ APZAutoDirWheelDeltaAdjuster adjuster(deltaX, deltaY, mX, mY, isRTL);
+ if (adjuster.ShouldBeAdjusted()) {
+ adjuster.Adjust();
+ adjustedByAutoDir = true;
+ }
+ }
+ }
+ // Ensure the calls to GetScrollWheelDelta are outside the mRecursiveMutex
+ // lock since these calls may acquire the APZ tree lock. Holding
+ // mRecursiveMutex while acquiring the APZ tree lock is lock ordering
+ // violation.
+ if (adjustedByAutoDir) {
+ // If the original delta values have been adjusted, we pass them to
+ // replace the original delta values in |aEvent| so that the delta values
+ // in parent-layer pixels are caculated based on the adjusted values, not
+ // the original ones.
+ // Pay special attention to the last two parameters. They are in a swaped
+ // order so that they still correspond to their delta after adjustment.
+ delta = GetScrollWheelDelta(aEvent, deltaX, deltaY,
+ aEvent.mUserDeltaMultiplierY,
+ aEvent.mUserDeltaMultiplierX);
+ } else {
+ // If the original delta values haven't been adjusted by auto-dir, just pass
+ // the |aEvent| and caculate the delta values in parent-layer pixels based
+ // on the original delta values from |aEvent|.
+ delta = GetScrollWheelDelta(aEvent);
+ }
+
+ APZC_LOG("%p got a scroll-wheel with delta in parent-layer pixels: %s\n",
+ this, ToString(delta).c_str());
+
+ if (adjustedByAutoDir) {
+ MOZ_ASSERT(delta.x || delta.y,
+ "Adjusted auto-dir delta values can never be all-zero.");
+ APZC_LOG("%p got a scroll-wheel with adjusted auto-dir delta values\n",
+ this);
+ } else if ((delta.x || delta.y) && !CanScrollWithWheel(delta)) {
+ // We can't scroll this apz anymore, so we simply drop the event.
+ if (mInputQueue->GetActiveWheelTransaction() &&
+ StaticPrefs::test_mousescroll()) {
+ if (RefPtr<GeckoContentController> controller =
+ GetGeckoContentController()) {
+ controller->NotifyMozMouseScrollEvent(GetScrollId(),
+ u"MozMouseScrollFailed"_ns);
+ }
+ }
+ return nsEventStatus_eConsumeNoDefault;
+ }
+
+ MOZ_ASSERT(mInputQueue->GetCurrentWheelBlock());
+ AdjustDeltaForAllowedScrollDirections(
+ delta, mInputQueue->GetCurrentWheelBlock()->GetAllowedScrollDirections());
+
+ if (delta.x == 0 && delta.y == 0) {
+ // Avoid spurious state changes and unnecessary work
+ return nsEventStatus_eIgnore;
+ }
+
+ switch (aEvent.mScrollMode) {
+ case ScrollWheelInput::SCROLLMODE_INSTANT: {
+ // Wheel events from "clicky" mouse wheels trigger scroll snapping to the
+ // next snap point. Check for this, and adjust the delta to take into
+ // account the snap point.
+ CSSPoint startPosition;
+ {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ startPosition = Metrics().GetVisualScrollOffset();
+ }
+ Maybe<CSSSnapTarget> snapTarget =
+ MaybeAdjustDeltaForScrollSnappingOnWheelInput(aEvent, delta,
+ startPosition);
+
+ ScreenPoint distance = ToScreenCoordinates(
+ ParentLayerPoint(fabs(delta.x), fabs(delta.y)), aEvent.mLocalOrigin);
+
+ CancelAnimation();
+
+ OverscrollHandoffState handoffState(
+ *mInputQueue->GetCurrentWheelBlock()->GetOverscrollHandoffChain(),
+ distance, ScrollSource::Wheel);
+ ParentLayerPoint startPoint = aEvent.mLocalOrigin;
+ ParentLayerPoint endPoint = aEvent.mLocalOrigin - delta;
+ RecordScrollPayload(aEvent.mTimeStamp);
+
+ CallDispatchScroll(startPoint, endPoint, handoffState);
+ ParentLayerPoint remainingDelta = endPoint - startPoint;
+ if (remainingDelta != delta) {
+ // If any scrolling happened, set WHEEL_SCROLL explicitly so that it
+ // will trigger a TransformEnd notification.
+ SetState(WHEEL_SCROLL);
+ }
+
+ if (snapTarget) {
+ {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ mLastSnapTargetIds = std::move(snapTarget->mTargetIds);
+ }
+ }
+ SetState(NOTHING);
+
+ // The calls above handle their own locking; moreover,
+ // ToScreenCoordinates() and CallDispatchScroll() can grab the tree lock.
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ RequestContentRepaint();
+
+ break;
+ }
+
+ case ScrollWheelInput::SCROLLMODE_SMOOTH: {
+ // The lock must be held across the entire update operation, so the
+ // compositor doesn't end the animation before we get a chance to
+ // update it.
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+
+ RecordScrollPayload(aEvent.mTimeStamp);
+ // Perform scroll snapping if appropriate.
+ // If we're already in a wheel scroll or smooth scroll animation,
+ // the delta is applied to its destination, not to the current
+ // scroll position. Take this into account when finding a snap point.
+ CSSPoint startPosition = GetCurrentAnimationDestination(lock).valueOr(
+ Metrics().GetVisualScrollOffset());
+
+ if (Maybe<CSSSnapTarget> snapTarget =
+ MaybeAdjustDeltaForScrollSnappingOnWheelInput(aEvent, delta,
+ startPosition)) {
+ // If we're scroll snapping, use a smooth scroll animation to get
+ // the desired physics. Note that SmoothMsdScrollTo() will re-use an
+ // existing smooth scroll animation if there is one.
+ APZC_LOG("%p wheel scrolling to snap point %s\n", this,
+ ToString(startPosition).c_str());
+ SmoothMsdScrollTo(std::move(*snapTarget), ScrollTriggeredByScript::No);
+ break;
+ }
+
+ // Otherwise, use a wheel scroll animation, also reusing one if possible.
+ if (mState != WHEEL_SCROLL) {
+ CancelAnimation();
+ SetState(WHEEL_SCROLL);
+
+ nsPoint initialPosition =
+ CSSPoint::ToAppUnits(Metrics().GetVisualScrollOffset());
+ StartAnimation(new WheelScrollAnimation(*this, initialPosition,
+ aEvent.mDeltaType));
+ }
+ // Convert velocity from ParentLayerPoints/ms to ParentLayerPoints/s and
+ // then to appunits/second.
+
+ nsPoint deltaInAppUnits;
+ nsPoint velocity;
+ if (Metrics().GetZoom() != CSSToParentLayerScale(0)) {
+ deltaInAppUnits = CSSPoint::ToAppUnits(delta / Metrics().GetZoom());
+ velocity =
+ CSSPoint::ToAppUnits(ParentLayerPoint(mX.GetVelocity() * 1000.0f,
+ mY.GetVelocity() * 1000.0f) /
+ Metrics().GetZoom());
+ }
+
+ WheelScrollAnimation* animation = mAnimation->AsWheelScrollAnimation();
+ animation->UpdateDelta(aEvent.mTimeStamp, deltaInAppUnits,
+ nsSize(velocity.x, velocity.y));
+ break;
+ }
+ }
+
+ return nsEventStatus_eConsumeNoDefault;
+}
+
+void AsyncPanZoomController::NotifyMozMouseScrollEvent(
+ const nsString& aString) const {
+ RefPtr<GeckoContentController> controller = GetGeckoContentController();
+ if (!controller) {
+ return;
+ }
+ controller->NotifyMozMouseScrollEvent(GetScrollId(), aString);
+}
+
+nsEventStatus AsyncPanZoomController::OnPanMayBegin(
+ const PanGestureInput& aEvent) {
+ APZC_LOG_DETAIL("got a pan-maybegin in state %s\n", this,
+ ToString(mState).c_str());
+
+ StartTouch(aEvent.mLocalPanStartPoint, aEvent.mTimeStamp);
+ MOZ_ASSERT(GetCurrentPanGestureBlock());
+ GetCurrentPanGestureBlock()->GetOverscrollHandoffChain()->CancelAnimations();
+
+ return nsEventStatus_eConsumeNoDefault;
+}
+
+nsEventStatus AsyncPanZoomController::OnPanCancelled(
+ const PanGestureInput& aEvent) {
+ APZC_LOG_DETAIL("got a pan-cancelled in state %s\n", this,
+ ToString(mState).c_str());
+
+ mX.CancelGesture();
+ mY.CancelGesture();
+
+ return nsEventStatus_eConsumeNoDefault;
+}
+
+nsEventStatus AsyncPanZoomController::OnPanBegin(
+ const PanGestureInput& aEvent) {
+ APZC_LOG_DETAIL("got a pan-begin in state %s\n", this,
+ ToString(mState).c_str());
+
+ if (mState == SMOOTHMSD_SCROLL) {
+ // SMOOTHMSD_SCROLL scrolls are cancelled by pan gestures.
+ CancelAnimation();
+ }
+
+ StartTouch(aEvent.mLocalPanStartPoint, aEvent.mTimeStamp);
+
+ if (!UsingStatefulAxisLock()) {
+ SetState(PANNING);
+ } else {
+ float dx = aEvent.mPanDisplacement.x, dy = aEvent.mPanDisplacement.y;
+
+ if (dx != 0.0f || dy != 0.0f) {
+ double angle = atan2(dy, dx); // range [-pi, pi]
+ angle = fabs(angle); // range [0, pi]
+ HandlePanning(angle);
+ } else {
+ SetState(PANNING);
+ }
+ }
+
+ // Call into OnPan in order to process any delta included in this event.
+ OnPan(aEvent, FingersOnTouchpad::Yes);
+
+ return nsEventStatus_eConsumeNoDefault;
+}
+
+std::tuple<ParentLayerPoint, ScreenPoint>
+AsyncPanZoomController::GetDisplacementsForPanGesture(
+ const PanGestureInput& aEvent) {
+ // Note that there is a multiplier that applies onto the "physical" pan
+ // displacement (how much the user's fingers moved) that produces the
+ // "logical" pan displacement (how much the page should move). For some of the
+ // code below it makes more sense to use the physical displacement rather than
+ // the logical displacement, and vice-versa.
+ ScreenPoint physicalPanDisplacement = aEvent.mPanDisplacement;
+ ParentLayerPoint logicalPanDisplacement =
+ aEvent.UserMultipliedLocalPanDisplacement();
+ if (aEvent.mDeltaType == PanGestureInput::PANDELTA_PAGE) {
+ // Pan events with page units are used by Gtk, so this replicates Gtk:
+ // https://gitlab.gnome.org/GNOME/gtk/blob/c734c7e9188b56f56c3a504abee05fa40c5475ac/gtk/gtkrange.c#L3065-3073
+ CSSSize pageScrollSize;
+ CSSToParentLayerScale zoom;
+ {
+ // Grab the lock to access the frame metrics.
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ pageScrollSize = mScrollMetadata.GetPageScrollAmount() /
+ Metrics().GetDevPixelsPerCSSPixel();
+ zoom = Metrics().GetZoom();
+ }
+ // scrollUnit* is in units of "ParentLayer pixels per page proportion"...
+ auto scrollUnitWidth = std::min(std::pow(pageScrollSize.width, 2.0 / 3.0),
+ pageScrollSize.width / 2.0) *
+ zoom.scale;
+ auto scrollUnitHeight = std::min(std::pow(pageScrollSize.height, 2.0 / 3.0),
+ pageScrollSize.height / 2.0) *
+ zoom.scale;
+ // ... and pan displacements are in units of "page proportion count"
+ // here, so the products of them and scrollUnit* are in ParentLayer pixels
+ ParentLayerPoint physicalPanDisplacementPL(
+ physicalPanDisplacement.x * scrollUnitWidth,
+ physicalPanDisplacement.y * scrollUnitHeight);
+ physicalPanDisplacement = ToScreenCoordinates(physicalPanDisplacementPL,
+ aEvent.mLocalPanStartPoint);
+ logicalPanDisplacement.x *= scrollUnitWidth;
+ logicalPanDisplacement.y *= scrollUnitHeight;
+
+ // Accelerate (decelerate) any pans by raising it to a user configurable
+ // power (apz.touch_acceleration_factor_x, apz.touch_acceleration_factor_y)
+ //
+ // Confine input for pow() to greater than or equal to 0 to avoid domain
+ // errors with non-integer exponents
+ if (mX.GetVelocity() != 0) {
+ float absVelocity = std::abs(mX.GetVelocity());
+ logicalPanDisplacement.x *=
+ std::pow(absVelocity,
+ StaticPrefs::apz_touch_acceleration_factor_x()) /
+ absVelocity;
+ }
+
+ if (mY.GetVelocity() != 0) {
+ float absVelocity = std::abs(mY.GetVelocity());
+ logicalPanDisplacement.y *=
+ std::pow(absVelocity,
+ StaticPrefs::apz_touch_acceleration_factor_y()) /
+ absVelocity;
+ }
+ }
+
+ MOZ_ASSERT(GetCurrentPanGestureBlock());
+ AdjustDeltaForAllowedScrollDirections(
+ logicalPanDisplacement,
+ GetCurrentPanGestureBlock()->GetAllowedScrollDirections());
+
+ if (GetAxisLockMode() == DOMINANT_AXIS) {
+ // Given a pan gesture and both directions have a delta, implement
+ // dominant axis scrolling and only use the delta for the larger
+ // axis.
+ if (logicalPanDisplacement.y != 0 && logicalPanDisplacement.x != 0) {
+ if (fabs(logicalPanDisplacement.y) >= fabs(logicalPanDisplacement.x)) {
+ logicalPanDisplacement.x = 0;
+ physicalPanDisplacement.x = 0;
+ } else {
+ logicalPanDisplacement.y = 0;
+ physicalPanDisplacement.y = 0;
+ }
+ }
+ }
+
+ return {logicalPanDisplacement, physicalPanDisplacement};
+}
+
+nsEventStatus AsyncPanZoomController::OnPan(
+ const PanGestureInput& aEvent, FingersOnTouchpad aFingersOnTouchpad) {
+ APZC_LOG_DETAIL("got a pan-pan in state %s\n", this,
+ ToString(GetState()).c_str());
+
+ if (GetState() == SMOOTHMSD_SCROLL) {
+ if (aFingersOnTouchpad == FingersOnTouchpad::No) {
+ // When a SMOOTHMSD_SCROLL scroll is being processed on a frame, mouse
+ // wheel and trackpad momentum scroll position updates will not cancel the
+ // SMOOTHMSD_SCROLL scroll animations, enabling scripts that depend on
+ // them to be responsive without forcing the user to wait for the momentum
+ // scrolling to completely stop.
+ return nsEventStatus_eConsumeNoDefault;
+ }
+
+ // SMOOTHMSD_SCROLL scrolls are cancelled by pan gestures.
+ CancelAnimation();
+ }
+
+ if (GetState() == NOTHING) {
+ // This event block was interrupted by something else. If the user's fingers
+ // are still on on the touchpad we want to resume scrolling, otherwise we
+ // ignore the rest of the scroll gesture.
+ if (aFingersOnTouchpad == FingersOnTouchpad::No) {
+ return nsEventStatus_eConsumeNoDefault;
+ }
+ // Resume / restart the pan.
+ // PanBegin will call back into this function with mState == PANNING.
+ return OnPanBegin(aEvent);
+ }
+
+ auto [logicalPanDisplacement, physicalPanDisplacement] =
+ GetDisplacementsForPanGesture(aEvent);
+
+ {
+ // Grab the lock to protect the animation from being canceled on the updater
+ // thread.
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ MOZ_ASSERT_IF(GetState() == OVERSCROLL_ANIMATION, mAnimation);
+
+ if (GetState() == OVERSCROLL_ANIMATION && mAnimation &&
+ aFingersOnTouchpad == FingersOnTouchpad::No) {
+ // If there is an on-going overscroll animation, we tell the animation
+ // whether the displacements should be handled by the animation or not.
+ MOZ_ASSERT(mAnimation->AsOverscrollAnimation());
+ if (RefPtr<OverscrollAnimation> overscrollAnimation =
+ mAnimation->AsOverscrollAnimation()) {
+ overscrollAnimation->HandlePanMomentum(logicalPanDisplacement);
+ // And then as a result of the above call, if the animation is currently
+ // affecting on the axis, drop the displacement value on the axis so
+ // that we stop further oversrolling on the axis.
+ if (overscrollAnimation->IsManagingXAxis()) {
+ logicalPanDisplacement.x = 0;
+ physicalPanDisplacement.x = 0;
+ }
+ if (overscrollAnimation->IsManagingYAxis()) {
+ logicalPanDisplacement.y = 0;
+ physicalPanDisplacement.y = 0;
+ }
+ }
+ }
+ }
+
+ HandlePanningUpdate(physicalPanDisplacement);
+
+ MOZ_ASSERT(GetCurrentPanGestureBlock());
+ ScreenPoint panDistance(fabs(physicalPanDisplacement.x),
+ fabs(physicalPanDisplacement.y));
+ OverscrollHandoffState handoffState(
+ *GetCurrentPanGestureBlock()->GetOverscrollHandoffChain(), panDistance,
+ ScrollSource::Touchpad);
+
+ // Create fake "touch" positions that will result in the desired scroll
+ // motion. Note that the pan displacement describes the change in scroll
+ // position: positive displacement values mean that the scroll position
+ // increases. However, an increase in scroll position means that the scrolled
+ // contents are moved to the left / upwards. Since our simulated "touches"
+ // determine the motion of the scrolled contents, not of the scroll position,
+ // they need to move in the opposite direction of the pan displacement.
+ ParentLayerPoint startPoint = aEvent.mLocalPanStartPoint;
+ ParentLayerPoint endPoint =
+ aEvent.mLocalPanStartPoint - logicalPanDisplacement;
+ if (logicalPanDisplacement != ParentLayerPoint()) {
+ // Don't expect a composite to be triggered if the displacement is zero
+ RecordScrollPayload(aEvent.mTimeStamp);
+ }
+
+ const ParentLayerPoint velocity = GetVelocityVector();
+ bool consumed = CallDispatchScroll(startPoint, endPoint, handoffState);
+
+ const ParentLayerPoint visualDisplacement = ToParentLayerCoordinates(
+ handoffState.mTotalMovement, aEvent.mPanStartPoint);
+ // We need to update the axis velocity in order to get a useful display port
+ // size and position. We need to do so even if this is a momentum pan (i.e.
+ // aFingersOnTouchpad == No); in that case the "with touch" part is not
+ // really appropriate, so we may want to rethink this at some point.
+ // Note that we have to make all simulated positions relative to
+ // Axis::GetPos(), because the current position is an invented position, and
+ // because resetting the position to the mouse position (e.g.
+ // aEvent.mLocalStartPoint) would mess up velocity calculation. (This is
+ // the only caller of UpdateWithTouchAtDevicePoint() for pan events, so
+ // there is no risk of other calls resetting the position.)
+ // Also note that if there is an on-going overscroll animation in the axis,
+ // we shouldn't call UpdateWithTouchAtDevicePoint because the call changes
+ // the velocity which should be managed by the overscroll animation.
+ // Finally, note that we do this *after* CallDispatchScroll(), so that the
+ // position we use reflects the actual amount of movement that occurred
+ // (in particular, if we're in overscroll, if reflects the amount of movement
+ // *after* applying resistance). This is important because we want the axis
+ // velocity to track the visual movement speed of the page.
+ if (visualDisplacement.x != 0) {
+ mX.UpdateWithTouchAtDevicePoint(mX.GetPos() - visualDisplacement.x,
+ aEvent.mTimeStamp);
+ }
+ if (visualDisplacement.y != 0) {
+ mY.UpdateWithTouchAtDevicePoint(mY.GetPos() - visualDisplacement.y,
+ aEvent.mTimeStamp);
+ }
+
+ if (aFingersOnTouchpad == FingersOnTouchpad::No) {
+ if (IsOverscrolled() && GetState() != OVERSCROLL_ANIMATION) {
+ StartOverscrollAnimation(velocity, GetOverscrollSideBits());
+ } else if (!consumed) {
+ // If there is unconsumed scroll and we're in the momentum part of the
+ // pan gesture, terminate the momentum scroll. This prevents momentum
+ // scroll events from unexpectedly causing scrolling later if somehow
+ // the APZC becomes scrollable again in this direction (e.g. if the user
+ // uses some other input method to scroll in the opposite direction).
+ SetState(NOTHING);
+ }
+ }
+
+ return nsEventStatus_eConsumeNoDefault;
+}
+
+nsEventStatus AsyncPanZoomController::OnPanEnd(const PanGestureInput& aEvent) {
+ APZC_LOG_DETAIL("got a pan-end in state %s\n", this,
+ ToString(mState).c_str());
+
+ // This can happen if the OS sends a second pan-end event after the first one
+ // has already started an overscroll animation or entered a fling state.
+ // This has been observed on some Wayland versions.
+ PanZoomState currentState = GetState();
+ if (currentState == OVERSCROLL_ANIMATION || currentState == NOTHING ||
+ currentState == FLING) {
+ return nsEventStatus_eIgnore;
+ }
+
+ if (aEvent.mPanDisplacement != ScreenPoint{}) {
+ // Call into OnPan in order to process the delta included in this event.
+ OnPan(aEvent, FingersOnTouchpad::Yes);
+ }
+
+ // Do not unlock the axis lock at the end of a pan gesture. The axis lock
+ // should extend into the momentum scroll.
+ EndTouch(aEvent.mTimeStamp, Axis::ClearAxisLock::No);
+
+ // Use HandleEndOfPan for fling on platforms that don't
+ // emit momentum events (Gtk).
+ if (aEvent.mSimulateMomentum) {
+ return HandleEndOfPan();
+ }
+
+ MOZ_ASSERT(GetCurrentPanGestureBlock());
+ RefPtr<const OverscrollHandoffChain> overscrollHandoffChain =
+ GetCurrentPanGestureBlock()->GetOverscrollHandoffChain();
+
+ // Call SnapBackOverscrolledApzcForMomentum regardless whether this APZC is
+ // overscrolled or not since overscroll animations for ancestor APZCs in this
+ // overscroll handoff chain might have been cancelled by the current pan
+ // gesture block.
+ overscrollHandoffChain->SnapBackOverscrolledApzcForMomentum(
+ this, GetVelocityVector());
+ // If this APZC is overscrolled, the above SnapBackOverscrolledApzcForMomentum
+ // triggers an overscroll animation. When we're finished with the overscroll
+ // animation, the state will be reset and a TransformEnd will be sent to the
+ // main thread.
+ currentState = GetState();
+ if (currentState != OVERSCROLL_ANIMATION) {
+ // Do not send a state change notification to the content controller here.
+ // Instead queue a delayed task to dispatch the notification if no
+ // momentum pan or scroll snap follows the pan-end.
+ RefPtr<GeckoContentController> controller = GetGeckoContentController();
+ if (controller) {
+ SetDelayedTransformEnd(true);
+ controller->PostDelayedTask(
+ NewRunnableMethod<PanZoomState>(
+ "layers::AsyncPanZoomController::"
+ "DoDelayedTransformEndNotification",
+ this, &AsyncPanZoomController::DoDelayedTransformEndNotification,
+ currentState),
+ StaticPrefs::apz_scrollend_event_content_delay_ms());
+ SetStateNoContentControllerDispatch(NOTHING);
+ } else {
+ SetState(NOTHING);
+ }
+ }
+
+ // Drop any velocity on axes where we don't have room to scroll anyways
+ // (in this APZC, or an APZC further in the handoff chain).
+ // This ensures that we don't enlarge the display port unnecessarily.
+ {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ if (!overscrollHandoffChain->CanScrollInDirection(
+ this, ScrollDirection::eHorizontal)) {
+ mX.SetVelocity(0);
+ }
+ if (!overscrollHandoffChain->CanScrollInDirection(
+ this, ScrollDirection::eVertical)) {
+ mY.SetVelocity(0);
+ }
+ }
+
+ RequestContentRepaint();
+ ScrollSnapToDestination();
+
+ return nsEventStatus_eConsumeNoDefault;
+}
+
+nsEventStatus AsyncPanZoomController::OnPanMomentumStart(
+ const PanGestureInput& aEvent) {
+ APZC_LOG_DETAIL("got a pan-momentumstart in state %s\n", this,
+ ToString(mState).c_str());
+
+ if (mState == SMOOTHMSD_SCROLL || mState == OVERSCROLL_ANIMATION) {
+ return nsEventStatus_eConsumeNoDefault;
+ }
+
+ if (IsDelayedTransformEndSet()) {
+ // Do not send another TransformBegin notification if we have not
+ // delivered a corresponding TransformEnd. Also ensure that any
+ // queued transform-end due to a pan-end is not sent. Instead rely
+ // on the transform-end sent due to the momentum pan.
+ SetDelayedTransformEnd(false);
+ SetStateNoContentControllerDispatch(PAN_MOMENTUM);
+ } else {
+ SetState(PAN_MOMENTUM);
+ }
+
+ // Call into OnPan in order to process any delta included in this event.
+ OnPan(aEvent, FingersOnTouchpad::No);
+
+ return nsEventStatus_eConsumeNoDefault;
+}
+
+nsEventStatus AsyncPanZoomController::OnPanMomentumEnd(
+ const PanGestureInput& aEvent) {
+ APZC_LOG_DETAIL("got a pan-momentumend in state %s\n", this,
+ ToString(mState).c_str());
+
+ if (mState == OVERSCROLL_ANIMATION) {
+ return nsEventStatus_eConsumeNoDefault;
+ }
+
+ // Call into OnPan in order to process any delta included in this event.
+ OnPan(aEvent, FingersOnTouchpad::No);
+
+ // We need to reset the velocity to zero. We don't really have a "touch"
+ // here because the touch has already ended long before the momentum
+ // animation started, but I guess it doesn't really matter for now.
+ mX.CancelGesture();
+ mY.CancelGesture();
+ SetState(NOTHING);
+
+ RequestContentRepaint();
+
+ return nsEventStatus_eConsumeNoDefault;
+}
+
+nsEventStatus AsyncPanZoomController::OnPanInterrupted(
+ const PanGestureInput& aEvent) {
+ APZC_LOG_DETAIL("got a pan-interrupted in state %s\n", this,
+ ToString(mState).c_str());
+
+ CancelAnimation();
+
+ return nsEventStatus_eIgnore;
+}
+
+nsEventStatus AsyncPanZoomController::OnLongPress(
+ const TapGestureInput& aEvent) {
+ APZC_LOG_DETAIL("got a long-press in state %s\n", this,
+ ToString(mState).c_str());
+ RefPtr<GeckoContentController> controller = GetGeckoContentController();
+ if (controller) {
+ if (Maybe<LayoutDevicePoint> geckoScreenPoint =
+ ConvertToGecko(aEvent.mPoint)) {
+ TouchBlockState* touch = GetCurrentTouchBlock();
+ if (!touch) {
+ APZC_LOG(
+ "%p dropping long-press because some non-touch block interrupted "
+ "it\n",
+ this);
+ return nsEventStatus_eIgnore;
+ }
+ if (touch->IsDuringFastFling()) {
+ APZC_LOG("%p dropping long-press because of fast fling\n", this);
+ return nsEventStatus_eIgnore;
+ }
+ uint64_t blockId = GetInputQueue()->InjectNewTouchBlock(this);
+ controller->HandleTap(TapType::eLongTap, *geckoScreenPoint,
+ aEvent.modifiers, GetGuid(), blockId);
+ return nsEventStatus_eConsumeNoDefault;
+ }
+ }
+ return nsEventStatus_eIgnore;
+}
+
+nsEventStatus AsyncPanZoomController::OnLongPressUp(
+ const TapGestureInput& aEvent) {
+ APZC_LOG_DETAIL("got a long-tap-up in state %s\n", this,
+ ToString(mState).c_str());
+ return GenerateSingleTap(TapType::eLongTapUp, aEvent.mPoint,
+ aEvent.modifiers);
+}
+
+nsEventStatus AsyncPanZoomController::GenerateSingleTap(
+ TapType aType, const ScreenIntPoint& aPoint,
+ mozilla::Modifiers aModifiers) {
+ RefPtr<GeckoContentController> controller = GetGeckoContentController();
+ if (controller) {
+ if (Maybe<LayoutDevicePoint> geckoScreenPoint = ConvertToGecko(aPoint)) {
+ TouchBlockState* touch = GetCurrentTouchBlock();
+ // |touch| may be null in the case where this function is
+ // invoked by GestureEventListener on a timeout. In that case we already
+ // verified that the single tap is allowed so we let it through.
+ // XXX there is a bug here that in such a case the touch block that
+ // generated this tap will not get its mSingleTapOccurred flag set.
+ // See https://bugzilla.mozilla.org/show_bug.cgi?id=1256344#c6
+ if (touch) {
+ if (touch->IsDuringFastFling()) {
+ APZC_LOG(
+ "%p dropping single-tap because it was during a fast-fling\n",
+ this);
+ return nsEventStatus_eIgnore;
+ }
+ touch->SetSingleTapOccurred();
+ }
+ // Because this may be being running as part of
+ // APZCTreeManager::ReceiveInputEvent, calling controller->HandleTap
+ // directly might mean that content receives the single tap message before
+ // the corresponding touch-up. To avoid that we schedule the singletap
+ // message to run on the next spin of the event loop. See bug 965381 for
+ // the issue this was causing.
+ APZC_LOG("posting runnable for HandleTap from GenerateSingleTap");
+ RefPtr<Runnable> runnable =
+ NewRunnableMethod<TapType, LayoutDevicePoint, mozilla::Modifiers,
+ ScrollableLayerGuid, uint64_t>(
+ "layers::GeckoContentController::HandleTap", controller,
+ &GeckoContentController::HandleTap, aType, *geckoScreenPoint,
+ aModifiers, GetGuid(), touch ? touch->GetBlockId() : 0);
+
+ controller->PostDelayedTask(runnable.forget(), 0);
+ return nsEventStatus_eConsumeNoDefault;
+ }
+ }
+ return nsEventStatus_eIgnore;
+}
+
+void AsyncPanZoomController::OnTouchEndOrCancel() {
+ if (RefPtr<GeckoContentController> controller = GetGeckoContentController()) {
+ MOZ_ASSERT(GetCurrentTouchBlock());
+ controller->NotifyAPZStateChange(
+ GetGuid(), APZStateChange::eEndTouch,
+ GetCurrentTouchBlock()->SingleTapOccurred(),
+ Some(GetCurrentTouchBlock()->GetBlockId()));
+ }
+}
+
+nsEventStatus AsyncPanZoomController::OnSingleTapUp(
+ const TapGestureInput& aEvent) {
+ APZC_LOG_DETAIL("got a single-tap-up in state %s\n", this,
+ ToString(mState).c_str());
+ // If mZoomConstraints.mAllowDoubleTapZoom is true we wait for a call to
+ // OnSingleTapConfirmed before sending event to content
+ MOZ_ASSERT(GetCurrentTouchBlock());
+ if (!(ZoomConstraintsAllowDoubleTapZoom() &&
+ GetCurrentTouchBlock()->TouchActionAllowsDoubleTapZoom())) {
+ return GenerateSingleTap(TapType::eSingleTap, aEvent.mPoint,
+ aEvent.modifiers);
+ }
+ return nsEventStatus_eIgnore;
+}
+
+nsEventStatus AsyncPanZoomController::OnSingleTapConfirmed(
+ const TapGestureInput& aEvent) {
+ APZC_LOG_DETAIL("got a single-tap-confirmed in state %s\n", this,
+ ToString(mState).c_str());
+ return GenerateSingleTap(TapType::eSingleTap, aEvent.mPoint,
+ aEvent.modifiers);
+}
+
+nsEventStatus AsyncPanZoomController::OnDoubleTap(
+ const TapGestureInput& aEvent) {
+ APZC_LOG_DETAIL("got a double-tap in state %s\n", this,
+ ToString(mState).c_str());
+ RefPtr<GeckoContentController> controller = GetGeckoContentController();
+ if (controller) {
+ if (ZoomConstraintsAllowDoubleTapZoom() &&
+ (!GetCurrentTouchBlock() ||
+ GetCurrentTouchBlock()->TouchActionAllowsDoubleTapZoom())) {
+ if (Maybe<LayoutDevicePoint> geckoScreenPoint =
+ ConvertToGecko(aEvent.mPoint)) {
+ controller->HandleTap(
+ TapType::eDoubleTap, *geckoScreenPoint, aEvent.modifiers, GetGuid(),
+ GetCurrentTouchBlock() ? GetCurrentTouchBlock()->GetBlockId() : 0);
+ }
+ }
+ return nsEventStatus_eConsumeNoDefault;
+ }
+ return nsEventStatus_eIgnore;
+}
+
+nsEventStatus AsyncPanZoomController::OnSecondTap(
+ const TapGestureInput& aEvent) {
+ APZC_LOG_DETAIL("got a second-tap in state %s\n", this,
+ ToString(mState).c_str());
+ return GenerateSingleTap(TapType::eSecondTap, aEvent.mPoint,
+ aEvent.modifiers);
+}
+
+nsEventStatus AsyncPanZoomController::OnCancelTap(
+ const TapGestureInput& aEvent) {
+ APZC_LOG_DETAIL("got a cancel-tap in state %s\n", this,
+ ToString(mState).c_str());
+ // XXX: Implement this.
+ return nsEventStatus_eIgnore;
+}
+
+ScreenToParentLayerMatrix4x4 AsyncPanZoomController::GetTransformToThis()
+ const {
+ if (APZCTreeManager* treeManagerLocal = GetApzcTreeManager()) {
+ return treeManagerLocal->GetScreenToApzcTransform(this);
+ }
+ return ScreenToParentLayerMatrix4x4();
+}
+
+ScreenPoint AsyncPanZoomController::ToScreenCoordinates(
+ const ParentLayerPoint& aVector, const ParentLayerPoint& aAnchor) const {
+ return TransformVector(GetTransformToThis().Inverse(), aVector, aAnchor);
+}
+
+// TODO: figure out a good way to check the w-coordinate is positive and return
+// the result
+ParentLayerPoint AsyncPanZoomController::ToParentLayerCoordinates(
+ const ScreenPoint& aVector, const ScreenPoint& aAnchor) const {
+ return TransformVector(GetTransformToThis(), aVector, aAnchor);
+}
+
+ParentLayerPoint AsyncPanZoomController::ToParentLayerCoordinates(
+ const ScreenPoint& aVector, const ExternalPoint& aAnchor) const {
+ return ToParentLayerCoordinates(
+ aVector,
+ ViewAs<ScreenPixel>(aAnchor, PixelCastJustification::ExternalIsScreen));
+}
+
+ExternalPoint AsyncPanZoomController::ToExternalPoint(
+ const ExternalPoint& aScreenOffset, const ScreenPoint& aScreenPoint) {
+ return aScreenOffset +
+ ViewAs<ExternalPixel>(aScreenPoint,
+ PixelCastJustification::ExternalIsScreen);
+}
+
+ScreenPoint AsyncPanZoomController::PanVector(const ExternalPoint& aPos) const {
+ return ScreenPoint(fabs(aPos.x - mStartTouch.x),
+ fabs(aPos.y - mStartTouch.y));
+}
+
+bool AsyncPanZoomController::Contains(const ScreenIntPoint& aPoint) const {
+ ScreenToParentLayerMatrix4x4 transformToThis = GetTransformToThis();
+ Maybe<ParentLayerIntPoint> point = UntransformBy(transformToThis, aPoint);
+ if (!point) {
+ return false;
+ }
+
+ ParentLayerIntRect cb;
+ {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ GetFrameMetrics().GetCompositionBounds().ToIntRect(&cb);
+ }
+ return cb.Contains(*point);
+}
+
+bool AsyncPanZoomController::IsInOverscrollGutter(
+ const ScreenPoint& aHitTestPoint) const {
+ if (!IsPhysicallyOverscrolled()) {
+ return false;
+ }
+
+ Maybe<ParentLayerPoint> apzcPoint =
+ UntransformBy(GetTransformToThis(), aHitTestPoint);
+ if (!apzcPoint) return false;
+ return IsInOverscrollGutter(*apzcPoint);
+}
+
+bool AsyncPanZoomController::IsInOverscrollGutter(
+ const ParentLayerPoint& aHitTestPoint) const {
+ ParentLayerRect compositionBounds;
+ {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ compositionBounds = GetFrameMetrics().GetCompositionBounds();
+ }
+ if (!compositionBounds.Contains(aHitTestPoint)) {
+ // Point is outside of scrollable element's bounds altogether.
+ return false;
+ }
+ auto overscrollTransform = GetOverscrollTransform(eForHitTesting);
+ ParentLayerPoint overscrollUntransformed =
+ overscrollTransform.Inverse().TransformPoint(aHitTestPoint);
+
+ if (compositionBounds.Contains(overscrollUntransformed)) {
+ // Point is over scrollable content.
+ return false;
+ }
+
+ // Point is in gutter.
+ return true;
+}
+
+bool AsyncPanZoomController::IsOverscrolled() const {
+ return mOverscrollEffect->IsOverscrolled();
+}
+
+bool AsyncPanZoomController::IsPhysicallyOverscrolled() const {
+ // As an optimization, avoid calling Apply/UnapplyAsyncTestAttributes
+ // unless we're in a test environment where we need it.
+ if (StaticPrefs::apz_overscroll_test_async_scroll_offset_enabled()) {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ AutoApplyAsyncTestAttributes testAttributeApplier(this, lock);
+ return mX.IsOverscrolled() || mY.IsOverscrolled();
+ }
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ return mX.IsOverscrolled() || mY.IsOverscrolled();
+}
+
+bool AsyncPanZoomController::IsInInvalidOverscroll() const {
+ return mX.IsInInvalidOverscroll() || mY.IsInInvalidOverscroll();
+}
+
+ParentLayerPoint AsyncPanZoomController::PanStart() const {
+ return ParentLayerPoint(mX.PanStart(), mY.PanStart());
+}
+
+const ParentLayerPoint AsyncPanZoomController::GetVelocityVector() const {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ return ParentLayerPoint(mX.GetVelocity(), mY.GetVelocity());
+}
+
+void AsyncPanZoomController::SetVelocityVector(
+ const ParentLayerPoint& aVelocityVector) {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ mX.SetVelocity(aVelocityVector.x);
+ mY.SetVelocity(aVelocityVector.y);
+}
+
+void AsyncPanZoomController::HandlePanningWithTouchAction(double aAngle) {
+ // Handling of cross sliding will need to be added in this method after
+ // touch-action released enabled by default.
+ MOZ_ASSERT(GetCurrentTouchBlock());
+ RefPtr<const OverscrollHandoffChain> overscrollHandoffChain =
+ GetCurrentInputBlock()->GetOverscrollHandoffChain();
+ bool canScrollHorizontal =
+ !mX.IsAxisLocked() && overscrollHandoffChain->CanScrollInDirection(
+ this, ScrollDirection::eHorizontal);
+ bool canScrollVertical =
+ !mY.IsAxisLocked() && overscrollHandoffChain->CanScrollInDirection(
+ this, ScrollDirection::eVertical);
+ if (GetCurrentTouchBlock()->TouchActionAllowsPanningXY()) {
+ if (canScrollHorizontal && canScrollVertical) {
+ if (apz::IsCloseToHorizontal(aAngle,
+ StaticPrefs::apz_axis_lock_lock_angle())) {
+ mY.SetAxisLocked(true);
+ SetState(PANNING_LOCKED_X);
+ } else if (apz::IsCloseToVertical(
+ aAngle, StaticPrefs::apz_axis_lock_lock_angle())) {
+ mX.SetAxisLocked(true);
+ SetState(PANNING_LOCKED_Y);
+ } else {
+ SetState(PANNING);
+ }
+ } else if (canScrollHorizontal || canScrollVertical) {
+ SetState(PANNING);
+ } else {
+ SetState(NOTHING);
+ }
+ } else if (GetCurrentTouchBlock()->TouchActionAllowsPanningX()) {
+ // Using bigger angle for panning to keep behavior consistent
+ // with IE.
+ if (apz::IsCloseToHorizontal(
+ aAngle, StaticPrefs::apz_axis_lock_direct_pan_angle())) {
+ mY.SetAxisLocked(true);
+ SetState(PANNING_LOCKED_X);
+ mPanDirRestricted = true;
+ } else {
+ // Don't treat these touches as pan/zoom movements since 'touch-action'
+ // value requires it.
+ SetState(NOTHING);
+ }
+ } else if (GetCurrentTouchBlock()->TouchActionAllowsPanningY()) {
+ if (apz::IsCloseToVertical(aAngle,
+ StaticPrefs::apz_axis_lock_direct_pan_angle())) {
+ mX.SetAxisLocked(true);
+ SetState(PANNING_LOCKED_Y);
+ mPanDirRestricted = true;
+ } else {
+ SetState(NOTHING);
+ }
+ } else {
+ SetState(NOTHING);
+ }
+ if (!IsInPanningState()) {
+ // If we didn't enter a panning state because touch-action disallowed it,
+ // make sure to clear any leftover velocity from the pre-threshold
+ // touchmoves.
+ mX.SetVelocity(0);
+ mY.SetVelocity(0);
+ }
+}
+
+void AsyncPanZoomController::HandlePanning(double aAngle) {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ MOZ_ASSERT(GetCurrentInputBlock());
+ RefPtr<const OverscrollHandoffChain> overscrollHandoffChain =
+ GetCurrentInputBlock()->GetOverscrollHandoffChain();
+ bool canScrollHorizontal =
+ !mX.IsAxisLocked() && overscrollHandoffChain->CanScrollInDirection(
+ this, ScrollDirection::eHorizontal);
+ bool canScrollVertical =
+ !mY.IsAxisLocked() && overscrollHandoffChain->CanScrollInDirection(
+ this, ScrollDirection::eVertical);
+
+ MOZ_ASSERT(UsingStatefulAxisLock());
+
+ if (!canScrollHorizontal || !canScrollVertical) {
+ SetState(PANNING);
+ } else if (apz::IsCloseToHorizontal(
+ aAngle, StaticPrefs::apz_axis_lock_lock_angle())) {
+ mY.SetAxisLocked(true);
+ if (canScrollHorizontal) {
+ SetState(PANNING_LOCKED_X);
+ }
+ } else if (apz::IsCloseToVertical(aAngle,
+ StaticPrefs::apz_axis_lock_lock_angle())) {
+ mX.SetAxisLocked(true);
+ if (canScrollVertical) {
+ SetState(PANNING_LOCKED_Y);
+ }
+ } else {
+ SetState(PANNING);
+ }
+}
+
+void AsyncPanZoomController::HandlePanningUpdate(
+ const ScreenPoint& aPanDistance) {
+ // If we're axis-locked, check if the user is trying to break the lock
+ if (GetAxisLockMode() == STICKY && !mPanDirRestricted) {
+ ParentLayerPoint vector =
+ ToParentLayerCoordinates(aPanDistance, mStartTouch);
+
+ float angle = atan2f(vector.y, vector.x); // range [-pi, pi]
+ angle = fabsf(angle); // range [0, pi]
+
+ float breakThreshold =
+ StaticPrefs::apz_axis_lock_breakout_threshold() * GetDPI();
+
+ if (fabs(aPanDistance.x) > breakThreshold ||
+ fabs(aPanDistance.y) > breakThreshold) {
+ switch (mState) {
+ case PANNING_LOCKED_X:
+ if (!apz::IsCloseToHorizontal(
+ angle, StaticPrefs::apz_axis_lock_breakout_angle())) {
+ mY.SetAxisLocked(false);
+ // If we are within the lock angle from the Y axis, lock
+ // onto the Y axis.
+ if (apz::IsCloseToVertical(
+ angle, StaticPrefs::apz_axis_lock_lock_angle())) {
+ mX.SetAxisLocked(true);
+ SetState(PANNING_LOCKED_Y);
+ } else {
+ SetState(PANNING);
+ }
+ }
+ break;
+
+ case PANNING_LOCKED_Y:
+ if (!apz::IsCloseToVertical(
+ angle, StaticPrefs::apz_axis_lock_breakout_angle())) {
+ mX.SetAxisLocked(false);
+ // If we are within the lock angle from the X axis, lock
+ // onto the X axis.
+ if (apz::IsCloseToHorizontal(
+ angle, StaticPrefs::apz_axis_lock_lock_angle())) {
+ mY.SetAxisLocked(true);
+ SetState(PANNING_LOCKED_X);
+ } else {
+ SetState(PANNING);
+ }
+ }
+ break;
+
+ case PANNING:
+ HandlePanning(angle);
+ break;
+
+ default:
+ break;
+ }
+ }
+ }
+}
+
+void AsyncPanZoomController::HandlePinchLocking(
+ const PinchGestureInput& aEvent) {
+ // Focus change and span distance calculated from an event buffer
+ // Used to handle pinch locking irrespective of touch screen sensitivity
+ // Note: both values fall back to the same value as
+ // their un-buffered counterparts if there is only one (the latest)
+ // event in the buffer. ie: when the touch screen is dispatching
+ // events slower than the lifetime of the buffer
+ ParentLayerCoord bufferedSpanDistance;
+ ParentLayerPoint focusPoint, bufferedFocusChange;
+ {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+
+ focusPoint = mPinchEventBuffer.back().mLocalFocusPoint -
+ Metrics().GetCompositionBounds().TopLeft();
+ ParentLayerPoint bufferedLastZoomFocus =
+ (mPinchEventBuffer.size() > 1)
+ ? mPinchEventBuffer.front().mLocalFocusPoint -
+ Metrics().GetCompositionBounds().TopLeft()
+ : mLastZoomFocus;
+
+ bufferedFocusChange = bufferedLastZoomFocus - focusPoint;
+ bufferedSpanDistance = fabsf(mPinchEventBuffer.front().mPreviousSpan -
+ mPinchEventBuffer.back().mCurrentSpan);
+ }
+
+ // Convert to screen coordinates
+ ScreenCoord spanDistance =
+ ToScreenCoordinates(ParentLayerPoint(0, bufferedSpanDistance), focusPoint)
+ .Length();
+ ScreenPoint focusChange =
+ ToScreenCoordinates(bufferedFocusChange, focusPoint);
+
+ if (mPinchLocked) {
+ if (GetPinchLockMode() == PINCH_STICKY) {
+ ScreenCoord spanBreakoutThreshold =
+ StaticPrefs::apz_pinch_lock_span_breakout_threshold() * GetDPI();
+ mPinchLocked = !(spanDistance > spanBreakoutThreshold);
+ }
+ } else {
+ if (GetPinchLockMode() != PINCH_FREE) {
+ ScreenCoord spanLockThreshold =
+ StaticPrefs::apz_pinch_lock_span_lock_threshold() * GetDPI();
+ ScreenCoord scrollLockThreshold =
+ StaticPrefs::apz_pinch_lock_scroll_lock_threshold() * GetDPI();
+
+ if (spanDistance < spanLockThreshold &&
+ focusChange.Length() > scrollLockThreshold) {
+ mPinchLocked = true;
+
+ // We are transitioning to a two-finger pan that could trigger
+ // a fling at its end, so start tracking velocity.
+ StartTouch(aEvent.mLocalFocusPoint, aEvent.mTimeStamp);
+ }
+ }
+ }
+}
+
+nsEventStatus AsyncPanZoomController::StartPanning(
+ const ExternalPoint& aStartPoint, const TimeStamp& aEventTime) {
+ ParentLayerPoint vector =
+ ToParentLayerCoordinates(PanVector(aStartPoint), mStartTouch);
+ double angle = atan2(vector.y, vector.x); // range [-pi, pi]
+ angle = fabs(angle); // range [0, pi]
+
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ HandlePanningWithTouchAction(angle);
+
+ if (IsInPanningState()) {
+ mTouchStartRestingTimeBeforePan = aEventTime - mTouchStartTime;
+ mMinimumVelocityDuringPan = Nothing();
+
+ if (RefPtr<GeckoContentController> controller =
+ GetGeckoContentController()) {
+ controller->NotifyAPZStateChange(GetGuid(),
+ APZStateChange::eStartPanning);
+ }
+ return nsEventStatus_eConsumeNoDefault;
+ }
+ // Don't consume an event that didn't trigger a panning.
+ return nsEventStatus_eIgnore;
+}
+
+void AsyncPanZoomController::UpdateWithTouchAtDevicePoint(
+ const MultiTouchInput& aEvent) {
+ const SingleTouchData& touchData = aEvent.mTouches[0];
+ // Take historical touch data into account in order to improve the accuracy
+ // of the velocity estimate. On many Android devices, the touch screen samples
+ // at a higher rate than vsync (e.g. 100Hz vs 60Hz), and the historical data
+ // lets us take advantage of those high-rate samples.
+ for (const auto& historicalData : touchData.mHistoricalData) {
+ ParentLayerPoint historicalPoint = historicalData.mLocalScreenPoint;
+ mX.UpdateWithTouchAtDevicePoint(historicalPoint.x,
+ historicalData.mTimeStamp);
+ mY.UpdateWithTouchAtDevicePoint(historicalPoint.y,
+ historicalData.mTimeStamp);
+ }
+ ParentLayerPoint point = touchData.mLocalScreenPoint;
+ mX.UpdateWithTouchAtDevicePoint(point.x, aEvent.mTimeStamp);
+ mY.UpdateWithTouchAtDevicePoint(point.y, aEvent.mTimeStamp);
+}
+
+Maybe<CompositionPayload> AsyncPanZoomController::NotifyScrollSampling() {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ return mSampledState.front().TakeScrollPayload();
+}
+
+bool AsyncPanZoomController::AttemptScroll(
+ ParentLayerPoint& aStartPoint, ParentLayerPoint& aEndPoint,
+ OverscrollHandoffState& aOverscrollHandoffState) {
+ // "start - end" rather than "end - start" because e.g. moving your finger
+ // down (*positive* direction along y axis) causes the vertical scroll offset
+ // to *decrease* as the page follows your finger.
+ ParentLayerPoint displacement = aStartPoint - aEndPoint;
+
+ ParentLayerPoint overscroll; // will be used outside monitor block
+
+ // If the direction of panning is reversed within the same input block,
+ // a later event in the block could potentially scroll an APZC earlier
+ // in the handoff chain, than an earlier event in the block (because
+ // the earlier APZC was scrolled to its extent in the original direction).
+ // We want to disallow this.
+ bool scrollThisApzc = false;
+ if (InputBlockState* block = GetCurrentInputBlock()) {
+ scrollThisApzc =
+ !block->GetScrolledApzc() || block->IsDownchainOfScrolledApzc(this);
+ }
+
+ ParentLayerPoint adjustedDisplacement;
+ if (scrollThisApzc) {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ bool respectDisregardedDirections =
+ ScrollSourceRespectsDisregardedDirections(
+ aOverscrollHandoffState.mScrollSource);
+ bool forcesVerticalOverscroll = respectDisregardedDirections &&
+ mScrollMetadata.GetDisregardedDirection() ==
+ Some(ScrollDirection::eVertical);
+ bool forcesHorizontalOverscroll =
+ respectDisregardedDirections &&
+ mScrollMetadata.GetDisregardedDirection() ==
+ Some(ScrollDirection::eHorizontal);
+
+ bool yChanged =
+ mY.AdjustDisplacement(displacement.y, adjustedDisplacement.y,
+ overscroll.y, forcesVerticalOverscroll);
+ bool xChanged =
+ mX.AdjustDisplacement(displacement.x, adjustedDisplacement.x,
+ overscroll.x, forcesHorizontalOverscroll);
+ if (xChanged || yChanged) {
+ ScheduleComposite();
+ }
+
+ if (!IsZero(adjustedDisplacement) &&
+ Metrics().GetZoom() != CSSToParentLayerScale(0)) {
+ ScrollBy(adjustedDisplacement / Metrics().GetZoom());
+ if (InputBlockState* block = GetCurrentInputBlock()) {
+ bool displacementIsUserVisible = true;
+
+ { // Release the APZC lock before calling ToScreenCoordinates which
+ // acquires the APZ tree lock. Note that this just unlocks the mutex
+ // once, so if we're locking it multiple times on the callstack then
+ // this will be insufficient.
+ RecursiveMutexAutoUnlock unlock(mRecursiveMutex);
+
+ ScreenIntPoint screenDisplacement = RoundedToInt(
+ ToScreenCoordinates(adjustedDisplacement, aStartPoint));
+ // If the displacement we just applied rounds to zero in screen space,
+ // then it's probably not going to be visible to the user. In that
+ // case let's not mark this APZC as scrolled, so that even if the
+ // immediate handoff pref is disabled, we'll allow doing the handoff
+ // to the next APZC.
+ if (screenDisplacement == ScreenIntPoint()) {
+ displacementIsUserVisible = false;
+ }
+ }
+ if (displacementIsUserVisible) {
+ block->SetScrolledApzc(this);
+ }
+ }
+ // Note that in the case of instant scrolling, the last snap target ids
+ // will be set after AttemptScroll call so that we can clobber them
+ // unconditionally here.
+ mLastSnapTargetIds = ScrollSnapTargetIds{};
+ ScheduleCompositeAndMaybeRepaint();
+ }
+
+ // Adjust the start point to reflect the consumed portion of the scroll.
+ aStartPoint = aEndPoint + overscroll;
+ } else {
+ overscroll = displacement;
+ }
+
+ // Accumulate the amount of actual scrolling that occurred into the handoff
+ // state. Note that ToScreenCoordinates() needs to be called outside the
+ // mutex.
+ if (!IsZero(adjustedDisplacement)) {
+ aOverscrollHandoffState.mTotalMovement +=
+ ToScreenCoordinates(adjustedDisplacement, aEndPoint);
+ }
+
+ // If we consumed the entire displacement as a normal scroll, great.
+ if (IsZero(overscroll)) {
+ return true;
+ }
+
+ if (AllowScrollHandoffInCurrentBlock()) {
+ // If there is overscroll, first try to hand it off to an APZC later
+ // in the handoff chain to consume (either as a normal scroll or as
+ // overscroll).
+ // Note: "+ overscroll" rather than "- overscroll" because "overscroll"
+ // is what's left of "displacement", and "displacement" is "start - end".
+ ++aOverscrollHandoffState.mChainIndex;
+ bool consumed =
+ CallDispatchScroll(aStartPoint, aEndPoint, aOverscrollHandoffState);
+ if (consumed) {
+ return true;
+ }
+
+ overscroll = aStartPoint - aEndPoint;
+ MOZ_ASSERT(!IsZero(overscroll));
+ }
+
+ // If there is no APZC later in the handoff chain that accepted the
+ // overscroll, try to accept it ourselves. We only accept it if we
+ // are pannable.
+ if (ScrollSourceAllowsOverscroll(aOverscrollHandoffState.mScrollSource)) {
+ APZC_LOG("%p taking overscroll during panning\n", this);
+
+ ParentLayerPoint prevVisualOverscroll = GetOverscrollAmount();
+
+ OverscrollForPanning(overscroll, aOverscrollHandoffState.mPanDistance);
+
+ // Accumulate the amount of change to the overscroll that occurred into the
+ // handoff state. Note that the input amount, |overscroll|, is turned into
+ // some smaller visual overscroll amount (queried via GetOverscrollAmount())
+ // by applying resistance (Axis::ApplyResistance()), and it's the latter we
+ // want to count towards OverscrollHandoffState::mTotalMovement.
+ ParentLayerPoint visualOverscrollChange =
+ GetOverscrollAmount() - prevVisualOverscroll;
+ if (!IsZero(visualOverscrollChange)) {
+ aOverscrollHandoffState.mTotalMovement +=
+ ToScreenCoordinates(visualOverscrollChange, aEndPoint);
+ }
+ }
+
+ aStartPoint = aEndPoint + overscroll;
+
+ return IsZero(overscroll);
+}
+
+void AsyncPanZoomController::OverscrollForPanning(
+ ParentLayerPoint& aOverscroll, const ScreenPoint& aPanDistance) {
+ // Only allow entering overscroll along an axis if the pan distance along
+ // that axis is greater than the pan distance along the other axis by a
+ // configurable factor. If we are already overscrolled, don't check this.
+ if (!IsOverscrolled()) {
+ if (aPanDistance.x <
+ StaticPrefs::apz_overscroll_min_pan_distance_ratio() * aPanDistance.y) {
+ aOverscroll.x = 0;
+ }
+ if (aPanDistance.y <
+ StaticPrefs::apz_overscroll_min_pan_distance_ratio() * aPanDistance.x) {
+ aOverscroll.y = 0;
+ }
+ }
+
+ OverscrollBy(aOverscroll);
+}
+
+ScrollDirections AsyncPanZoomController::GetOverscrollableDirections() const {
+ ScrollDirections result;
+
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+
+ // If the target has the disregarded direction, it means it's single line
+ // text control, thus we don't want to overscroll in both directions.
+ if (mScrollMetadata.GetDisregardedDirection()) {
+ return result;
+ }
+
+ if (mX.CanScroll() && mX.OverscrollBehaviorAllowsOverscrollEffect()) {
+ result += ScrollDirection::eHorizontal;
+ }
+
+ if (mY.CanScroll() && mY.OverscrollBehaviorAllowsOverscrollEffect()) {
+ result += ScrollDirection::eVertical;
+ }
+
+ return result;
+}
+
+void AsyncPanZoomController::OverscrollBy(ParentLayerPoint& aOverscroll) {
+ if (!StaticPrefs::apz_overscroll_enabled()) {
+ return;
+ }
+
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ // Do not go into overscroll in a direction in which we have no room to
+ // scroll to begin with.
+ ScrollDirections overscrollableDirections = GetOverscrollableDirections();
+ if (IsZero(aOverscroll.x)) {
+ overscrollableDirections -= ScrollDirection::eHorizontal;
+ }
+ if (IsZero(aOverscroll.y)) {
+ overscrollableDirections -= ScrollDirection::eVertical;
+ }
+
+ mOverscrollEffect->ConsumeOverscroll(aOverscroll, overscrollableDirections);
+}
+
+RefPtr<const OverscrollHandoffChain>
+AsyncPanZoomController::BuildOverscrollHandoffChain() {
+ if (APZCTreeManager* treeManagerLocal = GetApzcTreeManager()) {
+ return treeManagerLocal->BuildOverscrollHandoffChain(this);
+ }
+
+ // This APZC IsDestroyed(). To avoid callers having to special-case this
+ // scenario, just build a 1-element chain containing ourselves.
+ OverscrollHandoffChain* result = new OverscrollHandoffChain;
+ result->Add(this);
+ return result;
+}
+
+ParentLayerPoint AsyncPanZoomController::AttemptFling(
+ const FlingHandoffState& aHandoffState) {
+ // The PLPPI computation acquires the tree lock, so it needs to be performed
+ // on the controller thread, and before the APZC lock is acquired.
+ APZThreadUtils::AssertOnControllerThread();
+ float PLPPI = ComputePLPPI(PanStart(), aHandoffState.mVelocity);
+
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+
+ if (!IsPannable()) {
+ return aHandoffState.mVelocity;
+ }
+
+ // We may have a pre-existing velocity for whatever reason (for example,
+ // a previously handed off fling). We don't want to clobber that.
+ APZC_LOG("%p accepting fling with velocity %s\n", this,
+ ToString(aHandoffState.mVelocity).c_str());
+ ParentLayerPoint residualVelocity = aHandoffState.mVelocity;
+ if (mX.CanScroll()) {
+ mX.SetVelocity(mX.GetVelocity() + aHandoffState.mVelocity.x);
+ residualVelocity.x = 0;
+ }
+ if (mY.CanScroll()) {
+ mY.SetVelocity(mY.GetVelocity() + aHandoffState.mVelocity.y);
+ residualVelocity.y = 0;
+ }
+
+ // If we're not scrollable in at least one of the directions in which we
+ // were handed velocity, don't start a fling animation.
+ // The |IsFinite()| condition should only fail when running some tests
+ // that generate events faster than the clock resolution.
+ ParentLayerPoint velocity = GetVelocityVector();
+ if (!velocity.IsFinite() ||
+ velocity.Length() <= StaticPrefs::apz_fling_min_velocity_threshold()) {
+ // Relieve overscroll now if needed, since we will not transition to a fling
+ // animation and then an overscroll animation, and relieve it then.
+ aHandoffState.mChain->SnapBackOverscrolledApzc(this);
+ return residualVelocity;
+ }
+
+ // If there's a scroll snap point near the predicted fling destination,
+ // scroll there using a smooth scroll animation. Otherwise, start a
+ // fling animation.
+ ScrollSnapToDestination();
+ if (mState != SMOOTHMSD_SCROLL) {
+ SetState(FLING);
+ AsyncPanZoomAnimation* fling =
+ GetPlatformSpecificState()->CreateFlingAnimation(*this, aHandoffState,
+ PLPPI);
+ StartAnimation(fling);
+ }
+
+ return residualVelocity;
+}
+
+float AsyncPanZoomController::ComputePLPPI(ParentLayerPoint aPoint,
+ ParentLayerPoint aDirection) const {
+ // Avoid division-by-zero.
+ if (aDirection == ParentLayerPoint()) {
+ return GetDPI();
+ }
+
+ // Convert |aDirection| into a unit vector.
+ aDirection = aDirection / aDirection.Length();
+
+ // Place the vector at |aPoint| and convert to screen coordinates.
+ // The length of the resulting vector is the number of Screen coordinates
+ // that equal 1 ParentLayer coordinate in the given direction.
+ float screenPerParent = ToScreenCoordinates(aDirection, aPoint).Length();
+
+ // Finally, factor in the DPI scale.
+ return GetDPI() / screenPerParent;
+}
+
+Maybe<CSSPoint> AsyncPanZoomController::GetCurrentAnimationDestination(
+ const RecursiveMutexAutoLock& aProofOfLock) const {
+ if (mState == WHEEL_SCROLL) {
+ return Some(mAnimation->AsWheelScrollAnimation()->GetDestination());
+ }
+ if (mState == SMOOTH_SCROLL) {
+ return Some(mAnimation->AsSmoothScrollAnimation()->GetDestination());
+ }
+ if (mState == SMOOTHMSD_SCROLL) {
+ return Some(mAnimation->AsSmoothMsdScrollAnimation()->GetDestination());
+ }
+ if (mState == KEYBOARD_SCROLL) {
+ return Some(mAnimation->AsSmoothScrollAnimation()->GetDestination());
+ }
+
+ return Nothing();
+}
+
+ParentLayerPoint
+AsyncPanZoomController::AdjustHandoffVelocityForOverscrollBehavior(
+ ParentLayerPoint& aHandoffVelocity) const {
+ ParentLayerPoint residualVelocity;
+ ScrollDirections handoffDirections = GetAllowedHandoffDirections();
+ if (!handoffDirections.contains(ScrollDirection::eHorizontal)) {
+ residualVelocity.x = aHandoffVelocity.x;
+ aHandoffVelocity.x = 0;
+ }
+ if (!handoffDirections.contains(ScrollDirection::eVertical)) {
+ residualVelocity.y = aHandoffVelocity.y;
+ aHandoffVelocity.y = 0;
+ }
+ return residualVelocity;
+}
+
+bool AsyncPanZoomController::OverscrollBehaviorAllowsSwipe() const {
+ // Swipe navigation is a "non-local" overscroll behavior like handoff.
+ return GetAllowedHandoffDirections().contains(ScrollDirection::eHorizontal);
+}
+
+void AsyncPanZoomController::HandleFlingOverscroll(
+ const ParentLayerPoint& aVelocity, SideBits aOverscrollSideBits,
+ const RefPtr<const OverscrollHandoffChain>& aOverscrollHandoffChain,
+ const RefPtr<const AsyncPanZoomController>& aScrolledApzc) {
+ APZCTreeManager* treeManagerLocal = GetApzcTreeManager();
+ if (treeManagerLocal) {
+ const FlingHandoffState handoffState{
+ aVelocity, aOverscrollHandoffChain, Nothing(),
+ 0, true /* handoff */, aScrolledApzc};
+ ParentLayerPoint residualVelocity =
+ treeManagerLocal->DispatchFling(this, handoffState);
+ FLING_LOG("APZC %p left with residual velocity %s\n", this,
+ ToString(residualVelocity).c_str());
+ if (!IsZero(residualVelocity) && IsPannable() &&
+ StaticPrefs::apz_overscroll_enabled()) {
+ // Obey overscroll-behavior.
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ if (!mX.OverscrollBehaviorAllowsOverscrollEffect()) {
+ residualVelocity.x = 0;
+ }
+ if (!mY.OverscrollBehaviorAllowsOverscrollEffect()) {
+ residualVelocity.y = 0;
+ }
+
+ if (!IsZero(residualVelocity)) {
+ mOverscrollEffect->RelieveOverscroll(residualVelocity,
+ aOverscrollSideBits);
+ }
+ }
+ }
+}
+
+void AsyncPanZoomController::HandleSmoothScrollOverscroll(
+ const ParentLayerPoint& aVelocity, SideBits aOverscrollSideBits) {
+ // We must call BuildOverscrollHandoffChain from this deferred callback
+ // function in order to avoid a deadlock when acquiring the tree lock.
+ HandleFlingOverscroll(aVelocity, aOverscrollSideBits,
+ BuildOverscrollHandoffChain(), nullptr);
+}
+
+void AsyncPanZoomController::SmoothScrollTo(const CSSPoint& aDestination,
+ const ScrollOrigin& aOrigin) {
+ // Convert velocity from ParentLayerPoints/ms to ParentLayerPoints/s and then
+ // to appunits/second.
+ nsPoint destination = CSSPoint::ToAppUnits(aDestination);
+ nsSize velocity;
+ if (Metrics().GetZoom() != CSSToParentLayerScale(0)) {
+ velocity = CSSSize::ToAppUnits(ParentLayerSize(mX.GetVelocity() * 1000.0f,
+ mY.GetVelocity() * 1000.0f) /
+ Metrics().GetZoom());
+ }
+
+ if (mState == SMOOTH_SCROLL && mAnimation) {
+ RefPtr<SmoothScrollAnimation> animation(
+ mAnimation->AsSmoothScrollAnimation());
+ if (animation->GetScrollOrigin() == aOrigin) {
+ APZC_LOG("%p updating destination on existing animation\n", this);
+ animation->UpdateDestination(GetFrameTime().Time(), destination,
+ velocity);
+ return;
+ }
+ }
+
+ CancelAnimation();
+ SetState(SMOOTH_SCROLL);
+ nsPoint initialPosition =
+ CSSPoint::ToAppUnits(Metrics().GetVisualScrollOffset());
+ RefPtr<SmoothScrollAnimation> animation =
+ new SmoothScrollAnimation(*this, initialPosition, aOrigin);
+ animation->UpdateDestination(GetFrameTime().Time(), destination, velocity);
+ StartAnimation(animation.get());
+}
+
+void AsyncPanZoomController::SmoothMsdScrollTo(
+ CSSSnapTarget&& aDestination, ScrollTriggeredByScript aTriggeredByScript) {
+ if (mState == SMOOTHMSD_SCROLL && mAnimation) {
+ APZC_LOG("%p updating destination on existing animation\n", this);
+ RefPtr<SmoothMsdScrollAnimation> animation(
+ static_cast<SmoothMsdScrollAnimation*>(mAnimation.get()));
+ animation->SetDestination(aDestination.mPosition,
+ std::move(aDestination.mTargetIds),
+ aTriggeredByScript);
+ } else {
+ CancelAnimation();
+ SetState(SMOOTHMSD_SCROLL);
+ // Convert velocity from ParentLayerPoints/ms to ParentLayerPoints/s.
+ CSSPoint initialVelocity;
+ if (Metrics().GetZoom() != CSSToParentLayerScale(0)) {
+ initialVelocity = ParentLayerPoint(mX.GetVelocity() * 1000.0f,
+ mY.GetVelocity() * 1000.0f) /
+ Metrics().GetZoom();
+ }
+
+ StartAnimation(new SmoothMsdScrollAnimation(
+ *this, Metrics().GetVisualScrollOffset(), initialVelocity,
+ aDestination.mPosition,
+ StaticPrefs::layout_css_scroll_behavior_spring_constant(),
+ StaticPrefs::layout_css_scroll_behavior_damping_ratio(),
+ std::move(aDestination.mTargetIds), aTriggeredByScript));
+ }
+}
+
+void AsyncPanZoomController::StartOverscrollAnimation(
+ const ParentLayerPoint& aVelocity, SideBits aOverscrollSideBits) {
+ MOZ_ASSERT(mState != OVERSCROLL_ANIMATION);
+
+ SetState(OVERSCROLL_ANIMATION);
+
+ ParentLayerPoint velocity = aVelocity;
+ AdjustDeltaForAllowedScrollDirections(velocity,
+ GetOverscrollableDirections());
+ StartAnimation(new OverscrollAnimation(*this, velocity, aOverscrollSideBits));
+}
+
+bool AsyncPanZoomController::CallDispatchScroll(
+ ParentLayerPoint& aStartPoint, ParentLayerPoint& aEndPoint,
+ OverscrollHandoffState& aOverscrollHandoffState) {
+ // Make a local copy of the tree manager pointer and check if it's not
+ // null before calling DispatchScroll(). This is necessary because
+ // Destroy(), which nulls out mTreeManager, could be called concurrently.
+ APZCTreeManager* treeManagerLocal = GetApzcTreeManager();
+ if (!treeManagerLocal) {
+ return false;
+ }
+
+ // Obey overscroll-behavior.
+ ParentLayerPoint endPoint = aEndPoint;
+ if (aOverscrollHandoffState.mChainIndex > 0) {
+ ScrollDirections handoffDirections = GetAllowedHandoffDirections();
+ if (!handoffDirections.contains(ScrollDirection::eHorizontal)) {
+ endPoint.x = aStartPoint.x;
+ }
+ if (!handoffDirections.contains(ScrollDirection::eVertical)) {
+ endPoint.y = aStartPoint.y;
+ }
+ if (aStartPoint == endPoint) {
+ // Handoff not allowed in either direction - don't even bother.
+ return false;
+ }
+ }
+
+ return treeManagerLocal->DispatchScroll(this, aStartPoint, endPoint,
+ aOverscrollHandoffState);
+}
+
+void AsyncPanZoomController::RecordScrollPayload(const TimeStamp& aTimeStamp) {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ if (!mScrollPayload) {
+ mScrollPayload = Some(
+ CompositionPayload{CompositionPayloadType::eAPZScroll, aTimeStamp});
+ }
+}
+
+void AsyncPanZoomController::StartTouch(const ParentLayerPoint& aPoint,
+ TimeStamp aTimestamp) {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ mX.StartTouch(aPoint.x, aTimestamp);
+ mY.StartTouch(aPoint.y, aTimestamp);
+}
+
+void AsyncPanZoomController::EndTouch(TimeStamp aTimestamp,
+ Axis::ClearAxisLock aClearAxisLock) {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ mX.EndTouch(aTimestamp, aClearAxisLock);
+ mY.EndTouch(aTimestamp, aClearAxisLock);
+}
+
+void AsyncPanZoomController::TrackTouch(const MultiTouchInput& aEvent) {
+ ExternalPoint extPoint = GetFirstExternalTouchPoint(aEvent);
+ ScreenPoint panVector = PanVector(extPoint);
+ HandlePanningUpdate(panVector);
+
+ ParentLayerPoint prevTouchPoint(mX.GetPos(), mY.GetPos());
+ ParentLayerPoint touchPoint = GetFirstTouchPoint(aEvent);
+
+ UpdateWithTouchAtDevicePoint(aEvent);
+
+ auto velocity = GetVelocityVector().Length();
+ if (mMinimumVelocityDuringPan) {
+ mMinimumVelocityDuringPan =
+ Some(std::min(*mMinimumVelocityDuringPan, velocity));
+ } else {
+ mMinimumVelocityDuringPan = Some(velocity);
+ }
+
+ if (prevTouchPoint != touchPoint) {
+ MOZ_ASSERT(GetCurrentTouchBlock());
+ OverscrollHandoffState handoffState(
+ *GetCurrentTouchBlock()->GetOverscrollHandoffChain(), panVector,
+ ScrollSource::Touchscreen);
+ RecordScrollPayload(aEvent.mTimeStamp);
+ CallDispatchScroll(prevTouchPoint, touchPoint, handoffState);
+ }
+}
+
+ParentLayerPoint AsyncPanZoomController::GetFirstTouchPoint(
+ const MultiTouchInput& aEvent) {
+ return ((SingleTouchData&)aEvent.mTouches[0]).mLocalScreenPoint;
+}
+
+ExternalPoint AsyncPanZoomController::GetFirstExternalTouchPoint(
+ const MultiTouchInput& aEvent) {
+ return ToExternalPoint(aEvent.mScreenOffset,
+ ((SingleTouchData&)aEvent.mTouches[0]).mScreenPoint);
+}
+
+ParentLayerPoint AsyncPanZoomController::GetOverscrollAmount() const {
+ if (StaticPrefs::apz_overscroll_test_async_scroll_offset_enabled()) {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ AutoApplyAsyncTestAttributes testAttributeApplier(this, lock);
+ return GetOverscrollAmountInternal();
+ }
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ return GetOverscrollAmountInternal();
+}
+
+ParentLayerPoint AsyncPanZoomController::GetOverscrollAmountInternal() const {
+ return {mX.GetOverscroll(), mY.GetOverscroll()};
+}
+
+SideBits AsyncPanZoomController::GetOverscrollSideBits() const {
+ return apz::GetOverscrollSideBits({mX.GetOverscroll(), mY.GetOverscroll()});
+}
+
+void AsyncPanZoomController::RestoreOverscrollAmount(
+ const ParentLayerPoint& aOverscroll) {
+ mX.RestoreOverscroll(aOverscroll.x);
+ mY.RestoreOverscroll(aOverscroll.y);
+}
+
+void AsyncPanZoomController::StartAnimation(AsyncPanZoomAnimation* aAnimation) {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ mAnimation = aAnimation;
+ mLastSampleTime = GetFrameTime();
+ ScheduleComposite();
+}
+
+void AsyncPanZoomController::CancelAnimation(CancelAnimationFlags aFlags) {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ APZC_LOG_DETAIL("running CancelAnimation(0x%x) in state %s\n", this, aFlags,
+ ToString(mState).c_str());
+
+ if ((aFlags & ExcludeWheel) && mState == WHEEL_SCROLL) {
+ return;
+ }
+
+ if (mAnimation) {
+ mAnimation->Cancel(aFlags);
+ }
+
+ SetState(NOTHING);
+ mLastSnapTargetIds = ScrollSnapTargetIds{};
+ mAnimation = nullptr;
+ // Since there is no animation in progress now the axes should
+ // have no velocity either. If we are dropping the velocity from a non-zero
+ // value we should trigger a repaint as the displayport margins are dependent
+ // on the velocity and the last repaint request might not have good margins
+ // any more.
+ bool repaint = !IsZero(GetVelocityVector());
+ mX.SetVelocity(0);
+ mY.SetVelocity(0);
+ mX.SetAxisLocked(false);
+ mY.SetAxisLocked(false);
+ // Setting the state to nothing and cancelling the animation can
+ // preempt normal mechanisms for relieving overscroll, so we need to clear
+ // overscroll here.
+ if (!(aFlags & ExcludeOverscroll) && IsOverscrolled()) {
+ ClearOverscroll();
+ repaint = true;
+ }
+ // Similar to relieving overscroll, we also need to snap to any snap points
+ // if appropriate.
+ if (aFlags & CancelAnimationFlags::ScrollSnap) {
+ ScrollSnap(ScrollSnapFlags::IntendedEndPosition);
+ }
+ if (repaint) {
+ RequestContentRepaint();
+ ScheduleComposite();
+ }
+}
+
+void AsyncPanZoomController::ClearOverscroll() {
+ mOverscrollEffect->ClearOverscroll();
+}
+
+void AsyncPanZoomController::ClearPhysicalOverscroll() {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ mX.ClearOverscroll();
+ mY.ClearOverscroll();
+}
+
+void AsyncPanZoomController::SetCompositorController(
+ CompositorController* aCompositorController) {
+ mCompositorController = aCompositorController;
+}
+
+void AsyncPanZoomController::SetVisualScrollOffset(const CSSPoint& aOffset) {
+ Metrics().SetVisualScrollOffset(aOffset);
+ Metrics().RecalculateLayoutViewportOffset();
+}
+
+void AsyncPanZoomController::ClampAndSetVisualScrollOffset(
+ const CSSPoint& aOffset) {
+ Metrics().ClampAndSetVisualScrollOffset(aOffset);
+ Metrics().RecalculateLayoutViewportOffset();
+}
+
+void AsyncPanZoomController::ScrollBy(const CSSPoint& aOffset) {
+ SetVisualScrollOffset(Metrics().GetVisualScrollOffset() + aOffset);
+}
+
+void AsyncPanZoomController::ScrollByAndClamp(const CSSPoint& aOffset) {
+ ClampAndSetVisualScrollOffset(Metrics().GetVisualScrollOffset() + aOffset);
+}
+
+void AsyncPanZoomController::ScaleWithFocus(float aScale,
+ const CSSPoint& aFocus) {
+ Metrics().ZoomBy(aScale);
+ // We want to adjust the scroll offset such that the CSS point represented by
+ // aFocus remains at the same position on the screen before and after the
+ // change in zoom. The below code accomplishes this; see
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=923431#c6 for an in-depth
+ // explanation of how.
+ SetVisualScrollOffset((Metrics().GetVisualScrollOffset() + aFocus) -
+ (aFocus / aScale));
+}
+
+/*static*/
+gfx::IntSize AsyncPanZoomController::GetDisplayportAlignmentMultiplier(
+ const ScreenSize& aBaseSize) {
+ gfx::IntSize multiplier(1, 1);
+ float baseWidth = aBaseSize.width;
+ while (baseWidth > 500) {
+ baseWidth /= 2;
+ multiplier.width *= 2;
+ if (multiplier.width >= 8) {
+ break;
+ }
+ }
+ float baseHeight = aBaseSize.height;
+ while (baseHeight > 500) {
+ baseHeight /= 2;
+ multiplier.height *= 2;
+ if (multiplier.height >= 8) {
+ break;
+ }
+ }
+ return multiplier;
+}
+
+/**
+ * Enlarges the displayport along both axes based on the velocity.
+ */
+static CSSSize CalculateDisplayPortSize(
+ const CSSSize& aCompositionSize, const CSSPoint& aVelocity,
+ AsyncPanZoomController::ZoomInProgress aZoomInProgress,
+ const CSSToScreenScale2D& aDpPerCSS) {
+ bool xIsStationarySpeed =
+ fabsf(aVelocity.x) < StaticPrefs::apz_min_skate_speed();
+ bool yIsStationarySpeed =
+ fabsf(aVelocity.y) < StaticPrefs::apz_min_skate_speed();
+ float xMultiplier = xIsStationarySpeed
+ ? StaticPrefs::apz_x_stationary_size_multiplier()
+ : StaticPrefs::apz_x_skate_size_multiplier();
+ float yMultiplier = yIsStationarySpeed
+ ? StaticPrefs::apz_y_stationary_size_multiplier()
+ : StaticPrefs::apz_y_skate_size_multiplier();
+
+ if (IsHighMemSystem() && !xIsStationarySpeed) {
+ xMultiplier += StaticPrefs::apz_x_skate_highmem_adjust();
+ }
+
+ if (IsHighMemSystem() && !yIsStationarySpeed) {
+ yMultiplier += StaticPrefs::apz_y_skate_highmem_adjust();
+ }
+
+ if (aZoomInProgress == AsyncPanZoomController::ZoomInProgress::Yes) {
+ // If a zoom is in progress, we will be making content visible on the
+ // x and y axes in equal proportion, because the zoom operation scales
+ // equally on the x and y axes. The default multipliers computed above are
+ // biased towards the y-axis since that's where most scrolling occurs, but
+ // in the case of zooming, we should really use equal multipliers on both
+ // axes. This does that while preserving the total displayport area
+ // quantity (aCompositionSize.Area() * xMultiplier * yMultiplier).
+ // Note that normally changing the shape of the displayport is expensive
+ // and should be avoided, but if a zoom is in progress the displayport
+ // is likely going to be fully repainted anyway due to changes in resolution
+ // so there should be no marginal cost to also changing the shape of it.
+ float areaMultiplier = xMultiplier * yMultiplier;
+ xMultiplier = sqrt(areaMultiplier);
+ yMultiplier = xMultiplier;
+ }
+
+ // Scale down the margin multipliers by the alignment multiplier because
+ // the alignment code will expand the displayport outward to the multiplied
+ // alignment. This is not necessary for correctness, but for performance;
+ // if we don't do this the displayport can end up much larger. The math here
+ // is actually just scaling the part of the multipler that is > 1, so that
+ // we never end up with xMultiplier or yMultiplier being less than 1 (that
+ // would result in a guaranteed checkerboarding situation). Note that the
+ // calculation doesn't cancel exactly the increased margin from applying
+ // the alignment multiplier, but this is simple and should provide
+ // reasonable behaviour in most cases.
+ gfx::IntSize alignmentMultipler =
+ AsyncPanZoomController::GetDisplayportAlignmentMultiplier(
+ aCompositionSize * aDpPerCSS);
+ if (xMultiplier > 1) {
+ xMultiplier = ((xMultiplier - 1) / alignmentMultipler.width) + 1;
+ }
+ if (yMultiplier > 1) {
+ yMultiplier = ((yMultiplier - 1) / alignmentMultipler.height) + 1;
+ }
+
+ return aCompositionSize * CSSSize(xMultiplier, yMultiplier);
+}
+
+/**
+ * Ensures that the displayport is at least as large as the visible area
+ * inflated by the danger zone. If this is not the case then the
+ * "AboutToCheckerboard" function in TiledContentClient.cpp will return true
+ * even in the stable state.
+ */
+static CSSSize ExpandDisplayPortToDangerZone(
+ const CSSSize& aDisplayPortSize, const FrameMetrics& aFrameMetrics) {
+ CSSSize dangerZone(0.0f, 0.0f);
+ if (aFrameMetrics.DisplayportPixelsPerCSSPixel().xScale != 0 &&
+ aFrameMetrics.DisplayportPixelsPerCSSPixel().yScale != 0) {
+ dangerZone = ScreenSize(StaticPrefs::apz_danger_zone_x(),
+ StaticPrefs::apz_danger_zone_y()) /
+ aFrameMetrics.DisplayportPixelsPerCSSPixel();
+ }
+ const CSSSize compositionSize =
+ aFrameMetrics.CalculateBoundedCompositedSizeInCssPixels();
+
+ const float xSize = std::max(aDisplayPortSize.width,
+ compositionSize.width + (2 * dangerZone.width));
+
+ const float ySize =
+ std::max(aDisplayPortSize.height,
+ compositionSize.height + (2 * dangerZone.height));
+
+ return CSSSize(xSize, ySize);
+}
+
+/**
+ * Attempts to redistribute any area in the displayport that would get clipped
+ * by the scrollable rect, or be inaccessible due to disabled scrolling, to the
+ * other axis, while maintaining total displayport area.
+ */
+static void RedistributeDisplayPortExcess(CSSSize& aDisplayPortSize,
+ const CSSRect& aScrollableRect) {
+ // As aDisplayPortSize.height * aDisplayPortSize.width does not change,
+ // we are just scaling by the ratio and its inverse.
+ if (aDisplayPortSize.height > aScrollableRect.Height()) {
+ aDisplayPortSize.width *=
+ (aDisplayPortSize.height / aScrollableRect.Height());
+ aDisplayPortSize.height = aScrollableRect.Height();
+ } else if (aDisplayPortSize.width > aScrollableRect.Width()) {
+ aDisplayPortSize.height *=
+ (aDisplayPortSize.width / aScrollableRect.Width());
+ aDisplayPortSize.width = aScrollableRect.Width();
+ }
+}
+
+/* static */
+const ScreenMargin AsyncPanZoomController::CalculatePendingDisplayPort(
+ const FrameMetrics& aFrameMetrics, const ParentLayerPoint& aVelocity,
+ ZoomInProgress aZoomInProgress) {
+ if (aFrameMetrics.IsScrollInfoLayer()) {
+ // Don't compute margins. Since we can't asynchronously scroll this frame,
+ // we don't want to paint anything more than the composition bounds.
+ return ScreenMargin();
+ }
+
+ CSSSize compositionSize =
+ aFrameMetrics.CalculateBoundedCompositedSizeInCssPixels();
+ CSSPoint velocity;
+ if (aFrameMetrics.GetZoom() != CSSToParentLayerScale(0)) {
+ velocity = aVelocity / aFrameMetrics.GetZoom(); // avoid division by zero
+ }
+ CSSRect scrollableRect = aFrameMetrics.GetExpandedScrollableRect();
+
+ // Calculate the displayport size based on how fast we're moving along each
+ // axis.
+ CSSSize displayPortSize =
+ CalculateDisplayPortSize(compositionSize, velocity, aZoomInProgress,
+ aFrameMetrics.DisplayportPixelsPerCSSPixel());
+
+ displayPortSize =
+ ExpandDisplayPortToDangerZone(displayPortSize, aFrameMetrics);
+
+ if (StaticPrefs::apz_enlarge_displayport_when_clipped()) {
+ RedistributeDisplayPortExcess(displayPortSize, scrollableRect);
+ }
+
+ // We calculate a "displayport" here which is relative to the scroll offset.
+ // Note that the scroll offset we have here in the APZ code may not be the
+ // same as the base rect that gets used on the layout side when the
+ // displayport margins are actually applied, so it is important to only
+ // consider the displayport as margins relative to a scroll offset rather than
+ // relative to something more unchanging like the scrollable rect origin.
+
+ // Center the displayport based on its expansion over the composition size.
+ CSSRect displayPort((compositionSize.width - displayPortSize.width) / 2.0f,
+ (compositionSize.height - displayPortSize.height) / 2.0f,
+ displayPortSize.width, displayPortSize.height);
+
+ // Offset the displayport, depending on how fast we're moving and the
+ // estimated time it takes to paint, to try to minimise checkerboarding.
+ float paintFactor = kDefaultEstimatedPaintDurationMs;
+ displayPort.MoveBy(velocity * paintFactor * StaticPrefs::apz_velocity_bias());
+
+ APZC_LOGV_FM(aFrameMetrics,
+ "Calculated displayport as %s from velocity %s zooming %d paint "
+ "time %f metrics",
+ ToString(displayPort).c_str(), ToString(aVelocity).c_str(),
+ (int)aZoomInProgress, paintFactor);
+
+ CSSMargin cssMargins;
+ cssMargins.left = -displayPort.X();
+ cssMargins.top = -displayPort.Y();
+ cssMargins.right =
+ displayPort.Width() - compositionSize.width - cssMargins.left;
+ cssMargins.bottom =
+ displayPort.Height() - compositionSize.height - cssMargins.top;
+
+ return cssMargins * aFrameMetrics.DisplayportPixelsPerCSSPixel();
+}
+
+void AsyncPanZoomController::ScheduleComposite() {
+ if (mCompositorController) {
+ mCompositorController->ScheduleRenderOnCompositorThread(
+ wr::RenderReasons::APZ);
+ }
+}
+
+void AsyncPanZoomController::ScheduleCompositeAndMaybeRepaint() {
+ ScheduleComposite();
+ RequestContentRepaint();
+}
+
+void AsyncPanZoomController::FlushRepaintForOverscrollHandoff() {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ RequestContentRepaint();
+}
+
+void AsyncPanZoomController::FlushRepaintForNewInputBlock() {
+ APZC_LOG("%p flushing repaint for new input block\n", this);
+
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ RequestContentRepaint();
+}
+
+bool AsyncPanZoomController::SnapBackIfOverscrolled() {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ if (SnapBackIfOverscrolledForMomentum(ParentLayerPoint(0, 0))) {
+ return true;
+ }
+ // If we don't kick off an overscroll animation, we still need to snap to any
+ // nearby snap points, assuming we haven't already done so when we started
+ // this fling
+ if (mState != FLING) {
+ ScrollSnap(ScrollSnapFlags::IntendedEndPosition);
+ }
+ return false;
+}
+
+bool AsyncPanZoomController::SnapBackIfOverscrolledForMomentum(
+ const ParentLayerPoint& aVelocity) {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ // It's possible that we're already in the middle of an overscroll
+ // animation - if so, don't start a new one.
+ if (IsOverscrolled() && mState != OVERSCROLL_ANIMATION) {
+ APZC_LOG("%p is overscrolled, starting snap-back\n", this);
+ mOverscrollEffect->RelieveOverscroll(aVelocity, GetOverscrollSideBits());
+ return true;
+ }
+ return false;
+}
+
+bool AsyncPanZoomController::IsFlingingFast() const {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ if (mState == FLING && GetVelocityVector().Length() >
+ StaticPrefs::apz_fling_stop_on_tap_threshold()) {
+ APZC_LOG("%p is moving fast\n", this);
+ return true;
+ }
+ return false;
+}
+
+bool AsyncPanZoomController::IsPannable() const {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ return mX.CanScroll() || mY.CanScroll();
+}
+
+bool AsyncPanZoomController::IsScrollInfoLayer() const {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ return Metrics().IsScrollInfoLayer();
+}
+
+int32_t AsyncPanZoomController::GetLastTouchIdentifier() const {
+ RefPtr<GestureEventListener> listener = GetGestureEventListener();
+ return listener ? listener->GetLastTouchIdentifier() : -1;
+}
+
+void AsyncPanZoomController::RequestContentRepaint(
+ RepaintUpdateType aUpdateType) {
+ // Reinvoke this method on the repaint thread if it's not there already. It's
+ // important to do this before the call to CalculatePendingDisplayPort, so
+ // that CalculatePendingDisplayPort uses the most recent available version of
+ // Metrics(). just before the paint request is dispatched to content.
+ RefPtr<GeckoContentController> controller = GetGeckoContentController();
+ if (!controller) {
+ return;
+ }
+ if (!controller->IsRepaintThread()) {
+ // Even though we want to do the actual repaint request on the repaint
+ // thread, we want to update the expected gecko metrics synchronously.
+ // Otherwise we introduce a race condition where we might read from the
+ // expected gecko metrics on the controller thread before or after it gets
+ // updated on the repaint thread, when in fact we always want the updated
+ // version when reading.
+ { // scope lock
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ mExpectedGeckoMetrics.UpdateFrom(Metrics());
+ }
+
+ // use the local variable to resolve the function overload.
+ auto func =
+ static_cast<void (AsyncPanZoomController::*)(RepaintUpdateType)>(
+ &AsyncPanZoomController::RequestContentRepaint);
+ controller->DispatchToRepaintThread(NewRunnableMethod<RepaintUpdateType>(
+ "layers::AsyncPanZoomController::RequestContentRepaint", this, func,
+ aUpdateType));
+ return;
+ }
+
+ MOZ_ASSERT(controller->IsRepaintThread());
+
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ ParentLayerPoint velocity = GetVelocityVector();
+ ScreenMargin displayportMargins = CalculatePendingDisplayPort(
+ Metrics(), velocity,
+ (mState == PINCHING || mState == ANIMATING_ZOOM) ? ZoomInProgress::Yes
+ : ZoomInProgress::No);
+ Metrics().SetPaintRequestTime(TimeStamp::Now());
+ RequestContentRepaint(velocity, displayportMargins, aUpdateType);
+}
+
+static CSSRect GetDisplayPortRect(const FrameMetrics& aFrameMetrics,
+ const ScreenMargin& aDisplayportMargins) {
+ // This computation is based on what happens in CalculatePendingDisplayPort.
+ // If that changes then this might need to change too.
+ // Note that the display port rect APZ computes is relative to the visual
+ // scroll offset. It's adjusted to be relative to the layout scroll offset
+ // when the main thread processes a repaint request (in
+ // APZCCallbackHelper::AdjustDisplayPortForScrollDelta()) and ultimately
+ // applied (in DisplayPortUtils::GetDisplayPort()) in this adjusted form.
+ CSSRect baseRect(aFrameMetrics.GetVisualScrollOffset(),
+ aFrameMetrics.CalculateBoundedCompositedSizeInCssPixels());
+ baseRect.Inflate(aDisplayportMargins /
+ aFrameMetrics.DisplayportPixelsPerCSSPixel());
+ return baseRect;
+}
+
+void AsyncPanZoomController::RequestContentRepaint(
+ const ParentLayerPoint& aVelocity, const ScreenMargin& aDisplayportMargins,
+ RepaintUpdateType aUpdateType) {
+ mRecursiveMutex.AssertCurrentThreadIn();
+
+ RefPtr<GeckoContentController> controller = GetGeckoContentController();
+ if (!controller) {
+ return;
+ }
+ MOZ_ASSERT(controller->IsRepaintThread());
+
+ APZScrollAnimationType animationType = APZScrollAnimationType::No;
+ if (mAnimation) {
+ animationType = mAnimation->WasTriggeredByScript()
+ ? APZScrollAnimationType::TriggeredByScript
+ : APZScrollAnimationType::TriggeredByUserInput;
+ }
+ RepaintRequest request(Metrics(), aDisplayportMargins, aUpdateType,
+ animationType, mScrollGeneration, mLastSnapTargetIds,
+ IsInScrollingGesture());
+
+ if (request.IsRootContent() && request.GetZoom() != mLastNotifiedZoom &&
+ mState != PINCHING && mState != ANIMATING_ZOOM) {
+ controller->NotifyScaleGestureComplete(
+ GetGuid(),
+ (request.GetZoom() / request.GetDevPixelsPerCSSPixel()).scale);
+ mLastNotifiedZoom = request.GetZoom();
+ }
+
+ // If we're trying to paint what we already think is painted, discard this
+ // request since it's a pointless paint.
+ if (request.GetDisplayPortMargins().WithinEpsilonOf(
+ mLastPaintRequestMetrics.GetDisplayPortMargins(), EPSILON) &&
+ request.GetVisualScrollOffset().WithinEpsilonOf(
+ mLastPaintRequestMetrics.GetVisualScrollOffset(), EPSILON) &&
+ request.GetPresShellResolution() ==
+ mLastPaintRequestMetrics.GetPresShellResolution() &&
+ request.GetZoom() == mLastPaintRequestMetrics.GetZoom() &&
+ request.GetLayoutViewport().WithinEpsilonOf(
+ mLastPaintRequestMetrics.GetLayoutViewport(), EPSILON) &&
+ request.GetScrollGeneration() ==
+ mLastPaintRequestMetrics.GetScrollGeneration() &&
+ request.GetScrollUpdateType() ==
+ mLastPaintRequestMetrics.GetScrollUpdateType() &&
+ request.GetScrollAnimationType() ==
+ mLastPaintRequestMetrics.GetScrollAnimationType() &&
+ request.GetLastSnapTargetIds() ==
+ mLastPaintRequestMetrics.GetLastSnapTargetIds()) {
+ return;
+ }
+
+ APZC_LOGV("%p requesting content repaint %s", this,
+ ToString(request).c_str());
+ { // scope lock
+ MutexAutoLock lock(mCheckerboardEventLock);
+ if (mCheckerboardEvent && mCheckerboardEvent->IsRecordingTrace()) {
+ std::stringstream info;
+ info << " velocity " << aVelocity;
+ std::string str = info.str();
+ mCheckerboardEvent->UpdateRendertraceProperty(
+ CheckerboardEvent::RequestedDisplayPort,
+ GetDisplayPortRect(Metrics(), aDisplayportMargins), str);
+ }
+ }
+
+ controller->RequestContentRepaint(request);
+ mExpectedGeckoMetrics.UpdateFrom(Metrics());
+ mLastPaintRequestMetrics = request;
+
+ // We're holding the APZC lock here, so redispatch this so we can get
+ // the tree lock without the APZC lock.
+ controller->DispatchToRepaintThread(
+ NewRunnableMethod<AsyncPanZoomController*>(
+ "layers::APZCTreeManager::SendSubtreeTransformsToChromeMainThread",
+ GetApzcTreeManager(),
+ &APZCTreeManager::SendSubtreeTransformsToChromeMainThread, this));
+}
+
+bool AsyncPanZoomController::UpdateAnimation(
+ const RecursiveMutexAutoLock& aProofOfLock, const SampleTime& aSampleTime,
+ nsTArray<RefPtr<Runnable>>* aOutDeferredTasks) {
+ AssertOnSamplerThread();
+
+ // This function may get called multiple with the same sample time, if we
+ // composite multiple times at the same timestamp.
+ // However we only want to do one animation step per composition so we need
+ // to deduplicate these calls first.
+ if (mLastSampleTime == aSampleTime) {
+ return !!mAnimation;
+ }
+
+ // We're at a new timestamp, so advance to the next sample in the deque, if
+ // there is one. That one will be used for all the code that reads the
+ // eForCompositing transforms in this vsync interval.
+ AdvanceToNextSample();
+
+ // And then create a new sample, which will be used in the *next* vsync
+ // interval. We do the sample at this point and not later in order to try
+ // and enforce one frame delay between computing the async transform and
+ // compositing it to the screen. This one-frame delay gives code running on
+ // the main thread a chance to try and respond to the scroll position change,
+ // so that e.g. a main-thread animation can stay in sync with user-driven
+ // scrolling or a compositor animation.
+ bool needComposite = SampleCompositedAsyncTransform(aProofOfLock);
+
+ TimeDuration sampleTimeDelta = aSampleTime - mLastSampleTime;
+ mLastSampleTime = aSampleTime;
+
+ if (needComposite || mAnimation) {
+ // Bump the scroll generation before we call RequestContentRepaint below
+ // so that the RequestContentRepaint call will surely use the new
+ // generation.
+ if (APZCTreeManager* treeManagerLocal = GetApzcTreeManager()) {
+ mScrollGeneration = treeManagerLocal->NewAPZScrollGeneration();
+ }
+ }
+
+ if (mAnimation) {
+ bool continueAnimation = mAnimation->Sample(Metrics(), sampleTimeDelta);
+ bool wantsRepaints = mAnimation->WantsRepaints();
+ *aOutDeferredTasks = mAnimation->TakeDeferredTasks();
+ if (!continueAnimation) {
+ SetState(NOTHING);
+ if (mAnimation->AsSmoothMsdScrollAnimation()) {
+ {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ mLastSnapTargetIds =
+ mAnimation->AsSmoothMsdScrollAnimation()->TakeSnapTargetIds();
+ }
+ }
+ mAnimation = nullptr;
+ }
+ // Request a repaint at the end of the animation in case something such as a
+ // call to NotifyLayersUpdated was invoked during the animation and Gecko's
+ // current state is some intermediate point of the animation.
+ if (!continueAnimation || wantsRepaints) {
+ RequestContentRepaint();
+ }
+ needComposite = true;
+ }
+ return needComposite;
+}
+
+AsyncTransformComponentMatrix AsyncPanZoomController::GetOverscrollTransform(
+ AsyncTransformConsumer aMode) const {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ AutoApplyAsyncTestAttributes testAttributeApplier(this, lock);
+
+ if (aMode == eForCompositing && mScrollMetadata.IsApzForceDisabled()) {
+ return AsyncTransformComponentMatrix();
+ }
+
+ if (!IsPhysicallyOverscrolled()) {
+ return AsyncTransformComponentMatrix();
+ }
+
+ // The overscroll effect is a simple translation by the overscroll offset.
+ ParentLayerPoint overscrollOffset(-mX.GetOverscroll(), -mY.GetOverscroll());
+ return AsyncTransformComponentMatrix().PostTranslate(overscrollOffset.x,
+ overscrollOffset.y, 0);
+}
+
+bool AsyncPanZoomController::AdvanceAnimations(const SampleTime& aSampleTime) {
+ AssertOnSamplerThread();
+
+ // Don't send any state-change notifications until the end of the function,
+ // because we may go through some intermediate states while we finish
+ // animations and start new ones.
+ StateChangeNotificationBlocker blocker(this);
+
+ // The eventual return value of this function. The compositor needs to know
+ // whether or not to advance by a frame as soon as it can. For example, if a
+ // fling is happening, it has to keep compositing so that the animation is
+ // smooth. If an animation frame is requested, it is the compositor's
+ // responsibility to schedule a composite.
+ bool requestAnimationFrame = false;
+ nsTArray<RefPtr<Runnable>> deferredTasks;
+
+ {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ { // scope lock
+ CSSRect visibleRect = GetVisibleRect(lock);
+ MutexAutoLock lock2(mCheckerboardEventLock);
+ // Update RendertraceProperty before UpdateAnimation() call, since
+ // the UpdateAnimation() updates effective ScrollOffset for next frame
+ // if APZFrameDelay is enabled.
+ if (mCheckerboardEvent) {
+ mCheckerboardEvent->UpdateRendertraceProperty(
+ CheckerboardEvent::UserVisible, visibleRect);
+ }
+ }
+
+ requestAnimationFrame = UpdateAnimation(lock, aSampleTime, &deferredTasks);
+ }
+ // Execute any deferred tasks queued up by mAnimation's Sample() (called by
+ // UpdateAnimation()). This needs to be done after the monitor is released
+ // since the tasks are allowed to call APZCTreeManager methods which can grab
+ // the tree lock.
+ for (uint32_t i = 0; i < deferredTasks.Length(); ++i) {
+ APZThreadUtils::RunOnControllerThread(std::move(deferredTasks[i]));
+ }
+
+ // If any of the deferred tasks starts a new animation, it will request a
+ // new composite directly, so we can just return requestAnimationFrame here.
+ return requestAnimationFrame;
+}
+
+ParentLayerPoint AsyncPanZoomController::GetCurrentAsyncScrollOffset(
+ AsyncTransformConsumer aMode) const {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ AutoApplyAsyncTestAttributes testAttributeApplier(this, lock);
+
+ return GetEffectiveScrollOffset(aMode, lock) * GetEffectiveZoom(aMode, lock);
+}
+
+CSSRect AsyncPanZoomController::GetCurrentAsyncVisualViewport(
+ AsyncTransformConsumer aMode) const {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ AutoApplyAsyncTestAttributes testAttributeApplier(this, lock);
+
+ return CSSRect(
+ GetEffectiveScrollOffset(aMode, lock),
+ FrameMetrics::CalculateCompositedSizeInCssPixels(
+ Metrics().GetCompositionBounds(), GetEffectiveZoom(aMode, lock)));
+}
+
+AsyncTransform AsyncPanZoomController::GetCurrentAsyncTransform(
+ AsyncTransformConsumer aMode, AsyncTransformComponents aComponents,
+ std::size_t aSampleIndex) const {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ AutoApplyAsyncTestAttributes testAttributeApplier(this, lock);
+
+ CSSToParentLayerScale effectiveZoom;
+ if (aComponents.contains(AsyncTransformComponent::eVisual)) {
+ effectiveZoom = GetEffectiveZoom(aMode, lock, aSampleIndex);
+ } else {
+ effectiveZoom =
+ Metrics().LayersPixelsPerCSSPixel() * LayerToParentLayerScale(1.0f);
+ }
+
+ LayerToParentLayerScale compositedAsyncZoom =
+ effectiveZoom / Metrics().LayersPixelsPerCSSPixel();
+
+ ParentLayerPoint translation;
+ if (aComponents.contains(AsyncTransformComponent::eVisual)) {
+ // There is no "lastPaintVisualOffset" to subtract here; the visual offset
+ // is entirely async.
+
+ CSSPoint currentVisualOffset =
+ GetEffectiveScrollOffset(aMode, lock, aSampleIndex) -
+ GetEffectiveLayoutViewport(aMode, lock, aSampleIndex).TopLeft();
+
+ translation += currentVisualOffset * effectiveZoom;
+ }
+ if (aComponents.contains(AsyncTransformComponent::eLayout)) {
+ CSSPoint lastPaintLayoutOffset;
+ if (mLastContentPaintMetrics.IsScrollable()) {
+ lastPaintLayoutOffset = mLastContentPaintMetrics.GetLayoutScrollOffset();
+ }
+
+ CSSPoint currentLayoutOffset =
+ GetEffectiveLayoutViewport(aMode, lock, aSampleIndex).TopLeft();
+
+ translation +=
+ (currentLayoutOffset - lastPaintLayoutOffset) * effectiveZoom;
+ }
+
+ return AsyncTransform(compositedAsyncZoom, -translation);
+}
+
+AsyncTransformComponentMatrix
+AsyncPanZoomController::GetCurrentAsyncTransformWithOverscroll(
+ AsyncTransformConsumer aMode, AsyncTransformComponents aComponents,
+ std::size_t aSampleIndex) const {
+ AsyncTransformComponentMatrix asyncTransform =
+ GetCurrentAsyncTransform(aMode, aComponents, aSampleIndex);
+ // The overscroll transform is considered part of the layout component of
+ // the async transform, because it should not apply to fixed content.
+ if (aComponents.contains(AsyncTransformComponent::eLayout)) {
+ return asyncTransform * GetOverscrollTransform(aMode);
+ }
+ return asyncTransform;
+}
+
+LayoutDeviceToParentLayerScale AsyncPanZoomController::GetCurrentPinchZoomScale(
+ AsyncTransformConsumer aMode) const {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ AutoApplyAsyncTestAttributes testAttributeApplier(this, lock);
+ CSSToParentLayerScale scale = GetEffectiveZoom(aMode, lock);
+ return scale / Metrics().GetDevPixelsPerCSSPixel();
+}
+
+AutoTArray<wr::SampledScrollOffset, 2>
+AsyncPanZoomController::GetSampledScrollOffsets() const {
+ AssertOnSamplerThread();
+
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+
+ const AsyncTransformComponents asyncTransformComponents =
+ GetZoomAnimationId()
+ ? AsyncTransformComponents{AsyncTransformComponent::eLayout}
+ : LayoutAndVisual;
+
+ // If layerTranslation includes only the layout component of the async
+ // transform then it has not been scaled by the async zoom, so we want to
+ // divide it by the resolution. If layerTranslation includes the visual
+ // component, then we should use the pinch zoom scale, which includes the
+ // async zoom. However, we only use LayoutAndVisual for non-zoomable APZCs,
+ // so it makes no difference.
+ LayoutDeviceToParentLayerScale resolution =
+ GetCumulativeResolution() * LayerToParentLayerScale(1.0f);
+
+ AutoTArray<wr::SampledScrollOffset, 2> sampledOffsets;
+
+ for (std::deque<SampledAPZCState>::size_type index = 0;
+ index < mSampledState.size(); index++) {
+ ParentLayerPoint layerTranslation =
+ GetCurrentAsyncTransform(AsyncPanZoomController::eForCompositing,
+ asyncTransformComponents, index)
+ .mTranslation;
+
+ // Include the overscroll transform here in scroll offsets transform
+ // to ensure that we do not overscroll fixed content.
+ layerTranslation =
+ GetOverscrollTransform(AsyncPanZoomController::eForCompositing)
+ .TransformPoint(layerTranslation);
+ // The positive translation means the painted content is supposed to
+ // move down (or to the right), and that corresponds to a reduction in
+ // the scroll offset. Since we are effectively giving WR the async
+ // scroll delta here, we want to negate the translation.
+ LayoutDevicePoint asyncScrollDelta = -layerTranslation / resolution;
+ sampledOffsets.AppendElement(wr::SampledScrollOffset{
+ wr::ToLayoutVector2D(asyncScrollDelta),
+ wr::ToWrAPZScrollGeneration(mSampledState[index].Generation())});
+ }
+
+ return sampledOffsets;
+}
+
+bool AsyncPanZoomController::SuppressAsyncScrollOffset() const {
+ return mScrollMetadata.IsApzForceDisabled() ||
+ (Metrics().IsMinimalDisplayPort() &&
+ StaticPrefs::apz_prefer_jank_minimal_displayports());
+}
+
+CSSRect AsyncPanZoomController::GetEffectiveLayoutViewport(
+ AsyncTransformConsumer aMode, const RecursiveMutexAutoLock& aProofOfLock,
+ std::size_t aSampleIndex) const {
+ if (aMode == eForCompositing && SuppressAsyncScrollOffset()) {
+ return mLastContentPaintMetrics.GetLayoutViewport();
+ }
+ if (aMode == eForCompositing) {
+ return mSampledState[aSampleIndex].GetLayoutViewport();
+ }
+ return Metrics().GetLayoutViewport();
+}
+
+CSSPoint AsyncPanZoomController::GetEffectiveScrollOffset(
+ AsyncTransformConsumer aMode, const RecursiveMutexAutoLock& aProofOfLock,
+ std::size_t aSampleIndex) const {
+ if (aMode == eForCompositing && SuppressAsyncScrollOffset()) {
+ return mLastContentPaintMetrics.GetVisualScrollOffset();
+ }
+ if (aMode == eForCompositing) {
+ return mSampledState[aSampleIndex].GetVisualScrollOffset();
+ }
+ return Metrics().GetVisualScrollOffset();
+}
+
+CSSToParentLayerScale AsyncPanZoomController::GetEffectiveZoom(
+ AsyncTransformConsumer aMode, const RecursiveMutexAutoLock& aProofOfLock,
+ std::size_t aSampleIndex) const {
+ if (aMode == eForCompositing && SuppressAsyncScrollOffset()) {
+ return mLastContentPaintMetrics.GetZoom();
+ }
+ if (aMode == eForCompositing) {
+ return mSampledState[aSampleIndex].GetZoom();
+ }
+ return Metrics().GetZoom();
+}
+
+void AsyncPanZoomController::AdvanceToNextSample() {
+ AssertOnSamplerThread();
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ // Always keep at least one state in mSampledState.
+ if (mSampledState.size() > 1) {
+ mSampledState.pop_front();
+ }
+}
+
+bool AsyncPanZoomController::SampleCompositedAsyncTransform(
+ const RecursiveMutexAutoLock& aProofOfLock) {
+ MOZ_ASSERT(mSampledState.size() <= 2);
+ bool sampleChanged = (mSampledState.back() != SampledAPZCState(Metrics()));
+ mSampledState.emplace_back(Metrics(), std::move(mScrollPayload),
+ mScrollGeneration);
+ return sampleChanged;
+}
+
+void AsyncPanZoomController::ResampleCompositedAsyncTransform(
+ const RecursiveMutexAutoLock& aProofOfLock) {
+ // This only gets called during testing situations, so the fact that this
+ // drops the scroll payload from mSampledState.front() is not really a
+ // problem.
+ if (APZCTreeManager* treeManagerLocal = GetApzcTreeManager()) {
+ mScrollGeneration = treeManagerLocal->NewAPZScrollGeneration();
+ }
+ mSampledState.front() = SampledAPZCState(Metrics(), {}, mScrollGeneration);
+}
+
+void AsyncPanZoomController::ApplyAsyncTestAttributes(
+ const RecursiveMutexAutoLock& aProofOfLock) {
+ if (mTestAttributeAppliers == 0) {
+ if (mTestAsyncScrollOffset != CSSPoint() ||
+ mTestAsyncZoom != LayerToParentLayerScale()) {
+ // TODO Currently we update Metrics() and resample, which will cause
+ // the very latest user input to get immediately captured in the sample,
+ // and may defeat our attempt at "frame delay" (i.e. delaying the user
+ // input from affecting composition by one frame).
+ // Instead, maybe we should just apply the mTest* stuff directly to
+ // mSampledState.front(). We can even save/restore that SampledAPZCState
+ // instance in the AutoApplyAsyncTestAttributes instead of Metrics().
+ Metrics().ZoomBy(mTestAsyncZoom.scale);
+ CSSPoint asyncScrollPosition = Metrics().GetVisualScrollOffset();
+ CSSPoint requestedPoint =
+ asyncScrollPosition + this->mTestAsyncScrollOffset;
+ CSSPoint clampedPoint =
+ Metrics().CalculateScrollRange().ClampPoint(requestedPoint);
+ CSSPoint difference = mTestAsyncScrollOffset - clampedPoint;
+
+ ScrollByAndClamp(mTestAsyncScrollOffset);
+
+ if (StaticPrefs::apz_overscroll_test_async_scroll_offset_enabled()) {
+ ParentLayerPoint overscroll = difference * Metrics().GetZoom();
+ OverscrollBy(overscroll);
+ }
+ ResampleCompositedAsyncTransform(aProofOfLock);
+ }
+ }
+ ++mTestAttributeAppliers;
+}
+
+void AsyncPanZoomController::UnapplyAsyncTestAttributes(
+ const RecursiveMutexAutoLock& aProofOfLock,
+ const FrameMetrics& aPrevFrameMetrics,
+ const ParentLayerPoint& aPrevOverscroll) {
+ MOZ_ASSERT(mTestAttributeAppliers >= 1);
+ --mTestAttributeAppliers;
+ if (mTestAttributeAppliers == 0) {
+ if (mTestAsyncScrollOffset != CSSPoint() ||
+ mTestAsyncZoom != LayerToParentLayerScale()) {
+ Metrics() = aPrevFrameMetrics;
+ RestoreOverscrollAmount(aPrevOverscroll);
+ ResampleCompositedAsyncTransform(aProofOfLock);
+ }
+ }
+}
+
+Matrix4x4 AsyncPanZoomController::GetTransformToLastDispatchedPaint(
+ const AsyncTransformComponents& aComponents) const {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ CSSPoint componentOffset;
+
+ // The computation of the componentOffset should roughly be the negation
+ // of the translation in GetCurrentAsyncTransform() with the expected
+ // gecko metrics substituted for the effective scroll offsets.
+ if (aComponents.contains(AsyncTransformComponent::eVisual)) {
+ componentOffset += mExpectedGeckoMetrics.GetLayoutScrollOffset() -
+ mExpectedGeckoMetrics.GetVisualScrollOffset();
+ }
+
+ if (aComponents.contains(AsyncTransformComponent::eLayout)) {
+ CSSPoint lastPaintLayoutOffset;
+
+ if (mLastContentPaintMetrics.IsScrollable()) {
+ lastPaintLayoutOffset = mLastContentPaintMetrics.GetLayoutScrollOffset();
+ }
+
+ componentOffset +=
+ lastPaintLayoutOffset - mExpectedGeckoMetrics.GetLayoutScrollOffset();
+ }
+
+ LayerPoint scrollChange = componentOffset *
+ mLastContentPaintMetrics.GetDevPixelsPerCSSPixel() *
+ mLastContentPaintMetrics.GetCumulativeResolution();
+
+ // We're interested in the async zoom change. Factor out the content scale
+ // that may change when dragging the window to a monitor with a different
+ // content scale.
+ LayoutDeviceToParentLayerScale lastContentZoom =
+ mLastContentPaintMetrics.GetZoom() /
+ mLastContentPaintMetrics.GetDevPixelsPerCSSPixel();
+ LayoutDeviceToParentLayerScale lastDispatchedZoom =
+ mExpectedGeckoMetrics.GetZoom() /
+ mExpectedGeckoMetrics.GetDevPixelsPerCSSPixel();
+ float zoomChange = 1.0;
+ if (aComponents.contains(AsyncTransformComponent::eVisual) &&
+ lastDispatchedZoom != LayoutDeviceToParentLayerScale(0)) {
+ zoomChange = lastContentZoom.scale / lastDispatchedZoom.scale;
+ }
+ return Matrix4x4::Translation(scrollChange.x, scrollChange.y, 0)
+ .PostScale(zoomChange, zoomChange, 1);
+}
+
+CSSRect AsyncPanZoomController::GetVisibleRect(
+ const RecursiveMutexAutoLock& aProofOfLock) const {
+ AutoApplyAsyncTestAttributes testAttributeApplier(this, aProofOfLock);
+ CSSPoint currentScrollOffset = GetEffectiveScrollOffset(
+ AsyncPanZoomController::eForCompositing, aProofOfLock);
+ CSSRect visible = CSSRect(currentScrollOffset,
+ Metrics().CalculateCompositedSizeInCssPixels());
+ return visible;
+}
+
+static CSSRect GetPaintedRect(const FrameMetrics& aFrameMetrics) {
+ CSSRect displayPort = aFrameMetrics.GetDisplayPort();
+ if (displayPort.IsEmpty()) {
+ // Fallback to use the viewport if the diplayport hasn't been set.
+ // This situation often happens non-scrollable iframe's root scroller in
+ // Fission.
+ return aFrameMetrics.GetVisualViewport();
+ }
+
+ return displayPort + aFrameMetrics.GetLayoutScrollOffset();
+}
+
+uint32_t AsyncPanZoomController::GetCheckerboardMagnitude(
+ const ParentLayerRect& aClippedCompositionBounds) const {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+
+ CSSRect painted = GetPaintedRect(mLastContentPaintMetrics);
+ painted.Inflate(CSSMargin::FromAppUnits(
+ nsMargin(1, 1, 1, 1))); // fuzz for rounding error
+
+ CSSRect visible = GetVisibleRect(lock); // relative to scrolled frame origin
+ if (visible.IsEmpty() || painted.Contains(visible)) {
+ // early-exit if we're definitely not checkerboarding
+ return 0;
+ }
+
+ // aClippedCompositionBounds and Metrics().GetCompositionBounds() are both
+ // relative to the layer tree origin.
+ // The "*RelativeToItself*" variables are relative to the comp bounds origin
+ ParentLayerRect visiblePartOfCompBoundsRelativeToItself =
+ aClippedCompositionBounds - Metrics().GetCompositionBounds().TopLeft();
+ CSSRect visiblePartOfCompBoundsRelativeToItselfInCssSpace;
+ if (Metrics().GetZoom() != CSSToParentLayerScale(0)) {
+ visiblePartOfCompBoundsRelativeToItselfInCssSpace =
+ (visiblePartOfCompBoundsRelativeToItself / Metrics().GetZoom());
+ }
+
+ // This one is relative to the scrolled frame origin, same as `visible`
+ CSSRect visiblePartOfCompBoundsInCssSpace =
+ visiblePartOfCompBoundsRelativeToItselfInCssSpace + visible.TopLeft();
+
+ visible = visible.Intersect(visiblePartOfCompBoundsInCssSpace);
+
+ CSSIntRegion checkerboard;
+ // Round so as to minimize checkerboarding; if we're only showing fractional
+ // pixels of checkerboarding it's not really worth counting
+ checkerboard.Sub(RoundedIn(visible), RoundedOut(painted));
+ uint32_t area = checkerboard.Area();
+ if (area) {
+ APZC_LOG_FM(Metrics(),
+ "%p is currently checkerboarding (painted %s visible %s)", this,
+ ToString(painted).c_str(), ToString(visible).c_str());
+ }
+ return area;
+}
+
+void AsyncPanZoomController::ReportCheckerboard(
+ const SampleTime& aSampleTime,
+ const ParentLayerRect& aClippedCompositionBounds) {
+ if (mLastCheckerboardReport == aSampleTime) {
+ // This function will get called multiple times for each APZC on a single
+ // composite (once for each layer it is attached to). Only report the
+ // checkerboard once per composite though.
+ return;
+ }
+ mLastCheckerboardReport = aSampleTime;
+
+ bool recordTrace = StaticPrefs::apz_record_checkerboarding();
+ bool forTelemetry = Telemetry::CanRecordBase();
+ uint32_t magnitude = GetCheckerboardMagnitude(aClippedCompositionBounds);
+
+ // IsInTransformingState() acquires the APZC lock and thus needs to
+ // be called before acquiring mCheckerboardEventLock.
+ bool inTransformingState = IsInTransformingState();
+
+ MutexAutoLock lock(mCheckerboardEventLock);
+ if (!mCheckerboardEvent && (recordTrace || forTelemetry)) {
+ mCheckerboardEvent = MakeUnique<CheckerboardEvent>(recordTrace);
+ }
+ mPotentialCheckerboardTracker.InTransform(inTransformingState,
+ recordTrace || forTelemetry);
+ if (magnitude) {
+ mPotentialCheckerboardTracker.CheckerboardSeen();
+ }
+ UpdateCheckerboardEvent(lock, magnitude);
+}
+
+void AsyncPanZoomController::UpdateCheckerboardEvent(
+ const MutexAutoLock& aProofOfLock, uint32_t aMagnitude) {
+ if (mCheckerboardEvent && mCheckerboardEvent->RecordFrameInfo(aMagnitude)) {
+ // This checkerboard event is done. Report some metrics to telemetry.
+ mozilla::Telemetry::Accumulate(mozilla::Telemetry::CHECKERBOARD_SEVERITY,
+ mCheckerboardEvent->GetSeverity());
+ mozilla::Telemetry::Accumulate(mozilla::Telemetry::CHECKERBOARD_PEAK,
+ mCheckerboardEvent->GetPeak());
+ mozilla::Telemetry::Accumulate(
+ mozilla::Telemetry::CHECKERBOARD_DURATION,
+ (uint32_t)mCheckerboardEvent->GetDuration().ToMilliseconds());
+
+ // mCheckerboardEvent only gets created if we are supposed to record
+ // telemetry so we always pass true for aRecordTelemetry.
+ mPotentialCheckerboardTracker.CheckerboardDone(
+ /* aRecordTelemetry = */ true);
+
+ if (StaticPrefs::apz_record_checkerboarding()) {
+ // if the pref is enabled, also send it to the storage class. it may be
+ // chosen for public display on about:checkerboard, the hall of fame for
+ // checkerboard events.
+ uint32_t severity = mCheckerboardEvent->GetSeverity();
+ std::string log = mCheckerboardEvent->GetLog();
+ CheckerboardEventStorage::Report(severity, log);
+ }
+ mCheckerboardEvent = nullptr;
+ }
+}
+
+void AsyncPanZoomController::FlushActiveCheckerboardReport() {
+ MutexAutoLock lock(mCheckerboardEventLock);
+ // Pretend like we got a frame with 0 pixels checkerboarded. This will
+ // terminate the checkerboard event and flush it out
+ UpdateCheckerboardEvent(lock, 0);
+}
+
+void AsyncPanZoomController::NotifyLayersUpdated(
+ const ScrollMetadata& aScrollMetadata, bool aIsFirstPaint,
+ bool aThisLayerTreeUpdated) {
+ AssertOnUpdaterThread();
+
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ bool isDefault = mScrollMetadata.IsDefault();
+
+ const FrameMetrics& aLayerMetrics = aScrollMetadata.GetMetrics();
+
+ if ((aScrollMetadata == mLastContentPaintMetadata) && !isDefault) {
+ // No new information here, skip it.
+ APZC_LOGV("%p NotifyLayersUpdated short-circuit\n", this);
+ return;
+ }
+
+ // If the Metrics scroll offset is different from the last scroll offset
+ // that the main-thread sent us, then we know that the user has been doing
+ // something that triggers a scroll. This check is the APZ equivalent of the
+ // check on the main-thread at
+ // https://hg.mozilla.org/mozilla-central/file/97a52326b06a/layout/generic/nsGfxScrollFrame.cpp#l4050
+ // There is code below (the use site of userScrolled) that prevents a
+ // restored- scroll-position update from overwriting a user scroll, again
+ // equivalent to how the main thread code does the same thing.
+ // XXX Suspicious comparison between layout and visual scroll offsets.
+ // This may not do the right thing when we're zoomed in.
+ CSSPoint lastScrollOffset = mLastContentPaintMetrics.GetLayoutScrollOffset();
+ bool userScrolled = !FuzzyEqualsAdditive(Metrics().GetVisualScrollOffset().x,
+ lastScrollOffset.x) ||
+ !FuzzyEqualsAdditive(Metrics().GetVisualScrollOffset().y,
+ lastScrollOffset.y);
+
+ if (aScrollMetadata.DidContentGetPainted()) {
+ mLastContentPaintMetadata = aScrollMetadata;
+ }
+
+ mScrollMetadata.SetScrollParentId(aScrollMetadata.GetScrollParentId());
+ APZC_LOGV_FM(aLayerMetrics,
+ "%p got a NotifyLayersUpdated with aIsFirstPaint=%d, "
+ "aThisLayerTreeUpdated=%d",
+ this, aIsFirstPaint, aThisLayerTreeUpdated);
+
+ { // scope lock
+ MutexAutoLock lock(mCheckerboardEventLock);
+ if (mCheckerboardEvent && mCheckerboardEvent->IsRecordingTrace()) {
+ std::string str;
+ if (aThisLayerTreeUpdated) {
+ if (!aLayerMetrics.GetPaintRequestTime().IsNull()) {
+ // Note that we might get the paint request time as non-null, but with
+ // aThisLayerTreeUpdated false. That can happen if we get a layer
+ // transaction from a different process right after we get the layer
+ // transaction with aThisLayerTreeUpdated == true. In this case we
+ // want to ignore the paint request time because it was already dumped
+ // in the previous layer transaction.
+ TimeDuration paintTime =
+ TimeStamp::Now() - aLayerMetrics.GetPaintRequestTime();
+ std::stringstream info;
+ info << " painttime " << paintTime.ToMilliseconds();
+ str = info.str();
+ } else {
+ // This might be indicative of a wasted paint particularly if it
+ // happens during a checkerboard event.
+ str = " (this layertree updated)";
+ }
+ }
+ mCheckerboardEvent->UpdateRendertraceProperty(
+ CheckerboardEvent::Page, aLayerMetrics.GetScrollableRect());
+ mCheckerboardEvent->UpdateRendertraceProperty(
+ CheckerboardEvent::PaintedDisplayPort, GetPaintedRect(aLayerMetrics),
+ str);
+ }
+ }
+
+ // The main thread may send us a visual scroll offset update. This is
+ // different from a layout viewport offset update in that the layout viewport
+ // offset is limited to the layout scroll range, while the visual viewport
+ // offset is not.
+ // However, there are some conditions in which the layout update will clobber
+ // the visual update, and we want to ignore the visual update in those cases.
+ // This variable tracks that.
+ bool ignoreVisualUpdate = false;
+
+ // TODO if we're in a drag and scrollOffsetUpdated is set then we want to
+ // ignore it
+
+ bool needContentRepaint = false;
+ RepaintUpdateType contentRepaintType = RepaintUpdateType::eNone;
+ bool viewportSizeUpdated = false;
+ bool needToReclampScroll = false;
+
+ if ((aIsFirstPaint && aThisLayerTreeUpdated) || isDefault ||
+ Metrics().IsRootContent() != aLayerMetrics.IsRootContent()) {
+ if (Metrics().IsRootContent() && !aLayerMetrics.IsRootContent()) {
+ // We only support zooming on root content APZCs
+ SetZoomAnimationId(Nothing());
+ }
+
+ // Initialize our internal state to something sane when the content
+ // that was just painted is something we knew nothing about previously
+ CancelAnimation();
+
+ // Keep our existing scroll generation, if there are scroll updates. In this
+ // case we'll update our scroll generation when processing the scroll update
+ // array below. If there are no scroll updates, take the generation from the
+ // incoming metrics. Bug 1662019 will simplify this later.
+ ScrollGeneration oldScrollGeneration = Metrics().GetScrollGeneration();
+ mScrollMetadata = aScrollMetadata;
+ if (!aScrollMetadata.GetScrollUpdates().IsEmpty()) {
+ Metrics().SetScrollGeneration(oldScrollGeneration);
+ }
+
+ mExpectedGeckoMetrics.UpdateFrom(aLayerMetrics);
+
+ for (auto& sampledState : mSampledState) {
+ sampledState.UpdateScrollProperties(Metrics());
+ sampledState.UpdateZoomProperties(Metrics());
+ }
+
+ if (aLayerMetrics.HasNonZeroDisplayPortMargins()) {
+ // A non-zero display port margin here indicates a displayport has
+ // been set by a previous APZC for the content at this guid. The
+ // scrollable rect may have changed since then, making the margins
+ // wrong, so we need to calculate a new display port.
+ // It is important that we request a repaint here only when we need to
+ // otherwise we will end up setting a display port on every frame that
+ // gets a view id.
+ APZC_LOG("%p detected non-empty margins which probably need updating\n",
+ this);
+ needContentRepaint = true;
+ }
+ } else {
+ // If we're not taking the aLayerMetrics wholesale we still need to pull
+ // in some things into our local Metrics() because these things are
+ // determined by Gecko and our copy in Metrics() may be stale.
+
+ if (Metrics().GetLayoutViewport().Size() !=
+ aLayerMetrics.GetLayoutViewport().Size()) {
+ CSSRect layoutViewport = Metrics().GetLayoutViewport();
+ // The offset will be updated if necessary via
+ // RecalculateLayoutViewportOffset().
+ layoutViewport.SizeTo(aLayerMetrics.GetLayoutViewport().Size());
+ Metrics().SetLayoutViewport(layoutViewport);
+
+ needContentRepaint = true;
+ viewportSizeUpdated = true;
+ }
+
+ // TODO: Rely entirely on |aScrollMetadata.IsResolutionUpdated()| to
+ // determine which branch to take, and drop the other conditions.
+ CSSToParentLayerScale oldZoom = Metrics().GetZoom();
+ if (FuzzyEqualsAdditive(
+ Metrics().GetCompositionBoundsWidthIgnoringScrollbars(),
+ aLayerMetrics.GetCompositionBoundsWidthIgnoringScrollbars()) &&
+ Metrics().GetDevPixelsPerCSSPixel() ==
+ aLayerMetrics.GetDevPixelsPerCSSPixel() &&
+ !viewportSizeUpdated && !aScrollMetadata.IsResolutionUpdated()) {
+ // Any change to the pres shell resolution was requested by APZ and is
+ // already included in our zoom; however, other components of the
+ // cumulative resolution (a parent document's pres-shell resolution, or
+ // the css-driven resolution) may have changed, and we need to update
+ // our zoom to reflect that. Note that we can't just take
+ // aLayerMetrics.mZoom because the APZ may have additional async zoom
+ // since the repaint request.
+ float totalResolutionChange = 1.0;
+
+ if (Metrics().GetCumulativeResolution() != LayoutDeviceToLayerScale(0)) {
+ totalResolutionChange = aLayerMetrics.GetCumulativeResolution().scale /
+ Metrics().GetCumulativeResolution().scale;
+ }
+
+ float presShellResolutionChange = aLayerMetrics.GetPresShellResolution() /
+ Metrics().GetPresShellResolution();
+ if (presShellResolutionChange != 1.0f) {
+ needContentRepaint = true;
+ }
+ Metrics().ZoomBy(totalResolutionChange / presShellResolutionChange);
+ for (auto& sampledState : mSampledState) {
+ sampledState.ZoomBy(totalResolutionChange / presShellResolutionChange);
+ }
+ } else {
+ // Take the new zoom as either device scale or composition width or
+ // viewport size got changed (e.g. due to orientation change, or content
+ // changing the meta-viewport tag), or the main thread originated a
+ // resolution change for another reason (e.g. Ctrl+0 was pressed to
+ // reset the zoom).
+ Metrics().SetZoom(aLayerMetrics.GetZoom());
+ for (auto& sampledState : mSampledState) {
+ sampledState.UpdateZoomProperties(aLayerMetrics);
+ }
+ Metrics().SetDevPixelsPerCSSPixel(
+ aLayerMetrics.GetDevPixelsPerCSSPixel());
+ }
+
+ if (Metrics().GetZoom() != oldZoom) {
+ // If the zoom changed, the scroll range in CSS pixels may have changed
+ // even if the composition bounds didn't.
+ needToReclampScroll = true;
+ }
+
+ mExpectedGeckoMetrics.UpdateZoomFrom(aLayerMetrics);
+
+ if (!Metrics().GetScrollableRect().IsEqualEdges(
+ aLayerMetrics.GetScrollableRect())) {
+ Metrics().SetScrollableRect(aLayerMetrics.GetScrollableRect());
+ needContentRepaint = true;
+ needToReclampScroll = true;
+ }
+ if (!Metrics().GetCompositionBounds().IsEqualEdges(
+ aLayerMetrics.GetCompositionBounds())) {
+ Metrics().SetCompositionBounds(aLayerMetrics.GetCompositionBounds());
+ needToReclampScroll = true;
+ }
+ Metrics().SetCompositionBoundsWidthIgnoringScrollbars(
+ aLayerMetrics.GetCompositionBoundsWidthIgnoringScrollbars());
+
+ if (Metrics().IsRootContent() &&
+ Metrics().GetCompositionSizeWithoutDynamicToolbar() !=
+ aLayerMetrics.GetCompositionSizeWithoutDynamicToolbar()) {
+ Metrics().SetCompositionSizeWithoutDynamicToolbar(
+ aLayerMetrics.GetCompositionSizeWithoutDynamicToolbar());
+ needToReclampScroll = true;
+ }
+ Metrics().SetBoundingCompositionSize(
+ aLayerMetrics.GetBoundingCompositionSize());
+ Metrics().SetPresShellResolution(aLayerMetrics.GetPresShellResolution());
+ Metrics().SetCumulativeResolution(aLayerMetrics.GetCumulativeResolution());
+ Metrics().SetTransformToAncestorScale(
+ aLayerMetrics.GetTransformToAncestorScale());
+ mScrollMetadata.SetHasScrollgrab(aScrollMetadata.GetHasScrollgrab());
+ mScrollMetadata.SetLineScrollAmount(aScrollMetadata.GetLineScrollAmount());
+ mScrollMetadata.SetPageScrollAmount(aScrollMetadata.GetPageScrollAmount());
+ mScrollMetadata.SetSnapInfo(ScrollSnapInfo(aScrollMetadata.GetSnapInfo()));
+ mScrollMetadata.SetIsLayersIdRoot(aScrollMetadata.IsLayersIdRoot());
+ mScrollMetadata.SetIsAutoDirRootContentRTL(
+ aScrollMetadata.IsAutoDirRootContentRTL());
+ Metrics().SetIsScrollInfoLayer(aLayerMetrics.IsScrollInfoLayer());
+ Metrics().SetHasNonZeroDisplayPortMargins(
+ aLayerMetrics.HasNonZeroDisplayPortMargins());
+ Metrics().SetMinimalDisplayPort(aLayerMetrics.IsMinimalDisplayPort());
+ mScrollMetadata.SetForceDisableApz(aScrollMetadata.IsApzForceDisabled());
+ mScrollMetadata.SetIsRDMTouchSimulationActive(
+ aScrollMetadata.GetIsRDMTouchSimulationActive());
+ mScrollMetadata.SetForceMousewheelAutodir(
+ aScrollMetadata.ForceMousewheelAutodir());
+ mScrollMetadata.SetForceMousewheelAutodirHonourRoot(
+ aScrollMetadata.ForceMousewheelAutodirHonourRoot());
+ mScrollMetadata.SetIsPaginatedPresentation(
+ aScrollMetadata.IsPaginatedPresentation());
+ mScrollMetadata.SetDisregardedDirection(
+ aScrollMetadata.GetDisregardedDirection());
+ mScrollMetadata.SetOverscrollBehavior(
+ aScrollMetadata.GetOverscrollBehavior());
+ }
+
+ bool scrollOffsetUpdated = false;
+ bool smoothScrollRequested = false;
+ bool didCancelAnimation = false;
+ Maybe<CSSPoint> cumulativeRelativeDelta;
+ for (const auto& scrollUpdate : aScrollMetadata.GetScrollUpdates()) {
+ APZC_LOG("%p processing scroll update %s\n", this,
+ ToString(scrollUpdate).c_str());
+ if (!(Metrics().GetScrollGeneration() < scrollUpdate.GetGeneration())) {
+ // This is stale, let's ignore it
+ APZC_LOG("%p scrollupdate generation stale, dropping\n", this);
+ continue;
+ }
+ Metrics().SetScrollGeneration(scrollUpdate.GetGeneration());
+
+ MOZ_ASSERT(scrollUpdate.GetOrigin() != ScrollOrigin::Apz);
+ if (userScrolled &&
+ !nsLayoutUtils::CanScrollOriginClobberApz(scrollUpdate.GetOrigin())) {
+ APZC_LOG("%p scrollupdate cannot clobber APZ userScrolled\n", this);
+ continue;
+ }
+ // XXX: if we get here, |scrollUpdate| is clobbering APZ, so we may want
+ // to reset |userScrolled| back to false so that subsequent scrollUpdates
+ // in this loop don't get dropped by the check above. Need to add a test
+ // that exercises this scenario, as we don't currently have one.
+
+ if (scrollUpdate.GetMode() == ScrollMode::Smooth ||
+ scrollUpdate.GetMode() == ScrollMode::SmoothMsd) {
+ smoothScrollRequested = true;
+
+ // Requests to animate the visual scroll position override requests to
+ // simply update the visual scroll offset to a particular point. Since
+ // we have an animation request, we set ignoreVisualUpdate to true to
+ // indicate we don't need to apply the visual scroll update in
+ // aLayerMetrics.
+ ignoreVisualUpdate = true;
+
+ // For relative updates we want to add the relative offset to any existing
+ // destination, or the current visual offset if there is no existing
+ // destination.
+ CSSPoint base = GetCurrentAnimationDestination(lock).valueOr(
+ Metrics().GetVisualScrollOffset());
+
+ CSSPoint destination;
+ if (scrollUpdate.GetType() == ScrollUpdateType::Relative) {
+ CSSPoint delta =
+ scrollUpdate.GetDestination() - scrollUpdate.GetSource();
+ APZC_LOG("%p relative smooth scrolling from %s by %s\n", this,
+ ToString(base).c_str(), ToString(delta).c_str());
+ destination = Metrics().CalculateScrollRange().ClampPoint(base + delta);
+ } else if (scrollUpdate.GetType() == ScrollUpdateType::PureRelative) {
+ CSSPoint delta = scrollUpdate.GetDelta();
+ APZC_LOG("%p pure-relative smooth scrolling from %s by %s\n", this,
+ ToString(base).c_str(), ToString(delta).c_str());
+ destination = Metrics().CalculateScrollRange().ClampPoint(base + delta);
+ } else {
+ APZC_LOG("%p smooth scrolling to %s\n", this,
+ ToString(scrollUpdate.GetDestination()).c_str());
+ destination = scrollUpdate.GetDestination();
+ }
+
+ if (scrollUpdate.GetMode() == ScrollMode::SmoothMsd) {
+ SmoothMsdScrollTo(
+ CSSSnapTarget{destination, scrollUpdate.GetSnapTargetIds()},
+ scrollUpdate.GetScrollTriggeredByScript());
+ } else {
+ MOZ_ASSERT(scrollUpdate.GetMode() == ScrollMode::Smooth);
+ MOZ_ASSERT(!scrollUpdate.WasTriggeredByScript());
+ SmoothScrollTo(destination, scrollUpdate.GetOrigin());
+ }
+ continue;
+ }
+
+ MOZ_ASSERT(scrollUpdate.GetMode() == ScrollMode::Instant ||
+ scrollUpdate.GetMode() == ScrollMode::Normal);
+
+ // If the layout update is of a higher priority than the visual update, then
+ // we don't want to apply the visual update.
+ // If the layout update is of a clobbering type (or a smooth scroll request,
+ // which is handled above) then it takes precedence over an eRestore visual
+ // update. But we also allow the possibility for the main thread to ask us
+ // to scroll both the layout and visual viewports to distinct (but
+ // compatible) locations (via e.g. both updates being of a non-clobbering/
+ // eRestore type).
+ if (nsLayoutUtils::CanScrollOriginClobberApz(scrollUpdate.GetOrigin()) &&
+ aLayerMetrics.GetVisualScrollUpdateType() !=
+ FrameMetrics::eMainThread) {
+ ignoreVisualUpdate = true;
+ }
+
+ Maybe<CSSPoint> relativeDelta;
+ if (scrollUpdate.GetType() == ScrollUpdateType::Relative) {
+ APZC_LOG(
+ "%p relative updating scroll offset from %s by %s\n", this,
+ ToString(Metrics().GetVisualScrollOffset()).c_str(),
+ ToString(scrollUpdate.GetDestination() - scrollUpdate.GetSource())
+ .c_str());
+
+ scrollOffsetUpdated = true;
+
+ // It's possible that the main thread has ignored an APZ scroll offset
+ // update for the pending relative scroll that we have just received.
+ // When this happens, we need to send a new scroll offset update with
+ // the combined scroll offset or else the main thread may have an
+ // incorrect scroll offset for a period of time.
+ if (Metrics().HasPendingScroll(aLayerMetrics)) {
+ needContentRepaint = true;
+ contentRepaintType = RepaintUpdateType::eUserAction;
+ }
+
+ relativeDelta =
+ Some(Metrics().ApplyRelativeScrollUpdateFrom(scrollUpdate));
+ Metrics().RecalculateLayoutViewportOffset();
+ } else if (scrollUpdate.GetType() == ScrollUpdateType::PureRelative) {
+ APZC_LOG("%p pure-relative updating scroll offset from %s by %s\n", this,
+ ToString(Metrics().GetVisualScrollOffset()).c_str(),
+ ToString(scrollUpdate.GetDelta()).c_str());
+
+ scrollOffsetUpdated = true;
+
+ // Always need a repaint request with a repaint type for pure relative
+ // scrolls because apz is doing the scroll at the main thread's request.
+ // The main thread has not updated it's scroll offset yet, it is depending
+ // on apz to tell it where to scroll.
+ needContentRepaint = true;
+ contentRepaintType = RepaintUpdateType::eVisualUpdate;
+
+ // We have to ignore a visual scroll offset update otherwise it will
+ // clobber the relative scrolling we are about to do. We perform
+ // visualScrollOffset = visualScrollOffset + delta. Then the
+ // visualScrollOffsetUpdated block below will do visualScrollOffset =
+ // aLayerMetrics.GetVisualDestination(). We need visual scroll offset
+ // updates to be incorporated into this scroll update loop to properly fix
+ // this.
+ ignoreVisualUpdate = true;
+
+ relativeDelta =
+ Some(Metrics().ApplyPureRelativeScrollUpdateFrom(scrollUpdate));
+ Metrics().RecalculateLayoutViewportOffset();
+ } else {
+ APZC_LOG("%p updating scroll offset from %s to %s\n", this,
+ ToString(Metrics().GetVisualScrollOffset()).c_str(),
+ ToString(scrollUpdate.GetDestination()).c_str());
+ bool offsetChanged = Metrics().ApplyScrollUpdateFrom(scrollUpdate);
+ Metrics().RecalculateLayoutViewportOffset();
+
+ if (offsetChanged || scrollUpdate.GetMode() != ScrollMode::Instant ||
+ scrollUpdate.GetType() != ScrollUpdateType::Absolute ||
+ scrollUpdate.GetOrigin() != ScrollOrigin::None) {
+ // We get a NewScrollFrame update for newly created scroll frames. Only
+ // if this was not a NewScrollFrame update or the offset changed do we
+ // request repaint. This is important so that we don't request repaint
+ // for every new content and set a full display port on it.
+ scrollOffsetUpdated = true;
+ }
+ }
+
+ if (relativeDelta) {
+ cumulativeRelativeDelta =
+ !cumulativeRelativeDelta
+ ? relativeDelta
+ : Some(*cumulativeRelativeDelta + *relativeDelta);
+ } else {
+ // If the scroll update is not relative, clobber the cumulative delta,
+ // i.e. later updates win.
+ cumulativeRelativeDelta.reset();
+ }
+
+ // If an animation is underway, tell it about the scroll offset update.
+ // Some animations can handle some scroll offset updates and continue
+ // running. Those that can't will return false, and we cancel them.
+ if (ShouldCancelAnimationForScrollUpdate(relativeDelta)) {
+ // Cancel the animation (which might also trigger a repaint request)
+ // after we update the scroll offset above. Otherwise we can be left
+ // in a state where things are out of sync.
+ CancelAnimation();
+ didCancelAnimation = true;
+ }
+ }
+
+ if (scrollOffsetUpdated) {
+ for (auto& sampledState : mSampledState) {
+ if (!didCancelAnimation && cumulativeRelativeDelta.isSome()) {
+ sampledState.UpdateScrollPropertiesWithRelativeDelta(
+ Metrics(), *cumulativeRelativeDelta);
+ } else {
+ sampledState.UpdateScrollProperties(Metrics());
+ }
+ }
+
+ // Because of the scroll generation update, any inflight paint requests
+ // are going to be ignored by layout, and so mExpectedGeckoMetrics becomes
+ // incorrect for the purposes of calculating the LD transform. To correct
+ // this we need to update mExpectedGeckoMetrics to be the last thing we
+ // know was painted by Gecko.
+ mExpectedGeckoMetrics.UpdateFrom(aLayerMetrics);
+
+ // Since the scroll offset has changed, we need to recompute the
+ // displayport margins and send them to layout. Otherwise there might be
+ // scenarios where for example we scroll from the top of a page (where the
+ // top displayport margin is zero) to the bottom of a page, which will
+ // result in a displayport that doesn't extend upwards at all.
+ // Note that even if the CancelAnimation call above requested a repaint
+ // this is fine because we already have repaint request deduplication.
+ needContentRepaint = true;
+ // Since the main-thread scroll offset changed we should trigger a
+ // recomposite to make sure it becomes user-visible.
+ ScheduleComposite();
+ } else if (needToReclampScroll) {
+ // Even if we didn't accept a new scroll offset from content, the
+ // scrollable rect or composition bounds may have changed in a way that
+ // makes our local scroll offset out of bounds, so re-clamp it.
+ ClampAndSetVisualScrollOffset(Metrics().GetVisualScrollOffset());
+ for (auto& sampledState : mSampledState) {
+ sampledState.ClampVisualScrollOffset(Metrics());
+ }
+ }
+
+ // If our scroll range changed (for example, because the page dynamically
+ // loaded new content, thereby increasing the size of the scrollable rect),
+ // and we're overscrolled, being overscrolled may no longer be a valid
+ // state (for example, we may no longer be at the edge of our scroll range),
+ // so clear overscroll and discontinue any overscroll animation.
+ // Ideas for improvements here:
+ // - Instead of collapsing the overscroll gutter, try to "fill it"
+ // with newly loaded content. This would basically entail checking
+ // if (GetVisualScrollOffset() + GetOverscrollAmount()) is a valid
+ // visual scroll offset in our new scroll range, and if so, scrolling
+ // there.
+ if (needToReclampScroll) {
+ if (IsInInvalidOverscroll()) {
+ if (mState == OVERSCROLL_ANIMATION) {
+ CancelAnimation();
+ } else if (IsOverscrolled()) {
+ ClearOverscroll();
+ }
+ }
+ }
+
+ if (smoothScrollRequested && !scrollOffsetUpdated) {
+ mExpectedGeckoMetrics.UpdateFrom(aLayerMetrics);
+ // Need to acknowledge the request.
+ needContentRepaint = true;
+ }
+
+ // If `isDefault` is true, this APZC is a "new" one (this is the first time
+ // it's getting a NotifyLayersUpdated call). In this case we want to apply the
+ // visual scroll offset from the main thread to our scroll offset.
+ // The main thread may also ask us to scroll the visual viewport to a
+ // particular location. However, in all cases, we want to ignore the visual
+ // offset update if ignoreVisualUpdate is true, because we're clobbering
+ // the visual update with a layout update.
+ bool visualScrollOffsetUpdated =
+ !ignoreVisualUpdate &&
+ (isDefault ||
+ aLayerMetrics.GetVisualScrollUpdateType() != FrameMetrics::eNone);
+
+ if (visualScrollOffsetUpdated) {
+ APZC_LOG("%p updating visual scroll offset from %s to %s (updateType %d)\n",
+ this, ToString(Metrics().GetVisualScrollOffset()).c_str(),
+ ToString(aLayerMetrics.GetVisualDestination()).c_str(),
+ (int)aLayerMetrics.GetVisualScrollUpdateType());
+ bool offsetChanged = Metrics().ClampAndSetVisualScrollOffset(
+ aLayerMetrics.GetVisualDestination());
+
+ // If this is the first time we got metrics for this content (isDefault) and
+ // the update type was none and the offset didn't change then we don't have
+ // to do anything. This is important because we don't want to request
+ // repaint on the initial NotifyLayersUpdated for every content and thus set
+ // a full display port.
+ if (aLayerMetrics.GetVisualScrollUpdateType() == FrameMetrics::eNone &&
+ !offsetChanged) {
+ visualScrollOffsetUpdated = false;
+ }
+ }
+ if (visualScrollOffsetUpdated) {
+ // The rest of this branch largely follows the code in the
+ // |if (scrollOffsetUpdated)| branch above. Eventually it should get
+ // merged into that branch.
+ Metrics().RecalculateLayoutViewportOffset();
+ for (auto& sampledState : mSampledState) {
+ sampledState.UpdateScrollProperties(Metrics());
+ }
+ mExpectedGeckoMetrics.UpdateFrom(aLayerMetrics);
+ if (ShouldCancelAnimationForScrollUpdate(Nothing())) {
+ CancelAnimation();
+ }
+ // The main thread did not actually paint a displayport at the target
+ // visual offset, so we need to ask it to repaint. We need to set the
+ // contentRepaintType to something other than eNone, otherwise the main
+ // thread will short-circuit the repaint request.
+ // Don't do this for eRestore visual updates as a repaint coming from APZ
+ // breaks the scroll offset restoration mechanism.
+ needContentRepaint = true;
+ if (aLayerMetrics.GetVisualScrollUpdateType() ==
+ FrameMetrics::eMainThread) {
+ contentRepaintType = RepaintUpdateType::eVisualUpdate;
+ }
+ ScheduleComposite();
+ }
+
+ if (viewportSizeUpdated) {
+ // While we want to accept the main thread's layout viewport _size_,
+ // its position may be out of date in light of async scrolling, to
+ // adjust it if necessary to make sure it continues to enclose the
+ // visual viewport.
+ // Note: it's important to do this _after_ we've accepted any
+ // updated composition bounds.
+ Metrics().RecalculateLayoutViewportOffset();
+ }
+
+ if (needContentRepaint) {
+ // This repaint request could be driven by a user action if we accept a
+ // relative scroll offset update
+ RequestContentRepaint(contentRepaintType);
+ }
+}
+
+FrameMetrics& AsyncPanZoomController::Metrics() {
+ mRecursiveMutex.AssertCurrentThreadIn();
+ return mScrollMetadata.GetMetrics();
+}
+
+const FrameMetrics& AsyncPanZoomController::Metrics() const {
+ mRecursiveMutex.AssertCurrentThreadIn();
+ return mScrollMetadata.GetMetrics();
+}
+
+GeckoViewMetrics AsyncPanZoomController::GetGeckoViewMetrics() const {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ return GeckoViewMetrics{GetEffectiveScrollOffset(eForCompositing, lock),
+ GetEffectiveZoom(eForCompositing, lock)};
+}
+
+bool AsyncPanZoomController::UpdateRootFrameMetricsIfChanged(
+ GeckoViewMetrics& aMetrics) {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+
+ if (!Metrics().IsRootContent()) {
+ return false;
+ }
+
+ GeckoViewMetrics newMetrics = GetGeckoViewMetrics();
+ bool hasChanged = RoundedToInt(aMetrics.mVisualScrollOffset) !=
+ RoundedToInt(newMetrics.mVisualScrollOffset) ||
+ aMetrics.mZoom != newMetrics.mZoom;
+
+ if (hasChanged) {
+ aMetrics = newMetrics;
+ }
+
+ return hasChanged;
+}
+
+const FrameMetrics& AsyncPanZoomController::GetFrameMetrics() const {
+ return Metrics();
+}
+
+const ScrollMetadata& AsyncPanZoomController::GetScrollMetadata() const {
+ mRecursiveMutex.AssertCurrentThreadIn();
+ return mScrollMetadata;
+}
+
+void AsyncPanZoomController::AssertOnSamplerThread() const {
+ if (APZCTreeManager* treeManagerLocal = GetApzcTreeManager()) {
+ treeManagerLocal->AssertOnSamplerThread();
+ }
+}
+
+void AsyncPanZoomController::AssertOnUpdaterThread() const {
+ if (APZCTreeManager* treeManagerLocal = GetApzcTreeManager()) {
+ treeManagerLocal->AssertOnUpdaterThread();
+ }
+}
+
+APZCTreeManager* AsyncPanZoomController::GetApzcTreeManager() const {
+ mRecursiveMutex.AssertNotCurrentThreadIn();
+ return mTreeManager;
+}
+
+void AsyncPanZoomController::ZoomToRect(const ZoomTarget& aZoomTarget,
+ const uint32_t aFlags) {
+ CSSRect rect = aZoomTarget.targetRect;
+ if (!rect.IsFinite()) {
+ NS_WARNING("ZoomToRect got called with a non-finite rect; ignoring...");
+ return;
+ }
+
+ if (rect.IsEmpty() && (aFlags & DISABLE_ZOOM_OUT)) {
+ // Double-tap-to-zooming uses an empty rect to mean "zoom out".
+ // If zooming out is disabled, an empty rect is nonsensical
+ // and will produce undesirable scrolling.
+ NS_WARNING(
+ "ZoomToRect got called with an empty rect and zoom out disabled; "
+ "ignoring...");
+ return;
+ }
+
+ SetState(ANIMATING_ZOOM);
+
+ {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+
+ MOZ_ASSERT(Metrics().IsRootContent());
+
+ const float defaultZoomInAmount =
+ StaticPrefs::apz_doubletapzoom_defaultzoomin();
+
+ ParentLayerRect compositionBounds = Metrics().GetCompositionBounds();
+ CSSRect cssPageRect = Metrics().GetScrollableRect();
+ CSSPoint scrollOffset = Metrics().GetVisualScrollOffset();
+ CSSSize sizeBeforeZoom = Metrics().CalculateCompositedSizeInCssPixels();
+ CSSToParentLayerScale currentZoom = Metrics().GetZoom();
+ CSSToParentLayerScale targetZoom;
+
+ // The minimum zoom to prevent over-zoom-out.
+ // If the zoom factor is lower than this (i.e. we are zoomed more into the
+ // page), then the CSS content rect, in layers pixels, will be smaller than
+ // the composition bounds. If this happens, we can't fill the target
+ // composited area with this frame.
+ CSSToParentLayerScale localMinZoom(
+ std::max(compositionBounds.Width() / cssPageRect.Width(),
+ compositionBounds.Height() / cssPageRect.Height()));
+
+ localMinZoom.scale =
+ clamped(localMinZoom.scale, mZoomConstraints.mMinZoom.scale,
+ mZoomConstraints.mMaxZoom.scale);
+
+ localMinZoom = std::max(mZoomConstraints.mMinZoom, localMinZoom);
+ CSSToParentLayerScale localMaxZoom =
+ std::max(localMinZoom, mZoomConstraints.mMaxZoom);
+
+ if (!rect.IsEmpty()) {
+ // Intersect the zoom-to-rect to the CSS rect to make sure it fits.
+ rect = rect.Intersect(cssPageRect);
+ targetZoom = CSSToParentLayerScale(
+ std::min(compositionBounds.Width() / rect.Width(),
+ compositionBounds.Height() / rect.Height()));
+ if (aFlags & DISABLE_ZOOM_OUT) {
+ targetZoom = std::max(targetZoom, currentZoom);
+ }
+ }
+
+ // 1. If the rect is empty, the content-side logic for handling a double-tap
+ // requested that we zoom out.
+ // 2. currentZoom is equal to mZoomConstraints.mMaxZoom and user still
+ // double-tapping it
+ // Treat these cases as a request to zoom out as much as possible
+ // unless cantZoomOutBehavior == ZoomIn and currentZoom
+ // is equal to localMinZoom and user still double-tapping it, then try to
+ // zoom in a small amount to provide feedback to the user.
+ bool zoomOut = false;
+ // True if we are already zoomed out and we are asked to either stay there
+ // or zoom out more and cantZoomOutBehavior == ZoomIn.
+ bool zoomInDefaultAmount = false;
+ if (aFlags & DISABLE_ZOOM_OUT) {
+ zoomOut = false;
+ } else {
+ if (rect.IsEmpty()) {
+ if (currentZoom == localMinZoom &&
+ aZoomTarget.cantZoomOutBehavior == CantZoomOutBehavior::ZoomIn &&
+ (defaultZoomInAmount != 1.f)) {
+ zoomInDefaultAmount = true;
+ } else {
+ zoomOut = true;
+ }
+ } else if (currentZoom == localMaxZoom && targetZoom >= localMaxZoom) {
+ zoomOut = true;
+ }
+ }
+
+ // already at min zoom and asked to zoom out further
+ if (!zoomOut && currentZoom == localMinZoom && targetZoom <= localMinZoom &&
+ aZoomTarget.cantZoomOutBehavior == CantZoomOutBehavior::ZoomIn &&
+ (defaultZoomInAmount != 1.f)) {
+ zoomInDefaultAmount = true;
+ }
+ MOZ_ASSERT(!(zoomInDefaultAmount && zoomOut));
+
+ if (zoomInDefaultAmount) {
+ targetZoom =
+ CSSToParentLayerScale(currentZoom.scale * defaultZoomInAmount);
+ }
+
+ if (zoomOut) {
+ targetZoom = localMinZoom;
+ }
+
+ if (aFlags & PAN_INTO_VIEW_ONLY) {
+ targetZoom = currentZoom;
+ } else if (aFlags & ONLY_ZOOM_TO_DEFAULT_SCALE) {
+ CSSToParentLayerScale zoomAtDefaultScale =
+ Metrics().GetDevPixelsPerCSSPixel() *
+ LayoutDeviceToParentLayerScale(1.0);
+ if (targetZoom.scale > zoomAtDefaultScale.scale) {
+ // Only change the zoom if we are less than the default zoom
+ if (currentZoom.scale < zoomAtDefaultScale.scale) {
+ targetZoom = zoomAtDefaultScale;
+ } else {
+ targetZoom = currentZoom;
+ }
+ }
+ }
+
+ targetZoom.scale =
+ clamped(targetZoom.scale, localMinZoom.scale, localMaxZoom.scale);
+
+ FrameMetrics endZoomToMetrics = Metrics();
+ endZoomToMetrics.SetZoom(CSSToParentLayerScale(targetZoom));
+ CSSSize sizeAfterZoom =
+ endZoomToMetrics.CalculateCompositedSizeInCssPixels();
+
+ if (zoomInDefaultAmount || zoomOut) {
+ // For the zoom out case we should always center what was visible
+ // otherwise it feels like we are scrolling as well as zooming out. For
+ // the non-zoomOut case, if we've been provided a pointer location, zoom
+ // around that, otherwise just zoom in to the center of what's currently
+ // visible.
+ if (!zoomOut && aZoomTarget.documentRelativePointerPosition.isSome()) {
+ rect = CSSRect(aZoomTarget.documentRelativePointerPosition->x -
+ sizeAfterZoom.width / 2,
+ aZoomTarget.documentRelativePointerPosition->y -
+ sizeAfterZoom.height / 2,
+ sizeAfterZoom.Width(), sizeAfterZoom.Height());
+ } else {
+ rect = CSSRect(
+ scrollOffset.x + (sizeBeforeZoom.width - sizeAfterZoom.width) / 2,
+ scrollOffset.y + (sizeBeforeZoom.height - sizeAfterZoom.height) / 2,
+ sizeAfterZoom.Width(), sizeAfterZoom.Height());
+ }
+
+ rect = rect.Intersect(cssPageRect);
+ }
+
+ // Check if we can fit the full elementBoundingRect.
+ if (!aZoomTarget.targetRect.IsEmpty() && !zoomOut &&
+ aZoomTarget.elementBoundingRect.isSome()) {
+ MOZ_ASSERT(aZoomTarget.elementBoundingRect->Contains(rect));
+ CSSRect elementBoundingRect =
+ aZoomTarget.elementBoundingRect->Intersect(cssPageRect);
+ if (elementBoundingRect.width <= sizeAfterZoom.width &&
+ elementBoundingRect.height <= sizeAfterZoom.height) {
+ rect = elementBoundingRect;
+ }
+ }
+
+ // Vertically center the zoomed element in the screen.
+ if (!zoomOut && (sizeAfterZoom.height > rect.Height())) {
+ rect.MoveByY(-(sizeAfterZoom.height - rect.Height()) * 0.5f);
+ if (rect.Y() < 0.0f) {
+ rect.MoveToY(0.0f);
+ }
+ }
+
+ // Horizontally center the zoomed element in the screen.
+ if (!zoomOut && (sizeAfterZoom.width > rect.Width())) {
+ rect.MoveByX(-(sizeAfterZoom.width - rect.Width()) * 0.5f);
+ if (rect.X() < 0.0f) {
+ rect.MoveToX(0.0f);
+ }
+ }
+
+ bool intersectRectAgain = false;
+ // If we can't zoom out enough to show the full rect then shift the rect we
+ // are able to show to center what was visible.
+ // Note that this calculation works no matter the relation of sizeBeforeZoom
+ // to sizeAfterZoom, ie whether we are increasing or decreasing zoom.
+ if (!zoomOut && (sizeAfterZoom.height < rect.Height())) {
+ rect.y =
+ scrollOffset.y + (sizeBeforeZoom.height - sizeAfterZoom.height) / 2;
+ rect.height = sizeAfterZoom.Height();
+
+ intersectRectAgain = true;
+ }
+
+ if (!zoomOut && (sizeAfterZoom.width < rect.Width())) {
+ rect.x =
+ scrollOffset.x + (sizeBeforeZoom.width - sizeAfterZoom.width) / 2;
+ rect.width = sizeAfterZoom.Width();
+
+ intersectRectAgain = true;
+ }
+ if (intersectRectAgain) {
+ rect = rect.Intersect(cssPageRect);
+ }
+
+ // If any of these conditions are met, the page will be overscrolled after
+ // zoomed. Attempting to scroll outside of the valid scroll range will cause
+ // problems.
+ if (rect.Y() + sizeAfterZoom.height > cssPageRect.YMost()) {
+ rect.MoveToY(std::max(cssPageRect.Y(),
+ cssPageRect.YMost() - sizeAfterZoom.height));
+ }
+ if (rect.Y() < cssPageRect.Y()) {
+ rect.MoveToY(cssPageRect.Y());
+ }
+ if (rect.X() + sizeAfterZoom.width > cssPageRect.XMost()) {
+ rect.MoveToX(
+ std::max(cssPageRect.X(), cssPageRect.XMost() - sizeAfterZoom.width));
+ }
+ if (rect.X() < cssPageRect.X()) {
+ rect.MoveToY(cssPageRect.X());
+ }
+
+ endZoomToMetrics.SetVisualScrollOffset(rect.TopLeft());
+ endZoomToMetrics.RecalculateLayoutViewportOffset();
+
+ StartAnimation(new ZoomAnimation(
+ *this, Metrics().GetVisualScrollOffset(), Metrics().GetZoom(),
+ endZoomToMetrics.GetVisualScrollOffset(), endZoomToMetrics.GetZoom()));
+
+ RequestContentRepaint(RepaintUpdateType::eUserAction);
+ }
+}
+
+InputBlockState* AsyncPanZoomController::GetCurrentInputBlock() const {
+ return GetInputQueue()->GetCurrentBlock();
+}
+
+TouchBlockState* AsyncPanZoomController::GetCurrentTouchBlock() const {
+ return GetInputQueue()->GetCurrentTouchBlock();
+}
+
+PanGestureBlockState* AsyncPanZoomController::GetCurrentPanGestureBlock()
+ const {
+ return GetInputQueue()->GetCurrentPanGestureBlock();
+}
+
+PinchGestureBlockState* AsyncPanZoomController::GetCurrentPinchGestureBlock()
+ const {
+ return GetInputQueue()->GetCurrentPinchGestureBlock();
+}
+
+void AsyncPanZoomController::ResetTouchInputState() {
+ MultiTouchInput cancel(MultiTouchInput::MULTITOUCH_CANCEL, 0,
+ TimeStamp::Now(), 0);
+ RefPtr<GestureEventListener> listener = GetGestureEventListener();
+ if (listener) {
+ listener->HandleInputEvent(cancel);
+ }
+ CancelAnimationAndGestureState();
+ // Clear overscroll along the entire handoff chain, in case an APZC
+ // later in the chain is overscrolled.
+ if (TouchBlockState* block = GetCurrentTouchBlock()) {
+ block->GetOverscrollHandoffChain()->ClearOverscroll();
+ }
+}
+
+void AsyncPanZoomController::ResetPanGestureInputState() {
+ // No point sending a PANGESTURE_INTERRUPTED as all it does is
+ // call CancelAnimation(), which we also do here.
+ CancelAnimationAndGestureState();
+ // Clear overscroll along the entire handoff chain, in case an APZC
+ // later in the chain is overscrolled.
+ if (PanGestureBlockState* block = GetCurrentPanGestureBlock()) {
+ block->GetOverscrollHandoffChain()->ClearOverscroll();
+ }
+}
+
+void AsyncPanZoomController::CancelAnimationAndGestureState() {
+ mX.CancelGesture();
+ mY.CancelGesture();
+ CancelAnimation(CancelAnimationFlags::ScrollSnap);
+}
+
+bool AsyncPanZoomController::HasReadyTouchBlock() const {
+ return GetInputQueue()->HasReadyTouchBlock();
+}
+
+bool AsyncPanZoomController::CanHandleScrollOffsetUpdate(PanZoomState aState) {
+ return aState == PAN_MOMENTUM || aState == TOUCHING || IsPanningState(aState);
+}
+
+bool AsyncPanZoomController::ShouldCancelAnimationForScrollUpdate(
+ const Maybe<CSSPoint>& aRelativeDelta) {
+ // Never call CancelAnimation() for a no-op relative update.
+ if (aRelativeDelta == Some(CSSPoint())) {
+ return false;
+ }
+
+ if (mAnimation) {
+ return !mAnimation->HandleScrollOffsetUpdate(aRelativeDelta);
+ }
+
+ return !CanHandleScrollOffsetUpdate(mState);
+}
+
+AsyncPanZoomController::PanZoomState
+AsyncPanZoomController::SetStateNoContentControllerDispatch(
+ PanZoomState aNewState) {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ APZC_LOG_DETAIL("changing from state %s to %s\n", this,
+ ToString(mState).c_str(), ToString(aNewState).c_str());
+ PanZoomState oldState = mState;
+ mState = aNewState;
+ return oldState;
+}
+
+void AsyncPanZoomController::SetState(PanZoomState aNewState) {
+ // When a state transition to a transforming state is occuring and a delayed
+ // transform end notification exists, send the TransformEnd notification
+ // before the TransformBegin notification is sent for the input state change.
+ if (IsTransformingState(aNewState) && IsDelayedTransformEndSet()) {
+ MOZ_ASSERT(!IsTransformingState(mState));
+ SetDelayedTransformEnd(false);
+ DispatchStateChangeNotification(PANNING, NOTHING);
+ }
+
+ PanZoomState oldState = SetStateNoContentControllerDispatch(aNewState);
+
+ DispatchStateChangeNotification(oldState, aNewState);
+}
+
+auto AsyncPanZoomController::GetState() const -> PanZoomState {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ return mState;
+}
+
+void AsyncPanZoomController::DispatchStateChangeNotification(
+ PanZoomState aOldState, PanZoomState aNewState) {
+ { // scope the lock
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ if (mNotificationBlockers > 0) {
+ return;
+ }
+ }
+
+ if (RefPtr<GeckoContentController> controller = GetGeckoContentController()) {
+ if (!IsTransformingState(aOldState) && IsTransformingState(aNewState)) {
+ controller->NotifyAPZStateChange(GetGuid(),
+ APZStateChange::eTransformBegin);
+ } else if (IsTransformingState(aOldState) &&
+ !IsTransformingState(aNewState)) {
+ controller->NotifyAPZStateChange(GetGuid(),
+ APZStateChange::eTransformEnd);
+ }
+ }
+}
+
+bool AsyncPanZoomController::IsInTransformingState() const {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ return IsTransformingState(mState);
+}
+
+bool AsyncPanZoomController::IsTransformingState(PanZoomState aState) {
+ return !(aState == NOTHING || aState == TOUCHING);
+}
+
+bool AsyncPanZoomController::IsPanningState(PanZoomState aState) {
+ return (aState == PANNING || aState == PANNING_LOCKED_X ||
+ aState == PANNING_LOCKED_Y);
+}
+
+bool AsyncPanZoomController::IsInPanningState() const {
+ return IsPanningState(mState);
+}
+
+bool AsyncPanZoomController::IsInScrollingGesture() const {
+ return IsPanningState(mState) || mState == SCROLLBAR_DRAG ||
+ mState == TOUCHING || mState == PINCHING;
+}
+
+bool AsyncPanZoomController::IsDelayedTransformEndSet() {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ return mDelayedTransformEnd;
+}
+
+void AsyncPanZoomController::SetDelayedTransformEnd(bool aDelayedTransformEnd) {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ mDelayedTransformEnd = aDelayedTransformEnd;
+}
+
+void AsyncPanZoomController::UpdateZoomConstraints(
+ const ZoomConstraints& aConstraints) {
+ if ((MOZ_LOG_TEST(sApzCtlLog, LogLevel::Debug) &&
+ (aConstraints != mZoomConstraints)) ||
+ MOZ_LOG_TEST(sApzCtlLog, LogLevel::Verbose)) {
+ APZC_LOG("%p updating zoom constraints to %d %d %f %f\n", this,
+ aConstraints.mAllowZoom, aConstraints.mAllowDoubleTapZoom,
+ aConstraints.mMinZoom.scale, aConstraints.mMaxZoom.scale);
+ }
+
+ if (std::isnan(aConstraints.mMinZoom.scale) ||
+ std::isnan(aConstraints.mMaxZoom.scale)) {
+ NS_WARNING("APZC received zoom constraints with NaN values; dropping...");
+ return;
+ }
+
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ CSSToParentLayerScale min = Metrics().GetDevPixelsPerCSSPixel() *
+ ViewportMinScale() / ParentLayerToScreenScale(1);
+ CSSToParentLayerScale max = Metrics().GetDevPixelsPerCSSPixel() *
+ ViewportMaxScale() / ParentLayerToScreenScale(1);
+
+ // inf float values and other bad cases should be sanitized by the code below.
+ mZoomConstraints.mAllowZoom = aConstraints.mAllowZoom;
+ mZoomConstraints.mAllowDoubleTapZoom = aConstraints.mAllowDoubleTapZoom;
+ mZoomConstraints.mMinZoom =
+ (min > aConstraints.mMinZoom ? min : aConstraints.mMinZoom);
+ mZoomConstraints.mMaxZoom =
+ (max > aConstraints.mMaxZoom ? aConstraints.mMaxZoom : max);
+ if (mZoomConstraints.mMaxZoom < mZoomConstraints.mMinZoom) {
+ mZoomConstraints.mMaxZoom = mZoomConstraints.mMinZoom;
+ }
+}
+
+bool AsyncPanZoomController::ZoomConstraintsAllowZoom() const {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ return mZoomConstraints.mAllowZoom;
+}
+
+bool AsyncPanZoomController::ZoomConstraintsAllowDoubleTapZoom() const {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ return mZoomConstraints.mAllowDoubleTapZoom;
+}
+
+void AsyncPanZoomController::PostDelayedTask(already_AddRefed<Runnable> aTask,
+ int aDelayMs) {
+ APZThreadUtils::AssertOnControllerThread();
+ RefPtr<Runnable> task = aTask;
+ RefPtr<GeckoContentController> controller = GetGeckoContentController();
+ if (controller) {
+ controller->PostDelayedTask(task.forget(), aDelayMs);
+ }
+ // If there is no controller, that means this APZC has been destroyed, and
+ // we probably don't need to run the task. It will get destroyed when the
+ // RefPtr goes out of scope.
+}
+
+bool AsyncPanZoomController::Matches(const ScrollableLayerGuid& aGuid) {
+ return aGuid == GetGuid();
+}
+
+bool AsyncPanZoomController::HasTreeManager(
+ const APZCTreeManager* aTreeManager) const {
+ return GetApzcTreeManager() == aTreeManager;
+}
+
+void AsyncPanZoomController::GetGuid(ScrollableLayerGuid* aGuidOut) const {
+ if (aGuidOut) {
+ *aGuidOut = GetGuid();
+ }
+}
+
+ScrollableLayerGuid AsyncPanZoomController::GetGuid() const {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ return ScrollableLayerGuid(mLayersId, Metrics().GetPresShellId(),
+ Metrics().GetScrollId());
+}
+
+void AsyncPanZoomController::SetTestAsyncScrollOffset(const CSSPoint& aPoint) {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ mTestAsyncScrollOffset = aPoint;
+ ScheduleComposite();
+}
+
+void AsyncPanZoomController::SetTestAsyncZoom(
+ const LayerToParentLayerScale& aZoom) {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ mTestAsyncZoom = aZoom;
+ ScheduleComposite();
+}
+
+Maybe<CSSSnapTarget> AsyncPanZoomController::FindSnapPointNear(
+ const CSSPoint& aDestination, ScrollUnit aUnit,
+ ScrollSnapFlags aSnapFlags) {
+ mRecursiveMutex.AssertCurrentThreadIn();
+ APZC_LOG("%p scroll snapping near %s\n", this,
+ ToString(aDestination).c_str());
+ CSSRect scrollRange = Metrics().CalculateScrollRange();
+ if (auto snapTarget = ScrollSnapUtils::GetSnapPointForDestination(
+ mScrollMetadata.GetSnapInfo(), aUnit, aSnapFlags,
+ CSSRect::ToAppUnits(scrollRange),
+ CSSPoint::ToAppUnits(Metrics().GetVisualScrollOffset()),
+ CSSPoint::ToAppUnits(aDestination))) {
+ CSSPoint cssSnapPoint = CSSPoint::FromAppUnits(snapTarget->mPosition);
+ // GetSnapPointForDestination() can produce a destination that's outside
+ // of the scroll frame's scroll range. Clamp it here (this matches the
+ // behaviour of the main-thread code path, which clamps it in
+ // nsGfxScrollFrame::ScrollTo()).
+ return Some(CSSSnapTarget{scrollRange.ClampPoint(cssSnapPoint),
+ snapTarget->mTargetIds});
+ }
+ return Nothing();
+}
+
+Maybe<std::pair<MultiTouchInput, MultiTouchInput>>
+AsyncPanZoomController::MaybeSplitTouchMoveEvent(
+ const MultiTouchInput& aOriginalEvent, ScreenCoord aPanThreshold,
+ float aVectorLength, ExternalPoint& aExtPoint) {
+ if (aVectorLength <= aPanThreshold) {
+ return Nothing();
+ }
+
+ auto splitEvent = std::make_pair(aOriginalEvent, aOriginalEvent);
+
+ SingleTouchData& firstTouchData = splitEvent.first.mTouches[0];
+ SingleTouchData& secondTouchData = splitEvent.second.mTouches[0];
+
+ firstTouchData.mHistoricalData.Clear();
+ secondTouchData.mHistoricalData.Clear();
+
+ ExternalPoint destination = aExtPoint;
+ ExternalPoint thresholdPosition;
+
+ const float ratio = aPanThreshold / aVectorLength;
+ thresholdPosition.x = mStartTouch.x + ratio * (destination.x - mStartTouch.x);
+ thresholdPosition.y = mStartTouch.y + ratio * (destination.y - mStartTouch.y);
+
+ TouchSample start{mLastTouch};
+ // To compute the timestamp of the first event (which is at the threshold),
+ // use linear interpolation with the starting point |start| being the last
+ // event that's before the threshold, and the end point |end| being the first
+ // event after the threshold.
+
+ // The initial choice for |start| is the last touch event before
+ // |aOriginalEvent|, and the initial choice for |end| is |aOriginalEvent|.
+
+ // However, the historical data points stored in |aOriginalEvent| may contain
+ // intermediate positions that can serve as tighter bounds for the
+ // interpolation.
+ TouchSample end{destination, aOriginalEvent.mTimeStamp};
+
+ for (const auto& historicalData :
+ aOriginalEvent.mTouches[0].mHistoricalData) {
+ ExternalPoint histExtPoint = ToExternalPoint(aOriginalEvent.mScreenOffset,
+ historicalData.mScreenPoint);
+
+ if (PanVector(histExtPoint).Length() <
+ PanVector(thresholdPosition).Length()) {
+ start = {histExtPoint, historicalData.mTimeStamp};
+ } else {
+ break;
+ }
+ }
+
+ for (const SingleTouchData::HistoricalTouchData& histData :
+ Reversed(aOriginalEvent.mTouches[0].mHistoricalData)) {
+ ExternalPoint histExtPoint =
+ ToExternalPoint(aOriginalEvent.mScreenOffset, histData.mScreenPoint);
+
+ if (PanVector(histExtPoint).Length() >
+ PanVector(thresholdPosition).Length()) {
+ end = {histExtPoint, histData.mTimeStamp};
+ } else {
+ break;
+ }
+ }
+
+ const float totalLength =
+ ScreenPoint(fabs(end.mPosition.x - start.mPosition.x),
+ fabs(end.mPosition.y - start.mPosition.y))
+ .Length();
+ const float thresholdLength =
+ ScreenPoint(fabs(thresholdPosition.x - start.mPosition.x),
+ fabs(thresholdPosition.y - start.mPosition.y))
+ .Length();
+ const float splitRatio = thresholdLength / totalLength;
+
+ splitEvent.first.mTimeStamp =
+ start.mTimeStamp +
+ (end.mTimeStamp - start.mTimeStamp).MultDouble(splitRatio);
+
+ for (const auto& historicalData :
+ aOriginalEvent.mTouches[0].mHistoricalData) {
+ if (historicalData.mTimeStamp > splitEvent.first.mTimeStamp) {
+ secondTouchData.mHistoricalData.AppendElement(historicalData);
+ } else {
+ firstTouchData.mHistoricalData.AppendElement(historicalData);
+ }
+ }
+
+ firstTouchData.mScreenPoint = RoundedToInt(
+ ViewAs<ScreenPixel>(thresholdPosition - splitEvent.first.mScreenOffset,
+ PixelCastJustification::ExternalIsScreen));
+
+ // Recompute firstTouchData.mLocalScreenPoint.
+ splitEvent.first.TransformToLocal(GetTransformToThis());
+
+ // Pass |thresholdPosition| back out to the caller via |aExtPoint|
+ aExtPoint = thresholdPosition;
+
+ return Some(splitEvent);
+}
+
+void AsyncPanZoomController::ScrollSnapNear(const CSSPoint& aDestination,
+ ScrollSnapFlags aSnapFlags) {
+ if (Maybe<CSSSnapTarget> snapTarget = FindSnapPointNear(
+ aDestination, ScrollUnit::DEVICE_PIXELS, aSnapFlags)) {
+ if (snapTarget->mPosition != Metrics().GetVisualScrollOffset()) {
+ APZC_LOG("%p smooth scrolling to snap point %s\n", this,
+ ToString(snapTarget->mPosition).c_str());
+ SmoothMsdScrollTo(std::move(*snapTarget), ScrollTriggeredByScript::No);
+ }
+ }
+}
+
+void AsyncPanZoomController::ScrollSnap(ScrollSnapFlags aSnapFlags) {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ ScrollSnapNear(Metrics().GetVisualScrollOffset(), aSnapFlags);
+}
+
+void AsyncPanZoomController::ScrollSnapToDestination() {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+
+ float friction = StaticPrefs::apz_fling_friction();
+ ParentLayerPoint velocity(mX.GetVelocity(), mY.GetVelocity());
+ ParentLayerPoint predictedDelta;
+ // "-velocity / log(1.0 - friction)" is the integral of the deceleration
+ // curve modeled for flings in the "Axis" class.
+ if (velocity.x != 0.0f && friction != 0.0f) {
+ predictedDelta.x = -velocity.x / log(1.0 - friction);
+ }
+ if (velocity.y != 0.0f && friction != 0.0f) {
+ predictedDelta.y = -velocity.y / log(1.0 - friction);
+ }
+
+ // If the fling will overscroll, don't scroll snap, because then the user
+ // user would not see any overscroll animation.
+ bool flingWillOverscroll =
+ IsOverscrolled() && ((velocity.x.value * mX.GetOverscroll() >= 0) ||
+ (velocity.y.value * mY.GetOverscroll() >= 0));
+ if (flingWillOverscroll) {
+ return;
+ }
+
+ CSSPoint startPosition = Metrics().GetVisualScrollOffset();
+ ScrollSnapFlags snapFlags = ScrollSnapFlags::IntendedEndPosition;
+ if (predictedDelta != ParentLayerPoint()) {
+ snapFlags |= ScrollSnapFlags::IntendedDirection;
+ }
+ if (Maybe<CSSSnapTarget> snapTarget = MaybeAdjustDeltaForScrollSnapping(
+ ScrollUnit::DEVICE_PIXELS, snapFlags, predictedDelta,
+ startPosition)) {
+ APZC_LOG(
+ "%p fling snapping. friction: %f velocity: %f, %f "
+ "predictedDelta: %f, %f position: %f, %f "
+ "snapDestination: %f, %f\n",
+ this, friction, velocity.x.value, velocity.y.value,
+ predictedDelta.x.value, predictedDelta.y.value,
+ Metrics().GetVisualScrollOffset().x.value,
+ Metrics().GetVisualScrollOffset().y.value, startPosition.x.value,
+ startPosition.y.value);
+
+ // Ensure that any queued transform-end due to a pan-end is not
+ // sent. Instead rely on the transform-end sent due to the
+ // scroll snap animation.
+ SetDelayedTransformEnd(false);
+
+ SmoothMsdScrollTo(std::move(*snapTarget), ScrollTriggeredByScript::No);
+ }
+}
+
+Maybe<CSSSnapTarget> AsyncPanZoomController::MaybeAdjustDeltaForScrollSnapping(
+ ScrollUnit aUnit, ScrollSnapFlags aSnapFlags, ParentLayerPoint& aDelta,
+ CSSPoint& aStartPosition) {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ CSSToParentLayerScale zoom = Metrics().GetZoom();
+ if (zoom == CSSToParentLayerScale(0)) {
+ return Nothing();
+ }
+ CSSPoint destination = Metrics().CalculateScrollRange().ClampPoint(
+ aStartPosition + (aDelta / zoom));
+
+ if (Maybe<CSSSnapTarget> snapTarget =
+ FindSnapPointNear(destination, aUnit, aSnapFlags)) {
+ aDelta = (snapTarget->mPosition - aStartPosition) * zoom;
+ aStartPosition = snapTarget->mPosition;
+ return snapTarget;
+ }
+ return Nothing();
+}
+
+Maybe<CSSSnapTarget>
+AsyncPanZoomController::MaybeAdjustDeltaForScrollSnappingOnWheelInput(
+ const ScrollWheelInput& aEvent, ParentLayerPoint& aDelta,
+ CSSPoint& aStartPosition) {
+ // Don't scroll snap for pixel scrolls. This matches the main thread
+ // behaviour in EventStateManager::DoScrollText().
+ if (aEvent.mDeltaType == ScrollWheelInput::SCROLLDELTA_PIXEL) {
+ return Nothing();
+ }
+
+ // Note that this MaybeAdjustDeltaForScrollSnappingOnWheelInput also gets
+ // called for pan gestures at least on older Mac and Windows. In such cases
+ // `aEvent.mDeltaType` is `SCROLLDELTA_PIXEL` which should be filtered out by
+ // the above `if` block, so we assume all incoming `aEvent` are purely wheel
+ // events, thus we basically use `IntendedDirection` here.
+ // If we want to change the behavior, i.e. we want to do scroll snap for
+ // such cases as well, we need to use `IntendedEndPoint`.
+ ScrollSnapFlags snapFlags = ScrollSnapFlags::IntendedDirection;
+ if (aEvent.mDeltaType == ScrollWheelInput::SCROLLDELTA_PAGE) {
+ // On Windows there are a couple of cases where scroll events happen with
+ // SCROLLDELTA_PAGE, in such case we consider it's a page scroll.
+ snapFlags |= ScrollSnapFlags::IntendedEndPosition;
+ }
+ return MaybeAdjustDeltaForScrollSnapping(
+ ScrollWheelInput::ScrollUnitForDeltaType(aEvent.mDeltaType),
+ ScrollSnapFlags::IntendedDirection, aDelta, aStartPosition);
+}
+
+Maybe<CSSSnapTarget>
+AsyncPanZoomController::MaybeAdjustDestinationForScrollSnapping(
+ const KeyboardInput& aEvent, CSSPoint& aDestination,
+ ScrollSnapFlags aSnapFlags) {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ ScrollUnit unit = KeyboardScrollAction::GetScrollUnit(aEvent.mAction.mType);
+
+ if (Maybe<CSSSnapTarget> snapPoint =
+ FindSnapPointNear(aDestination, unit, aSnapFlags)) {
+ aDestination = snapPoint->mPosition;
+ return snapPoint;
+ }
+ return Nothing();
+}
+
+void AsyncPanZoomController::SetZoomAnimationId(
+ const Maybe<uint64_t>& aZoomAnimationId) {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ mZoomAnimationId = aZoomAnimationId;
+}
+
+Maybe<uint64_t> AsyncPanZoomController::GetZoomAnimationId() const {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ return mZoomAnimationId;
+}
+
+std::ostream& operator<<(std::ostream& aOut,
+ const AsyncPanZoomController::PanZoomState& aState) {
+ switch (aState) {
+ case AsyncPanZoomController::PanZoomState::NOTHING:
+ aOut << "NOTHING";
+ break;
+ case AsyncPanZoomController::PanZoomState::FLING:
+ aOut << "FLING";
+ break;
+ case AsyncPanZoomController::PanZoomState::TOUCHING:
+ aOut << "TOUCHING";
+ break;
+ case AsyncPanZoomController::PanZoomState::PANNING:
+ aOut << "PANNING";
+ break;
+ case AsyncPanZoomController::PanZoomState::PANNING_LOCKED_X:
+ aOut << "PANNING_LOCKED_X";
+ break;
+ case AsyncPanZoomController::PanZoomState::PANNING_LOCKED_Y:
+ aOut << "PANNING_LOCKED_Y";
+ break;
+ case AsyncPanZoomController::PanZoomState::PAN_MOMENTUM:
+ aOut << "PAN_MOMENTUM";
+ break;
+ case AsyncPanZoomController::PanZoomState::PINCHING:
+ aOut << "PINCHING";
+ break;
+ case AsyncPanZoomController::PanZoomState::ANIMATING_ZOOM:
+ aOut << "ANIMATING_ZOOM";
+ break;
+ case AsyncPanZoomController::PanZoomState::OVERSCROLL_ANIMATION:
+ aOut << "OVERSCROLL_ANIMATION";
+ break;
+ case AsyncPanZoomController::PanZoomState::SMOOTH_SCROLL:
+ aOut << "SMOOTH_SCROLL";
+ break;
+ case AsyncPanZoomController::PanZoomState::SMOOTHMSD_SCROLL:
+ aOut << "SMOOTHMSD_SCROLL";
+ break;
+ case AsyncPanZoomController::PanZoomState::WHEEL_SCROLL:
+ aOut << "WHEEL_SCROLL";
+ break;
+ case AsyncPanZoomController::PanZoomState::KEYBOARD_SCROLL:
+ aOut << "KEYBOARD_SCROLL";
+ break;
+ case AsyncPanZoomController::PanZoomState::AUTOSCROLL:
+ aOut << "AUTOSCROLL";
+ break;
+ case AsyncPanZoomController::PanZoomState::SCROLLBAR_DRAG:
+ aOut << "SCROLLBAR_DRAG";
+ break;
+ default:
+ aOut << "UNKNOWN_STATE";
+ break;
+ }
+ return aOut;
+}
+
+bool operator==(const PointerEventsConsumableFlags& aLhs,
+ const PointerEventsConsumableFlags& aRhs) {
+ return (aLhs.mHasRoom == aRhs.mHasRoom) &&
+ (aLhs.mAllowedByTouchAction == aRhs.mAllowedByTouchAction);
+}
+
+std::ostream& operator<<(std::ostream& aOut,
+ const PointerEventsConsumableFlags& aFlags) {
+ aOut << std::boolalpha << "{ hasRoom: " << aFlags.mHasRoom
+ << ", allowedByTouchAction: " << aFlags.mAllowedByTouchAction << "}";
+ return aOut;
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/apz/src/AsyncPanZoomController.h b/gfx/layers/apz/src/AsyncPanZoomController.h
new file mode 100644
index 0000000000..1f589df826
--- /dev/null
+++ b/gfx/layers/apz/src/AsyncPanZoomController.h
@@ -0,0 +1,1943 @@
+/* -*- 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_AsyncPanZoomController_h
+#define mozilla_layers_AsyncPanZoomController_h
+
+#include "mozilla/layers/GeckoContentController.h"
+#include "mozilla/layers/RepaintRequest.h"
+#include "mozilla/layers/SampleTime.h"
+#include "mozilla/layers/ScrollbarData.h"
+#include "mozilla/layers/ZoomConstraints.h"
+#include "mozilla/Atomics.h"
+#include "mozilla/Attributes.h"
+#include "mozilla/EventForwards.h"
+#include "mozilla/Monitor.h"
+#include "mozilla/RecursiveMutex.h"
+#include "mozilla/RefPtr.h"
+#include "mozilla/ScrollTypes.h"
+#include "mozilla/StaticPrefs_apz.h"
+#include "mozilla/UniquePtr.h"
+#include "InputData.h"
+#include "Axis.h" // for Axis, Side, etc.
+#include "ExpectedGeckoMetrics.h"
+#include "FlingAccelerator.h"
+#include "InputQueue.h"
+#include "APZUtils.h"
+#include "LayersTypes.h"
+#include "mozilla/gfx/Matrix.h"
+#include "nsRegion.h"
+#include "nsTArray.h"
+#include "PotentialCheckerboardDurationTracker.h"
+#include "RecentEventsBuffer.h" // for RecentEventsBuffer
+#include "SampledAPZCState.h"
+
+#include <iosfwd>
+
+namespace mozilla {
+
+namespace ipc {
+
+class SharedMemoryBasic;
+
+} // namespace ipc
+
+namespace wr {
+struct SampledScrollOffset;
+} // namespace wr
+
+namespace layers {
+
+class AsyncDragMetrics;
+class APZCTreeManager;
+struct ScrollableLayerGuid;
+class CompositorController;
+class GestureEventListener;
+struct AsyncTransform;
+class AsyncPanZoomAnimation;
+class StackScrollerFlingAnimation;
+template <typename FlingPhysics>
+class GenericFlingAnimation;
+class AndroidFlingPhysics;
+class DesktopFlingPhysics;
+class InputBlockState;
+struct FlingHandoffState;
+class TouchBlockState;
+class PanGestureBlockState;
+class OverscrollHandoffChain;
+struct OverscrollHandoffState;
+class StateChangeNotificationBlocker;
+class CheckerboardEvent;
+class OverscrollEffectBase;
+class WidgetOverscrollEffect;
+class GenericOverscrollEffect;
+class AndroidSpecificState;
+struct KeyboardScrollAction;
+struct ZoomTarget;
+
+namespace apz {
+struct AsyncScrollThumbTransformer;
+}
+
+// Base class for grouping platform-specific APZC state variables.
+class PlatformSpecificStateBase {
+ public:
+ virtual ~PlatformSpecificStateBase() = default;
+ virtual AndroidSpecificState* AsAndroidSpecificState() { return nullptr; }
+ // PLPPI = "ParentLayer pixels per (Screen) inch"
+ virtual AsyncPanZoomAnimation* CreateFlingAnimation(
+ AsyncPanZoomController& aApzc, const FlingHandoffState& aHandoffState,
+ float aPLPPI);
+ virtual UniquePtr<VelocityTracker> CreateVelocityTracker(Axis* aAxis);
+
+ static void InitializeGlobalState() {}
+};
+
+/*
+ * Represents a transform from the ParentLayer coordinate space of an APZC
+ * to the ParentLayer coordinate space of its parent APZC.
+ * Each layer along the way contributes to the transform. We track
+ * contributions that are perspective transforms separately, as sometimes
+ * these require special handling.
+ */
+struct AncestorTransform {
+ gfx::Matrix4x4 mTransform;
+ gfx::Matrix4x4 mPerspectiveTransform;
+
+ AncestorTransform() = default;
+
+ AncestorTransform(const gfx::Matrix4x4& aTransform,
+ bool aTransformIsPerspective) {
+ (aTransformIsPerspective ? mPerspectiveTransform : mTransform) = aTransform;
+ }
+
+ AncestorTransform(const gfx::Matrix4x4& aTransform,
+ const gfx::Matrix4x4& aPerspectiveTransform)
+ : mTransform(aTransform), mPerspectiveTransform(aPerspectiveTransform) {}
+
+ gfx::Matrix4x4 CombinedTransform() const {
+ return mTransform * mPerspectiveTransform;
+ }
+
+ bool ContainsPerspectiveTransform() const {
+ return !mPerspectiveTransform.IsIdentity();
+ }
+
+ gfx::Matrix4x4 GetPerspectiveTransform() const {
+ return mPerspectiveTransform;
+ }
+
+ friend AncestorTransform operator*(const AncestorTransform& aA,
+ const AncestorTransform& aB) {
+ return AncestorTransform{
+ aA.mTransform * aB.mTransform,
+ aA.mPerspectiveTransform * aB.mPerspectiveTransform};
+ }
+};
+
+// Flags returned by AsyncPanZoomController::ArePointerEventsConsumable().
+// See the function for more details.
+struct PointerEventsConsumableFlags {
+ // The APZC has room to pan or zoom in response to the touch event.
+ bool mHasRoom = false;
+
+ // The panning or zooming is allowed by the touch-action property.
+ bool mAllowedByTouchAction = false;
+
+ bool IsConsumable() const { return mHasRoom && mAllowedByTouchAction; }
+ friend bool operator==(const PointerEventsConsumableFlags& aLhs,
+ const PointerEventsConsumableFlags& aRhs);
+ friend std::ostream& operator<<(std::ostream& aOut,
+ const PointerEventsConsumableFlags& aFlags);
+};
+
+/**
+ * Controller for all panning and zooming logic. Any time a user input is
+ * detected and it must be processed in some way to affect what the user sees,
+ * it goes through here. Listens for any input event from InputData and can
+ * optionally handle WidgetGUIEvent-derived touch events, but this must be done
+ * on the main thread. Note that this class completely cross-platform.
+ *
+ * Input events originate on the UI thread of the platform that this runs on,
+ * and are then sent to this class. This class processes the event in some way;
+ * for example, a touch move will usually lead to a panning of content (though
+ * of course there are exceptions, such as if content preventDefaults the event,
+ * or if the target frame is not scrollable). The compositor interacts with this
+ * class by locking it and querying it for the current transform matrix based on
+ * the panning and zooming logic that was invoked on the UI thread.
+ *
+ * Currently, each outer DOM window (i.e. a website in a tab, but not any
+ * subframes) has its own AsyncPanZoomController. In the future, to support
+ * asynchronously scrolled subframes, we want to have one AsyncPanZoomController
+ * per frame.
+ */
+class AsyncPanZoomController {
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(AsyncPanZoomController)
+
+ typedef mozilla::MonitorAutoLock MonitorAutoLock;
+ typedef mozilla::gfx::Matrix4x4 Matrix4x4;
+ typedef mozilla::layers::RepaintRequest::ScrollOffsetUpdateType
+ RepaintUpdateType;
+
+ public:
+ enum GestureBehavior {
+ // The platform code is responsible for forwarding gesture events here. We
+ // will not attempt to generate gesture events from MultiTouchInputs.
+ DEFAULT_GESTURES,
+ // An instance of GestureEventListener is used to detect gestures. This is
+ // handled completely internally within this class.
+ USE_GESTURE_DETECTOR
+ };
+
+ /**
+ * Gets the DPI from the tree manager.
+ */
+ float GetDPI() const;
+
+ /**
+ * Constant describing the tolerance in distance we use, multiplied by the
+ * device DPI, before we start panning the screen. This is to prevent us from
+ * accidentally processing taps as touch moves, and from very short/accidental
+ * touches moving the screen.
+ * Note: It's an abuse of the 'Coord' class to use it to represent a 2D
+ * distance, but it's the closest thing we currently have.
+ */
+ ScreenCoord GetTouchStartTolerance() const;
+ /**
+ * Same as GetTouchStartTolerance, but the tolerance for how far the touch
+ * has to move before it starts allowing touchmove events to be dispatched
+ * to content, for non-scrollable content.
+ */
+ ScreenCoord GetTouchMoveTolerance() const;
+ /**
+ * Same as GetTouchStartTolerance, but the tolerance for how close the second
+ * tap has to be to the first tap in order to be counted as part of a
+ * multi-tap gesture (double-tap or one-touch-pinch).
+ */
+ ScreenCoord GetSecondTapTolerance() const;
+
+ AsyncPanZoomController(LayersId aLayersId, APZCTreeManager* aTreeManager,
+ const RefPtr<InputQueue>& aInputQueue,
+ GeckoContentController* aController,
+ GestureBehavior aGestures = DEFAULT_GESTURES);
+
+ // --------------------------------------------------------------------------
+ // These methods must only be called on the gecko thread.
+ //
+
+ /**
+ * Read the various prefs and do any global initialization for all APZC
+ * instances. This must be run on the gecko thread before any APZC instances
+ * are actually used for anything meaningful.
+ */
+ static void InitializeGlobalState();
+
+ // --------------------------------------------------------------------------
+ // These methods must only be called on the controller/UI thread.
+ //
+
+ /**
+ * Kicks an animation to zoom to a rect. This may be either a zoom out or zoom
+ * in. The actual animation is done on the sampler thread after being set
+ * up.
+ */
+ void ZoomToRect(const ZoomTarget& aZoomTarget, const uint32_t aFlags);
+
+ /**
+ * Updates any zoom constraints contained in the <meta name="viewport"> tag.
+ */
+ void UpdateZoomConstraints(const ZoomConstraints& aConstraints);
+
+ /**
+ * Schedules a runnable to run on the controller/UI thread at some time
+ * in the future.
+ */
+ void PostDelayedTask(already_AddRefed<Runnable> aTask, int aDelayMs);
+
+ // --------------------------------------------------------------------------
+ // These methods must only be called on the sampler thread.
+ //
+
+ /**
+ * Advances any animations currently running to the given timestamp.
+ * This may be called multiple times with the same timestamp.
+ *
+ * The return value indicates whether or not any currently running animation
+ * should continue. If true, the compositor should schedule another composite.
+ */
+ bool AdvanceAnimations(const SampleTime& aSampleTime);
+
+ bool UpdateAnimation(const RecursiveMutexAutoLock& aProofOfLock,
+ const SampleTime& aSampleTime,
+ nsTArray<RefPtr<Runnable>>* aOutDeferredTasks);
+
+ // --------------------------------------------------------------------------
+ // These methods must only be called on the updater thread.
+ //
+
+ /**
+ * A shadow layer update has arrived. |aScrollMetdata| is the new
+ * ScrollMetadata for the container layer corresponding to this APZC.
+ * |aIsFirstPaint| is a flag passed from the shadow
+ * layers code indicating that the scroll metadata being sent with this call
+ * are the initial metadata and the initial paint of the frame has just
+ * happened.
+ */
+ void NotifyLayersUpdated(const ScrollMetadata& aScrollMetadata,
+ bool aIsFirstPaint, bool aThisLayerTreeUpdated);
+
+ /**
+ * The platform implementation must set the compositor controller so that we
+ * can request composites.
+ */
+ void SetCompositorController(CompositorController* aCompositorController);
+
+ // --------------------------------------------------------------------------
+ // These methods can be called from any thread.
+ //
+
+ /**
+ * Shut down the controller/UI thread state and prepare to be
+ * deleted (which may happen from any thread).
+ */
+ void Destroy();
+
+ /**
+ * Returns true if Destroy() has already been called on this APZC instance.
+ */
+ bool IsDestroyed() const;
+
+ /**
+ * Returns the transform to take something from the coordinate space of the
+ * last thing we know gecko painted, to the coordinate space of the last thing
+ * we asked gecko to paint. In cases where that last request has not yet been
+ * processed, this is needed to transform input events properly into a space
+ * gecko will understand.
+ */
+ Matrix4x4 GetTransformToLastDispatchedPaint(
+ const AsyncTransformComponents& aComponents = LayoutAndVisual) const;
+
+ /**
+ * Returns the number of CSS pixels of checkerboard according to the metrics
+ * in this APZC. The argument provided by the caller is the composition bounds
+ * of this APZC, additionally clipped by the composition bounds of any
+ * ancestor APZCs, accounting for all the async transforms.
+ */
+ uint32_t GetCheckerboardMagnitude(
+ const ParentLayerRect& aClippedCompositionBounds) const;
+
+ /**
+ * Report the number of CSSPixel-milliseconds of checkerboard to telemetry.
+ * See GetCheckerboardMagnitude for documentation of the
+ * aClippedCompositionBounds argument that needs to be provided by the caller.
+ */
+ void ReportCheckerboard(const SampleTime& aSampleTime,
+ const ParentLayerRect& aClippedCompositionBounds);
+
+ /**
+ * Flush any active checkerboard report that's in progress. This basically
+ * pretends like any in-progress checkerboard event has terminated, and pushes
+ * out the report to the checkerboard reporting service and telemetry. If the
+ * checkerboard event has not really finished, it will start a new event
+ * on the next composite.
+ */
+ void FlushActiveCheckerboardReport();
+
+ /**
+ * See documentation on corresponding method in APZPublicUtils.h
+ */
+ static gfx::IntSize GetDisplayportAlignmentMultiplier(
+ const ScreenSize& aBaseSize);
+
+ enum class ZoomInProgress {
+ No,
+ Yes,
+ };
+
+ /**
+ * Recalculates the displayport. Ideally, this should paint an area bigger
+ * than the composite-to dimensions so that when you scroll down, you don't
+ * checkerboard immediately. This includes a bunch of logic, including
+ * algorithms to bias painting in the direction of the velocity and other
+ * such things.
+ */
+ static const ScreenMargin CalculatePendingDisplayPort(
+ const FrameMetrics& aFrameMetrics, const ParentLayerPoint& aVelocity,
+ ZoomInProgress aZoomInProgress);
+
+ nsEventStatus HandleDragEvent(const MouseInput& aEvent,
+ const AsyncDragMetrics& aDragMetrics,
+ OuterCSSCoord aInitialThumbPos);
+
+ /**
+ * Handler for events which should not be intercepted by the touch listener.
+ */
+ nsEventStatus HandleInputEvent(
+ const InputData& aEvent,
+ const ScreenToParentLayerMatrix4x4& aTransformToApzc);
+
+ /**
+ * Handler for gesture events.
+ * Currently some gestures are detected in GestureEventListener that calls
+ * APZC back through this handler in order to avoid recursive calls to
+ * APZC::HandleInputEvent() which is supposed to do the work for
+ * ReceiveInputEvent().
+ */
+ nsEventStatus HandleGestureEvent(const InputData& aEvent);
+
+ /**
+ * Start autoscrolling this APZC, anchored at the provided location.
+ */
+ void StartAutoscroll(const ScreenPoint& aAnchorLocation);
+
+ /**
+ * Stop autoscrolling this APZC.
+ */
+ void StopAutoscroll();
+
+ /**
+ * Populates the provided object (if non-null) with the scrollable guid of
+ * this apzc.
+ */
+ void GetGuid(ScrollableLayerGuid* aGuidOut) const;
+
+ /**
+ * Returns the scrollable guid of this apzc.
+ */
+ ScrollableLayerGuid GetGuid() const;
+
+ /**
+ * Returns true if this APZC instance is for the layer identified by the guid.
+ */
+ bool Matches(const ScrollableLayerGuid& aGuid);
+
+ /**
+ * Returns true if the tree manager of this APZC is the same as the one
+ * passed in.
+ */
+ bool HasTreeManager(const APZCTreeManager* aTreeManager) const;
+
+ void StartAnimation(AsyncPanZoomAnimation* aAnimation);
+
+ /**
+ * Cancels any currently running animation.
+ * aFlags is a bit-field to provide specifics of how to cancel the animation.
+ * See CancelAnimationFlags.
+ */
+ void CancelAnimation(CancelAnimationFlags aFlags = Default);
+
+ /**
+ * Clear any overscroll on this APZC.
+ */
+ void ClearOverscroll();
+ void ClearPhysicalOverscroll();
+
+ /**
+ * Returns whether this APZC is for an element marked with the 'scrollgrab'
+ * attribute.
+ */
+ bool HasScrollgrab() const { return mScrollMetadata.GetHasScrollgrab(); }
+
+ /**
+ * Returns whether this APZC has scroll snap points.
+ */
+ bool HasScrollSnapping() const {
+ return mScrollMetadata.GetSnapInfo().HasScrollSnapping();
+ }
+
+ /**
+ * Returns whether this APZC has room to be panned (in any direction).
+ */
+ bool IsPannable() const;
+
+ /**
+ * Returns whether this APZC represents a scroll info layer.
+ */
+ bool IsScrollInfoLayer() const;
+
+ /**
+ * Returns true if the APZC has been flung with a velocity greater than the
+ * stop-on-tap fling velocity threshold (which is pref-controlled).
+ */
+ bool IsFlingingFast() const;
+
+ /**
+ * Returns whether this APZC is currently autoscrolling.
+ */
+ bool IsAutoscroll() const {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ return mState == AUTOSCROLL;
+ }
+
+ /**
+ * Returns the identifier of the touch in the last touch event processed by
+ * this APZC. This should only be called when the last touch event contained
+ * only one touch.
+ */
+ int32_t GetLastTouchIdentifier() const;
+
+ /**
+ * Returns the matrix that transforms points from global screen space into
+ * this APZC's ParentLayer space.
+ * To respect the lock ordering, mRecursiveMutex must NOT be held when calling
+ * this function (since this function acquires the tree lock).
+ */
+ ScreenToParentLayerMatrix4x4 GetTransformToThis() const;
+
+ /**
+ * Convert the vector |aVector|, rooted at the point |aAnchor|, from
+ * this APZC's ParentLayer coordinates into screen coordinates.
+ * The anchor is necessary because with 3D tranforms, the location of the
+ * vector can affect the result of the transform.
+ * To respect the lock ordering, mRecursiveMutex must NOT be held when calling
+ * this function (since this function acquires the tree lock).
+ */
+ ScreenPoint ToScreenCoordinates(const ParentLayerPoint& aVector,
+ const ParentLayerPoint& aAnchor) const;
+
+ /**
+ * Convert the vector |aVector|, rooted at the point |aAnchor|, from
+ * screen coordinates into this APZC's ParentLayer coordinates.
+ * The anchor is necessary because with 3D tranforms, the location of the
+ * vector can affect the result of the transform.
+ * To respect the lock ordering, mRecursiveMutex must NOT be held when calling
+ * this function (since this function acquires the tree lock).
+ */
+ ParentLayerPoint ToParentLayerCoordinates(const ScreenPoint& aVector,
+ const ScreenPoint& aAnchor) const;
+
+ /**
+ * Same as above, but uses an ExternalPoint as the anchor.
+ */
+ ParentLayerPoint ToParentLayerCoordinates(const ScreenPoint& aVector,
+ const ExternalPoint& aAnchor) const;
+
+ /**
+ * Combines an offset defined as an external point, with a window-relative
+ * offset to give an absolute external point.
+ */
+ static ExternalPoint ToExternalPoint(const ExternalPoint& aScreenOffset,
+ const ScreenPoint& aScreenPoint);
+
+ /**
+ * Gets a vector where the head is the given point, and the tail is
+ * the touch start position.
+ */
+ ScreenPoint PanVector(const ExternalPoint& aPos) const;
+
+ // Return whether or not a wheel event will be able to scroll in either
+ // direction.
+ bool CanScroll(const InputData& aEvent) const;
+
+ // Return the directions in which this APZC allows handoff (as governed by
+ // overscroll-behavior).
+ ScrollDirections GetAllowedHandoffDirections() const;
+
+ // Return the directions in which this APZC allows overscrolling.
+ ScrollDirections GetOverscrollableDirections() const;
+
+ // Return whether or not a scroll delta will be able to scroll in either
+ // direction.
+ bool CanScroll(const ParentLayerPoint& aDelta) const;
+
+ // Return whether or not a scroll delta will be able to scroll in either
+ // direction with wheel.
+ bool CanScrollWithWheel(const ParentLayerPoint& aDelta) const;
+
+ // Return whether or not there is room to scroll this APZC
+ // in the given direction.
+ bool CanScroll(ScrollDirection aDirection) const;
+
+ // Return the directions in which this APZC is able to scroll.
+ SideBits ScrollableDirections() const;
+
+ // Return true if there is room to scroll along with moving the dynamic
+ // toolbar.
+ //
+ // NOTE: This function should be used only for the root content APZC.
+ bool CanVerticalScrollWithDynamicToolbar() const;
+
+ // Return true if there is room to scroll downwards.
+ bool CanScrollDownwards() const;
+
+ /**
+ * Convert a point on the scrollbar from this APZC's ParentLayer coordinates
+ * to OuterCSS coordinates relative to the beginning of the scroll track.
+ * Only the component in the direction of scrolling is returned.
+ */
+ OuterCSSCoord ConvertScrollbarPoint(const ParentLayerPoint& aScrollbarPoint,
+ const ScrollbarData& aThumbData) const;
+
+ void NotifyMozMouseScrollEvent(const nsString& aString) const;
+
+ bool OverscrollBehaviorAllowsSwipe() const;
+
+ //|Metrics()| and |Metrics() const| are getter functions that both return
+ // mScrollMetadata.mMetrics
+
+ const FrameMetrics& Metrics() const;
+ FrameMetrics& Metrics();
+
+ /**
+ * Get the GeckoViewMetrics to be sent to Gecko for the current composite.
+ */
+ GeckoViewMetrics GetGeckoViewMetrics() const;
+
+ // Helper function to compare root frame metrics and update them
+ // Returns true when the metrics have changed and were updated.
+ bool UpdateRootFrameMetricsIfChanged(GeckoViewMetrics& aMetrics);
+
+ // Returns the cached current frame time.
+ SampleTime GetFrameTime() const;
+
+ bool IsZero(const ParentLayerPoint& aPoint) const;
+ bool IsZero(ParentLayerCoord aCoord) const;
+
+ bool FuzzyGreater(ParentLayerCoord aCoord1, ParentLayerCoord aCoord2) const;
+
+ private:
+ // Get whether the horizontal content of the honoured target of auto-dir
+ // scrolling starts from right to left. If you don't know of auto-dir
+ // scrolling or what a honoured target means,
+ // @see mozilla::WheelDeltaAdjustmentStrategy
+ bool IsContentOfHonouredTargetRightToLeft(bool aHonoursRoot) const;
+
+ protected:
+ // Protected destructor, to discourage deletion outside of Release():
+ virtual ~AsyncPanZoomController();
+
+ /**
+ * Helper method for touches beginning. Sets everything up for panning and any
+ * multitouch gestures.
+ */
+ nsEventStatus OnTouchStart(const MultiTouchInput& aEvent);
+
+ /**
+ * Helper method for touches moving. Does any transforms needed when panning.
+ */
+ nsEventStatus OnTouchMove(const MultiTouchInput& aEvent);
+
+ /**
+ * Helper method for touches ending. Redraws the screen if necessary and does
+ * any cleanup after a touch has ended.
+ */
+ nsEventStatus OnTouchEnd(const MultiTouchInput& aEvent);
+
+ /**
+ * Helper method for touches being cancelled. Treated roughly the same as a
+ * touch ending (OnTouchEnd()).
+ */
+ nsEventStatus OnTouchCancel(const MultiTouchInput& aEvent);
+
+ /**
+ * Helper method for scales beginning. Distinct from the OnTouch* handlers in
+ * that this implies some outside implementation has determined that the user
+ * is pinching.
+ */
+ nsEventStatus OnScaleBegin(const PinchGestureInput& aEvent);
+
+ /**
+ * Helper method for scaling. As the user moves their fingers when pinching,
+ * this changes the scale of the page.
+ */
+ nsEventStatus OnScale(const PinchGestureInput& aEvent);
+
+ /**
+ * Helper method for scales ending. Redraws the screen if necessary and does
+ * any cleanup after a scale has ended.
+ */
+ nsEventStatus OnScaleEnd(const PinchGestureInput& aEvent);
+
+ /**
+ * Helper methods for handling pan events.
+ */
+ nsEventStatus OnPanMayBegin(const PanGestureInput& aEvent);
+ nsEventStatus OnPanCancelled(const PanGestureInput& aEvent);
+ nsEventStatus OnPanBegin(const PanGestureInput& aEvent);
+ enum class FingersOnTouchpad {
+ Yes,
+ No,
+ };
+ nsEventStatus OnPan(const PanGestureInput& aEvent,
+ FingersOnTouchpad aFingersOnTouchpad);
+ nsEventStatus OnPanEnd(const PanGestureInput& aEvent);
+ nsEventStatus OnPanMomentumStart(const PanGestureInput& aEvent);
+ nsEventStatus OnPanMomentumEnd(const PanGestureInput& aEvent);
+ nsEventStatus HandleEndOfPan();
+ nsEventStatus OnPanInterrupted(const PanGestureInput& aEvent);
+
+ /**
+ * Helper methods for handling scroll wheel events.
+ */
+ nsEventStatus OnScrollWheel(const ScrollWheelInput& aEvent);
+
+ /**
+ * Gets the scroll wheel delta's values in parent-layer pixels from the
+ * original delta's values of a wheel input.
+ */
+ ParentLayerPoint GetScrollWheelDelta(const ScrollWheelInput& aEvent) const;
+
+ /**
+ * This function is like GetScrollWheelDelta(aEvent).
+ * The difference is the four added parameters provide values as alternatives
+ * to the original wheel input's delta values, so |aEvent|'s delta values are
+ * ignored in this function, we only use some other member variables and
+ * functions of |aEvent|.
+ */
+ ParentLayerPoint GetScrollWheelDelta(const ScrollWheelInput& aEvent,
+ double aDeltaX, double aDeltaY,
+ double aMultiplierX,
+ double aMultiplierY) const;
+
+ /**
+ * This deleted function is used for:
+ * 1. avoiding accidental implicit value type conversions of input delta
+ * values when callers intend to call the above function;
+ * 2. decoupling the manual relationship between the delta value type and the
+ * above function. If by any chance the defined delta value type in
+ * ScrollWheelInput has changed, this will automatically result in build
+ * time failure, so we can learn of it the first time and accordingly
+ * redefine those parameters' value types in the above function.
+ */
+ template <typename T>
+ ParentLayerPoint GetScrollWheelDelta(ScrollWheelInput&, T, T, T, T) = delete;
+
+ /**
+ * Helper methods for handling keyboard events.
+ */
+ nsEventStatus OnKeyboard(const KeyboardInput& aEvent);
+
+ CSSPoint GetKeyboardDestination(const KeyboardScrollAction& aAction) const;
+
+ // Returns the corresponding ScrollSnapFlags for the given |aAction|.
+ // See https://drafts.csswg.org/css-scroll-snap/#scroll-types
+ ScrollSnapFlags GetScrollSnapFlagsForKeyboardAction(
+ const KeyboardScrollAction& aAction) const;
+
+ /**
+ * Helper methods for long press gestures.
+ */
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY
+ nsEventStatus OnLongPress(const TapGestureInput& aEvent);
+ nsEventStatus OnLongPressUp(const TapGestureInput& aEvent);
+
+ /**
+ * Helper method for single tap gestures.
+ */
+ nsEventStatus OnSingleTapUp(const TapGestureInput& aEvent);
+
+ /**
+ * Helper method for a single tap confirmed.
+ */
+ nsEventStatus OnSingleTapConfirmed(const TapGestureInput& aEvent);
+
+ /**
+ * Helper method for double taps.
+ */
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY
+ nsEventStatus OnDoubleTap(const TapGestureInput& aEvent);
+
+ /**
+ * Helper method for double taps where the double-tap gesture is disabled.
+ */
+ nsEventStatus OnSecondTap(const TapGestureInput& aEvent);
+
+ /**
+ * Helper method to cancel any gesture currently going to Gecko. Used
+ * primarily when a user taps the screen over some clickable content but then
+ * pans down instead of letting go (i.e. to cancel a previous touch so that a
+ * new one can properly take effect.
+ */
+ nsEventStatus OnCancelTap(const TapGestureInput& aEvent);
+
+ /**
+ * The following five methods modify the scroll offset. For the APZC
+ * representing the RCD-RSF, they also recalculate the offset of the layout
+ * viewport.
+ */
+
+ /**
+ * Scroll the scroll frame to an X,Y offset.
+ */
+ void SetVisualScrollOffset(const CSSPoint& aOffset);
+
+ /**
+ * Scroll the scroll frame to an X,Y offset, clamping the resulting scroll
+ * offset to the scroll range.
+ */
+ void ClampAndSetVisualScrollOffset(const CSSPoint& aOffset);
+
+ /**
+ * Scroll the scroll frame by an X,Y offset.
+ * The resulting scroll offset is not clamped to the scrollable rect;
+ * the caller must ensure it stays within range.
+ */
+ void ScrollBy(const CSSPoint& aOffset);
+
+ /**
+ * Scroll the scroll frame by an X,Y offset, clamping the resulting
+ * scroll offset to the scroll range.
+ */
+ void ScrollByAndClamp(const CSSPoint& aOffset);
+
+ /**
+ * Scales the viewport by an amount (note that it multiplies this scale in to
+ * the current scale, it doesn't set it to |aScale|). Also considers a focus
+ * point so that the page zooms inward/outward from that point.
+ */
+ void ScaleWithFocus(float aScale, const CSSPoint& aFocus);
+
+ /**
+ * Schedules a composite on the compositor thread.
+ */
+ void ScheduleComposite();
+
+ /**
+ * Schedules a composite, and if enough time has elapsed since the last
+ * paint, a paint.
+ */
+ void ScheduleCompositeAndMaybeRepaint();
+
+ /**
+ * Gets the start point of the current touch.
+ * This only makes sense if a touch is currently happening and OnTouchMove()
+ * or the equivalent for pan gestures is being invoked.
+ */
+ ParentLayerPoint PanStart() const;
+
+ /**
+ * Gets a vector of the velocities of each axis.
+ */
+ const ParentLayerPoint GetVelocityVector() const;
+
+ /**
+ * Sets the velocities of each axis.
+ */
+ void SetVelocityVector(const ParentLayerPoint& aVelocityVector);
+
+ /**
+ * Gets the first touch point from a MultiTouchInput. This gets only
+ * the first one and assumes the rest are either missing or not relevant.
+ */
+ ParentLayerPoint GetFirstTouchPoint(const MultiTouchInput& aEvent);
+
+ /**
+ * Gets the relevant point in the event
+ * (eg. first touch, or pinch focus point) of the given InputData.
+ */
+ ExternalPoint GetExternalPoint(const InputData& aEvent);
+
+ /**
+ * Gets the relevant point in the event, in external screen coordinates.
+ */
+ ExternalPoint GetFirstExternalTouchPoint(const MultiTouchInput& aEvent);
+
+ /**
+ * Gets the amount by which this APZC is overscrolled along both axes.
+ */
+ ParentLayerPoint GetOverscrollAmount() const;
+
+ private:
+ // Internal version of GetOverscrollAmount() which does not set
+ // the test async properties.
+ ParentLayerPoint GetOverscrollAmountInternal() const;
+
+ protected:
+ /**
+ * Returns SideBits where this APZC is overscrolled.
+ */
+ SideBits GetOverscrollSideBits() const;
+
+ /**
+ * Restore the amount by which this APZC is overscrolled along both axes
+ * to the specified amount. This is for test-related use; overscrolling
+ * as a result of user input should happen via OverscrollBy().
+ */
+ void RestoreOverscrollAmount(const ParentLayerPoint& aOverscroll);
+
+ /**
+ * Sets the panning state basing on the pan direction angle and current
+ * touch-action value.
+ */
+ void HandlePanningWithTouchAction(double angle);
+
+ /**
+ * Sets the panning state ignoring the touch action value.
+ */
+ void HandlePanning(double angle);
+
+ /**
+ * Update the panning state and axis locks.
+ */
+ void HandlePanningUpdate(const ScreenPoint& aDelta);
+
+ /**
+ * Set and update the pinch lock
+ */
+ void HandlePinchLocking(const PinchGestureInput& aEvent);
+
+ /**
+ * Sets up anything needed for panning. This takes us out of the "TOUCHING"
+ * state and starts actually panning us. We provide the physical pixel
+ * position of the start point so that the pan gesture is calculated
+ * regardless of if the window/GeckoView moved during the pan.
+ */
+ nsEventStatus StartPanning(const ExternalPoint& aStartPoint,
+ const TimeStamp& aEventTime);
+
+ /**
+ * Wrapper for Axis::UpdateWithTouchAtDevicePoint(). Calls this function for
+ * both axes and factors in the time delta from the last update.
+ */
+ void UpdateWithTouchAtDevicePoint(const MultiTouchInput& aEvent);
+
+ /**
+ * Does any panning required due to a new touch event.
+ */
+ void TrackTouch(const MultiTouchInput& aEvent);
+
+ /**
+ * Register the start of a touch or pan gesture at the given position and
+ * time.
+ */
+ void StartTouch(const ParentLayerPoint& aPoint, TimeStamp aTimestamp);
+
+ /**
+ * Register the end of a touch or pan gesture at the given time.
+ */
+ void EndTouch(TimeStamp aTimestamp, Axis::ClearAxisLock aClearAxisLock);
+
+ /**
+ * Utility function to send updated FrameMetrics to Gecko so that it can paint
+ * the displayport area. Calls into GeckoContentController to do the actual
+ * work. This call will use the current metrics. If this function is called
+ * from a non-main thread, it will redispatch itself to the main thread, and
+ * use the latest metrics during the redispatch.
+ */
+ void RequestContentRepaint(
+ RepaintUpdateType aUpdateType = RepaintUpdateType::eUserAction);
+
+ /**
+ * Send Metrics() to Gecko to trigger a repaint. This function may filter
+ * duplicate calls with the same metrics. This function must be called on the
+ * main thread.
+ */
+ void RequestContentRepaint(const ParentLayerPoint& aVelocity,
+ const ScreenMargin& aDisplayportMargins,
+ RepaintUpdateType aUpdateType);
+
+ /**
+ * Gets the current frame metrics. This is *not* the Gecko copy stored in the
+ * layers code.
+ */
+ const FrameMetrics& GetFrameMetrics() const;
+
+ /**
+ * Gets the current scroll metadata. This is *not* the Gecko copy stored in
+ * the layers code/
+ */
+ const ScrollMetadata& GetScrollMetadata() const;
+
+ /**
+ * Gets the pointer to the apzc tree manager. All the access to tree manager
+ * should be made via this method and not via private variable since this
+ * method ensures that no lock is set.
+ */
+ APZCTreeManager* GetApzcTreeManager() const;
+
+ void AssertOnSamplerThread() const;
+ void AssertOnUpdaterThread() const;
+
+ /**
+ * Convert ScreenPoint relative to the screen to LayoutDevicePoint relative
+ * to the parent document. This excludes the transient compositor transform.
+ * NOTE: This must be converted to LayoutDevicePoint relative to the child
+ * document before sending over IPC to a child process.
+ */
+ Maybe<LayoutDevicePoint> ConvertToGecko(const ScreenIntPoint& aPoint);
+
+ enum AxisLockMode {
+ FREE, /* No locking at all */
+ STANDARD, /* Default axis locking mode that remains locked until pan ends */
+ STICKY, /* Allow lock to be broken, with hysteresis */
+ DOMINANT_AXIS, /* Only allow movement on one axis */
+ };
+
+ static AxisLockMode GetAxisLockMode();
+
+ bool UsingStatefulAxisLock() const;
+
+ enum PinchLockMode {
+ PINCH_FREE, /* No locking at all */
+ PINCH_STANDARD, /* Default pinch locking mode that remains locked until
+ pinch gesture ends*/
+ PINCH_STICKY, /* Allow lock to be broken, with hysteresis */
+ };
+
+ static PinchLockMode GetPinchLockMode();
+
+ // Helper function for OnSingleTapUp(), OnSingleTapConfirmed(), and
+ // OnLongPressUp().
+ nsEventStatus GenerateSingleTap(GeckoContentController::TapType aType,
+ const ScreenIntPoint& aPoint,
+ mozilla::Modifiers aModifiers);
+
+ // Common processing at the end of a touch block.
+ void OnTouchEndOrCancel();
+
+ LayersId mLayersId;
+ RefPtr<CompositorController> mCompositorController;
+
+ /* Access to the following two fields is protected by the mRefPtrMonitor,
+ since they are accessed on the UI thread but can be cleared on the
+ updater thread. */
+ RefPtr<GeckoContentController> mGeckoContentController;
+ RefPtr<GestureEventListener> mGestureEventListener;
+ mutable Monitor mRefPtrMonitor MOZ_UNANNOTATED;
+
+ // This is a raw pointer to avoid introducing a reference cycle between
+ // AsyncPanZoomController and APZCTreeManager. Since these objects don't
+ // live on the main thread, we can't use the cycle collector with them.
+ // The APZCTreeManager owns the lifetime of the APZCs, so nulling this
+ // pointer out in Destroy() will prevent accessing deleted memory.
+ Atomic<APZCTreeManager*> mTreeManager;
+
+ /* Utility functions that return a addrefed pointer to the corresponding
+ * fields. */
+ already_AddRefed<GeckoContentController> GetGeckoContentController() const;
+ already_AddRefed<GestureEventListener> GetGestureEventListener() const;
+
+ PlatformSpecificStateBase* GetPlatformSpecificState();
+
+ /**
+ * Convenience functions to get the corresponding fields of mZoomContraints
+ * while holding mRecursiveMutex.
+ */
+ bool ZoomConstraintsAllowZoom() const;
+ bool ZoomConstraintsAllowDoubleTapZoom() const;
+
+ protected:
+ // Both |mScrollMetadata| and |mLastContentPaintMetrics| are protected by the
+ // monitor. Do not read from or modify them without locking.
+ ScrollMetadata mScrollMetadata;
+
+ // Protects |mScrollMetadata|, |mLastContentPaintMetrics|, |mState| and
+ // |mLastSnapTargetIds|. Before manipulating |mScrollMetadata|,
+ // |mLastContentPaintMetrics| or |mLastSnapTargetIds| the monitor should be
+ // held. When setting |mState|, either the SetState() function can be used, or
+ // the monitor can be held and then |mState| updated.
+ // IMPORTANT: See the note about lock ordering at the top of
+ // APZCTreeManager.h. This is mutable to allow entering it from 'const'
+ // methods; doing otherwise would significantly limit what methods could be
+ // 'const'.
+ // FIXME: Please keep in mind that due to some existing coupled relationships
+ // among the class members, we should be aware of indirect usage of the
+ // monitor-protected members. That is, although this monitor isn't required to
+ // be held before manipulating non-protected class members, some functions on
+ // those members might indirectly manipulate the protected members; in such
+ // cases, the monitor should still be held. Let's take mX.CanScroll for
+ // example:
+ // Axis::CanScroll(ParentLayerCoord) calls Axis::CanScroll() which calls
+ // Axis::GetPageLength() which calls Axis::GetFrameMetrics() which calls
+ // AsyncPanZoomController::GetFrameMetrics(), therefore, this monitor should
+ // be held before calling the CanScroll function of |mX| and |mY|. These
+ // coupled relationships bring us the burden of taking care of when the
+ // monitor should be held, so they should be decoupled in the future.
+ mutable RecursiveMutex mRecursiveMutex MOZ_UNANNOTATED;
+
+ private:
+ // Metadata of the container layer corresponding to this APZC. This is
+ // stored here so that it is accessible from the UI/controller thread.
+ // These are the metrics at last content paint, the most recent
+ // values we were notified of in NotifyLayersUpdate(). Since it represents
+ // the Gecko state, it should be used as a basis for untransformation when
+ // sending messages back to Gecko.
+ ScrollMetadata mLastContentPaintMetadata;
+ FrameMetrics& mLastContentPaintMetrics; // for convenience, refers to
+ // mLastContentPaintMetadata.mMetrics
+ // The last content repaint request.
+ RepaintRequest mLastPaintRequestMetrics;
+ // The metrics that we expect content to have. This is updated when we
+ // request a content repaint, and when we receive a shadow layers update.
+ // This allows us to transform events into Gecko's coordinate space.
+ ExpectedGeckoMetrics mExpectedGeckoMetrics;
+
+ // This holds important state from the Metrics() at previous times
+ // SampleCompositedAsyncTransform() was called. This will always have at least
+ // one item. mRecursiveMutex must be held when using or modifying this member.
+ // Samples should be inserted to the "back" of the deque and extracted from
+ // the "front".
+ std::deque<SampledAPZCState> mSampledState;
+
+ // Groups state variables that are specific to a platform.
+ // Initialized on first use.
+ UniquePtr<PlatformSpecificStateBase> mPlatformSpecificState;
+
+ // This flag is set to true when we are in a axis-locked pan as a result of
+ // the touch-action CSS property.
+ bool mPanDirRestricted;
+
+ // This flag is set to true when we are in a pinch-locked state. ie: user
+ // is performing a two-finger pan rather than a pinch gesture
+ bool mPinchLocked;
+
+ // Stores the pinch events that occured within a given timeframe. Used to
+ // calculate the focusChange and spanDistance within a fixed timeframe.
+ // RecentEventsBuffer is not threadsafe. Should only be accessed on the
+ // controller thread.
+ RecentEventsBuffer<PinchGestureInput> mPinchEventBuffer;
+
+ // Most up-to-date constraints on zooming. These should always be reasonable
+ // values; for example, allowing a min zoom of 0.0 can cause very bad things
+ // to happen. Hold mRecursiveMutex when accessing this.
+ ZoomConstraints mZoomConstraints;
+
+ // The last time the compositor has sampled the content transform for this
+ // frame.
+ SampleTime mLastSampleTime;
+
+ // The last sample time at which we submitted a checkerboarding report.
+ SampleTime mLastCheckerboardReport;
+
+ // Stores the previous focus point if there is a pinch gesture happening. Used
+ // to allow panning by moving multiple fingers (thus moving the focus point).
+ ParentLayerPoint mLastZoomFocus;
+
+ // Stores the previous zoom level at which we last sent a ScaleGestureComplete
+ // notification.
+ CSSToParentLayerScale mLastNotifiedZoom;
+
+ // Accessing mAnimation needs to be protected by mRecursiveMutex
+ RefPtr<AsyncPanZoomAnimation> mAnimation;
+
+ UniquePtr<OverscrollEffectBase> mOverscrollEffect;
+
+ // Zoom animation id, used for zooming in WebRender. This should only be
+ // set on the APZC instance for the root content document (i.e. the one we
+ // support zooming on), and is only used if WebRender is enabled. The
+ // animation id itself refers to the transform animation id that was set on
+ // the stacking context in the WR display list. By changing the transform
+ // associated with this id, we can adjust the scaling that WebRender applies,
+ // thereby controlling the zoom.
+ Maybe<uint64_t> mZoomAnimationId;
+
+ // Position on screen where user first put their finger down.
+ ExternalPoint mStartTouch;
+
+ // Accessing mScrollPayload needs to be protected by mRecursiveMutex
+ Maybe<CompositionPayload> mScrollPayload;
+
+ // Representing sampled scroll offset generation, this value is bumped up
+ // every time this APZC sampled new scroll offset.
+ APZScrollGeneration mScrollGeneration;
+
+ friend class Axis;
+
+ public:
+ Maybe<CompositionPayload> NotifyScrollSampling();
+
+ /**
+ * Invoke |callable|, passing |mLastContentPaintMetrics| as argument,
+ * while holding the APZC lock required to access |mLastContentPaintMetrics|.
+ * This allows code outside of an AsyncPanZoomController method implementation
+ * to access |mLastContentPaintMetrics| without having to make a copy of it.
+ * Passes through the return value of |callable|.
+ */
+ template <typename Callable>
+ auto CallWithLastContentPaintMetrics(const Callable& callable) const
+ -> decltype(callable(mLastContentPaintMetrics)) {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ return callable(mLastContentPaintMetrics);
+ }
+
+ void SetZoomAnimationId(const Maybe<uint64_t>& aZoomAnimationId);
+ Maybe<uint64_t> GetZoomAnimationId() const;
+
+ /* ===================================================================
+ * The functions and members in this section are used to expose
+ * the current async transform state to callers.
+ */
+ public:
+ /**
+ * Allows consumers of async transforms to specify for what purpose they are
+ * using the async transform:
+ *
+ * |eForHitTesting| is intended for hit-testing and other uses that need
+ * the most up-to-date transform, reflecting all events
+ * that have been processed so far, even if the transform
+ * is not yet reflected visually.
+ * |eForCompositing| is intended for the transform that should be reflected
+ * visually.
+ *
+ * For example, if an APZC has metrics with the mForceDisableApz flag set,
+ * then the |eForCompositing| async transform will be empty, while the
+ * |eForHitTesting| async transform will reflect processed input events
+ * regardless of mForceDisableApz.
+ */
+ enum AsyncTransformConsumer {
+ eForHitTesting,
+ eForCompositing,
+ };
+
+ /**
+ * Get the current scroll offset of the scrollable frame corresponding
+ * to this APZC, including the effects of any asynchronous panning and
+ * zooming, in ParentLayer pixels.
+ */
+ ParentLayerPoint GetCurrentAsyncScrollOffset(
+ AsyncTransformConsumer aMode) const;
+
+ /**
+ * Get the current visual viewport of the scrollable frame corresponding
+ * to this APZC, including the effects of any asynchronous panning and
+ * zooming, in CSS pixels.
+ */
+ CSSRect GetCurrentAsyncVisualViewport(AsyncTransformConsumer aMode) const;
+
+ /**
+ * Return a visual effect that reflects this apzc's
+ * overscrolled state, if any.
+ */
+ AsyncTransformComponentMatrix GetOverscrollTransform(
+ AsyncTransformConsumer aMode) const;
+
+ /**
+ * Returns the incremental transformation corresponding to the async pan/zoom
+ * in progress. That is, when this transform is multiplied with the layer's
+ * existing transform, it will make the layer appear with the desired pan/zoom
+ * amount.
+ * The transform can have both scroll and zoom components; the caller can
+ * request just one or the other, or both, via the |aComponents| parameter.
+ * When only the eLayout component is requested, the returned translation
+ * should really be a LayerPoint, rather than a ParentLayerPoint, as it will
+ * not be scaled by the asynchronous zoom.
+ * |aMode| specifies whether the async transform is queried for the purpose of
+ * hit testing (eHitTesting) in which case the latest values from |Metrics()|
+ * are used, or for compositing (eCompositing) in which case a sampled value
+ * from |mSampledState| is used.
+ * |aSampleIndex| specifies which sample in |mSampledState| to use.
+ */
+ AsyncTransform GetCurrentAsyncTransform(
+ AsyncTransformConsumer aMode,
+ AsyncTransformComponents aComponents = LayoutAndVisual,
+ std::size_t aSampleIndex = 0) const;
+
+ /**
+ * Returns the same transform as GetCurrentAsyncTransform(), but includes
+ * any transform due to axis over-scroll.
+ */
+ AsyncTransformComponentMatrix GetCurrentAsyncTransformWithOverscroll(
+ AsyncTransformConsumer aMode,
+ AsyncTransformComponents aComponents = LayoutAndVisual,
+ std::size_t aSampleIndex = 0) const;
+
+ AutoTArray<wr::SampledScrollOffset, 2> GetSampledScrollOffsets() const;
+
+ /**
+ * Returns the "zoom" bits of the transform. This includes both the rasterized
+ * (layout device to layer scale) and async (layer scale to parent layer
+ * scale) components of the zoom.
+ */
+ LayoutDeviceToParentLayerScale GetCurrentPinchZoomScale(
+ AsyncTransformConsumer aMode) const;
+
+ ParentLayerRect GetCompositionBounds() const {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ return mScrollMetadata.GetMetrics().GetCompositionBounds();
+ }
+
+ LayoutDeviceToLayerScale GetCumulativeResolution() const {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ return mScrollMetadata.GetMetrics().GetCumulativeResolution();
+ }
+
+ // Returns the delta for the given InputData.
+ ParentLayerPoint GetDeltaForEvent(const InputData& aEvent) const;
+
+ /**
+ * Get the current scroll range of the scrollable frame coreesponding to this
+ * APZC.
+ */
+ CSSRect GetCurrentScrollRangeInCssPixels() const;
+
+ private:
+ /**
+ * Advances to the next sample, if there is one, the list of sampled states
+ * stored in mSampledState. This will make the result of
+ * |GetCurrentAsyncTransform(eForCompositing)| and similar functions reflect
+ * the async scroll offset and zoom of the next sample. See also
+ * SampleCompositedAsyncTransform which creates the samples.
+ */
+ void AdvanceToNextSample();
+
+ /**
+ * Samples the composited async transform, storing the result into
+ * mSampledState. This will make the result of
+ * |GetCurrentAsyncTransform(eForCompositing)| and similar functions reflect
+ * the async scroll offset and zoom stored in |Metrics()| when the sample
+ * is activated via some future call to |AdvanceToNextSample|.
+ *
+ * Returns true if the newly sampled value is different from the last
+ * sampled value.
+ */
+ bool SampleCompositedAsyncTransform(
+ const RecursiveMutexAutoLock& aProofOfLock);
+
+ /**
+ * Updates the sample at the front of mSampledState with the latest
+ * metrics. This makes the result of
+ * |GetCurrentAsyncTransform(eForCompositing)| reflect the current Metrics().
+ */
+ void ResampleCompositedAsyncTransform(
+ const RecursiveMutexAutoLock& aProofOfLock);
+
+ /*
+ * Helper functions to query the async layout viewport, scroll offset, and
+ * zoom either directly from |Metrics()|, or from cached variables that
+ * store the required value from the last time it was sampled by calling
+ * SampleCompositedAsyncTransform(), depending on who is asking.
+ */
+ CSSRect GetEffectiveLayoutViewport(AsyncTransformConsumer aMode,
+ const RecursiveMutexAutoLock& aProofOfLock,
+ std::size_t aSampleIndex = 0) const;
+ CSSPoint GetEffectiveScrollOffset(AsyncTransformConsumer aMode,
+ const RecursiveMutexAutoLock& aProofOfLock,
+ std::size_t aSampleIndex = 0) const;
+ CSSToParentLayerScale GetEffectiveZoom(
+ AsyncTransformConsumer aMode, const RecursiveMutexAutoLock& aProofOfLock,
+ std::size_t aSampleIndex = 0) const;
+
+ /**
+ * Returns the visible portion of the content scrolled by this APZC, in
+ * CSS pixels. The caller must have acquired the mRecursiveMutex lock.
+ */
+ CSSRect GetVisibleRect(const RecursiveMutexAutoLock& aProofOfLock) const;
+
+ /**
+ * Returns a pair of displacements both in logical/physical units for
+ * |aEvent|.
+ */
+ std::tuple<ParentLayerPoint, ScreenPoint> GetDisplacementsForPanGesture(
+ const PanGestureInput& aEvent);
+
+ private:
+ friend class AutoApplyAsyncTestAttributes;
+
+ bool SuppressAsyncScrollOffset() const;
+
+ /**
+ * Applies |mTestAsyncScrollOffset| and |mTestAsyncZoom| to this
+ * AsyncPanZoomController. Calls |SampleCompositedAsyncTransform| to ensure
+ * that the GetCurrentAsync* functions consider the test offset and zoom in
+ * their computations.
+ */
+ void ApplyAsyncTestAttributes(const RecursiveMutexAutoLock& aProofOfLock);
+
+ /**
+ * Sets this AsyncPanZoomController's FrameMetrics to |aPrevFrameMetrics| and
+ * calls |SampleCompositedAsyncTransform| to unapply any test values applied
+ * by |ApplyAsyncTestAttributes|.
+ */
+ void UnapplyAsyncTestAttributes(const RecursiveMutexAutoLock& aProofOfLock,
+ const FrameMetrics& aPrevFrameMetrics,
+ const ParentLayerPoint& aPrevOverscroll);
+
+ /* ===================================================================
+ * The functions and members in this section are used to manage
+ * the state that tracks what this APZC is doing with the input events.
+ */
+ protected:
+ enum PanZoomState {
+ NOTHING, /* no touch-start events received */
+ FLING, /* all touches removed, but we're still scrolling page */
+ TOUCHING, /* one touch-start event received */
+
+ PANNING, /* panning the frame */
+ PANNING_LOCKED_X, /* touch-start followed by move (i.e. panning with axis
+ lock) X axis */
+ PANNING_LOCKED_Y, /* as above for Y axis */
+
+ PAN_MOMENTUM, /* like PANNING, but controlled by momentum PanGestureInput
+ events */
+
+ PINCHING, /* nth touch-start, where n > 1. this mode allows pan and zoom */
+ ANIMATING_ZOOM, /* animated zoom to a new rect */
+ OVERSCROLL_ANIMATION, /* Spring-based animation used to relieve overscroll
+ once the finger is lifted. */
+ SMOOTH_SCROLL, /* Smooth scrolling to destination, with physics
+ controlled by prefs specific to the scroll origin. */
+ SMOOTHMSD_SCROLL, /* SmoothMSD scrolling to destination. Used by
+ CSSOM-View smooth scroll-behavior */
+ WHEEL_SCROLL, /* Smooth scrolling to a destination for a wheel event. */
+ KEYBOARD_SCROLL, /* Smooth scrolling to a destination for a keyboard event.
+ */
+ AUTOSCROLL, /* Autoscroll animation. */
+ SCROLLBAR_DRAG /* Async scrollbar drag. */
+ };
+ // This is in theory protected by |mRecursiveMutex|; that is, it should be
+ // held whenever this is updated. In practice though... see bug 897017.
+ PanZoomState mState;
+
+ AxisX mX;
+ AxisY mY;
+
+ /**
+ * Returns wheter the given input state is a user pan-gesture.
+ *
+ * Note: momentum pan gesture states are not considered a panning state.
+ */
+ static bool IsPanningState(PanZoomState aState);
+
+ /**
+ * Returns wheter a delayed transform end is queued.
+ */
+ bool IsDelayedTransformEndSet();
+
+ /**
+ * Returns wheter a delayed transform end is queued.
+ */
+ void SetDelayedTransformEnd(bool aDelayedTransformEnd);
+
+ /**
+ * Returns whether the specified PanZoomState does not need to be reset when
+ * a scroll offset update is processed.
+ */
+ static bool CanHandleScrollOffsetUpdate(PanZoomState aState);
+
+ /**
+ * Determine whether a main-thread scroll offset update should result in
+ * a call to CancelAnimation() (which interrupts in-progress animations and
+ * gestures).
+ *
+ * If the update is a relative update, |aRelativeDelta| contains its amount.
+ * If the update is not a relative update, GetMetrics() should already reflect
+ * the new offset at the time of the call.
+ */
+ bool ShouldCancelAnimationForScrollUpdate(
+ const Maybe<CSSPoint>& aRelativeDelta);
+
+ private:
+ friend class StateChangeNotificationBlocker;
+ /**
+ * A counter of how many StateChangeNotificationBlockers are active.
+ * A non-zero count will prevent state change notifications from
+ * being dispatched. Only code that holds mRecursiveMutex should touch this.
+ */
+ int mNotificationBlockers;
+
+ /**
+ * Helper to set the current state, without content controller events
+ * for the state change. This is useful in cases where the content
+ * controller events may need to be delayed.
+ */
+ PanZoomState SetStateNoContentControllerDispatch(PanZoomState aNewState);
+
+ /**
+ * Helper to set the current state. Holds mRecursiveMutex before actually
+ * setting it and fires content controller events based on state changes.
+ * Always set the state using this call, do not set it directly.
+ */
+ void SetState(PanZoomState aNewState);
+ /**
+ * Helper for getting the current state which acquires mRecursiveMutex
+ * before accessing the field.
+ */
+ PanZoomState GetState() const;
+ /**
+ * Fire content controller notifications about state changes, assuming no
+ * StateChangeNotificationBlocker has been activated.
+ */
+ void DispatchStateChangeNotification(PanZoomState aOldState,
+ PanZoomState aNewState);
+ /**
+ * Internal helpers for checking general state of this apzc.
+ */
+ bool IsInTransformingState() const;
+ static bool IsTransformingState(PanZoomState aState);
+
+ /* ===================================================================
+ * The functions and members in this section are used to manage
+ * blocks of touch events and the state needed to deal with content
+ * listeners.
+ */
+ public:
+ /**
+ * Flush a repaint request if one is needed, without throttling it with the
+ * paint throttler.
+ */
+ void FlushRepaintForNewInputBlock();
+
+ /**
+ * Given an input event and the touch block it belongs to, check if the
+ * event can lead to a panning/zooming behavior.
+ * This is used for logic related to the pointer events spec (figuring out
+ * when to dispatch the pointercancel event), as well as an input to the
+ * computation of the APZHandledResult for the event (used on Android to
+ * govern dynamic toolbar and pull-to-refresh behaviour).
+ */
+ PointerEventsConsumableFlags ArePointerEventsConsumable(
+ TouchBlockState* aBlock, const MultiTouchInput& aInput);
+
+ /**
+ * Clear internal state relating to touch input handling.
+ */
+ void ResetTouchInputState();
+
+ /**
+ Clear internal state relating to pan gesture input handling.
+ */
+ void ResetPanGestureInputState();
+
+ /**
+ * Gets a ref to the input queue that is shared across the entire tree
+ * manager.
+ */
+ const RefPtr<InputQueue>& GetInputQueue() const;
+
+ private:
+ void CancelAnimationAndGestureState();
+
+ RefPtr<InputQueue> mInputQueue;
+ InputBlockState* GetCurrentInputBlock() const;
+ TouchBlockState* GetCurrentTouchBlock() const;
+ bool HasReadyTouchBlock() const;
+
+ PanGestureBlockState* GetCurrentPanGestureBlock() const;
+ PinchGestureBlockState* GetCurrentPinchGestureBlock() const;
+
+ private:
+ /* ===================================================================
+ * The functions and members in this section are used to manage
+ * fling animations, smooth scroll animations, and overscroll
+ * during a fling or smooth scroll.
+ */
+ public:
+ /**
+ * Attempt a fling with the velocity specified in |aHandoffState|.
+ * |aHandoffState.mIsHandoff| should be true iff. the fling was handed off
+ * from a previous APZC, and determines whether acceleration is applied
+ * to the fling.
+ * We only accept the fling in the direction(s) in which we are pannable.
+ * Returns the "residual velocity", i.e. the portion of
+ * |aHandoffState.mVelocity| that this APZC did not consume.
+ */
+ ParentLayerPoint AttemptFling(const FlingHandoffState& aHandoffState);
+
+ ParentLayerPoint AdjustHandoffVelocityForOverscrollBehavior(
+ ParentLayerPoint& aHandoffVelocity) const;
+
+ private:
+ friend class StackScrollerFlingAnimation;
+ friend class AutoscrollAnimation;
+ template <typename FlingPhysics>
+ friend class GenericFlingAnimation;
+ friend class AndroidFlingPhysics;
+ friend class DesktopFlingPhysics;
+ friend class OverscrollAnimation;
+ friend class SmoothMsdScrollAnimation;
+ friend class GenericScrollAnimation;
+ friend class WheelScrollAnimation;
+ friend class ZoomAnimation;
+
+ friend class GenericOverscrollEffect;
+ friend class WidgetOverscrollEffect;
+ friend struct apz::AsyncScrollThumbTransformer;
+
+ FlingAccelerator mFlingAccelerator;
+
+ // Indicates if the repaint-during-pinch timer is currently set
+ bool mPinchPaintTimerSet;
+
+ // Indicates a delayed transform end notification is queued, and the
+ // transform-end timer is currently set. mRecursiveMutex must be held
+ // when using or modifying this member.
+ bool mDelayedTransformEnd;
+
+ // Deal with overscroll resulting from a fling animation. This is only ever
+ // called on APZC instances that were actually performing a fling.
+ // The overscroll is handled by trying to hand the fling off to an APZC
+ // later in the handoff chain, or if there are no takers, continuing the
+ // fling and entering an overscrolled state.
+ void HandleFlingOverscroll(
+ const ParentLayerPoint& aVelocity, SideBits aOverscrollSideBits,
+ const RefPtr<const OverscrollHandoffChain>& aOverscrollHandoffChain,
+ const RefPtr<const AsyncPanZoomController>& aScrolledApzc);
+
+ void HandleSmoothScrollOverscroll(const ParentLayerPoint& aVelocity,
+ SideBits aOverscrollSideBits);
+
+ // Start an overscroll animation with the given initial velocity.
+ void StartOverscrollAnimation(const ParentLayerPoint& aVelocity,
+ SideBits aOverscrollSideBits);
+
+ // Start a smooth-scrolling animation to the given destination, with physics
+ // based on the prefs for the indicated origin.
+ void SmoothScrollTo(const CSSPoint& aDestination,
+ const ScrollOrigin& aOrigin);
+
+ // Start a smooth-scrolling animation to the given destination, with MSD
+ // physics that is suited for scroll-snapping.
+ void SmoothMsdScrollTo(CSSSnapTarget&& aDestination,
+ ScrollTriggeredByScript aTriggeredByScript);
+
+ // Returns whether overscroll is allowed during an event.
+ bool AllowScrollHandoffInCurrentBlock() const;
+
+ // Invoked by the pinch repaint timer.
+ void DoDelayedRequestContentRepaint();
+
+ // Invoked by the on pan-end handler to ensure that scrollend is only
+ // fired once when a momentum pan or scroll snap is triggered as a
+ // result of the pan gesture.
+ void DoDelayedTransformEndNotification(PanZoomState aOldState);
+
+ // Compute the number of ParentLayer pixels per (Screen) inch at the given
+ // point and in the given direction.
+ float ComputePLPPI(ParentLayerPoint aPoint,
+ ParentLayerPoint aDirection) const;
+
+ Maybe<CSSPoint> GetCurrentAnimationDestination(
+ const RecursiveMutexAutoLock& aProofOfLock) const;
+
+ /* ===================================================================
+ * The functions and members in this section are used to make ancestor chains
+ * out of APZC instances. These chains can only be walked or manipulated
+ * while holding the lock in the associated APZCTreeManager instance.
+ */
+ public:
+ void SetParent(AsyncPanZoomController* aParent) { mParent = aParent; }
+
+ AsyncPanZoomController* GetParent() const { return mParent; }
+
+ /* Returns true if there is no APZC higher in the tree with the same
+ * layers id.
+ */
+ bool HasNoParentWithSameLayersId() const {
+ return !mParent || (mParent->mLayersId != mLayersId);
+ }
+
+ bool IsRootForLayersId() const {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ return mScrollMetadata.IsLayersIdRoot();
+ }
+
+ bool IsRootContent() const {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ return Metrics().IsRootContent();
+ }
+
+ private:
+ // |mTreeManager| belongs in this section but it's declaration is a bit
+ // further above due to initialization-order constraints.
+
+ RefPtr<AsyncPanZoomController> mParent;
+
+ /* ===================================================================
+ * The functions and members in this section are used for scrolling,
+ * including handing off scroll to another APZC, and overscrolling.
+ */
+
+ ScrollableLayerGuid::ViewID GetScrollId() const {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ return Metrics().GetScrollId();
+ }
+
+ public:
+ ScrollableLayerGuid::ViewID GetScrollHandoffParentId() const {
+ return mScrollMetadata.GetScrollParentId();
+ }
+
+ /**
+ * Attempt to scroll in response to a touch-move from |aStartPoint| to
+ * |aEndPoint|, which are in our (transformed) screen coordinates.
+ * Due to overscroll handling, there may not actually have been a touch-move
+ * at these points, but this function will scroll as if there had been.
+ * If this attempt causes overscroll (i.e. the layer cannot be scrolled
+ * by the entire amount requested), the overscroll is passed back to the
+ * tree manager via APZCTreeManager::DispatchScroll(). If the tree manager
+ * does not find an APZC further in the handoff chain to accept the
+ * overscroll, and this APZC is pannable, this APZC enters an overscrolled
+ * state.
+ * |aOverscrollHandoffChain| and |aOverscrollHandoffChainIndex| are used by
+ * the tree manager to keep track of which APZC to hand off the overscroll
+ * to; this function increments the chain and the index and passes it on to
+ * APZCTreeManager::DispatchScroll() in the event of overscroll.
+ * Returns true iff. this APZC, or an APZC further down the
+ * handoff chain, accepted the scroll (possibly entering an overscrolled
+ * state). If this returns false, the caller APZC knows that it should enter
+ * an overscrolled state itself if it can.
+ * aStartPoint and aEndPoint are modified depending on how much of the
+ * scroll gesture was consumed by APZCs in the handoff chain.
+ */
+ bool AttemptScroll(ParentLayerPoint& aStartPoint, ParentLayerPoint& aEndPoint,
+ OverscrollHandoffState& aOverscrollHandoffState);
+
+ void FlushRepaintForOverscrollHandoff();
+
+ /**
+ * If overscrolled, start a snap-back animation and return true. Even if not
+ * overscrolled, this function tries to snap back to if there's an applicable
+ * scroll snap point.
+ * Otherwise return false.
+ */
+ bool SnapBackIfOverscrolled();
+
+ /**
+ * NOTE: Similar to above but this function doesn't snap back to the scroll
+ * snap point.
+ */
+ bool SnapBackIfOverscrolledForMomentum(const ParentLayerPoint& aVelocity);
+
+ /**
+ * Build the chain of APZCs along which scroll will be handed off when
+ * this APZC receives input events.
+ *
+ * Notes on lifetime and const-correctness:
+ * - The returned handoff chain is |const|, to indicate that it cannot be
+ * changed after being built.
+ * - When passing the chain to a function that uses it without storing it,
+ * pass it by reference-to-const (as in |const OverscrollHandoffChain&|).
+ * - When storing the chain, store it by RefPtr-to-const (as in
+ * |RefPtr<const OverscrollHandoffChain>|). This ensures the chain is
+ * kept alive. Note that queueing a task that uses the chain as an
+ * argument constitutes storing, as the task may outlive its queuer.
+ * - When passing the chain to a function that will store it, pass it as
+ * |const RefPtr<const OverscrollHandoffChain>&|. This allows the
+ * function to copy it into the |RefPtr<const OverscrollHandoffChain>|
+ * that will store it, while avoiding an unnecessary copy (and thus
+ * AddRef() and Release()) when passing it.
+ */
+ RefPtr<const OverscrollHandoffChain> BuildOverscrollHandoffChain();
+
+ private:
+ /**
+ * A helper function for calling APZCTreeManager::DispatchScroll().
+ * Guards against the case where the APZC is being concurrently destroyed
+ * (and thus mTreeManager is being nulled out).
+ */
+ bool CallDispatchScroll(ParentLayerPoint& aStartPoint,
+ ParentLayerPoint& aEndPoint,
+ OverscrollHandoffState& aOverscrollHandoffState);
+
+ void RecordScrollPayload(const TimeStamp& aTimeStamp);
+
+ /**
+ * A helper function for overscrolling during panning. This is a wrapper
+ * around OverscrollBy() that also implements restrictions on entering
+ * overscroll based on the pan angle.
+ */
+ void OverscrollForPanning(ParentLayerPoint& aOverscroll,
+ const ScreenPoint& aPanDistance);
+
+ /**
+ * Try to overscroll by 'aOverscroll'.
+ * If we are pannable on a particular axis, that component of 'aOverscroll'
+ * is transferred to any existing overscroll.
+ */
+ void OverscrollBy(ParentLayerPoint& aOverscroll);
+
+ /* ===================================================================
+ * The functions and members in this section are used to maintain the
+ * area that this APZC instance is responsible for. This is used when
+ * hit-testing to see which APZC instance should handle touch events.
+ */
+ public:
+ void SetAncestorTransform(const AncestorTransform& aAncestorTransform) {
+ mAncestorTransform = aAncestorTransform;
+ }
+
+ Matrix4x4 GetAncestorTransform() const {
+ return mAncestorTransform.CombinedTransform();
+ }
+
+ bool AncestorTransformContainsPerspective() const {
+ return mAncestorTransform.ContainsPerspectiveTransform();
+ }
+
+ // Return the perspective transform component of the ancestor transform.
+ Matrix4x4 GetAncestorTransformPerspective() const {
+ return mAncestorTransform.GetPerspectiveTransform();
+ }
+
+ // Returns whether or not this apzc contains the given screen point within
+ // its composition bounds.
+ bool Contains(const ScreenIntPoint& aPoint) const;
+
+ bool IsInOverscrollGutter(const ScreenPoint& aHitTestPoint) const;
+ bool IsInOverscrollGutter(const ParentLayerPoint& aHitTestPoint) const;
+
+ bool IsOverscrolled() const;
+
+ bool IsOverscrollAnimationRunning() const {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ return mState == OVERSCROLL_ANIMATION;
+ }
+
+ // IsPhysicallyOverscrolled() checks whether the APZC is overscrolled
+ // by an overscroll effect which applies a transform to the APZC's contents.
+ bool IsPhysicallyOverscrolled() const;
+
+ private:
+ bool IsInInvalidOverscroll() const;
+
+ public:
+ bool IsInPanningState() const;
+
+ // Returns whether being in the middle of a gesture. E.g., this APZC has
+ // started handling a pan gesture but hasn't yet received pan-end, etc.
+ bool IsInScrollingGesture() const;
+
+ private:
+ /* This is the cumulative CSS transform for all the layers from (and
+ * including) the parent APZC down to (but excluding) this one, and excluding
+ * any perspective transforms. */
+ AncestorTransform mAncestorTransform;
+
+ /* ===================================================================
+ * The functions and members in this section are used for testing
+ * and assertion purposes only.
+ */
+ public:
+ /**
+ * Gets whether this APZC has performed async key scrolling.
+ */
+ bool TestHasAsyncKeyScrolled() const { return mTestHasAsyncKeyScrolled; }
+
+ /**
+ * Set an extra offset for testing async scrolling.
+ */
+ void SetTestAsyncScrollOffset(const CSSPoint& aPoint);
+ /**
+ * Set an extra offset for testing async scrolling.
+ */
+ void SetTestAsyncZoom(const LayerToParentLayerScale& aZoom);
+
+ LayersId GetLayersId() const { return mLayersId; }
+
+ bool IsAsyncZooming() const {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ return mState == PINCHING || mState == ANIMATING_ZOOM;
+ }
+
+ private:
+ // The timestamp of the latest touch start event.
+ TimeStamp mTouchStartTime;
+ // Used for interpolating touch events that cross the touch-start
+ // tolerance threshold.
+ struct TouchSample {
+ ExternalPoint mPosition;
+ TimeStamp mTimeStamp;
+ };
+ // Information about the latest touch event.
+ // This is only populated when we're in the TOUCHING state
+ // (and thus the last touch event has only one touch point).
+ TouchSample mLastTouch;
+ // The time duration between mTouchStartTime and the touchmove event that
+ // started the pan (the touchmove event that transitioned this APZC from the
+ // TOUCHING state to one of the PANNING* states). Only valid while this APZC
+ // is in a panning state.
+ TimeDuration mTouchStartRestingTimeBeforePan;
+ Maybe<ParentLayerCoord> mMinimumVelocityDuringPan;
+ // This variable needs to be protected by |mRecursiveMutex|.
+ ScrollSnapTargetIds mLastSnapTargetIds;
+ // Extra offset to add to the async scroll position for testing
+ CSSPoint mTestAsyncScrollOffset;
+ // Extra zoom to include in the aync zoom for testing
+ LayerToParentLayerScale mTestAsyncZoom;
+ uint8_t mTestAttributeAppliers;
+ // Flag to track whether or not this APZC has ever async key scrolled.
+ bool mTestHasAsyncKeyScrolled;
+
+ /* ===================================================================
+ * The functions and members in this section are used for checkerboard
+ * recording.
+ */
+ private:
+ // Helper function to update the in-progress checkerboard event, if any.
+ void UpdateCheckerboardEvent(const MutexAutoLock& aProofOfLock,
+ uint32_t aMagnitude);
+
+ // Mutex protecting mCheckerboardEvent
+ Mutex mCheckerboardEventLock MOZ_UNANNOTATED;
+ // This is created when this APZC instance is first included as part of a
+ // composite. If a checkerboard event takes place, this is destroyed at the
+ // end of the event, and a new one is created on the next composite.
+ UniquePtr<CheckerboardEvent> mCheckerboardEvent;
+ // This is used to track the total amount of time that we could reasonably
+ // be checkerboarding. Combined with other info, this allows us to
+ // meaningfully say how frequently users actually encounter checkerboarding.
+ PotentialCheckerboardDurationTracker mPotentialCheckerboardTracker;
+
+ /* ===================================================================
+ * The functions in this section are used for CSS scroll snapping.
+ */
+
+ // If moving |aStartPosition| by |aDelta| should trigger scroll snapping,
+ // adjust |aDelta| to reflect the snapping (that is, make it a delta that will
+ // take us to the desired snap point). The delta is interpreted as being
+ // relative to |aStartPosition|, and if a target snap point is found,
+ // |aStartPosition| is also updated, to the value of the snap point.
+ // |aUnit| affects the snapping behaviour (see ScrollSnapUtils::
+ // GetSnapPointForDestination).
+ // Returns true iff. a target snap point was found.
+ Maybe<CSSSnapTarget> MaybeAdjustDeltaForScrollSnapping(
+ ScrollUnit aUnit, ScrollSnapFlags aSnapFlags, ParentLayerPoint& aDelta,
+ CSSPoint& aStartPosition);
+
+ // A wrapper function of MaybeAdjustDeltaForScrollSnapping for
+ // ScrollWheelInput.
+ Maybe<CSSSnapTarget> MaybeAdjustDeltaForScrollSnappingOnWheelInput(
+ const ScrollWheelInput& aEvent, ParentLayerPoint& aDelta,
+ CSSPoint& aStartPosition);
+
+ Maybe<CSSSnapTarget> MaybeAdjustDestinationForScrollSnapping(
+ const KeyboardInput& aEvent, CSSPoint& aDestination,
+ ScrollSnapFlags aSnapFlags);
+
+ // Snap to a snap position nearby the current scroll position, if appropriate.
+ void ScrollSnap(ScrollSnapFlags aSnapFlags);
+
+ // Snap to a snap position nearby the destination predicted based on the
+ // current velocity, if appropriate.
+ void ScrollSnapToDestination();
+
+ // Snap to a snap position nearby the provided destination, if appropriate.
+ void ScrollSnapNear(const CSSPoint& aDestination, ScrollSnapFlags aSnapFlags);
+
+ // Find a snap point near |aDestination| that we should snap to.
+ // Returns the snap point if one was found, or an empty Maybe otherwise.
+ // |aUnit| affects the snapping behaviour (see ScrollSnapUtils::
+ // GetSnapPointForDestination). It should generally be determined by the
+ // type of event that's triggering the scroll.
+ Maybe<CSSSnapTarget> FindSnapPointNear(const CSSPoint& aDestination,
+ ScrollUnit aUnit,
+ ScrollSnapFlags aSnapFlags);
+
+ // If |aOriginalEvent| crosses the touch-start tolerance threshold, split it
+ // into two events: one that just reaches the threshold, and the remainder.
+ //
+ // |aPanThreshold| is the touch-start tolerance, and |aVectorLength| is
+ // the length of the vector from the touch-start position to |aOriginalEvent|.
+ // These values could be computed from |aOriginalEvent| but they are
+ // passed in for convenience since the caller also needs to compute them.
+ //
+ // |aExtPoint| is the position of |aOriginalEvent| in External coordinates,
+ // and in case of a split is modified by the function to reflect the position
+ // of of the first event. This is a workaround for the fact that recomputing
+ // the External position from the returned event would require a round-trip
+ // through |mScreenPoint| which is an integer.
+ Maybe<std::pair<MultiTouchInput, MultiTouchInput>> MaybeSplitTouchMoveEvent(
+ const MultiTouchInput& aOriginalEvent, ScreenCoord aPanThreshold,
+ float aVectorLength, ExternalPoint& aExtPoint);
+
+ friend std::ostream& operator<<(
+ std::ostream& aOut, const AsyncPanZoomController::PanZoomState& aState);
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif // mozilla_layers_PanZoomController_h
diff --git a/gfx/layers/apz/src/AutoDirWheelDeltaAdjuster.h b/gfx/layers/apz/src/AutoDirWheelDeltaAdjuster.h
new file mode 100644
index 0000000000..187641514a
--- /dev/null
+++ b/gfx/layers/apz/src/AutoDirWheelDeltaAdjuster.h
@@ -0,0 +1,89 @@
+/* -*- 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_AutoDirWheelDeltaAdjuster_h__
+#define __mozilla_layers_AutoDirWheelDeltaAdjuster_h__
+
+#include "Axis.h" // for AxisX, AxisY, Side
+#include "mozilla/WheelHandlingHelper.h" // for AutoDirWheelDeltaAdjuster
+
+namespace mozilla {
+namespace layers {
+
+/**
+ * About AutoDirWheelDeltaAdjuster:
+ * For an AutoDir wheel scroll, there's some situations where we should adjust a
+ * wheel event's delta values. AutoDirWheelDeltaAdjuster converts delta values
+ * for AutoDir scrolling. An AutoDir wheel scroll lets the user scroll a frame
+ * with only one scrollbar, using either a vertical or a horzizontal wheel.
+ * For more detail about the concept of AutoDir scrolling, see the comments in
+ * AutoDirWheelDeltaAdjuster.
+ *
+ * This is the APZ implementation of AutoDirWheelDeltaAdjuster.
+ */
+class MOZ_STACK_CLASS APZAutoDirWheelDeltaAdjuster final
+ : public AutoDirWheelDeltaAdjuster {
+ public:
+ /**
+ * @param aDeltaX DeltaX for a wheel event whose delta values will
+ * be adjusted upon calling adjust() when
+ * ShouldBeAdjusted() returns true.
+ * @param aDeltaY DeltaY for a wheel event, like DeltaX.
+ * @param aAxisX The X axis information provider for the current
+ * frame, such as whether the frame can be scrolled
+ * horizontally, leftwards or rightwards.
+ * @param aAxisY The Y axis information provider for the current
+ * frame, such as whether the frame can be scrolled
+ * vertically, upwards or downwards.
+ * @param aIsHorizontalContentRightToLeft
+ * Indicates whether the horizontal content starts
+ * at rightside. This value will decide which edge
+ * the adjusted scroll goes towards, in other words,
+ * it will decide the sign of the adjusted delta
+ * values). For detailed information, see
+ * IsHorizontalContentRightToLeft() in
+ * the base class AutoDirWheelDeltaAdjuster.
+ */
+ APZAutoDirWheelDeltaAdjuster(double& aDeltaX, double& aDeltaY,
+ const AxisX& aAxisX, const AxisY& aAxisY,
+ bool aIsHorizontalContentRightToLeft)
+ : AutoDirWheelDeltaAdjuster(aDeltaX, aDeltaY),
+ mAxisX(aAxisX),
+ mAxisY(aAxisY),
+ mIsHorizontalContentRightToLeft(aIsHorizontalContentRightToLeft) {}
+
+ private:
+ virtual bool CanScrollAlongXAxis() const override {
+ return mAxisX.CanScroll();
+ }
+ virtual bool CanScrollAlongYAxis() const override {
+ return mAxisY.CanScroll();
+ }
+ virtual bool CanScrollUpwards() const override {
+ return mAxisY.CanScrollTo(eSideTop);
+ }
+ virtual bool CanScrollDownwards() const override {
+ return mAxisY.CanScrollTo(eSideBottom);
+ }
+ virtual bool CanScrollLeftwards() const override {
+ return mAxisX.CanScrollTo(eSideLeft);
+ }
+ virtual bool CanScrollRightwards() const override {
+ return mAxisX.CanScrollTo(eSideRight);
+ }
+ virtual bool IsHorizontalContentRightToLeft() const override {
+ return mIsHorizontalContentRightToLeft;
+ }
+
+ const AxisX& mAxisX;
+ const AxisY& mAxisY;
+ bool mIsHorizontalContentRightToLeft;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif // __mozilla_layers_AutoDirWheelDeltaAdjuster_h__
diff --git a/gfx/layers/apz/src/AutoscrollAnimation.cpp b/gfx/layers/apz/src/AutoscrollAnimation.cpp
new file mode 100644
index 0000000000..8d4b8fca10
--- /dev/null
+++ b/gfx/layers/apz/src/AutoscrollAnimation.cpp
@@ -0,0 +1,93 @@
+/* -*- 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 "AutoscrollAnimation.h"
+
+#include <cmath> // for sqrtf()
+
+#include "AsyncPanZoomController.h"
+#include "APZCTreeManager.h"
+#include "FrameMetrics.h"
+#include "mozilla/Telemetry.h" // for Telemetry
+
+namespace mozilla {
+namespace layers {
+
+// Helper function for AutoscrollAnimation::DoSample().
+// Basically copied as-is from toolkit/actors/AutoScrollChild.jsm.
+static float Accelerate(ScreenCoord curr, ScreenCoord start) {
+ static const int speed = 12;
+ float val = (curr - start) / speed;
+ if (val > 1) {
+ return val * sqrtf(val) - 1;
+ }
+ if (val < -1) {
+ return val * sqrtf(-val) + 1;
+ }
+ return 0;
+}
+
+AutoscrollAnimation::AutoscrollAnimation(AsyncPanZoomController& aApzc,
+ const ScreenPoint& aAnchorLocation)
+ : mApzc(aApzc), mAnchorLocation(aAnchorLocation) {}
+
+bool AutoscrollAnimation::DoSample(FrameMetrics& aFrameMetrics,
+ const TimeDuration& aDelta) {
+ APZCTreeManager* treeManager = mApzc.GetApzcTreeManager();
+ if (!treeManager) {
+ return false;
+ }
+
+ ScreenPoint mouseLocation = treeManager->GetCurrentMousePosition();
+
+ // The implementation of this function closely mirrors that of its main-
+ // thread equivalent, the autoscrollLoop() function in
+ // toolkit/actors/AutoScrollChild.jsm.
+
+ // Avoid long jumps when the browser hangs for more than |maxTimeDelta| ms.
+ static const TimeDuration maxTimeDelta = TimeDuration::FromMilliseconds(100);
+ TimeDuration timeDelta = TimeDuration::Min(aDelta, maxTimeDelta);
+
+ float timeCompensation = timeDelta.ToMilliseconds() / 20;
+
+ // Notes:
+ // - The main-thread implementation rounds the scroll delta to an integer,
+ // and keeps track of the fractional part as an "error". It does this
+ // because it uses Window.scrollBy() or Element.scrollBy() to perform
+ // the scrolling, and those functions truncate the fractional part of
+ // the offset. APZ does no such truncation, so there's no need to keep
+ // track of the fractional part separately.
+ // - The Accelerate() function takes Screen coordinates as inputs, but
+ // its output is interpreted as CSS coordinates. This is intentional,
+ // insofar as autoscrollLoop() does the same thing.
+ CSSPoint scrollDelta{
+ Accelerate(mouseLocation.x, mAnchorLocation.x) * timeCompensation,
+ Accelerate(mouseLocation.y, mAnchorLocation.y) * timeCompensation};
+
+ mApzc.ScrollByAndClamp(scrollDelta);
+
+ // An autoscroll animation never ends of its own accord.
+ // It can be stopped in response to various input events, in which case
+ // AsyncPanZoomController::StopAutoscroll() will stop it via
+ // CancelAnimation().
+ return true;
+}
+
+void AutoscrollAnimation::Cancel(CancelAnimationFlags aFlags) {
+ // The cancellation was initiated by browser.js, so there's no need to
+ // notify it.
+ if (aFlags & TriggeredExternally) {
+ return;
+ }
+
+ if (RefPtr<GeckoContentController> controller =
+ mApzc.GetGeckoContentController()) {
+ controller->CancelAutoscroll(mApzc.GetGuid());
+ }
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/apz/src/AutoscrollAnimation.h b/gfx/layers/apz/src/AutoscrollAnimation.h
new file mode 100644
index 0000000000..a37f6d473a
--- /dev/null
+++ b/gfx/layers/apz/src/AutoscrollAnimation.h
@@ -0,0 +1,42 @@
+/* -*- 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_AutocrollAnimation_h_
+#define mozilla_layers_AutocrollAnimation_h_
+
+#include "AsyncPanZoomAnimation.h"
+
+namespace mozilla {
+namespace layers {
+
+class AsyncPanZoomController;
+
+class AutoscrollAnimation : public AsyncPanZoomAnimation {
+ public:
+ AutoscrollAnimation(AsyncPanZoomController& aApzc,
+ const ScreenPoint& aAnchorLocation);
+
+ bool DoSample(FrameMetrics& aFrameMetrics,
+ const TimeDuration& aDelta) override;
+
+ bool HandleScrollOffsetUpdate(
+ const Maybe<CSSPoint>& aRelativeDelta) override {
+ // Autoscroll works using screen space coordinates, so there's no work we
+ // need to do to handle either a relative or an absolute scroll update.
+ return true;
+ }
+
+ void Cancel(CancelAnimationFlags aFlags) override;
+
+ private:
+ AsyncPanZoomController& mApzc;
+ ScreenPoint mAnchorLocation;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif // mozilla_layers_AutoscrollAnimation_h_
diff --git a/gfx/layers/apz/src/Axis.cpp b/gfx/layers/apz/src/Axis.cpp
new file mode 100644
index 0000000000..f0842864d8
--- /dev/null
+++ b/gfx/layers/apz/src/Axis.cpp
@@ -0,0 +1,733 @@
+/* -*- 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 "Axis.h"
+
+#include <math.h> // for fabsf, pow, powf
+#include <algorithm> // for max
+
+#include "APZCTreeManager.h" // for APZCTreeManager
+#include "AsyncPanZoomController.h" // for AsyncPanZoomController
+#include "FrameMetrics.h" // for FrameMetrics
+#include "SimpleVelocityTracker.h" // for FrameMetrics
+#include "mozilla/Attributes.h" // for final
+#include "mozilla/Preferences.h" // for Preferences
+#include "mozilla/gfx/Rect.h" // for RoundedIn
+#include "mozilla/layers/APZThreadUtils.h" // for AssertOnControllerThread
+#include "mozilla/mozalloc.h" // for operator new
+#include "nsMathUtils.h" // for NS_lround
+#include "nsPrintfCString.h" // for nsPrintfCString
+#include "nsThreadUtils.h" // for NS_DispatchToMainThread, etc
+#include "nscore.h" // for NS_IMETHOD
+
+static mozilla::LazyLogModule sApzAxsLog("apz.axis");
+#define AXIS_LOG(...) MOZ_LOG(sApzAxsLog, LogLevel::Debug, (__VA_ARGS__))
+
+namespace mozilla {
+namespace layers {
+
+bool FuzzyEqualsCoordinate(CSSCoord aValue1, CSSCoord aValue2) {
+ return FuzzyEqualsAdditive(aValue1, aValue2, COORDINATE_EPSILON) ||
+ FuzzyEqualsMultiplicative(aValue1, aValue2);
+}
+
+Axis::Axis(AsyncPanZoomController* aAsyncPanZoomController)
+ : mPos(0),
+ mVelocity(0.0f, "Axis::mVelocity"),
+ mAxisLocked(false),
+ mAsyncPanZoomController(aAsyncPanZoomController),
+ mOverscroll(0),
+ mMSDModel(0.0, 0.0, 0.0, StaticPrefs::apz_overscroll_spring_stiffness(),
+ StaticPrefs::apz_overscroll_damping()),
+ mVelocityTracker(mAsyncPanZoomController->GetPlatformSpecificState()
+ ->CreateVelocityTracker(this)) {}
+
+float Axis::ToLocalVelocity(float aVelocityInchesPerMs) const {
+ ScreenPoint velocity =
+ MakePoint(aVelocityInchesPerMs * mAsyncPanZoomController->GetDPI());
+ // Use ToScreenCoordinates() to convert a point rather than a vector by
+ // treating the point as a vector, and using (0, 0) as the anchor.
+ ScreenPoint panStart = mAsyncPanZoomController->ToScreenCoordinates(
+ mAsyncPanZoomController->PanStart(), ParentLayerPoint());
+ ParentLayerPoint localVelocity =
+ mAsyncPanZoomController->ToParentLayerCoordinates(velocity, panStart);
+ return localVelocity.Length();
+}
+
+void Axis::UpdateWithTouchAtDevicePoint(ParentLayerCoord aPos,
+ TimeStamp aTimestamp) {
+ // mVelocityTracker is controller-thread only
+ APZThreadUtils::AssertOnControllerThread();
+
+ mPos = aPos;
+
+ AXIS_LOG("%p|%s got position %f\n", mAsyncPanZoomController, Name(),
+ mPos.value);
+ if (Maybe<float> newVelocity =
+ mVelocityTracker->AddPosition(aPos, aTimestamp)) {
+ DoSetVelocity(mAxisLocked ? 0 : *newVelocity);
+ AXIS_LOG("%p|%s velocity from tracker is %f%s\n", mAsyncPanZoomController,
+ Name(), *newVelocity,
+ mAxisLocked ? ", but we are axis locked" : "");
+ }
+}
+
+void Axis::StartTouch(ParentLayerCoord aPos, TimeStamp aTimestamp) {
+ mStartPos = aPos;
+ mPos = aPos;
+ mVelocityTracker->StartTracking(aPos, aTimestamp);
+ mAxisLocked = false;
+}
+
+bool Axis::AdjustDisplacement(ParentLayerCoord aDisplacement,
+ ParentLayerCoord& aDisplacementOut,
+ ParentLayerCoord& aOverscrollAmountOut,
+ bool aForceOverscroll /* = false */) {
+ if (mAxisLocked) {
+ aOverscrollAmountOut = 0;
+ aDisplacementOut = 0;
+ return false;
+ }
+ if (aForceOverscroll) {
+ aOverscrollAmountOut = aDisplacement;
+ aDisplacementOut = 0;
+ return false;
+ }
+
+ ParentLayerCoord displacement = aDisplacement;
+
+ // First consume any overscroll in the opposite direction along this axis.
+ ParentLayerCoord consumedOverscroll = 0;
+ if (mOverscroll > 0 && aDisplacement < 0) {
+ consumedOverscroll = std::min(mOverscroll, -aDisplacement);
+ } else if (mOverscroll < 0 && aDisplacement > 0) {
+ consumedOverscroll = 0.f - std::min(-mOverscroll, aDisplacement);
+ }
+ mOverscroll -= consumedOverscroll;
+ displacement += consumedOverscroll;
+
+ if (consumedOverscroll != 0.0f) {
+ AXIS_LOG("%p|%s changed overscroll amount to %f\n", mAsyncPanZoomController,
+ Name(), mOverscroll.value);
+ }
+
+ // Split the requested displacement into an allowed displacement that does
+ // not overscroll, and an overscroll amount.
+ aOverscrollAmountOut = DisplacementWillOverscrollAmount(displacement);
+ if (aOverscrollAmountOut != 0.0f) {
+ // No need to have a velocity along this axis anymore; it won't take us
+ // anywhere, so we're just spinning needlessly.
+ AXIS_LOG("%p|%s has overscrolled, clearing velocity\n",
+ mAsyncPanZoomController, Name());
+ DoSetVelocity(0.0f);
+ displacement -= aOverscrollAmountOut;
+ }
+ aDisplacementOut = displacement;
+ return fabsf(consumedOverscroll) > EPSILON;
+}
+
+ParentLayerCoord Axis::ApplyResistance(
+ ParentLayerCoord aRequestedOverscroll) const {
+ // 'resistanceFactor' is a value between 0 and 1/16, which:
+ // - tends to 1/16 as the existing overscroll tends to 0
+ // - tends to 0 as the existing overscroll tends to the composition length
+ // The actual overscroll is the requested overscroll multiplied by this
+ // factor.
+ float resistanceFactor =
+ (1 - fabsf(GetOverscroll()) / GetCompositionLength()) / 16;
+ float result = resistanceFactor < 0 ? ParentLayerCoord(0)
+ : aRequestedOverscroll * resistanceFactor;
+ result = clamped(result, -8.0f, 8.0f);
+ return result;
+}
+
+void Axis::OverscrollBy(ParentLayerCoord aOverscroll) {
+ MOZ_ASSERT(CanScroll());
+ // We can get some spurious calls to OverscrollBy() with near-zero values
+ // due to rounding error. Ignore those (they might trip the asserts below.)
+ if (mAsyncPanZoomController->IsZero(aOverscroll)) {
+ return;
+ }
+ EndOverscrollAnimation();
+ aOverscroll = ApplyResistance(aOverscroll);
+ if (aOverscroll > 0) {
+#ifdef DEBUG
+ if (!IsScrolledToEnd()) {
+ nsPrintfCString message(
+ "composition end (%f) is not equal (within error) to page end (%f)\n",
+ GetCompositionEnd().value, GetPageEnd().value);
+ NS_ASSERTION(false, message.get());
+ MOZ_CRASH("GFX: Overscroll issue > 0");
+ }
+#endif
+ MOZ_ASSERT(mOverscroll >= 0);
+ } else if (aOverscroll < 0) {
+#ifdef DEBUG
+ if (!IsScrolledToStart()) {
+ nsPrintfCString message(
+ "composition origin (%f) is not equal (within error) to page origin "
+ "(%f)\n",
+ GetOrigin().value, GetPageStart().value);
+ NS_ASSERTION(false, message.get());
+ MOZ_CRASH("GFX: Overscroll issue < 0");
+ }
+#endif
+ MOZ_ASSERT(mOverscroll <= 0);
+ }
+ mOverscroll += aOverscroll;
+
+ AXIS_LOG("%p|%s changed overscroll amount to %f\n", mAsyncPanZoomController,
+ Name(), mOverscroll.value);
+}
+
+ParentLayerCoord Axis::GetOverscroll() const { return mOverscroll; }
+
+void Axis::RestoreOverscroll(ParentLayerCoord aOverscroll) {
+ mOverscroll = aOverscroll;
+}
+
+void Axis::StartOverscrollAnimation(float aVelocity) {
+ const float maxVelocity = StaticPrefs::apz_overscroll_max_velocity();
+ aVelocity = clamped(aVelocity / 2.0f, -maxVelocity, maxVelocity);
+ SetVelocity(aVelocity);
+ mMSDModel.SetPosition(mOverscroll);
+ // Convert velocity from ParentLayerCoords/millisecond to
+ // ParentLayerCoords/second.
+ mMSDModel.SetVelocity(DoGetVelocity() * 1000.0);
+
+ AXIS_LOG(
+ "%p|%s beginning overscroll animation with amount %f and velocity %f\n",
+ mAsyncPanZoomController, Name(), mOverscroll.value, DoGetVelocity());
+}
+
+void Axis::EndOverscrollAnimation() {
+ mMSDModel.SetPosition(0.0);
+ mMSDModel.SetVelocity(0.0);
+}
+
+bool Axis::SampleOverscrollAnimation(const TimeDuration& aDelta,
+ SideBits aOverscrollSideBits) {
+ mMSDModel.Simulate(aDelta);
+ mOverscroll = mMSDModel.GetPosition();
+
+ if (((aOverscrollSideBits & (SideBits::eTop | SideBits::eLeft)) &&
+ mOverscroll > 0) ||
+ ((aOverscrollSideBits & (SideBits::eBottom | SideBits::eRight)) &&
+ mOverscroll < 0)) {
+ // Stop the overscroll model immediately if it's going to get across the
+ // boundary.
+ mMSDModel.SetPosition(0.0);
+ mMSDModel.SetVelocity(0.0);
+ }
+
+ AXIS_LOG("%p|%s changed overscroll amount to %f\n", mAsyncPanZoomController,
+ Name(), mOverscroll.value);
+
+ if (mMSDModel.IsFinished(1.0)) {
+ // "Jump" to the at-rest state. The jump shouldn't be noticeable as the
+ // velocity and overscroll are already low.
+ AXIS_LOG("%p|%s oscillation dropped below threshold, going to rest\n",
+ mAsyncPanZoomController, Name());
+ ClearOverscroll();
+ DoSetVelocity(0);
+ return false;
+ }
+
+ // Otherwise, continue the animation.
+ return true;
+}
+
+bool Axis::IsOverscrollAnimationRunning() const {
+ return !mMSDModel.IsFinished(1.0);
+}
+
+bool Axis::IsOverscrollAnimationAlive() const {
+ // Unlike IsOverscrollAnimationRunning, check the position and the velocity to
+ // be sure that the animation has started but hasn't yet finished.
+ return mMSDModel.GetPosition() != 0.0 || mMSDModel.GetVelocity() != 0.0;
+}
+
+bool Axis::IsOverscrolled() const { return mOverscroll != 0.f; }
+
+bool Axis::IsScrolledToStart() const {
+ const auto zoom = GetFrameMetrics().GetZoom();
+
+ if (zoom == CSSToParentLayerScale(0)) {
+ return true;
+ }
+
+ return FuzzyEqualsCoordinate(GetOrigin() / zoom, GetPageStart() / zoom);
+}
+
+bool Axis::IsScrolledToEnd() const {
+ const auto zoom = GetFrameMetrics().GetZoom();
+
+ if (zoom == CSSToParentLayerScale(0)) {
+ return true;
+ }
+
+ return FuzzyEqualsCoordinate(GetCompositionEnd() / zoom, GetPageEnd() / zoom);
+}
+
+bool Axis::IsInInvalidOverscroll() const {
+ if (mOverscroll > 0) {
+ return !IsScrolledToEnd();
+ } else if (mOverscroll < 0) {
+ return !IsScrolledToStart();
+ }
+ return false;
+}
+
+void Axis::ClearOverscroll() {
+ EndOverscrollAnimation();
+ mOverscroll = 0;
+}
+
+ParentLayerCoord Axis::PanStart() const { return mStartPos; }
+
+ParentLayerCoord Axis::PanDistance() const { return fabs(mPos - mStartPos); }
+
+ParentLayerCoord Axis::PanDistance(ParentLayerCoord aPos) const {
+ return fabs(aPos - mStartPos);
+}
+
+void Axis::EndTouch(TimeStamp aTimestamp, ClearAxisLock aClearAxisLock) {
+ // mVelocityQueue is controller-thread only
+ APZThreadUtils::AssertOnControllerThread();
+
+ // If the velocity tracker wasn't able to compute a velocity, zero out
+ // the velocity to make sure we don't get a fling based on some old and
+ // no-longer-relevant value of mVelocity. Also if the axis is locked then
+ // just reset the velocity to 0 since we don't need any velocity to carry
+ // into the fling.
+ if (mAxisLocked) {
+ DoSetVelocity(0);
+ } else if (Maybe<float> velocity =
+ mVelocityTracker->ComputeVelocity(aTimestamp)) {
+ DoSetVelocity(*velocity);
+ } else {
+ DoSetVelocity(0);
+ }
+ if (aClearAxisLock == ClearAxisLock::Yes) {
+ mAxisLocked = false;
+ }
+ AXIS_LOG("%p|%s ending touch, computed velocity %f\n",
+ mAsyncPanZoomController, Name(), DoGetVelocity());
+}
+
+void Axis::CancelGesture() {
+ // mVelocityQueue is controller-thread only
+ APZThreadUtils::AssertOnControllerThread();
+
+ AXIS_LOG("%p|%s cancelling touch, clearing velocity queue\n",
+ mAsyncPanZoomController, Name());
+ DoSetVelocity(0.0f);
+ mVelocityTracker->Clear();
+ SetAxisLocked(false);
+}
+
+bool Axis::CanScroll() const {
+ return mAsyncPanZoomController->FuzzyGreater(GetPageLength(),
+ GetCompositionLength());
+}
+
+bool Axis::CanScroll(CSSCoord aDelta) const {
+ return CanScroll(aDelta * GetFrameMetrics().GetZoom());
+}
+
+bool Axis::CanScroll(ParentLayerCoord aDelta) const {
+ if (!CanScroll()) {
+ return false;
+ }
+
+ const auto zoom = GetFrameMetrics().GetZoom();
+ CSSCoord availableToScroll = 0;
+
+ if (zoom != CSSToParentLayerScale(0)) {
+ availableToScroll =
+ ParentLayerCoord(
+ fabs(DisplacementWillOverscrollAmount(aDelta) - aDelta)) /
+ zoom;
+ }
+
+ return availableToScroll > COORDINATE_EPSILON;
+}
+
+CSSCoord Axis::ClampOriginToScrollableRect(CSSCoord aOrigin) const {
+ CSSToParentLayerScale zoom = GetFrameMetrics().GetZoom();
+ ParentLayerCoord origin = aOrigin * zoom;
+ ParentLayerCoord result;
+ if (origin < GetPageStart()) {
+ result = GetPageStart();
+ } else if (origin + GetCompositionLength() > GetPageEnd()) {
+ result = GetPageEnd() - GetCompositionLength();
+ } else {
+ return aOrigin;
+ }
+ if (zoom == CSSToParentLayerScale(0)) {
+ return aOrigin;
+ }
+ return result / zoom;
+}
+
+bool Axis::CanScrollNow() const { return !mAxisLocked && CanScroll(); }
+
+ParentLayerCoord Axis::DisplacementWillOverscrollAmount(
+ ParentLayerCoord aDisplacement) const {
+ ParentLayerCoord newOrigin = GetOrigin() + aDisplacement;
+ ParentLayerCoord newCompositionEnd = GetCompositionEnd() + aDisplacement;
+ // If the current pan plus a displacement takes the window to the left of or
+ // above the current page rect.
+ bool minus = newOrigin < GetPageStart();
+ // If the current pan plus a displacement takes the window to the right of or
+ // below the current page rect.
+ bool plus = newCompositionEnd > GetPageEnd();
+ if (minus && plus) {
+ // Don't handle overscrolled in both directions; a displacement can't cause
+ // this, it must have already been zoomed out too far.
+ return 0;
+ }
+ if (minus) {
+ return newOrigin - GetPageStart();
+ }
+ if (plus) {
+ return newCompositionEnd - GetPageEnd();
+ }
+ return 0;
+}
+
+CSSCoord Axis::ScaleWillOverscrollAmount(float aScale, CSSCoord aFocus) const {
+ // Internally, do computations in ParentLayer coordinates *before* the scale
+ // is applied.
+ CSSToParentLayerScale zoom = GetFrameMetrics().GetZoom();
+ ParentLayerCoord focus = aFocus * zoom;
+ ParentLayerCoord originAfterScale = (GetOrigin() + focus) - (focus / aScale);
+
+ bool both = ScaleWillOverscrollBothSides(aScale);
+ bool minus = GetPageStart() - originAfterScale > COORDINATE_EPSILON;
+ bool plus =
+ (originAfterScale + (GetCompositionLength() / aScale)) - GetPageEnd() >
+ COORDINATE_EPSILON;
+
+ if ((minus && plus) || both) {
+ // If we ever reach here it's a bug in the client code.
+ MOZ_ASSERT(false,
+ "In an OVERSCROLL_BOTH condition in ScaleWillOverscrollAmount");
+ return 0;
+ }
+ if (minus && zoom != CSSToParentLayerScale(0)) {
+ return (originAfterScale - GetPageStart()) / zoom;
+ }
+ if (plus && zoom != CSSToParentLayerScale(0)) {
+ return (originAfterScale + (GetCompositionLength() / aScale) -
+ GetPageEnd()) /
+ zoom;
+ }
+ return 0;
+}
+
+bool Axis::IsAxisLocked() const { return mAxisLocked; }
+
+float Axis::GetVelocity() const { return mAxisLocked ? 0 : DoGetVelocity(); }
+
+void Axis::SetVelocity(float aVelocity) {
+ AXIS_LOG("%p|%s direct-setting velocity to %f\n", mAsyncPanZoomController,
+ Name(), aVelocity);
+ DoSetVelocity(aVelocity);
+}
+
+ParentLayerCoord Axis::GetCompositionEnd() const {
+ return GetOrigin() + GetCompositionLength();
+}
+
+ParentLayerCoord Axis::GetPageEnd() const {
+ return GetPageStart() + GetPageLength();
+}
+
+ParentLayerCoord Axis::GetScrollRangeEnd() const {
+ return GetPageEnd() - GetCompositionLength();
+}
+
+ParentLayerCoord Axis::GetOrigin() const {
+ ParentLayerPoint origin =
+ GetFrameMetrics().GetVisualScrollOffset() * GetFrameMetrics().GetZoom();
+ return GetPointOffset(origin);
+}
+
+ParentLayerCoord Axis::GetCompositionLength() const {
+ return GetRectLength(GetFrameMetrics().GetCompositionBounds());
+}
+
+ParentLayerCoord Axis::GetPageStart() const {
+ ParentLayerRect pageRect = GetFrameMetrics().GetExpandedScrollableRect() *
+ GetFrameMetrics().GetZoom();
+ return GetRectOffset(pageRect);
+}
+
+ParentLayerCoord Axis::GetPageLength() const {
+ ParentLayerRect pageRect = GetFrameMetrics().GetExpandedScrollableRect() *
+ GetFrameMetrics().GetZoom();
+ return GetRectLength(pageRect);
+}
+
+bool Axis::ScaleWillOverscrollBothSides(float aScale) const {
+ const FrameMetrics& metrics = GetFrameMetrics();
+ ParentLayerRect screenCompositionBounds =
+ metrics.GetCompositionBounds() / ParentLayerToParentLayerScale(aScale);
+ return GetRectLength(screenCompositionBounds) - GetPageLength() >
+ COORDINATE_EPSILON;
+}
+
+float Axis::DoGetVelocity() const {
+ auto velocity = mVelocity.Lock();
+ return velocity.ref();
+}
+void Axis::DoSetVelocity(float aVelocity) {
+ auto velocity = mVelocity.Lock();
+ velocity.ref() = aVelocity;
+}
+
+const FrameMetrics& Axis::GetFrameMetrics() const {
+ return mAsyncPanZoomController->GetFrameMetrics();
+}
+
+const ScrollMetadata& Axis::GetScrollMetadata() const {
+ return mAsyncPanZoomController->GetScrollMetadata();
+}
+
+bool Axis::OverscrollBehaviorAllowsHandoff() const {
+ // Scroll handoff is a "non-local" overscroll behavior, so it's allowed
+ // with "auto" and disallowed with "contain" and "none".
+ return GetOverscrollBehavior() == OverscrollBehavior::Auto;
+}
+
+bool Axis::OverscrollBehaviorAllowsOverscrollEffect() const {
+ // An overscroll effect is a "local" overscroll behavior, so it's allowed
+ // with "auto" and "contain" and disallowed with "none".
+ return GetOverscrollBehavior() != OverscrollBehavior::None;
+}
+
+AxisX::AxisX(AsyncPanZoomController* aAsyncPanZoomController)
+ : Axis(aAsyncPanZoomController) {}
+
+CSSCoord AxisX::GetPointOffset(const CSSPoint& aPoint) const {
+ return aPoint.x;
+}
+
+OuterCSSCoord AxisX::GetPointOffset(const OuterCSSPoint& aPoint) const {
+ return aPoint.x;
+}
+
+ParentLayerCoord AxisX::GetPointOffset(const ParentLayerPoint& aPoint) const {
+ return aPoint.x;
+}
+
+CSSToParentLayerScale AxisX::GetAxisScale(
+ const CSSToParentLayerScale2D& aScale) const {
+ return CSSToParentLayerScale(aScale.xScale);
+}
+
+ParentLayerCoord AxisX::GetRectLength(const ParentLayerRect& aRect) const {
+ return aRect.Width();
+}
+
+CSSCoord AxisX::GetRectLength(const CSSRect& aRect) const {
+ return aRect.Width();
+}
+
+ParentLayerCoord AxisX::GetRectOffset(const ParentLayerRect& aRect) const {
+ return aRect.X();
+}
+
+CSSCoord AxisX::GetRectOffset(const CSSRect& aRect) const { return aRect.X(); }
+
+float AxisX::GetTransformScale(
+ const AsyncTransformComponentMatrix& aMatrix) const {
+ return aMatrix._11;
+}
+
+ParentLayerCoord AxisX::GetTransformTranslation(
+ const AsyncTransformComponentMatrix& aMatrix) const {
+ return aMatrix._41;
+}
+
+void AxisX::PostScale(AsyncTransformComponentMatrix& aMatrix,
+ float aScale) const {
+ aMatrix.PostScale(aScale, 1.f, 1.f);
+}
+
+void AxisX::PostTranslate(AsyncTransformComponentMatrix& aMatrix,
+ ParentLayerCoord aTranslation) const {
+ aMatrix.PostTranslate(aTranslation, 0, 0);
+}
+
+ScreenPoint AxisX::MakePoint(ScreenCoord aCoord) const {
+ return ScreenPoint(aCoord, 0);
+}
+
+const char* AxisX::Name() const { return "X"; }
+
+bool AxisX::CanScrollTo(Side aSide) const {
+ switch (aSide) {
+ case eSideLeft:
+ return CanScroll(CSSCoord(-COORDINATE_EPSILON * 2));
+ case eSideRight:
+ return CanScroll(CSSCoord(COORDINATE_EPSILON * 2));
+ default:
+ MOZ_ASSERT_UNREACHABLE("aSide is out of valid values");
+ return false;
+ }
+}
+
+SideBits AxisX::ScrollableDirections() const {
+ SideBits directions = SideBits::eNone;
+
+ if (CanScrollTo(eSideLeft)) {
+ directions |= SideBits::eLeft;
+ }
+ if (CanScrollTo(eSideRight)) {
+ directions |= SideBits::eRight;
+ }
+
+ return directions;
+}
+
+OverscrollBehavior AxisX::GetOverscrollBehavior() const {
+ return GetScrollMetadata().GetOverscrollBehavior().mBehaviorX;
+}
+
+AxisY::AxisY(AsyncPanZoomController* aAsyncPanZoomController)
+ : Axis(aAsyncPanZoomController) {}
+
+CSSCoord AxisY::GetPointOffset(const CSSPoint& aPoint) const {
+ return aPoint.y;
+}
+
+OuterCSSCoord AxisY::GetPointOffset(const OuterCSSPoint& aPoint) const {
+ return aPoint.y;
+}
+
+ParentLayerCoord AxisY::GetPointOffset(const ParentLayerPoint& aPoint) const {
+ return aPoint.y;
+}
+
+CSSToParentLayerScale AxisY::GetAxisScale(
+ const CSSToParentLayerScale2D& aScale) const {
+ return CSSToParentLayerScale(aScale.yScale);
+}
+
+ParentLayerCoord AxisY::GetRectLength(const ParentLayerRect& aRect) const {
+ return aRect.Height();
+}
+
+CSSCoord AxisY::GetRectLength(const CSSRect& aRect) const {
+ return aRect.Height();
+}
+
+ParentLayerCoord AxisY::GetRectOffset(const ParentLayerRect& aRect) const {
+ return aRect.Y();
+}
+
+CSSCoord AxisY::GetRectOffset(const CSSRect& aRect) const { return aRect.Y(); }
+
+float AxisY::GetTransformScale(
+ const AsyncTransformComponentMatrix& aMatrix) const {
+ return aMatrix._22;
+}
+
+ParentLayerCoord AxisY::GetTransformTranslation(
+ const AsyncTransformComponentMatrix& aMatrix) const {
+ return aMatrix._42;
+}
+
+void AxisY::PostScale(AsyncTransformComponentMatrix& aMatrix,
+ float aScale) const {
+ aMatrix.PostScale(1.f, aScale, 1.f);
+}
+
+void AxisY::PostTranslate(AsyncTransformComponentMatrix& aMatrix,
+ ParentLayerCoord aTranslation) const {
+ aMatrix.PostTranslate(0, aTranslation, 0);
+}
+
+ScreenPoint AxisY::MakePoint(ScreenCoord aCoord) const {
+ return ScreenPoint(0, aCoord);
+}
+
+const char* AxisY::Name() const { return "Y"; }
+
+bool AxisY::CanScrollTo(Side aSide) const {
+ switch (aSide) {
+ case eSideTop:
+ return CanScroll(CSSCoord(-COORDINATE_EPSILON * 2));
+ case eSideBottom:
+ return CanScroll(CSSCoord(COORDINATE_EPSILON * 2));
+ default:
+ MOZ_ASSERT_UNREACHABLE("aSide is out of valid values");
+ return false;
+ }
+}
+
+SideBits AxisY::ScrollableDirections() const {
+ SideBits directions = SideBits::eNone;
+
+ if (CanScrollTo(eSideTop)) {
+ directions |= SideBits::eTop;
+ }
+ if (CanScrollTo(eSideBottom)) {
+ directions |= SideBits::eBottom;
+ }
+
+ return directions;
+}
+
+bool AxisY::HasDynamicToolbar() const {
+ return GetCompositionLengthWithoutDynamicToolbar() != ParentLayerCoord(0);
+}
+
+SideBits AxisY::ScrollableDirectionsWithDynamicToolbar(
+ const ScreenMargin& aFixedLayerMargins) const {
+ MOZ_ASSERT(mAsyncPanZoomController->IsRootContent());
+
+ SideBits directions = ScrollableDirections();
+
+ if (HasDynamicToolbar()) {
+ ParentLayerCoord toolbarHeight =
+ GetCompositionLength() - GetCompositionLengthWithoutDynamicToolbar();
+
+ ParentLayerMargin fixedLayerMargins = ViewAs<ParentLayerPixel>(
+ aFixedLayerMargins, PixelCastJustification::ScreenIsParentLayerForRoot);
+
+ if (!mAsyncPanZoomController->IsZero(fixedLayerMargins.bottom)) {
+ directions |= SideBits::eTop;
+ }
+ if (mAsyncPanZoomController->FuzzyGreater(
+ aFixedLayerMargins.bottom + toolbarHeight, 0)) {
+ directions |= SideBits::eBottom;
+ }
+ }
+
+ return directions;
+}
+
+bool AxisY::CanVerticalScrollWithDynamicToolbar() const {
+ return !HasDynamicToolbar()
+ ? CanScroll()
+ : mAsyncPanZoomController->FuzzyGreater(
+ GetPageLength(),
+ GetCompositionLengthWithoutDynamicToolbar());
+}
+
+OverscrollBehavior AxisY::GetOverscrollBehavior() const {
+ return GetScrollMetadata().GetOverscrollBehavior().mBehaviorY;
+}
+
+ParentLayerCoord AxisY::GetCompositionLengthWithoutDynamicToolbar() const {
+ return GetFrameMetrics().GetCompositionSizeWithoutDynamicToolbar().Height();
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/apz/src/Axis.h b/gfx/layers/apz/src/Axis.h
new file mode 100644
index 0000000000..072c0a297b
--- /dev/null
+++ b/gfx/layers/apz/src/Axis.h
@@ -0,0 +1,462 @@
+/* -*- 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_Axis_h
+#define mozilla_layers_Axis_h
+
+#include <sys/types.h> // for int32_t
+
+#include "APZUtils.h"
+#include "AxisPhysicsMSDModel.h"
+#include "mozilla/DataMutex.h" // for DataMutex
+#include "mozilla/gfx/Types.h" // for Side
+#include "mozilla/TimeStamp.h" // for TimeDuration
+#include "nsTArray.h" // for nsTArray
+#include "Units.h"
+
+namespace mozilla {
+namespace layers {
+
+const float EPSILON = 0.0001f;
+
+/**
+ * Compare two coordinates for equality, accounting for rounding error.
+ * Use both FuzzyEqualsAdditive() with COORDINATE_EPISLON, which accounts for
+ * things like the error introduced by rounding during a round-trip to app
+ * units, and FuzzyEqualsMultiplicative(), which accounts for accumulated error
+ * due to floating-point operations (which can be larger than COORDINATE_EPISLON
+ * for sufficiently large coordinate values).
+ */
+bool FuzzyEqualsCoordinate(CSSCoord aValue1, CSSCoord aValue2);
+
+struct FrameMetrics;
+class AsyncPanZoomController;
+
+/**
+ * Interface for computing velocities along the axis based on
+ * position samples.
+ */
+class VelocityTracker {
+ public:
+ virtual ~VelocityTracker() = default;
+
+ /**
+ * Start tracking velocity along this axis, starting with the given
+ * initial position and corresponding timestamp.
+ */
+ virtual void StartTracking(ParentLayerCoord aPos, TimeStamp aTimestamp) = 0;
+ /**
+ * Record a new position along this axis, at the given timestamp.
+ * Returns the average velocity between the last sample and this one, or
+ * or Nothing() if a reasonable average cannot be computed.
+ */
+ virtual Maybe<float> AddPosition(ParentLayerCoord aPos,
+ TimeStamp aTimestamp) = 0;
+ /**
+ * Compute an estimate of the axis's current velocity, based on recent
+ * position samples. It's up to implementation how many samples to consider
+ * and how to perform the computation.
+ * If the tracker doesn't have enough samples to compute a result, it
+ * may return Nothing{}.
+ */
+ virtual Maybe<float> ComputeVelocity(TimeStamp aTimestamp) = 0;
+ /**
+ * Clear all state in the velocity tracker.
+ */
+ virtual void Clear() = 0;
+};
+
+/**
+ * Helper class to maintain each axis of movement (X,Y) for panning and zooming.
+ * Note that everything here is specific to one axis; that is, the X axis knows
+ * nothing about the Y axis and vice versa.
+ */
+class Axis {
+ public:
+ explicit Axis(AsyncPanZoomController* aAsyncPanZoomController);
+
+ /**
+ * Notify this Axis that a new touch has been received, including a timestamp
+ * for when the touch was received. This triggers a recalculation of velocity.
+ * This can also used for pan gesture events. For those events, |aPos| is
+ * an invented position corresponding to the mouse position plus any
+ * accumulated displacements over the course of the pan gesture.
+ */
+ void UpdateWithTouchAtDevicePoint(ParentLayerCoord aPos,
+ TimeStamp aTimestamp);
+
+ public:
+ /**
+ * Notify this Axis that a touch has begun, i.e. the user has put their finger
+ * on the screen but has not yet tried to pan.
+ */
+ void StartTouch(ParentLayerCoord aPos, TimeStamp aTimestamp);
+
+ /**
+ * Helper enum class for specifying if EndTouch() should clear the axis lock.
+ */
+ enum class ClearAxisLock { Yes, No };
+
+ /**
+ * Notify this Axis that a touch has ended gracefully. This may perform
+ * recalculations of the axis velocity.
+ */
+ void EndTouch(TimeStamp aTimestamp, ClearAxisLock aClearAxisLock);
+
+ /**
+ * Notify this Axis that the gesture has ended forcefully. Useful for stopping
+ * flings when a user puts their finger down in the middle of one (i.e. to
+ * stop a previous touch including its fling so that a new one can take its
+ * place).
+ */
+ void CancelGesture();
+
+ /**
+ * Takes a requested displacement to the position of this axis, and adjusts it
+ * to account for overscroll (which might decrease the displacement; this is
+ * to prevent the viewport from overscrolling the page rect), and axis locking
+ * (which might prevent any displacement from happening). If overscroll
+ * ocurred, its amount is written to |aOverscrollAmountOut|.
+ * The |aDisplacementOut| parameter is set to the adjusted displacement, and
+ * the function returns true if and only if internal overscroll amounts were
+ * changed.
+ */
+ bool AdjustDisplacement(ParentLayerCoord aDisplacement,
+ ParentLayerCoord& aDisplacementOut,
+ ParentLayerCoord& aOverscrollAmountOut,
+ bool aForceOverscroll = false);
+
+ /**
+ * Overscrolls this axis by the requested amount in the requested direction.
+ * The axis must be at the end of its scroll range in this direction.
+ */
+ void OverscrollBy(ParentLayerCoord aOverscroll);
+
+ /**
+ * Return the amount of overscroll on this axis, in ParentLayer pixels.
+ *
+ * If this amount is nonzero, the relevant component of
+ * mAsyncPanZoomController->Metrics().mScrollOffset must be at its
+ * extreme allowed value in the relevant direction (that is, it must be at
+ * its maximum value if we are overscrolled at our composition length, and
+ * at its minimum value if we are overscrolled at the origin).
+ */
+ ParentLayerCoord GetOverscroll() const;
+
+ /**
+ * Restore the amount by which this axis is overscrolled to the specified
+ * amount. This is for test-related use; overscrolling as a result of user
+ * input should happen via OverscrollBy().
+ */
+ void RestoreOverscroll(ParentLayerCoord aOverscroll);
+
+ /**
+ * Start an overscroll animation with the given initial velocity.
+ */
+ void StartOverscrollAnimation(float aVelocity);
+
+ /**
+ * Sample the snap-back animation to relieve overscroll.
+ * |aDelta| is the time since the last sample, |aOverscrollSideBits| is
+ * the direction where the overscroll happens on this axis.
+ */
+ bool SampleOverscrollAnimation(const TimeDuration& aDelta,
+ SideBits aOverscrollSideBits);
+
+ /**
+ * Stop an overscroll animation.
+ */
+ void EndOverscrollAnimation();
+
+ /**
+ * Return whether this axis is overscrolled in either direction.
+ */
+ bool IsOverscrolled() const;
+
+ /**
+ * Return true if this axis is overscrolled but its scroll offset
+ * has changed in a way that makes the oversrolled state no longer
+ * valid (for example, it is overscrolled at the top but the
+ * scroll offset is no longer zero).
+ */
+ bool IsInInvalidOverscroll() const;
+
+ /**
+ * Clear any overscroll amount on this axis.
+ */
+ void ClearOverscroll();
+
+ /**
+ * Returns whether the overscroll animation is alive.
+ */
+ bool IsOverscrollAnimationAlive() const;
+
+ /**
+ * Returns whether the overscroll animation is running.
+ * Note that unlike the above IsOverscrollAnimationAlive, this function
+ * returns false even if the animation is still there but is very close to
+ * the destination position and its velocity is quite low, i.e. it's time to
+ * finish.
+ */
+ bool IsOverscrollAnimationRunning() const;
+
+ /**
+ * Gets the starting position of the touch supplied in StartTouch().
+ */
+ ParentLayerCoord PanStart() const;
+
+ /**
+ * Gets the distance between the starting position of the touch supplied in
+ * StartTouch() and the current touch from the last
+ * UpdateWithTouchAtDevicePoint().
+ */
+ ParentLayerCoord PanDistance() const;
+
+ /**
+ * Gets the distance between the starting position of the touch supplied in
+ * StartTouch() and the supplied position.
+ */
+ ParentLayerCoord PanDistance(ParentLayerCoord aPos) const;
+
+ /**
+ * Returns true if the page has room to be scrolled along this axis.
+ */
+ bool CanScroll() const;
+
+ /**
+ * Returns whether this axis can scroll any more in a particular direction.
+ */
+ bool CanScroll(CSSCoord aDelta) const;
+ bool CanScroll(ParentLayerCoord aDelta) const;
+
+ /**
+ * Returns true if the page has room to be scrolled along this axis
+ * and this axis is not scroll-locked.
+ */
+ bool CanScrollNow() const;
+
+ /**
+ * Clamp a point to the page's scrollable bounds. That is, a scroll
+ * destination to the returned point will not contain any overscroll.
+ */
+ CSSCoord ClampOriginToScrollableRect(CSSCoord aOrigin) const;
+
+ void SetAxisLocked(bool aAxisLocked) { mAxisLocked = aAxisLocked; }
+
+ /**
+ * Gets the raw velocity of this axis at this moment.
+ */
+ float GetVelocity() const;
+
+ /**
+ * Sets the raw velocity of this axis at this moment.
+ * Intended to be called only when the axis "takes over" a velocity from
+ * another APZC, in which case there are no touch points available to call
+ * UpdateWithTouchAtDevicePoint. In other circumstances,
+ * UpdateWithTouchAtDevicePoint should be used and the velocity calculated
+ * there.
+ */
+ void SetVelocity(float aVelocity);
+
+ /**
+ * If a displacement will overscroll the axis, this returns the amount and in
+ * what direction.
+ */
+ ParentLayerCoord DisplacementWillOverscrollAmount(
+ ParentLayerCoord aDisplacement) const;
+
+ /**
+ * If a scale will overscroll the axis, this returns the amount and in what
+ * direction.
+ *
+ * |aFocus| is the point at which the scale is focused at. We will offset the
+ * scroll offset in such a way that it remains in the same place on the page
+ * relative.
+ *
+ * Note: Unlike most other functions in Axis, this functions operates in
+ * CSS coordinates so there is no confusion as to whether the
+ * ParentLayer coordinates it operates in are before or after the scale
+ * is applied.
+ */
+ CSSCoord ScaleWillOverscrollAmount(float aScale, CSSCoord aFocus) const;
+
+ /**
+ * Checks if an axis will overscroll in both directions by computing the
+ * content rect and checking that its height/width (depending on the axis)
+ * does not overextend past the viewport.
+ *
+ * This gets called by ScaleWillOverscroll().
+ */
+ bool ScaleWillOverscrollBothSides(float aScale) const;
+
+ /**
+ * Returns true if movement on this axis is locked.
+ */
+ bool IsAxisLocked() const;
+
+ ParentLayerCoord GetOrigin() const;
+ ParentLayerCoord GetCompositionLength() const;
+ ParentLayerCoord GetPageStart() const;
+ ParentLayerCoord GetPageLength() const;
+ ParentLayerCoord GetCompositionEnd() const;
+ ParentLayerCoord GetPageEnd() const;
+ ParentLayerCoord GetScrollRangeEnd() const;
+
+ bool IsScrolledToStart() const;
+ bool IsScrolledToEnd() const;
+
+ ParentLayerCoord GetPos() const { return mPos; }
+
+ bool OverscrollBehaviorAllowsHandoff() const;
+ bool OverscrollBehaviorAllowsOverscrollEffect() const;
+
+ virtual CSSToParentLayerScale GetAxisScale(
+ const CSSToParentLayerScale2D& aScale) const = 0;
+ virtual CSSCoord GetPointOffset(const CSSPoint& aPoint) const = 0;
+ virtual OuterCSSCoord GetPointOffset(const OuterCSSPoint& aPoint) const = 0;
+ virtual ParentLayerCoord GetPointOffset(
+ const ParentLayerPoint& aPoint) const = 0;
+ virtual ParentLayerCoord GetRectLength(
+ const ParentLayerRect& aRect) const = 0;
+ virtual CSSCoord GetRectLength(const CSSRect& aRect) const = 0;
+ virtual ParentLayerCoord GetRectOffset(
+ const ParentLayerRect& aRect) const = 0;
+ virtual CSSCoord GetRectOffset(const CSSRect& aRect) const = 0;
+ virtual float GetTransformScale(
+ const AsyncTransformComponentMatrix& aMatrix) const = 0;
+ virtual ParentLayerCoord GetTransformTranslation(
+ const AsyncTransformComponentMatrix& aMatrix) const = 0;
+ virtual void PostScale(AsyncTransformComponentMatrix& aMatrix,
+ float aScale) const = 0;
+ virtual void PostTranslate(AsyncTransformComponentMatrix& aMatrix,
+ ParentLayerCoord aTranslation) const = 0;
+
+ virtual ScreenPoint MakePoint(ScreenCoord aCoord) const = 0;
+
+ const void* OpaqueApzcPointer() const { return mAsyncPanZoomController; }
+
+ virtual const char* Name() const = 0;
+
+ // Convert a velocity from global inches/ms into ParentLayerCoords/ms.
+ float ToLocalVelocity(float aVelocityInchesPerMs) const;
+
+ protected:
+ // A position along the axis, used during input event processing to
+ // track velocities (and for touch gestures, to track the length of
+ // the gesture). For touch events, this represents the position of
+ // the finger (or in the case of two-finger scrolling, the midpoint
+ // of the two fingers). For pan gesture events, this represents an
+ // invented position corresponding to the mouse position at the start
+ // of the pan, plus deltas representing the displacement of the pan.
+ ParentLayerCoord mPos;
+
+ ParentLayerCoord mStartPos;
+ // The velocity can be accessed from multiple threads (e.g. APZ
+ // controller thread and APZ sampler thread), so needs to be
+ // protected by a mutex.
+ // Units: ParentLayerCoords per millisecond
+ mutable DataMutex<float> mVelocity;
+ bool mAxisLocked; // Whether movement on this axis is locked.
+ AsyncPanZoomController* mAsyncPanZoomController;
+
+ // The amount by which we are overscrolled; see GetOverscroll().
+ ParentLayerCoord mOverscroll;
+
+ // The mass-spring-damper model for overscroll physics.
+ AxisPhysicsMSDModel mMSDModel;
+
+ // Used to track velocity over a series of input events and compute
+ // a resulting velocity to use for e.g. starting a fling animation.
+ // This member can only be accessed on the controller/UI thread.
+ UniquePtr<VelocityTracker> mVelocityTracker;
+
+ float DoGetVelocity() const;
+ void DoSetVelocity(float aVelocity);
+
+ const FrameMetrics& GetFrameMetrics() const;
+ const ScrollMetadata& GetScrollMetadata() const;
+
+ // Do not use this function directly, use
+ // AsyncPanZoomController::GetAllowedHandoffDirections instead.
+ virtual OverscrollBehavior GetOverscrollBehavior() const = 0;
+
+ // Adjust a requested overscroll amount for resistance, yielding a smaller
+ // actual overscroll amount.
+ ParentLayerCoord ApplyResistance(ParentLayerCoord aOverscroll) const;
+
+ // Helper function for SampleOverscrollAnimation().
+ void StepOverscrollAnimation(double aStepDurationMilliseconds);
+};
+
+class AxisX : public Axis {
+ public:
+ explicit AxisX(AsyncPanZoomController* mAsyncPanZoomController);
+ CSSToParentLayerScale GetAxisScale(
+ const CSSToParentLayerScale2D& aScale) const override;
+ CSSCoord GetPointOffset(const CSSPoint& aPoint) const override;
+ OuterCSSCoord GetPointOffset(const OuterCSSPoint& aPoint) const override;
+ ParentLayerCoord GetPointOffset(
+ const ParentLayerPoint& aPoint) const override;
+ ParentLayerCoord GetRectLength(const ParentLayerRect& aRect) const override;
+ CSSCoord GetRectLength(const CSSRect& aRect) const override;
+ ParentLayerCoord GetRectOffset(const ParentLayerRect& aRect) const override;
+ CSSCoord GetRectOffset(const CSSRect& aRect) const override;
+ float GetTransformScale(
+ const AsyncTransformComponentMatrix& aMatrix) const override;
+ ParentLayerCoord GetTransformTranslation(
+ const AsyncTransformComponentMatrix& aMatrix) const override;
+ void PostScale(AsyncTransformComponentMatrix& aMatrix,
+ float aScale) const override;
+ void PostTranslate(AsyncTransformComponentMatrix& aMatrix,
+ ParentLayerCoord aTranslation) const override;
+ ScreenPoint MakePoint(ScreenCoord aCoord) const override;
+ const char* Name() const override;
+ bool CanScrollTo(Side aSide) const;
+ SideBits ScrollableDirections() const;
+
+ private:
+ OverscrollBehavior GetOverscrollBehavior() const override;
+};
+
+class AxisY : public Axis {
+ public:
+ explicit AxisY(AsyncPanZoomController* mAsyncPanZoomController);
+ CSSCoord GetPointOffset(const CSSPoint& aPoint) const override;
+ OuterCSSCoord GetPointOffset(const OuterCSSPoint& aPoint) const override;
+ ParentLayerCoord GetPointOffset(
+ const ParentLayerPoint& aPoint) const override;
+ CSSToParentLayerScale GetAxisScale(
+ const CSSToParentLayerScale2D& aScale) const override;
+ ParentLayerCoord GetRectLength(const ParentLayerRect& aRect) const override;
+ CSSCoord GetRectLength(const CSSRect& aRect) const override;
+ ParentLayerCoord GetRectOffset(const ParentLayerRect& aRect) const override;
+ CSSCoord GetRectOffset(const CSSRect& aRect) const override;
+ float GetTransformScale(
+ const AsyncTransformComponentMatrix& aMatrix) const override;
+ ParentLayerCoord GetTransformTranslation(
+ const AsyncTransformComponentMatrix& aMatrix) const override;
+ void PostScale(AsyncTransformComponentMatrix& aMatrix,
+ float aScale) const override;
+ void PostTranslate(AsyncTransformComponentMatrix& aMatrix,
+ ParentLayerCoord aTranslation) const override;
+ ScreenPoint MakePoint(ScreenCoord aCoord) const override;
+ const char* Name() const override;
+ bool CanScrollTo(Side aSide) const;
+ bool CanVerticalScrollWithDynamicToolbar() const;
+ SideBits ScrollableDirections() const;
+ SideBits ScrollableDirectionsWithDynamicToolbar(
+ const ScreenMargin& aFixedLayerMargins) const;
+
+ private:
+ OverscrollBehavior GetOverscrollBehavior() const override;
+ ParentLayerCoord GetCompositionLengthWithoutDynamicToolbar() const;
+ bool HasDynamicToolbar() const;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif
diff --git a/gfx/layers/apz/src/CheckerboardEvent.cpp b/gfx/layers/apz/src/CheckerboardEvent.cpp
new file mode 100644
index 0000000000..8f518c7383
--- /dev/null
+++ b/gfx/layers/apz/src/CheckerboardEvent.cpp
@@ -0,0 +1,195 @@
+/* -*- 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 "CheckerboardEvent.h"
+#include "mozilla/Logging.h"
+
+#include <algorithm> // for std::sort
+
+static mozilla::LazyLogModule sApzCheckLog("apz.checkerboard");
+
+namespace mozilla {
+namespace layers {
+
+// Relatively arbitrary limit to prevent a perma-checkerboard event from
+// eating up gobs of memory. Ideally we shouldn't have perma-checkerboarding
+// but better to guard against it.
+#define LOG_LENGTH_LIMIT (50 * 1024)
+
+const char* CheckerboardEvent::sDescriptions[] = {
+ "page",
+ "painted displayport",
+ "requested displayport",
+ "viewport",
+};
+
+const char* CheckerboardEvent::sColors[] = {
+ "brown",
+ "lightgreen",
+ "yellow",
+ "red",
+};
+
+CheckerboardEvent::CheckerboardEvent(bool aRecordTrace)
+ : mRecordTrace(aRecordTrace),
+ mOriginTime(TimeStamp::Now()),
+ mCheckerboardingActive(false),
+ mLastSampleTime(mOriginTime),
+ mFrameCount(0),
+ mTotalPixelMs(0),
+ mPeakPixels(0),
+ mRendertraceLock("Rendertrace") {}
+
+uint32_t CheckerboardEvent::GetSeverity() {
+ // Scale the total into a 32-bit value
+ return (uint32_t)sqrt((double)mTotalPixelMs);
+}
+
+uint32_t CheckerboardEvent::GetPeak() { return mPeakPixels; }
+
+TimeDuration CheckerboardEvent::GetDuration() { return mEndTime - mStartTime; }
+
+std::string CheckerboardEvent::GetLog() {
+ MonitorAutoLock lock(mRendertraceLock);
+ return mRendertraceInfo.str();
+}
+
+bool CheckerboardEvent::IsRecordingTrace() { return mRecordTrace; }
+
+void CheckerboardEvent::UpdateRendertraceProperty(
+ RendertraceProperty aProperty, const CSSRect& aRect,
+ const std::string& aExtraInfo) {
+ if (!mRecordTrace) {
+ return;
+ }
+ MonitorAutoLock lock(mRendertraceLock);
+ if (!mCheckerboardingActive) {
+ mBufferedProperties[aProperty].Update(aProperty, aRect, aExtraInfo, lock);
+ } else {
+ LogInfo(aProperty, TimeStamp::Now(), aRect, aExtraInfo, lock);
+ }
+}
+
+void CheckerboardEvent::LogInfo(RendertraceProperty aProperty,
+ const TimeStamp& aTimestamp,
+ const CSSRect& aRect,
+ const std::string& aExtraInfo,
+ const MonitorAutoLock& aProofOfLock) {
+ MOZ_ASSERT(mRecordTrace);
+ if (mRendertraceInfo.tellp() >= LOG_LENGTH_LIMIT) {
+ // The log is already long enough, don't put more things into it. We'll
+ // append a truncation message when this event ends.
+ return;
+ }
+ // The log is consumed by the page at about:checkerboard. The format is not
+ // formally specced, but an informal description can be found at
+ // https://searchfox.org/mozilla-central/rev/d866b96d74ec2a63f09ee418f048d23f4fd379a2/toolkit/components/aboutcheckerboard/content/aboutCheckerboard.js#86
+ mRendertraceInfo << "RENDERTRACE "
+ << (aTimestamp - mOriginTime).ToMilliseconds() << " rect "
+ << sColors[aProperty] << " " << aRect.X() << " " << aRect.Y()
+ << " " << aRect.Width() << " " << aRect.Height() << " "
+ << "// " << sDescriptions[aProperty] << aExtraInfo
+ << std::endl;
+}
+
+bool CheckerboardEvent::RecordFrameInfo(uint32_t aCssPixelsCheckerboarded) {
+ TimeStamp sampleTime = TimeStamp::Now();
+ bool eventEnding = false;
+ if (aCssPixelsCheckerboarded > 0) {
+ if (!mCheckerboardingActive) {
+ StartEvent();
+ }
+ MOZ_ASSERT(mCheckerboardingActive);
+ MOZ_ASSERT(sampleTime >= mLastSampleTime);
+ mTotalPixelMs +=
+ (uint64_t)((sampleTime - mLastSampleTime).ToMilliseconds() *
+ aCssPixelsCheckerboarded);
+ if (aCssPixelsCheckerboarded > mPeakPixels) {
+ mPeakPixels = aCssPixelsCheckerboarded;
+ }
+ mFrameCount++;
+ } else {
+ if (mCheckerboardingActive) {
+ StopEvent();
+ eventEnding = true;
+ }
+ MOZ_ASSERT(!mCheckerboardingActive);
+ }
+ mLastSampleTime = sampleTime;
+ return eventEnding;
+}
+
+void CheckerboardEvent::StartEvent() {
+ MOZ_LOG(sApzCheckLog, LogLevel::Debug, ("Starting checkerboard event"));
+ MOZ_ASSERT(!mCheckerboardingActive);
+ mCheckerboardingActive = true;
+ mStartTime = TimeStamp::Now();
+
+ if (!mRecordTrace) {
+ return;
+ }
+ MonitorAutoLock lock(mRendertraceLock);
+ std::vector<PropertyValue> history;
+ for (PropertyBuffer& bufferedProperty : mBufferedProperties) {
+ bufferedProperty.Flush(history, lock);
+ }
+ std::sort(history.begin(), history.end());
+ for (const PropertyValue& p : history) {
+ LogInfo(p.mProperty, p.mTimeStamp, p.mRect, p.mExtraInfo, lock);
+ }
+ mRendertraceInfo << " -- checkerboarding starts below --" << std::endl;
+}
+
+void CheckerboardEvent::StopEvent() {
+ MOZ_LOG(sApzCheckLog, LogLevel::Debug, ("Stopping checkerboard event"));
+ mCheckerboardingActive = false;
+ mEndTime = TimeStamp::Now();
+
+ if (!mRecordTrace) {
+ return;
+ }
+ MonitorAutoLock lock(mRendertraceLock);
+ if (mRendertraceInfo.tellp() >= LOG_LENGTH_LIMIT) {
+ mRendertraceInfo << "[logging aborted due to length limitations]\n";
+ }
+ mRendertraceInfo << "Checkerboarded for " << mFrameCount << " frames ("
+ << (mEndTime - mStartTime).ToMilliseconds() << " ms), "
+ << mPeakPixels << " peak, " << GetSeverity() << " severity."
+ << std::endl;
+}
+
+bool CheckerboardEvent::PropertyValue::operator<(
+ const PropertyValue& aOther) const {
+ if (mTimeStamp < aOther.mTimeStamp) {
+ return true;
+ } else if (mTimeStamp > aOther.mTimeStamp) {
+ return false;
+ }
+ return mProperty < aOther.mProperty;
+}
+
+CheckerboardEvent::PropertyBuffer::PropertyBuffer() : mIndex(0) {}
+
+void CheckerboardEvent::PropertyBuffer::Update(
+ RendertraceProperty aProperty, const CSSRect& aRect,
+ const std::string& aExtraInfo, const MonitorAutoLock& aProofOfLock) {
+ mValues[mIndex] = {aProperty, TimeStamp::Now(), aRect, aExtraInfo};
+ mIndex = (mIndex + 1) % BUFFER_SIZE;
+}
+
+void CheckerboardEvent::PropertyBuffer::Flush(
+ std::vector<PropertyValue>& aOut, const MonitorAutoLock& aProofOfLock) {
+ for (uint32_t i = 0; i < BUFFER_SIZE; i++) {
+ uint32_t ix = (mIndex + i) % BUFFER_SIZE;
+ if (!mValues[ix].mTimeStamp.IsNull()) {
+ aOut.push_back(mValues[ix]);
+ mValues[ix].mTimeStamp = TimeStamp();
+ }
+ }
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/apz/src/CheckerboardEvent.h b/gfx/layers/apz/src/CheckerboardEvent.h
new file mode 100644
index 0000000000..ad7fa83b2b
--- /dev/null
+++ b/gfx/layers/apz/src/CheckerboardEvent.h
@@ -0,0 +1,218 @@
+/* -*- 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_CheckerboardEvent_h
+#define mozilla_layers_CheckerboardEvent_h
+
+#include "mozilla/DefineEnum.h"
+#include "mozilla/Monitor.h"
+#include "mozilla/TimeStamp.h"
+#include <sstream>
+#include "Units.h"
+#include <vector>
+
+namespace mozilla {
+namespace layers {
+
+/**
+ * This class records information relevant to one "checkerboard event", which is
+ * a contiguous set of frames where a given APZC was checkerboarding. The intent
+ * of this class is to record enough information that it can provide actionable
+ * steps to reduce the occurrence of checkerboarding. Furthermore, it records
+ * information about the severity of the checkerboarding so as to allow
+ * prioritizing the debugging of some checkerboarding events over others.
+ */
+class CheckerboardEvent final {
+ public:
+ // clang-format off
+ MOZ_DEFINE_ENUM_AT_CLASS_SCOPE(
+ RendertraceProperty, (
+ Page,
+ PaintedDisplayPort,
+ RequestedDisplayPort,
+ UserVisible
+ ));
+ // clang-format on
+
+ static const char* sDescriptions[sRendertracePropertyCount];
+ static const char* sColors[sRendertracePropertyCount];
+
+ public:
+ explicit CheckerboardEvent(bool aRecordTrace);
+
+ /**
+ * Gets the "severity" of the checkerboard event. This doesn't have units,
+ * it's just useful for comparing two checkerboard events to see which one
+ * is worse, for some implementation-specific definition of "worse".
+ */
+ uint32_t GetSeverity();
+
+ /**
+ * Gets the number of CSS pixels that were checkerboarded at the peak of the
+ * checkerboard event.
+ */
+ uint32_t GetPeak();
+
+ /**
+ * Gets the length of the checkerboard event.
+ */
+ TimeDuration GetDuration();
+
+ /**
+ * Gets the raw log of the checkerboard event. This can be called any time,
+ * although it really only makes sense to pull once the event is done, after
+ * RecordFrameInfo returns true.
+ */
+ std::string GetLog();
+
+ /**
+ * Returns true iff this event is recording a detailed trace of the event.
+ * This is the argument passed in to the constructor.
+ */
+ bool IsRecordingTrace();
+
+ /**
+ * Provide a new value for one of the rects that is tracked for
+ * checkerboard events.
+ */
+ void UpdateRendertraceProperty(RendertraceProperty aProperty,
+ const CSSRect& aRect,
+ const std::string& aExtraInfo = std::string());
+
+ /**
+ * Provide the number of CSS pixels that are checkerboarded in a composite
+ * at the current time.
+ * @return true if the checkerboard event has completed. The caller should
+ * stop updating this object once this happens.
+ */
+ bool RecordFrameInfo(uint32_t aCssPixelsCheckerboarded);
+
+ private:
+ /**
+ * Helper method to do stuff when checkeboarding starts.
+ */
+ void StartEvent();
+ /**
+ * Helper method to do stuff when checkerboarding stops.
+ */
+ void StopEvent();
+
+ /**
+ * Helper method to log a rendertrace property and its value to the
+ * rendertrace info buffer (mRendertraceInfo).
+ */
+ void LogInfo(RendertraceProperty aProperty, const TimeStamp& aTimestamp,
+ const CSSRect& aRect, const std::string& aExtraInfo,
+ const MonitorAutoLock& aProofOfLock);
+
+ /**
+ * Helper struct that holds a single rendertrace property value.
+ */
+ struct PropertyValue {
+ RendertraceProperty mProperty;
+ TimeStamp mTimeStamp;
+ CSSRect mRect;
+ std::string mExtraInfo;
+
+ bool operator<(const PropertyValue& aOther) const;
+ };
+
+ /**
+ * A circular buffer that stores the most recent BUFFER_SIZE values of a
+ * given property.
+ */
+ class PropertyBuffer {
+ public:
+ PropertyBuffer();
+ /**
+ * Add a new value to the buffer, overwriting the oldest one if needed.
+ */
+ void Update(RendertraceProperty aProperty, const CSSRect& aRect,
+ const std::string& aExtraInfo,
+ const MonitorAutoLock& aProofOfLock);
+ /**
+ * Dump the recorded values, oldest to newest, to the given vector, and
+ * remove them from this buffer.
+ */
+ void Flush(std::vector<PropertyValue>& aOut,
+ const MonitorAutoLock& aProofOfLock);
+
+ private:
+ static const uint32_t BUFFER_SIZE = 5;
+
+ /**
+ * The index of the oldest value in the buffer. This is the next index
+ * that will be written to.
+ */
+ uint32_t mIndex;
+ PropertyValue mValues[BUFFER_SIZE];
+ };
+
+ private:
+ /**
+ * If true, we should log the various properties during the checkerboard
+ * event. If false, we only need to record things we need for telemetry
+ * measures.
+ */
+ const bool mRecordTrace;
+ /**
+ * A base time so that the other timestamps can be turned into durations.
+ */
+ const TimeStamp mOriginTime;
+ /**
+ * Whether or not a checkerboard event is currently occurring.
+ */
+ bool mCheckerboardingActive;
+
+ /**
+ * The start time of the checkerboard event.
+ */
+ TimeStamp mStartTime;
+ /**
+ * The end time of the checkerboard event.
+ */
+ TimeStamp mEndTime;
+ /**
+ * The sample time of the last frame recorded.
+ */
+ TimeStamp mLastSampleTime;
+ /**
+ * The number of contiguous frames with checkerboard.
+ */
+ uint32_t mFrameCount;
+ /**
+ * The total number of pixel-milliseconds of checkerboarding visible to
+ * the user during the checkerboarding event.
+ */
+ uint64_t mTotalPixelMs;
+ /**
+ * The largest number of pixels of checkerboarding visible to the user
+ * during any one frame, during this checkerboarding event.
+ */
+ uint32_t mPeakPixels;
+
+ /**
+ * Monitor that needs to be acquired before touching mBufferedProperties
+ * or mRendertraceInfo.
+ */
+ mutable Monitor mRendertraceLock MOZ_UNANNOTATED;
+ /**
+ * A circular buffer to store some properties. This is used before the
+ * checkerboarding actually starts, so that we have some data on what
+ * was happening before the checkerboarding started.
+ */
+ PropertyBuffer mBufferedProperties[sRendertracePropertyCount];
+ /**
+ * The rendertrace info buffer that gives us info on what was happening
+ * during the checkerboard event.
+ */
+ std::ostringstream mRendertraceInfo;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif // mozilla_layers_CheckerboardEvent_h
diff --git a/gfx/layers/apz/src/DesktopFlingPhysics.h b/gfx/layers/apz/src/DesktopFlingPhysics.h
new file mode 100644
index 0000000000..e93cc07a23
--- /dev/null
+++ b/gfx/layers/apz/src/DesktopFlingPhysics.h
@@ -0,0 +1,67 @@
+/* -*- 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_DesktopFlingPhysics_h_
+#define mozilla_layers_DesktopFlingPhysics_h_
+
+#include "AsyncPanZoomController.h"
+#include "Units.h"
+#include "mozilla/Assertions.h"
+#include "mozilla/StaticPrefs_apz.h"
+
+namespace mozilla {
+namespace layers {
+
+class DesktopFlingPhysics {
+ public:
+ void Init(const ParentLayerPoint& aStartingVelocity,
+ float aPLPPI /* unused */) {
+ mVelocity = aStartingVelocity;
+ }
+ void Sample(const TimeDuration& aDelta, ParentLayerPoint* aOutVelocity,
+ ParentLayerPoint* aOutOffset) {
+ float friction = StaticPrefs::apz_fling_friction();
+ float threshold = StaticPrefs::apz_fling_stopped_threshold();
+
+ mVelocity = ParentLayerPoint(
+ ApplyFrictionOrCancel(mVelocity.x, aDelta, friction, threshold),
+ ApplyFrictionOrCancel(mVelocity.y, aDelta, friction, threshold));
+
+ *aOutVelocity = mVelocity;
+ *aOutOffset = mVelocity * aDelta.ToMilliseconds();
+ }
+
+ private:
+ /**
+ * Applies friction to the given velocity and returns the result, or
+ * returns zero if the velocity is too low.
+ * |aVelocity| is the incoming velocity.
+ * |aDelta| is the amount of time that has passed since the last time
+ * friction was applied.
+ * |aFriction| is the amount of friction to apply.
+ * |aThreshold| is the velocity below which the fling is cancelled.
+ */
+ static float ApplyFrictionOrCancel(float aVelocity,
+ const TimeDuration& aDelta,
+ float aFriction, float aThreshold) {
+ if (fabsf(aVelocity) <= aThreshold) {
+ // If the velocity is very low, just set it to 0 and stop the fling,
+ // otherwise we'll just asymptotically approach 0 and the user won't
+ // actually see any changes.
+ return 0.0f;
+ }
+
+ aVelocity *= pow(1.0f - aFriction, float(aDelta.ToMilliseconds()));
+ return aVelocity;
+ }
+
+ ParentLayerPoint mVelocity;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif // mozilla_layers_DesktopFlingPhysics_h_
diff --git a/gfx/layers/apz/src/DragTracker.cpp b/gfx/layers/apz/src/DragTracker.cpp
new file mode 100644
index 0000000000..aa3b2a34f7
--- /dev/null
+++ b/gfx/layers/apz/src/DragTracker.cpp
@@ -0,0 +1,59 @@
+/* -*- 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 "DragTracker.h"
+
+#include "InputData.h"
+#include "mozilla/Logging.h"
+
+static mozilla::LazyLogModule sApzDrgLog("apz.drag");
+#define DRAG_LOG(...) MOZ_LOG(sApzDrgLog, LogLevel::Debug, (__VA_ARGS__))
+
+namespace mozilla {
+namespace layers {
+
+DragTracker::DragTracker() : mInDrag(false) {}
+
+/*static*/
+bool DragTracker::StartsDrag(const MouseInput& aInput) {
+ return aInput.IsLeftButton() && aInput.mType == MouseInput::MOUSE_DOWN;
+}
+
+/*static*/
+bool DragTracker::EndsDrag(const MouseInput& aInput) {
+ // On Windows, we don't receive a MOUSE_UP at the end of a drag if an
+ // actual drag session took place. As a backup, we detect the end of the
+ // drag using the MOUSE_DRAG_END event, which normally is routed directly
+ // to content, but we're specially routing to APZ for this purpose. Bug
+ // 1265105 tracks a solution to this at the Windows widget layer; once
+ // that is implemented, this workaround can be removed.
+ return (aInput.IsLeftButton() && aInput.mType == MouseInput::MOUSE_UP) ||
+ aInput.mType == MouseInput::MOUSE_DRAG_END;
+}
+
+void DragTracker::Update(const MouseInput& aInput) {
+ if (StartsDrag(aInput)) {
+ DRAG_LOG("Starting drag\n");
+ mInDrag = true;
+ } else if (EndsDrag(aInput)) {
+ DRAG_LOG("Ending drag\n");
+ mInDrag = false;
+ mOnScrollbar = Nothing();
+ }
+}
+
+bool DragTracker::InDrag() const { return mInDrag; }
+
+bool DragTracker::IsOnScrollbar(bool aOnScrollbar) {
+ if (!mOnScrollbar) {
+ DRAG_LOG("Setting hitscrollbar %d\n", aOnScrollbar);
+ mOnScrollbar = Some(aOnScrollbar);
+ }
+ return mOnScrollbar.value();
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/apz/src/DragTracker.h b/gfx/layers/apz/src/DragTracker.h
new file mode 100644
index 0000000000..92678d53c1
--- /dev/null
+++ b/gfx/layers/apz/src/DragTracker.h
@@ -0,0 +1,39 @@
+/* -*- 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_DragTracker_h
+#define mozilla_layers_DragTracker_h
+
+#include "mozilla/EventForwards.h"
+#include "mozilla/Maybe.h"
+
+namespace mozilla {
+
+class MouseInput;
+
+namespace layers {
+
+// DragTracker simply tracks a sequence of mouse inputs and allows us to tell
+// if we are in a drag or not (i.e. the left mouse button went down and hasn't
+// gone up yet).
+class DragTracker {
+ public:
+ DragTracker();
+ static bool StartsDrag(const MouseInput& aInput);
+ static bool EndsDrag(const MouseInput& aInput);
+ void Update(const MouseInput& aInput);
+ bool InDrag() const;
+ bool IsOnScrollbar(bool aOnScrollbar);
+
+ private:
+ Maybe<bool> mOnScrollbar;
+ bool mInDrag;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif /* mozilla_layers_DragTracker_h */
diff --git a/gfx/layers/apz/src/ExpectedGeckoMetrics.cpp b/gfx/layers/apz/src/ExpectedGeckoMetrics.cpp
new file mode 100644
index 0000000000..26b94e94d4
--- /dev/null
+++ b/gfx/layers/apz/src/ExpectedGeckoMetrics.cpp
@@ -0,0 +1,26 @@
+/* -*- 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 "ExpectedGeckoMetrics.h"
+#include "FrameMetrics.h"
+
+namespace mozilla {
+namespace layers {
+
+void ExpectedGeckoMetrics::UpdateFrom(const FrameMetrics& aMetrics) {
+ mVisualScrollOffset = aMetrics.GetVisualScrollOffset();
+ mLayoutScrollOffset = aMetrics.GetLayoutScrollOffset();
+ mZoom = aMetrics.GetZoom();
+ mDevPixelsPerCSSPixel = aMetrics.GetDevPixelsPerCSSPixel();
+}
+
+void ExpectedGeckoMetrics::UpdateZoomFrom(const FrameMetrics& aMetrics) {
+ mZoom = aMetrics.GetZoom();
+ mDevPixelsPerCSSPixel = aMetrics.GetDevPixelsPerCSSPixel();
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/apz/src/ExpectedGeckoMetrics.h b/gfx/layers/apz/src/ExpectedGeckoMetrics.h
new file mode 100644
index 0000000000..ca5aa76eba
--- /dev/null
+++ b/gfx/layers/apz/src/ExpectedGeckoMetrics.h
@@ -0,0 +1,44 @@
+/* -*- 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_ExpectedGeckoMetrics_h
+#define mozilla_layers_ExpectedGeckoMetrics_h
+
+#include "Units.h"
+
+namespace mozilla {
+namespace layers {
+
+struct FrameMetrics;
+
+// A class that stores a subset of the FrameMetrics information
+// than an APZC instance expects Gecko to have (either the
+// metrics that were most recently sent to Gecko, or the ones
+// most recently received from Gecko).
+class ExpectedGeckoMetrics {
+ public:
+ ExpectedGeckoMetrics() = default;
+ void UpdateFrom(const FrameMetrics& aMetrics);
+ void UpdateZoomFrom(const FrameMetrics& aMetrics);
+
+ const CSSPoint& GetVisualScrollOffset() const { return mVisualScrollOffset; }
+ const CSSPoint& GetLayoutScrollOffset() const { return mLayoutScrollOffset; }
+ const CSSToParentLayerScale& GetZoom() const { return mZoom; }
+ const CSSToLayoutDeviceScale& GetDevPixelsPerCSSPixel() const {
+ return mDevPixelsPerCSSPixel;
+ }
+
+ private:
+ CSSPoint mVisualScrollOffset;
+ CSSPoint mLayoutScrollOffset;
+ CSSToParentLayerScale mZoom;
+ CSSToLayoutDeviceScale mDevPixelsPerCSSPixel;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif
diff --git a/gfx/layers/apz/src/FlingAccelerator.cpp b/gfx/layers/apz/src/FlingAccelerator.cpp
new file mode 100644
index 0000000000..f37d04c77b
--- /dev/null
+++ b/gfx/layers/apz/src/FlingAccelerator.cpp
@@ -0,0 +1,128 @@
+/* -*- 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 "FlingAccelerator.h"
+
+#include "mozilla/StaticPrefs_apz.h"
+
+#include "GenericFlingAnimation.h" // for FLING_LOG and FlingHandoffState
+
+namespace mozilla {
+namespace layers {
+
+void FlingAccelerator::Reset() {
+ mPreviousFlingStartingVelocity = ParentLayerPoint{};
+ mPreviousFlingCancelVelocity = ParentLayerPoint{};
+ mIsTracking = false;
+}
+
+static bool SameDirection(float aVelocity1, float aVelocity2) {
+ return (aVelocity1 == 0.0f) || (aVelocity2 == 0.0f) ||
+ (IsNegative(aVelocity1) == IsNegative(aVelocity2));
+}
+
+static float Accelerate(float aBase, float aSupplemental) {
+ return (aBase * StaticPrefs::apz_fling_accel_base_mult()) +
+ (aSupplemental * StaticPrefs::apz_fling_accel_supplemental_mult());
+}
+
+ParentLayerPoint FlingAccelerator::GetFlingStartingVelocity(
+ const SampleTime& aNow, const ParentLayerPoint& aVelocity,
+ const FlingHandoffState& aHandoffState) {
+ // If the fling should be accelerated and is in the same direction as the
+ // previous fling, boost the velocity to be the sum of the two. Check separate
+ // axes separately because we could have two vertical flings with small
+ // horizontal components on the opposite side of zero, and we still want the
+ // y-fling to get accelerated.
+ ParentLayerPoint velocity = aVelocity;
+ if (ShouldAccelerate(aNow, aVelocity, aHandoffState)) {
+ if (velocity.x != 0 &&
+ SameDirection(velocity.x, mPreviousFlingStartingVelocity.x)) {
+ velocity.x = Accelerate(velocity.x, mPreviousFlingStartingVelocity.x);
+ FLING_LOG("%p Applying fling x-acceleration from %f to %f (delta %f)\n",
+ this, aVelocity.x.value, velocity.x.value,
+ mPreviousFlingStartingVelocity.x.value);
+ }
+ if (velocity.y != 0 &&
+ SameDirection(velocity.y, mPreviousFlingStartingVelocity.y)) {
+ velocity.y = Accelerate(velocity.y, mPreviousFlingStartingVelocity.y);
+ FLING_LOG("%p Applying fling y-acceleration from %f to %f (delta %f)\n",
+ this, aVelocity.y.value, velocity.y.value,
+ mPreviousFlingStartingVelocity.y.value);
+ }
+ }
+
+ Reset();
+
+ mPreviousFlingStartingVelocity = velocity;
+ mIsTracking = true;
+
+ return velocity;
+}
+
+bool FlingAccelerator::ShouldAccelerate(
+ const SampleTime& aNow, const ParentLayerPoint& aVelocity,
+ const FlingHandoffState& aHandoffState) const {
+ if (!IsTracking()) {
+ FLING_LOG("%p Fling accelerator was reset, not accelerating.\n", this);
+ return false;
+ }
+
+ if (!aHandoffState.mTouchStartRestingTime) {
+ FLING_LOG("%p Don't have a touch start resting time, not accelerating.\n",
+ this);
+ return false;
+ }
+
+ double msBetweenTouchStartAndPanStart =
+ aHandoffState.mTouchStartRestingTime->ToMilliseconds();
+ FLING_LOG(
+ "%p ShouldAccelerate with pan velocity %f pixels/ms, min pan velocity %f "
+ "pixels/ms, previous fling cancel velocity %f pixels/ms, time elapsed "
+ "since starting previous time between touch start and pan "
+ "start %fms.\n",
+ this, float(aVelocity.Length()), float(aHandoffState.mMinPanVelocity),
+ float(mPreviousFlingCancelVelocity.Length()),
+ float(msBetweenTouchStartAndPanStart));
+
+ if (aVelocity.Length() < StaticPrefs::apz_fling_accel_min_fling_velocity()) {
+ FLING_LOG("%p Fling velocity too low (%f), not accelerating.\n", this,
+ float(aVelocity.Length()));
+ return false;
+ }
+
+ if (aHandoffState.mMinPanVelocity <
+ StaticPrefs::apz_fling_accel_min_pan_velocity()) {
+ FLING_LOG(
+ "%p Panning velocity was too slow at some point during the pan (%f), "
+ "not accelerating.\n",
+ this, float(aHandoffState.mMinPanVelocity));
+ return false;
+ }
+
+ if (mPreviousFlingCancelVelocity.Length() <
+ StaticPrefs::apz_fling_accel_min_fling_velocity()) {
+ FLING_LOG(
+ "%p The previous fling animation had slowed down too much when it was "
+ "interrupted (%f), not accelerating.\n",
+ this, float(mPreviousFlingCancelVelocity.Length()));
+ return false;
+ }
+
+ if (msBetweenTouchStartAndPanStart >=
+ StaticPrefs::apz_fling_accel_max_pause_interval_ms()) {
+ FLING_LOG(
+ "%p Too much time (%fms) elapsed between touch start and pan start, "
+ "not accelerating.\n",
+ this, msBetweenTouchStartAndPanStart);
+ return false;
+ }
+
+ return true;
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/apz/src/FlingAccelerator.h b/gfx/layers/apz/src/FlingAccelerator.h
new file mode 100644
index 0000000000..49e9a7b257
--- /dev/null
+++ b/gfx/layers/apz/src/FlingAccelerator.h
@@ -0,0 +1,59 @@
+/* -*- 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_FlingAccelerator_h
+#define mozilla_layers_FlingAccelerator_h
+
+#include "mozilla/layers/SampleTime.h"
+#include "Units.h"
+
+namespace mozilla {
+namespace layers {
+
+struct FlingHandoffState;
+
+/**
+ * This class is used to track state that is used when determining whether a
+ * fling should be accelerated.
+ */
+class FlingAccelerator final {
+ public:
+ FlingAccelerator() {}
+
+ // Resets state so that the next fling will not be accelerated.
+ void Reset();
+
+ // Returns false after a reset or before the first fling.
+ bool IsTracking() const { return mIsTracking; }
+
+ // Starts a new fling, and returns the (potentially accelerated) velocity that
+ // should be used for that fling.
+ ParentLayerPoint GetFlingStartingVelocity(
+ const SampleTime& aNow, const ParentLayerPoint& aVelocity,
+ const FlingHandoffState& aHandoffState);
+
+ void ObserveFlingCanceled(const ParentLayerPoint& aVelocity) {
+ mPreviousFlingCancelVelocity = aVelocity;
+ }
+
+ protected:
+ bool ShouldAccelerate(const SampleTime& aNow,
+ const ParentLayerPoint& aVelocity,
+ const FlingHandoffState& aHandoffState) const;
+
+ // The initial velocity of the most recent fling.
+ ParentLayerPoint mPreviousFlingStartingVelocity;
+ // The velocity that the previous fling animation had at the point it was
+ // interrupted.
+ ParentLayerPoint mPreviousFlingCancelVelocity;
+ // Whether the upcoming fling is eligible for acceleration.
+ bool mIsTracking = false;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif // mozilla_layers_FlingAccelerator_h
diff --git a/gfx/layers/apz/src/FocusState.cpp b/gfx/layers/apz/src/FocusState.cpp
new file mode 100644
index 0000000000..b7510bcef4
--- /dev/null
+++ b/gfx/layers/apz/src/FocusState.cpp
@@ -0,0 +1,225 @@
+/* -*- 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 "FocusState.h"
+
+#include "mozilla/Logging.h"
+#include "mozilla/layers/APZThreadUtils.h"
+
+static mozilla::LazyLogModule sApzFstLog("apz.focusstate");
+#define FS_LOG(...) MOZ_LOG(sApzFstLog, LogLevel::Debug, (__VA_ARGS__))
+
+namespace mozilla {
+namespace layers {
+
+FocusState::FocusState()
+ : mMutex("FocusStateMutex"),
+ mLastAPZProcessedEvent(1),
+ mLastContentProcessedEvent(0),
+ mFocusHasKeyEventListeners(false),
+ mReceivedUpdate(false),
+ mFocusLayersId{0},
+ mFocusHorizontalTarget(ScrollableLayerGuid::NULL_SCROLL_ID),
+ mFocusVerticalTarget(ScrollableLayerGuid::NULL_SCROLL_ID) {}
+
+uint64_t FocusState::LastAPZProcessedEvent() const {
+ APZThreadUtils::AssertOnControllerThread();
+ MutexAutoLock lock(mMutex);
+
+ return mLastAPZProcessedEvent;
+}
+
+bool FocusState::IsCurrent(const MutexAutoLock& aProofOfLock) const {
+ FS_LOG("Checking IsCurrent() with cseq=%" PRIu64 ", aseq=%" PRIu64 "\n",
+ mLastContentProcessedEvent, mLastAPZProcessedEvent);
+
+ MOZ_ASSERT(mLastContentProcessedEvent <= mLastAPZProcessedEvent);
+ return mLastContentProcessedEvent == mLastAPZProcessedEvent;
+}
+
+void FocusState::ReceiveFocusChangingEvent() {
+ APZThreadUtils::AssertOnControllerThread();
+ MutexAutoLock lock(mMutex);
+
+ if (!mReceivedUpdate) {
+ // In the initial state don't advance mLastAPZProcessedEvent because we
+ // might blow away the information that we're in a freshly-restarted GPU
+ // process. This information (i.e. that mLastAPZProcessedEvent == 1) needs
+ // to be preserved until the first call to Update() which will then advance
+ // mLastAPZProcessedEvent to match the content-side sequence number.
+ return;
+ }
+ mLastAPZProcessedEvent += 1;
+ FS_LOG("Focus changing event incremented aseq to %" PRIu64 "\n",
+ mLastAPZProcessedEvent);
+}
+
+void FocusState::Update(LayersId aRootLayerTreeId,
+ LayersId aOriginatingLayersId,
+ const FocusTarget& aState) {
+ // This runs on the updater thread, it's not worth passing around extra raw
+ // pointers just to assert it.
+
+ MutexAutoLock lock(mMutex);
+
+ FS_LOG("Update with rlt=%" PRIu64 ", olt=%" PRIu64 ", ft=(%s, %" PRIu64 ")\n",
+ aRootLayerTreeId.mId, aOriginatingLayersId.mId, aState.Type(),
+ aState.mSequenceNumber);
+ mReceivedUpdate = true;
+
+ // Update the focus tree with the latest target
+ mFocusTree[aOriginatingLayersId] = aState;
+
+ // Reset our internal state so we can recalculate it
+ mFocusHasKeyEventListeners = false;
+ mFocusLayersId = aRootLayerTreeId;
+ mFocusHorizontalTarget = ScrollableLayerGuid::NULL_SCROLL_ID;
+ mFocusVerticalTarget = ScrollableLayerGuid::NULL_SCROLL_ID;
+
+ // To update the focus state for the entire APZCTreeManager, we need
+ // to traverse the focus tree to find the current leaf which is the global
+ // focus target we can use for async keyboard scrolling
+ while (true) {
+ auto currentNode = mFocusTree.find(mFocusLayersId);
+ if (currentNode == mFocusTree.end()) {
+ FS_LOG("Setting target to nil (cannot find lt=%" PRIu64 ")\n",
+ mFocusLayersId.mId);
+ return;
+ }
+
+ const FocusTarget& target = currentNode->second;
+
+ // Accumulate event listener flags on the path to the focus target
+ mFocusHasKeyEventListeners |= target.mFocusHasKeyEventListeners;
+
+ // Match on the data stored in mData
+ // The match functions return true or false depending on whether the
+ // enclosing method, FocusState::Update, should return or continue to the
+ // next iteration of the while loop, respectively.
+ struct FocusTargetDataMatcher {
+ FocusState& mFocusState;
+ const uint64_t mSequenceNumber;
+
+ bool operator()(const FocusTarget::NoFocusTarget& aNoFocusTarget) {
+ FS_LOG("Setting target to nil (reached a nil target) with seq=%" PRIu64
+ "\n",
+ mSequenceNumber);
+
+ // Mark what sequence number this target has for debugging purposes so
+ // we can always accurately report on whether we are stale or not
+ mFocusState.mLastContentProcessedEvent = mSequenceNumber;
+
+ // If this focus state was just created and content has experienced more
+ // events then us, then assume we were recreated and sync focus sequence
+ // numbers.
+ if (mFocusState.mLastAPZProcessedEvent == 1 &&
+ mFocusState.mLastContentProcessedEvent >
+ mFocusState.mLastAPZProcessedEvent) {
+ mFocusState.mLastAPZProcessedEvent =
+ mFocusState.mLastContentProcessedEvent;
+ }
+ return true;
+ }
+
+ bool operator()(const LayersId& aRefLayerId) {
+ // Guard against infinite loops
+ MOZ_ASSERT(mFocusState.mFocusLayersId != aRefLayerId);
+ if (mFocusState.mFocusLayersId == aRefLayerId) {
+ FS_LOG(
+ "Setting target to nil (bailing out of infinite loop, lt=%" PRIu64
+ ")\n",
+ mFocusState.mFocusLayersId.mId);
+ return true;
+ }
+
+ FS_LOG("Looking for target in lt=%" PRIu64 "\n", aRefLayerId.mId);
+
+ // The focus target is in a child layer tree
+ mFocusState.mFocusLayersId = aRefLayerId;
+ return false;
+ }
+
+ bool operator()(const FocusTarget::ScrollTargets& aScrollTargets) {
+ FS_LOG("Setting target to h=%" PRIu64 ", v=%" PRIu64
+ ", and seq=%" PRIu64 "\n",
+ aScrollTargets.mHorizontal, aScrollTargets.mVertical,
+ mSequenceNumber);
+
+ // This is the global focus target
+ mFocusState.mFocusHorizontalTarget = aScrollTargets.mHorizontal;
+ mFocusState.mFocusVerticalTarget = aScrollTargets.mVertical;
+
+ // Mark what sequence number this target has so we can determine whether
+ // it is stale or not
+ mFocusState.mLastContentProcessedEvent = mSequenceNumber;
+
+ // If this focus state was just created and content has experienced more
+ // events then us, then assume we were recreated and sync focus sequence
+ // numbers.
+ if (mFocusState.mLastAPZProcessedEvent == 1 &&
+ mFocusState.mLastContentProcessedEvent >
+ mFocusState.mLastAPZProcessedEvent) {
+ mFocusState.mLastAPZProcessedEvent =
+ mFocusState.mLastContentProcessedEvent;
+ }
+ return true;
+ }
+ }; // struct FocusTargetDataMatcher
+
+ if (target.mData.match(
+ FocusTargetDataMatcher{*this, target.mSequenceNumber})) {
+ return;
+ }
+ }
+}
+
+void FocusState::RemoveFocusTarget(LayersId aLayersId) {
+ // This runs on the updater thread, it's not worth passing around extra raw
+ // pointers just to assert it.
+ MutexAutoLock lock(mMutex);
+
+ mFocusTree.erase(aLayersId);
+}
+
+Maybe<ScrollableLayerGuid> FocusState::GetHorizontalTarget() const {
+ APZThreadUtils::AssertOnControllerThread();
+ MutexAutoLock lock(mMutex);
+
+ // There is not a scrollable layer to async scroll if
+ // 1. We aren't current
+ // 2. There are event listeners that could change the focus
+ // 3. The target has not been layerized
+ if (!IsCurrent(lock) || mFocusHasKeyEventListeners ||
+ mFocusHorizontalTarget == ScrollableLayerGuid::NULL_SCROLL_ID) {
+ return Nothing();
+ }
+ return Some(ScrollableLayerGuid(mFocusLayersId, 0, mFocusHorizontalTarget));
+}
+
+Maybe<ScrollableLayerGuid> FocusState::GetVerticalTarget() const {
+ APZThreadUtils::AssertOnControllerThread();
+ MutexAutoLock lock(mMutex);
+
+ // There is not a scrollable layer to async scroll if:
+ // 1. We aren't current
+ // 2. There are event listeners that could change the focus
+ // 3. The target has not been layerized
+ if (!IsCurrent(lock) || mFocusHasKeyEventListeners ||
+ mFocusVerticalTarget == ScrollableLayerGuid::NULL_SCROLL_ID) {
+ return Nothing();
+ }
+ return Some(ScrollableLayerGuid(mFocusLayersId, 0, mFocusVerticalTarget));
+}
+
+bool FocusState::CanIgnoreKeyboardShortcutMisses() const {
+ APZThreadUtils::AssertOnControllerThread();
+ MutexAutoLock lock(mMutex);
+
+ return IsCurrent(lock) && !mFocusHasKeyEventListeners;
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/apz/src/FocusState.h b/gfx/layers/apz/src/FocusState.h
new file mode 100644
index 0000000000..14d536be3e
--- /dev/null
+++ b/gfx/layers/apz/src/FocusState.h
@@ -0,0 +1,175 @@
+/* -*- 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_FocusState_h
+#define mozilla_layers_FocusState_h
+
+#include <unordered_map> // for std::unordered_map
+#include <unordered_set> // for std::unordered_set
+
+#include "mozilla/layers/FocusTarget.h" // for FocusTarget
+#include "mozilla/layers/ScrollableLayerGuid.h" // for ViewID
+#include "mozilla/Mutex.h" // for Mutex
+
+namespace mozilla {
+namespace layers {
+
+/**
+ * This class is used for tracking chrome and content focus targets and
+ * calculating global focus information from them for use by APZCTreeManager
+ * for async keyboard scrolling.
+ *
+ * # Calculating the element to scroll
+ *
+ * Chrome and content processes have independently focused elements. This makes
+ * it difficult to calculate the global focused element and its scrollable
+ * frame from the chrome or content side. So instead we send the local focus
+ * information from each process to here and then calculate the global focus
+ * information. This local information resides in a `focus target`.
+ *
+ * A focus target indicates that either:
+ * 1. The focused element is a remote browser along with its layer tree ID
+ * 2. The focused element is not scrollable
+ * 3. The focused element is scrollable along with the ViewID's of its
+ scrollable layers
+ *
+ * Using this information we can determine the global focus information by
+ * starting at the focus target of the root layer tree ID and following remote
+ * browsers until we reach a scrollable or non-scrollable focus target.
+ *
+ * # Determinism and sequence numbers
+ *
+ * The focused element in content can be changed within any javascript code. And
+ * javascript can run in response to an event or at any moment from `setTimeout`
+ * and others. This makes it impossible to always have the current focus
+ * information in APZ as it can be changed asynchronously at any moment. If we
+ * don't have the latest focus information, we may incorrectly scroll a target
+ * when we shouldn't.
+ *
+ * A tradeoff is designed here whereby we will maintain deterministic focus
+ * changes for user input, but not for other javascript code. The reasoning
+ * here is that `setTimeout` and others are already non-deterministic and so it
+ * might not be as breaking to web content.
+ *
+ * To maintain deterministic focus changes for a given stream of user inputs,
+ * we invalidate our focus state whenever we receive a user input that may
+ * trigger event listeners. We then attach a new sequence number to these
+ * events and dispatch them to content. Content will then include the latest
+ * sequence number it has processed to every focus update. Using this we can
+ * determine whether any potentially focus changing events have yet to be
+ * handled by content.
+ *
+ * Once we have received the latest focus sequence number from content, we know
+ * that all event listeners triggered by user inputs, and their resulting focus
+ * changes, have been processed and so we have a current target that we can use
+ * again.
+ */
+class FocusState final {
+ public:
+ FocusState();
+
+ /**
+ * The sequence number of the last potentially focus changing event processed
+ * by APZ. This number starts at one and increases monotonically. This number
+ * will never be zero as that is used to catch uninitialized focus sequence
+ * numbers on input events.
+ */
+ uint64_t LastAPZProcessedEvent() const;
+
+ /**
+ * Notify focus state of a potentially focus changing event. This will
+ * increment the current focus sequence number. The new value can be gotten
+ * from LastAPZProcessedEvent().
+ */
+ void ReceiveFocusChangingEvent();
+
+ /**
+ * Update the internal focus tree and recalculate the global focus target for
+ * a focus target update received from chrome or content.
+ *
+ * @param aRootLayerTreeId the layer tree ID of the root layer for the
+ parent APZCTreeManager
+ * @param aOriginatingLayersId the layer tree ID that this focus target
+ belongs to
+ */
+ void Update(LayersId aRootLayerTreeId, LayersId aOriginatingLayersId,
+ const FocusTarget& aTarget);
+
+ /**
+ * Removes a focus target by its layer tree ID.
+ */
+ void RemoveFocusTarget(LayersId aLayersId);
+
+ /**
+ * Gets the scrollable layer that should be horizontally scrolled for a key
+ * event, if any. The returned ScrollableLayerGuid doesn't contain a
+ * presShellId, and so it should not be used in comparisons.
+ *
+ * No scrollable layer is returned if any of the following are true:
+ * 1. We don't have a current focus target
+ * 2. There are event listeners that could change the focus
+ * 3. The target has not been layerized
+ */
+ Maybe<ScrollableLayerGuid> GetHorizontalTarget() const;
+ /**
+ * The same as GetHorizontalTarget() but for vertical scrolling.
+ */
+ Maybe<ScrollableLayerGuid> GetVerticalTarget() const;
+
+ /**
+ * Gets whether it is safe to not increment the focus sequence number for an
+ * unmatched keyboard event.
+ */
+ bool CanIgnoreKeyboardShortcutMisses() const;
+
+ private:
+ /**
+ * Whether the current focus state is known to be current or else if an event
+ * has been processed that could change the focus but we have not received an
+ * update with a new confirmed target.
+ * This can only be called by methods that have already acquired mMutex; they
+ * have to pass their lock as compile-time proof.
+ */
+ bool IsCurrent(const MutexAutoLock& aLock) const;
+
+ private:
+ // All methods should hold this lock, since this class is accessed via both
+ // the updater and controller threads.
+ mutable Mutex mMutex MOZ_UNANNOTATED;
+
+ // The set of focus targets received indexed by their layer tree ID
+ std::unordered_map<LayersId, FocusTarget, LayersId::HashFn> mFocusTree;
+
+ // The focus sequence number of the last potentially focus changing event
+ // processed by APZ. This number starts at one and increases monotonically.
+ // We don't worry about wrap around here because at a pace of 100
+ // increments/sec, it would take 5.85*10^9 years before we would wrap around.
+ // This number will never be zero as that is used to catch uninitialized focus
+ // sequence numbers on input events.
+ uint64_t mLastAPZProcessedEvent;
+ // The focus sequence number last received in a focus update.
+ uint64_t mLastContentProcessedEvent;
+
+ // A flag whether there is a key listener on the event target chain for the
+ // focused element
+ bool mFocusHasKeyEventListeners;
+ // A flag that is false until the first call to Update().
+ bool mReceivedUpdate;
+
+ // The layer tree ID which contains the scrollable frame of the focused
+ // element
+ LayersId mFocusLayersId;
+ // The scrollable layer corresponding to the scrollable frame that is used to
+ // scroll the focused element. This depends on the direction the user is
+ // scrolling.
+ ScrollableLayerGuid::ViewID mFocusHorizontalTarget;
+ ScrollableLayerGuid::ViewID mFocusVerticalTarget;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif // mozilla_layers_FocusState_h
diff --git a/gfx/layers/apz/src/FocusTarget.cpp b/gfx/layers/apz/src/FocusTarget.cpp
new file mode 100644
index 0000000000..f1f6463e5c
--- /dev/null
+++ b/gfx/layers/apz/src/FocusTarget.cpp
@@ -0,0 +1,233 @@
+/* -*- 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 "mozilla/layers/FocusTarget.h"
+#include "mozilla/dom/BrowserBridgeChild.h" // for BrowserBridgeChild
+#include "mozilla/dom/EventTarget.h" // for EventTarget
+#include "mozilla/dom/RemoteBrowser.h" // For RemoteBrowser
+#include "mozilla/EventDispatcher.h" // for EventDispatcher
+#include "mozilla/PresShell.h" // For PresShell
+#include "mozilla/StaticPrefs_apz.h"
+#include "nsIContentInlines.h" // for nsINode::IsEditable()
+#include "nsLayoutUtils.h" // for nsLayoutUtils
+
+static mozilla::LazyLogModule sApzFtgLog("apz.focustarget");
+#define FT_LOG(...) MOZ_LOG(sApzFtgLog, LogLevel::Debug, (__VA_ARGS__))
+
+using namespace mozilla::dom;
+using namespace mozilla::layout;
+
+namespace mozilla {
+namespace layers {
+
+static PresShell* GetRetargetEventPresShell(PresShell* aRootPresShell) {
+ MOZ_ASSERT(aRootPresShell);
+
+ // Use the last focused window in this PresShell and its
+ // associated PresShell
+ nsCOMPtr<nsPIDOMWindowOuter> window =
+ aRootPresShell->GetFocusedDOMWindowInOurWindow();
+ if (!window) {
+ return nullptr;
+ }
+
+ RefPtr<Document> retargetEventDoc = window->GetExtantDoc();
+ if (!retargetEventDoc) {
+ return nullptr;
+ }
+
+ return retargetEventDoc->GetPresShell();
+}
+
+// _BOUNDARY because Dispatch() with `targets` must not handle the event.
+MOZ_CAN_RUN_SCRIPT_BOUNDARY static bool HasListenersForKeyEvents(
+ nsIContent* aContent) {
+ if (!aContent) {
+ return false;
+ }
+
+ WidgetEvent event(true, eVoidEvent);
+ nsTArray<EventTarget*> targets;
+ nsresult rv = EventDispatcher::Dispatch(aContent, nullptr, &event, nullptr,
+ nullptr, nullptr, &targets);
+ NS_ENSURE_SUCCESS(rv, false);
+ for (size_t i = 0; i < targets.Length(); i++) {
+ if (targets[i]->HasNonSystemGroupListenersForUntrustedKeyEvents()) {
+ return true;
+ }
+ }
+ return false;
+}
+
+// _BOUNDARY because Dispatch() with `targets` must not handle the event.
+MOZ_CAN_RUN_SCRIPT_BOUNDARY static bool HasListenersForNonPassiveKeyEvents(
+ nsIContent* aContent) {
+ if (!aContent) {
+ return false;
+ }
+
+ WidgetEvent event(true, eVoidEvent);
+ nsTArray<EventTarget*> targets;
+ nsresult rv = EventDispatcher::Dispatch(aContent, nullptr, &event, nullptr,
+ nullptr, nullptr, &targets);
+ NS_ENSURE_SUCCESS(rv, false);
+ for (size_t i = 0; i < targets.Length(); i++) {
+ if (targets[i]
+ ->HasNonPassiveNonSystemGroupListenersForUntrustedKeyEvents()) {
+ return true;
+ }
+ }
+ return false;
+}
+
+static bool IsEditableNode(nsINode* aNode) {
+ return aNode && aNode->IsEditable();
+}
+
+FocusTarget::FocusTarget()
+ : mSequenceNumber(0),
+ mFocusHasKeyEventListeners(false),
+ mData(AsVariant(NoFocusTarget())) {}
+
+FocusTarget::FocusTarget(PresShell* aRootPresShell,
+ uint64_t aFocusSequenceNumber)
+ : mSequenceNumber(aFocusSequenceNumber),
+ mFocusHasKeyEventListeners(false),
+ mData(AsVariant(NoFocusTarget())) {
+ MOZ_ASSERT(aRootPresShell);
+ MOZ_ASSERT(NS_IsMainThread());
+
+ // Key events can be retargeted to a child PresShell when there is an iframe
+ RefPtr<PresShell> presShell = GetRetargetEventPresShell(aRootPresShell);
+
+ if (!presShell) {
+ FT_LOG("Creating nil target with seq=%" PRIu64
+ " (can't find retargeted presshell)\n",
+ aFocusSequenceNumber);
+
+ return;
+ }
+
+ RefPtr<Document> document = presShell->GetDocument();
+ if (!document) {
+ FT_LOG("Creating nil target with seq=%" PRIu64 " (no document)\n",
+ aFocusSequenceNumber);
+
+ return;
+ }
+
+ // Find the focused content and use it to determine whether there are key
+ // event listeners or whether key events will be targeted at a different
+ // process through a remote browser.
+ nsCOMPtr<nsIContent> focusedContent =
+ presShell->GetFocusedContentInOurWindow();
+ nsCOMPtr<nsIContent> keyEventTarget = focusedContent;
+
+ // If there is no focused element then event dispatch goes to the body of
+ // the page if it exists or the root element.
+ if (!keyEventTarget) {
+ keyEventTarget = document->GetUnfocusedKeyEventTarget();
+ }
+
+ // Check if there are key event listeners that could prevent default or change
+ // the focus or selection of the page.
+ if (StaticPrefs::apz_keyboard_passive_listeners()) {
+ mFocusHasKeyEventListeners =
+ HasListenersForNonPassiveKeyEvents(keyEventTarget.get());
+ } else {
+ mFocusHasKeyEventListeners = HasListenersForKeyEvents(keyEventTarget.get());
+ }
+
+ // Check if the key event target is content editable or if the document
+ // is in design mode.
+ if (IsEditableNode(keyEventTarget) || IsEditableNode(document)) {
+ FT_LOG("Creating nil target with seq=%" PRIu64
+ ", kl=%d (disabling for editable node)\n",
+ aFocusSequenceNumber, static_cast<int>(mFocusHasKeyEventListeners));
+
+ return;
+ }
+
+ // Check if the key event target is a remote browser
+ if (RemoteBrowser* remoteBrowser = RemoteBrowser::GetFrom(keyEventTarget)) {
+ LayersId layersId = remoteBrowser->GetLayersId();
+
+ // The globally focused element for scrolling is in a remote layer tree
+ if (layersId.IsValid()) {
+ FT_LOG("Creating reflayer target with seq=%" PRIu64 ", kl=%d, lt=%" PRIu64
+ "\n",
+ aFocusSequenceNumber, mFocusHasKeyEventListeners, layersId.mId);
+
+ mData = AsVariant<LayersId>(std::move(layersId));
+ return;
+ }
+
+ FT_LOG("Creating nil target with seq=%" PRIu64
+ ", kl=%d (remote browser missing layers id)\n",
+ aFocusSequenceNumber, mFocusHasKeyEventListeners);
+
+ return;
+ }
+
+ // The content to scroll is either the focused element or the focus node of
+ // the selection. It's difficult to determine if an element is an interactive
+ // element requiring async keyboard scrolling to be disabled. So we only
+ // allow async key scrolling based on the selection, which doesn't have
+ // this problem and is more common.
+ if (focusedContent) {
+ FT_LOG("Creating nil target with seq=%" PRIu64
+ ", kl=%d (disabling for focusing an element)\n",
+ aFocusSequenceNumber, mFocusHasKeyEventListeners);
+
+ return;
+ }
+
+ nsCOMPtr<nsIContent> selectedContent =
+ presShell->GetSelectedContentForScrolling();
+
+ // Gather the scrollable frames that would be scrolled in each direction
+ // for this scroll target
+ nsIScrollableFrame* horizontal =
+ presShell->GetScrollableFrameToScrollForContent(
+ selectedContent.get(), HorizontalScrollDirection);
+ nsIScrollableFrame* vertical =
+ presShell->GetScrollableFrameToScrollForContent(selectedContent.get(),
+ VerticalScrollDirection);
+
+ // We might have the globally focused element for scrolling. Gather a ViewID
+ // for the horizontal and vertical scroll targets of this element.
+ ScrollTargets target;
+ target.mHorizontal = nsLayoutUtils::FindIDForScrollableFrame(horizontal);
+ target.mVertical = nsLayoutUtils::FindIDForScrollableFrame(vertical);
+ mData = AsVariant(target);
+
+ FT_LOG("Creating scroll target with seq=%" PRIu64 ", kl=%d, h=%" PRIu64
+ ", v=%" PRIu64 "\n",
+ aFocusSequenceNumber, mFocusHasKeyEventListeners, target.mHorizontal,
+ target.mVertical);
+}
+
+bool FocusTarget::operator==(const FocusTarget& aRhs) const {
+ return mSequenceNumber == aRhs.mSequenceNumber &&
+ mFocusHasKeyEventListeners == aRhs.mFocusHasKeyEventListeners &&
+ mData == aRhs.mData;
+}
+
+const char* FocusTarget::Type() const {
+ if (mData.is<LayersId>()) {
+ return "LayersId";
+ }
+ if (mData.is<ScrollTargets>()) {
+ return "ScrollTargets";
+ }
+ if (mData.is<NoFocusTarget>()) {
+ return "NoFocusTarget";
+ }
+ return "<unknown>";
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/apz/src/FocusTarget.h b/gfx/layers/apz/src/FocusTarget.h
new file mode 100644
index 0000000000..f4caa5d070
--- /dev/null
+++ b/gfx/layers/apz/src/FocusTarget.h
@@ -0,0 +1,71 @@
+/* -*- 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_FocusTarget_h
+#define mozilla_layers_FocusTarget_h
+
+#include <stdint.h> // for int32_t, uint32_t
+
+#include "mozilla/DefineEnum.h" // for MOZ_DEFINE_ENUM
+#include "mozilla/layers/ScrollableLayerGuid.h" // for ViewID
+#include "mozilla/Variant.h" // for Variant
+#include "mozilla/Maybe.h" // for Maybe
+
+namespace mozilla {
+
+class PresShell;
+
+namespace layers {
+
+/**
+ * This class is used for communicating information about the currently focused
+ * element of a document and the scrollable frames to use when keyboard
+ * scrolling it. It is created on the main thread at paint-time, but is then
+ * passed over IPC to the compositor/APZ code.
+ */
+class FocusTarget final {
+ public:
+ struct ScrollTargets {
+ ScrollableLayerGuid::ViewID mHorizontal;
+ ScrollableLayerGuid::ViewID mVertical;
+
+ bool operator==(const ScrollTargets& aRhs) const {
+ return (mHorizontal == aRhs.mHorizontal && mVertical == aRhs.mVertical);
+ }
+ };
+
+ // We need this to represent the case where mData has no focus target data
+ // because we can't have an empty variant
+ struct NoFocusTarget {
+ bool operator==(const NoFocusTarget& aRhs) const { return true; }
+ };
+
+ FocusTarget();
+
+ /**
+ * Construct a focus target for the specified top level PresShell
+ */
+ FocusTarget(PresShell* aRootPresShell, uint64_t aFocusSequenceNumber);
+
+ bool operator==(const FocusTarget& aRhs) const;
+
+ const char* Type() const;
+
+ public:
+ // The content sequence number recorded at the time of this class's creation
+ uint64_t mSequenceNumber;
+
+ // Whether there are keydown, keypress, or keyup event listeners
+ // in the event target chain of the focused element
+ bool mFocusHasKeyEventListeners;
+
+ mozilla::Variant<LayersId, ScrollTargets, NoFocusTarget> mData;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif // mozilla_layers_FocusTarget_h
diff --git a/gfx/layers/apz/src/GenericFlingAnimation.h b/gfx/layers/apz/src/GenericFlingAnimation.h
new file mode 100644
index 0000000000..82f981ae0a
--- /dev/null
+++ b/gfx/layers/apz/src/GenericFlingAnimation.h
@@ -0,0 +1,207 @@
+/* -*- 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_GenericFlingAnimation_h_
+#define mozilla_layers_GenericFlingAnimation_h_
+
+#include "APZUtils.h"
+#include "AsyncPanZoomAnimation.h"
+#include "AsyncPanZoomController.h"
+#include "FrameMetrics.h"
+#include "Units.h"
+#include "OverscrollHandoffState.h"
+#include "mozilla/Assertions.h"
+#include "mozilla/Monitor.h"
+#include "mozilla/RefPtr.h"
+#include "mozilla/StaticPrefs_apz.h"
+#include "mozilla/TimeStamp.h"
+#include "mozilla/ToString.h"
+#include "nsThreadUtils.h"
+
+static mozilla::LazyLogModule sApzFlgLog("apz.fling");
+#define FLING_LOG(...) MOZ_LOG(sApzFlgLog, LogLevel::Debug, (__VA_ARGS__))
+
+namespace mozilla {
+namespace layers {
+
+/**
+ * The FlingPhysics template parameter determines the physics model
+ * that the fling animation follows. It must have the following methods:
+ *
+ * - Default constructor.
+ *
+ * - Init(const ParentLayerPoint& aStartingVelocity, float aPLPPI).
+ * Called at the beginning of the fling, with the fling's starting velocity,
+ * and the number of ParentLayer pixels per (Screen) inch at the point of
+ * the fling's start in the fling's direction.
+ *
+ * - Sample(const TimeDuration& aDelta,
+ * ParentLayerPoint* aOutVelocity,
+ * ParentLayerPoint* aOutOffset);
+ * Called on each sample of the fling.
+ * |aDelta| is the time elapsed since the last sample.
+ * |aOutVelocity| should be the desired velocity after the current sample,
+ * in ParentLayer pixels per millisecond.
+ * |aOutOffset| should be the desired _delta_ to the scroll offset after
+ * the current sample. |aOutOffset| should _not_ be clamped to the APZC's
+ * scrollable bounds; the caller will do the clamping, and it needs to
+ * know the unclamped value to handle handoff/overscroll correctly.
+ */
+template <typename FlingPhysics>
+class GenericFlingAnimation : public AsyncPanZoomAnimation,
+ public FlingPhysics {
+ public:
+ GenericFlingAnimation(AsyncPanZoomController& aApzc,
+ const FlingHandoffState& aHandoffState, float aPLPPI)
+ : mApzc(aApzc),
+ mOverscrollHandoffChain(aHandoffState.mChain),
+ mScrolledApzc(aHandoffState.mScrolledApzc) {
+ MOZ_ASSERT(mOverscrollHandoffChain);
+
+ // Drop any velocity on axes where we don't have room to scroll anyways
+ // (in this APZC, or an APZC further in the handoff chain).
+ // This ensures that we don't take the 'overscroll' path in Sample()
+ // on account of one axis which can't scroll having a velocity.
+ if (!mOverscrollHandoffChain->CanScrollInDirection(
+ &mApzc, ScrollDirection::eHorizontal)) {
+ RecursiveMutexAutoLock lock(mApzc.mRecursiveMutex);
+ mApzc.mX.SetVelocity(0);
+ }
+ if (!mOverscrollHandoffChain->CanScrollInDirection(
+ &mApzc, ScrollDirection::eVertical)) {
+ RecursiveMutexAutoLock lock(mApzc.mRecursiveMutex);
+ mApzc.mY.SetVelocity(0);
+ }
+
+ if (aHandoffState.mIsHandoff) {
+ // Only apply acceleration in the APZC that originated the fling, not in
+ // APZCs further down the handoff chain during handoff.
+ mApzc.mFlingAccelerator.Reset();
+ }
+
+ ParentLayerPoint velocity =
+ mApzc.mFlingAccelerator.GetFlingStartingVelocity(
+ aApzc.GetFrameTime(), mApzc.GetVelocityVector(), aHandoffState);
+
+ mApzc.SetVelocityVector(velocity);
+
+ FlingPhysics::Init(mApzc.GetVelocityVector(), aPLPPI);
+ }
+
+ /**
+ * Advances a fling by an interpolated amount based on the passed in |aDelta|.
+ * This should be called whenever sampling the content transform for this
+ * frame. Returns true if the fling animation should be advanced by one frame,
+ * or false if there is no fling or the fling has ended.
+ */
+ virtual bool DoSample(FrameMetrics& aFrameMetrics,
+ const TimeDuration& aDelta) override {
+ CSSToParentLayerScale zoom(aFrameMetrics.GetZoom());
+ if (zoom == CSSToParentLayerScale(0)) {
+ return false;
+ }
+
+ ParentLayerPoint velocity;
+ ParentLayerPoint offset;
+ FlingPhysics::Sample(aDelta, &velocity, &offset);
+
+ mApzc.SetVelocityVector(velocity);
+
+ // If we shouldn't continue the fling, let's just stop and repaint.
+ if (IsZero(velocity / zoom)) {
+ FLING_LOG("%p ending fling animation. overscrolled=%d\n", &mApzc,
+ mApzc.IsOverscrolled());
+ // This APZC or an APZC further down the handoff chain may be be
+ // overscrolled. Start a snap-back animation on the overscrolled APZC.
+ // Note:
+ // This needs to be a deferred task even though it can safely run
+ // while holding mRecursiveMutex, because otherwise, if the overscrolled
+ // APZC is this one, then the SetState(NOTHING) in UpdateAnimation will
+ // stomp on the SetState(SNAP_BACK) it does.
+ mDeferredTasks.AppendElement(NewRunnableMethod<AsyncPanZoomController*>(
+ "layers::OverscrollHandoffChain::SnapBackOverscrolledApzc",
+ mOverscrollHandoffChain.get(),
+ &OverscrollHandoffChain::SnapBackOverscrolledApzc, &mApzc));
+ return false;
+ }
+
+ // Ordinarily we might need to do a ScheduleComposite if either of
+ // the following AdjustDisplacement calls returns true, but this
+ // is already running as part of a FlingAnimation, so we'll be compositing
+ // per frame of animation anyway.
+ ParentLayerPoint overscroll;
+ ParentLayerPoint adjustedOffset;
+ mApzc.mX.AdjustDisplacement(offset.x, adjustedOffset.x, overscroll.x);
+ mApzc.mY.AdjustDisplacement(offset.y, adjustedOffset.y, overscroll.y);
+ if (aFrameMetrics.GetZoom() != CSSToParentLayerScale(0)) {
+ mApzc.ScrollBy(adjustedOffset / aFrameMetrics.GetZoom());
+ }
+
+ // The fling may have caused us to reach the end of our scroll range.
+ if (!IsZero(overscroll / zoom)) {
+ // Hand off the fling to the next APZC in the overscroll handoff chain.
+
+ // We may have reached the end of the scroll range along one axis but
+ // not the other. In such a case we only want to hand off the relevant
+ // component of the fling.
+ if (mApzc.IsZero(overscroll.x)) {
+ velocity.x = 0;
+ } else if (mApzc.IsZero(overscroll.y)) {
+ velocity.y = 0;
+ }
+
+ // To hand off the fling, we attempt to find a target APZC and start a new
+ // fling with the same velocity on that APZC. For simplicity, the actual
+ // overscroll of the current sample is discarded rather than being handed
+ // off. The compositor should sample animations sufficiently frequently
+ // that this is not noticeable. The target APZC is chosen by seeing if
+ // there is an APZC further in the handoff chain which is pannable; if
+ // there isn't, we take the new fling ourselves, entering an overscrolled
+ // state.
+ // Note: APZC is holding mRecursiveMutex, so directly calling
+ // HandleFlingOverscroll() (which acquires the tree lock) would violate
+ // the lock ordering. Instead we schedule HandleFlingOverscroll() to be
+ // called after mRecursiveMutex is released.
+ FLING_LOG("%p fling went into overscroll, handing off with velocity %s\n",
+ &mApzc, ToString(velocity).c_str());
+ mDeferredTasks.AppendElement(
+ NewRunnableMethod<ParentLayerPoint, SideBits,
+ RefPtr<const OverscrollHandoffChain>,
+ RefPtr<const AsyncPanZoomController>>(
+ "layers::AsyncPanZoomController::HandleFlingOverscroll", &mApzc,
+ &AsyncPanZoomController::HandleFlingOverscroll, velocity,
+ apz::GetOverscrollSideBits(overscroll), mOverscrollHandoffChain,
+ mScrolledApzc));
+
+ // If there is a remaining velocity on this APZC, continue this fling
+ // as well. (This fling and the handed-off fling will run concurrently.)
+ // Note that AdjustDisplacement() will have zeroed out the velocity
+ // along the axes where we're overscrolled.
+ return !IsZero(mApzc.GetVelocityVector() / zoom);
+ }
+
+ return true;
+ }
+
+ void Cancel(CancelAnimationFlags aFlags) override {
+ mApzc.mFlingAccelerator.ObserveFlingCanceled(mApzc.GetVelocityVector());
+ }
+
+ virtual bool HandleScrollOffsetUpdate(
+ const Maybe<CSSPoint>& aRelativeDelta) override {
+ return true;
+ }
+
+ private:
+ AsyncPanZoomController& mApzc;
+ RefPtr<const OverscrollHandoffChain> mOverscrollHandoffChain;
+ RefPtr<const AsyncPanZoomController> mScrolledApzc;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif // mozilla_layers_GenericFlingAnimation_h_
diff --git a/gfx/layers/apz/src/GenericScrollAnimation.cpp b/gfx/layers/apz/src/GenericScrollAnimation.cpp
new file mode 100644
index 0000000000..9320482295
--- /dev/null
+++ b/gfx/layers/apz/src/GenericScrollAnimation.cpp
@@ -0,0 +1,120 @@
+/* -*- 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 "GenericScrollAnimation.h"
+
+#include "AsyncPanZoomController.h"
+#include "FrameMetrics.h"
+#include "nsPoint.h"
+#include "ScrollAnimationPhysics.h"
+#include "ScrollAnimationBezierPhysics.h"
+#include "ScrollAnimationMSDPhysics.h"
+#include "mozilla/StaticPrefs_general.h"
+
+namespace mozilla {
+namespace layers {
+
+GenericScrollAnimation::GenericScrollAnimation(
+ AsyncPanZoomController& aApzc, const nsPoint& aInitialPosition,
+ const ScrollAnimationBezierPhysicsSettings& aSettings)
+ : mApzc(aApzc), mFinalDestination(aInitialPosition) {
+ // ScrollAnimationBezierPhysics (despite it's name) handles the case of
+ // general.smoothScroll being disabled whereas ScrollAnimationMSDPhysics does
+ // not (ie it scrolls smoothly).
+ if (StaticPrefs::general_smoothScroll() &&
+ StaticPrefs::general_smoothScroll_msdPhysics_enabled()) {
+ mAnimationPhysics = MakeUnique<ScrollAnimationMSDPhysics>(aInitialPosition);
+ } else {
+ mAnimationPhysics =
+ MakeUnique<ScrollAnimationBezierPhysics>(aInitialPosition, aSettings);
+ }
+}
+
+void GenericScrollAnimation::UpdateDelta(TimeStamp aTime, const nsPoint& aDelta,
+ const nsSize& aCurrentVelocity) {
+ mFinalDestination += aDelta;
+
+ Update(aTime, aCurrentVelocity);
+}
+
+void GenericScrollAnimation::UpdateDestination(TimeStamp aTime,
+ const nsPoint& aDestination,
+ const nsSize& aCurrentVelocity) {
+ mFinalDestination = aDestination;
+
+ Update(aTime, aCurrentVelocity);
+}
+
+void GenericScrollAnimation::Update(TimeStamp aTime,
+ const nsSize& aCurrentVelocity) {
+ // Clamp the final destination to the scrollable area.
+ CSSPoint clamped = CSSPoint::FromAppUnits(mFinalDestination);
+ clamped.x = mApzc.mX.ClampOriginToScrollableRect(clamped.x);
+ clamped.y = mApzc.mY.ClampOriginToScrollableRect(clamped.y);
+ mFinalDestination = CSSPoint::ToAppUnits(clamped);
+
+ mAnimationPhysics->Update(aTime, mFinalDestination, aCurrentVelocity);
+}
+
+bool GenericScrollAnimation::DoSample(FrameMetrics& aFrameMetrics,
+ const TimeDuration& aDelta) {
+ TimeStamp now = mApzc.GetFrameTime().Time();
+ CSSToParentLayerScale zoom(aFrameMetrics.GetZoom());
+ if (zoom == CSSToParentLayerScale(0)) {
+ return false;
+ }
+
+ // If the animation is finished, make sure the final position is correct by
+ // using one last displacement. Otherwise, compute the delta via the timing
+ // function as normal.
+ bool finished = mAnimationPhysics->IsFinished(now);
+ nsPoint sampledDest = mAnimationPhysics->PositionAt(now);
+ ParentLayerPoint displacement = (CSSPoint::FromAppUnits(sampledDest) -
+ aFrameMetrics.GetVisualScrollOffset()) *
+ zoom;
+
+ if (finished) {
+ mApzc.mX.SetVelocity(0);
+ mApzc.mY.SetVelocity(0);
+ } else if (!IsZero(displacement / zoom)) {
+ // Convert velocity from AppUnits/Seconds to ParentLayerCoords/Milliseconds
+ nsSize velocity = mAnimationPhysics->VelocityAt(now);
+ ParentLayerPoint velocityPL =
+ CSSPoint::FromAppUnits(nsPoint(velocity.width, velocity.height)) * zoom;
+ mApzc.mX.SetVelocity(velocityPL.x / 1000.0);
+ mApzc.mY.SetVelocity(velocityPL.y / 1000.0);
+ }
+ // Note: we ignore overscroll for generic animations.
+ ParentLayerPoint adjustedOffset, overscroll;
+ mApzc.mX.AdjustDisplacement(
+ displacement.x, adjustedOffset.x, overscroll.x,
+ mDirectionForcedToOverscroll == Some(ScrollDirection::eHorizontal));
+ mApzc.mY.AdjustDisplacement(
+ displacement.y, adjustedOffset.y, overscroll.y,
+ mDirectionForcedToOverscroll == Some(ScrollDirection::eVertical));
+ // If we expected to scroll, but there's no more scroll range on either axis,
+ // then end the animation early. Note that the initial displacement could be 0
+ // if the compositor ran very quickly (<1ms) after the animation was created.
+ // When that happens we want to make sure the animation continues.
+ if (!IsZero(displacement / zoom) && IsZero(adjustedOffset / zoom)) {
+ // Nothing more to do - end the animation.
+ return false;
+ }
+ mApzc.ScrollBy(adjustedOffset / zoom);
+ return !finished;
+}
+
+bool GenericScrollAnimation::HandleScrollOffsetUpdate(
+ const Maybe<CSSPoint>& aRelativeDelta) {
+ if (aRelativeDelta) {
+ mAnimationPhysics->ApplyContentShift(*aRelativeDelta);
+ return true;
+ }
+ return false;
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/apz/src/GenericScrollAnimation.h b/gfx/layers/apz/src/GenericScrollAnimation.h
new file mode 100644
index 0000000000..56a64dc5ec
--- /dev/null
+++ b/gfx/layers/apz/src/GenericScrollAnimation.h
@@ -0,0 +1,59 @@
+/* -*- 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_GenericScrollAnimation_h_
+#define mozilla_layers_GenericScrollAnimation_h_
+
+#include "AsyncPanZoomAnimation.h"
+
+namespace mozilla {
+
+struct ScrollAnimationBezierPhysicsSettings;
+class ScrollAnimationPhysics;
+
+namespace layers {
+
+class AsyncPanZoomController;
+
+class GenericScrollAnimation : public AsyncPanZoomAnimation {
+ public:
+ GenericScrollAnimation(AsyncPanZoomController& aApzc,
+ const nsPoint& aInitialPosition,
+ const ScrollAnimationBezierPhysicsSettings& aSettings);
+
+ bool DoSample(FrameMetrics& aFrameMetrics,
+ const TimeDuration& aDelta) override;
+
+ bool HandleScrollOffsetUpdate(const Maybe<CSSPoint>& aRelativeDelta) override;
+
+ void UpdateDelta(TimeStamp aTime, const nsPoint& aDelta,
+ const nsSize& aCurrentVelocity);
+ void UpdateDestination(TimeStamp aTime, const nsPoint& aDestination,
+ const nsSize& aCurrentVelocity);
+
+ CSSPoint GetDestination() const {
+ return CSSPoint::FromAppUnits(mFinalDestination);
+ }
+
+ private:
+ void Update(TimeStamp aTime, const nsSize& aCurrentVelocity);
+
+ protected:
+ AsyncPanZoomController& mApzc;
+ UniquePtr<ScrollAnimationPhysics> mAnimationPhysics;
+ nsPoint mFinalDestination;
+ // If a direction is forced to overscroll, it means it's axis in that
+ // direction is locked, and scroll in that direction is treated as overscroll
+ // of an equal amount, which, for example, may then bubble up a scroll action
+ // to its parent, or may behave as whatever an overscroll occurence requires
+ // to behave
+ Maybe<ScrollDirection> mDirectionForcedToOverscroll;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif // mozilla_layers_GenericScrollAnimation_h_
diff --git a/gfx/layers/apz/src/GestureEventListener.cpp b/gfx/layers/apz/src/GestureEventListener.cpp
new file mode 100644
index 0000000000..b54674b593
--- /dev/null
+++ b/gfx/layers/apz/src/GestureEventListener.cpp
@@ -0,0 +1,663 @@
+/* -*- 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 "GestureEventListener.h"
+#include <algorithm> // for max
+#include <math.h> // for fabsf
+#include <stddef.h> // for size_t
+#include "AsyncPanZoomController.h" // for AsyncPanZoomController
+#include "InputBlockState.h" // for TouchBlockState
+#include "base/task.h" // for CancelableTask, etc
+#include "InputBlockState.h" // for TouchBlockState
+#include "mozilla/StaticPrefs_apz.h"
+#include "mozilla/StaticPrefs_ui.h"
+#include "nsDebug.h" // for NS_WARNING
+#include "nsMathUtils.h" // for NS_hypot
+
+static mozilla::LazyLogModule sApzGelLog("apz.gesture");
+#define GEL_LOG(...) MOZ_LOG(sApzGelLog, LogLevel::Debug, (__VA_ARGS__))
+
+namespace mozilla {
+namespace layers {
+
+/**
+ * Amount of span or focus change needed to take us from the
+ * GESTURE_WAITING_PINCH state to the GESTURE_PINCH state. This is measured as
+ * either a change in distance between the fingers used to compute the span
+ * ratio, or the a change in position of the focus point between the two
+ * fingers.
+ */
+static const float PINCH_START_THRESHOLD = 35.0f;
+
+/**
+ * Determines how fast a one touch pinch zooms in and out. The greater the
+ * value, the faster it zooms.
+ */
+static const float ONE_TOUCH_PINCH_SPEED = 0.005f;
+
+static bool sLongTapEnabled = true;
+
+static ScreenPoint GetCurrentFocus(const MultiTouchInput& aEvent) {
+ const ScreenPoint& firstTouch = aEvent.mTouches[0].mScreenPoint;
+ const ScreenPoint& secondTouch = aEvent.mTouches[1].mScreenPoint;
+ return (firstTouch + secondTouch) / 2;
+}
+
+static ScreenCoord GetCurrentSpan(const MultiTouchInput& aEvent) {
+ const ScreenPoint& firstTouch = aEvent.mTouches[0].mScreenPoint;
+ const ScreenPoint& secondTouch = aEvent.mTouches[1].mScreenPoint;
+ ScreenPoint delta = secondTouch - firstTouch;
+ return delta.Length();
+}
+
+ScreenCoord GestureEventListener::GetYSpanFromGestureStartPoint() {
+ // use the position that began the one-touch-pinch gesture rather
+ // mTouchStartPosition
+ const ScreenPoint start = mOneTouchPinchStartPosition;
+ const ScreenPoint& current = mTouches[0].mScreenPoint;
+ return current.y - start.y;
+}
+
+static TapGestureInput CreateTapEvent(const MultiTouchInput& aTouch,
+ TapGestureInput::TapGestureType aType) {
+ return TapGestureInput(aType, aTouch.mTimeStamp,
+ aTouch.mTouches[0].mScreenPoint, aTouch.modifiers);
+}
+
+GestureEventListener::GestureEventListener(
+ AsyncPanZoomController* aAsyncPanZoomController)
+ : mAsyncPanZoomController(aAsyncPanZoomController),
+ mState(GESTURE_NONE),
+ mSpanChange(0.0f),
+ mPreviousSpan(0.0f),
+ mFocusChange(0.0f),
+ mLastTouchInput(MultiTouchInput::MULTITOUCH_START, 0, TimeStamp(), 0),
+ mLastTapInput(MultiTouchInput::MULTITOUCH_START, 0, TimeStamp(), 0),
+ mLongTapTimeoutTask(nullptr),
+ mMaxTapTimeoutTask(nullptr) {}
+
+GestureEventListener::~GestureEventListener() = default;
+
+nsEventStatus GestureEventListener::HandleInputEvent(
+ const MultiTouchInput& aEvent) {
+ GEL_LOG("Receiving event type %d with %zu touches in state %d\n",
+ aEvent.mType, aEvent.mTouches.Length(), mState);
+
+ nsEventStatus rv = nsEventStatus_eIgnore;
+
+ // Cache the current event since it may become the single or long tap that we
+ // send.
+ mLastTouchInput = aEvent;
+
+ switch (aEvent.mType) {
+ case MultiTouchInput::MULTITOUCH_START:
+ mTouches.Clear();
+ // Cache every touch.
+ for (size_t i = 0; i < aEvent.mTouches.Length(); i++) {
+ mTouches.AppendElement(aEvent.mTouches[i]);
+ }
+
+ if (aEvent.mTouches.Length() == 1) {
+ rv = HandleInputTouchSingleStart();
+ } else {
+ rv = HandleInputTouchMultiStart();
+ }
+ break;
+ case MultiTouchInput::MULTITOUCH_MOVE:
+ // Update the screen points of the cached touches.
+ for (size_t i = 0; i < aEvent.mTouches.Length(); i++) {
+ for (size_t j = 0; j < mTouches.Length(); j++) {
+ if (aEvent.mTouches[i].mIdentifier == mTouches[j].mIdentifier) {
+ mTouches[j].mScreenPoint = aEvent.mTouches[i].mScreenPoint;
+ mTouches[j].mLocalScreenPoint =
+ aEvent.mTouches[i].mLocalScreenPoint;
+ }
+ }
+ }
+ rv = HandleInputTouchMove();
+ break;
+ case MultiTouchInput::MULTITOUCH_END:
+ // Remove the cache of the touch that ended.
+ for (size_t i = 0; i < aEvent.mTouches.Length(); i++) {
+ for (size_t j = 0; j < mTouches.Length(); j++) {
+ if (aEvent.mTouches[i].mIdentifier == mTouches[j].mIdentifier) {
+ mTouches.RemoveElementAt(j);
+ break;
+ }
+ }
+ }
+
+ rv = HandleInputTouchEnd();
+ break;
+ case MultiTouchInput::MULTITOUCH_CANCEL:
+ mTouches.Clear();
+ rv = HandleInputTouchCancel();
+ break;
+ }
+
+ return rv;
+}
+
+int32_t GestureEventListener::GetLastTouchIdentifier() const {
+ if (mTouches.Length() != 1) {
+ NS_WARNING(
+ "GetLastTouchIdentifier() called when last touch event "
+ "did not have one touch");
+ }
+ return mTouches.IsEmpty() ? -1 : mTouches[0].mIdentifier;
+}
+
+/* static */
+void GestureEventListener::SetLongTapEnabled(bool aLongTapEnabled) {
+ sLongTapEnabled = aLongTapEnabled;
+}
+
+/* static */
+bool GestureEventListener::IsLongTapEnabled() { return sLongTapEnabled; }
+
+void GestureEventListener::EnterFirstSingleTouchDown() {
+ SetState(GESTURE_FIRST_SINGLE_TOUCH_DOWN);
+ mTouchStartPosition = mLastTouchInput.mTouches[0].mScreenPoint;
+ mTouchStartOffset = mLastTouchInput.mScreenOffset;
+
+ if (sLongTapEnabled) {
+ CreateLongTapTimeoutTask();
+ }
+ CreateMaxTapTimeoutTask();
+}
+
+nsEventStatus GestureEventListener::HandleInputTouchSingleStart() {
+ switch (mState) {
+ case GESTURE_NONE:
+ EnterFirstSingleTouchDown();
+ break;
+ case GESTURE_FIRST_SINGLE_TOUCH_UP:
+ if (SecondTapIsFar()) {
+ // If the second tap goes down far away from the first, then bail out
+ // of any gesture that includes the first tap.
+ CancelLongTapTimeoutTask();
+ CancelMaxTapTimeoutTask();
+ mSingleTapSent = Nothing();
+
+ // But still allow the second tap to participate in a gesture
+ // (e.g. lead to a single tap, or a double tap if an additional
+ // tap occurs near the same location).
+ EnterFirstSingleTouchDown();
+ } else {
+ // Otherwise, reset the touch start position so that, if this turns into
+ // a one-touch-pinch gesture, it uses the second tap's down position as
+ // the focus, rather than the first tap's.
+ mTouchStartPosition = mLastTouchInput.mTouches[0].mScreenPoint;
+ mTouchStartOffset = mLastTouchInput.mScreenOffset;
+ SetState(GESTURE_SECOND_SINGLE_TOUCH_DOWN);
+ }
+ break;
+ default:
+ NS_WARNING("Unhandled state upon single touch start");
+ SetState(GESTURE_NONE);
+ break;
+ }
+
+ return nsEventStatus_eIgnore;
+}
+
+nsEventStatus GestureEventListener::HandleInputTouchMultiStart() {
+ nsEventStatus rv = nsEventStatus_eIgnore;
+
+ switch (mState) {
+ case GESTURE_NONE:
+ SetState(GESTURE_MULTI_TOUCH_DOWN);
+ break;
+ case GESTURE_FIRST_SINGLE_TOUCH_DOWN:
+ CancelLongTapTimeoutTask();
+ CancelMaxTapTimeoutTask();
+ SetState(GESTURE_MULTI_TOUCH_DOWN);
+ // Prevent APZC::OnTouchStart() from handling MULTITOUCH_START event
+ rv = nsEventStatus_eConsumeNoDefault;
+ break;
+ case GESTURE_FIRST_SINGLE_TOUCH_MAX_TAP_DOWN:
+ CancelLongTapTimeoutTask();
+ SetState(GESTURE_MULTI_TOUCH_DOWN);
+ // Prevent APZC::OnTouchStart() from handling MULTITOUCH_START event
+ rv = nsEventStatus_eConsumeNoDefault;
+ break;
+ case GESTURE_FIRST_SINGLE_TOUCH_UP:
+ case GESTURE_SECOND_SINGLE_TOUCH_DOWN:
+ // Cancel wait for double tap
+ CancelMaxTapTimeoutTask();
+ MOZ_ASSERT(mSingleTapSent.isSome());
+ if (!mSingleTapSent.value()) {
+ TriggerSingleTapConfirmedEvent();
+ }
+ mSingleTapSent = Nothing();
+ SetState(GESTURE_MULTI_TOUCH_DOWN);
+ // Prevent APZC::OnTouchStart() from handling MULTITOUCH_START event
+ rv = nsEventStatus_eConsumeNoDefault;
+ break;
+ case GESTURE_LONG_TOUCH_DOWN:
+ SetState(GESTURE_MULTI_TOUCH_DOWN);
+ break;
+ case GESTURE_MULTI_TOUCH_DOWN:
+ case GESTURE_PINCH:
+ // Prevent APZC::OnTouchStart() from handling MULTITOUCH_START event
+ rv = nsEventStatus_eConsumeNoDefault;
+ break;
+ default:
+ NS_WARNING("Unhandled state upon multitouch start");
+ SetState(GESTURE_NONE);
+ break;
+ }
+
+ return rv;
+}
+
+bool GestureEventListener::MoveDistanceExceeds(ScreenCoord aThreshold) const {
+ ExternalPoint start = AsyncPanZoomController::ToExternalPoint(
+ mTouchStartOffset, mTouchStartPosition);
+ ExternalPoint end = AsyncPanZoomController::ToExternalPoint(
+ mLastTouchInput.mScreenOffset, mLastTouchInput.mTouches[0].mScreenPoint);
+ return (start - end).Length() > aThreshold;
+}
+
+bool GestureEventListener::MoveDistanceIsLarge() const {
+ return MoveDistanceExceeds(mAsyncPanZoomController->GetTouchStartTolerance());
+}
+
+bool GestureEventListener::SecondTapIsFar() const {
+ // Allow a little more room here, because the is actually lifting their finger
+ // off the screen before replacing it, and that tends to have more error than
+ // wiggling the finger while on the screen.
+ return MoveDistanceExceeds(mAsyncPanZoomController->GetSecondTapTolerance());
+}
+
+nsEventStatus GestureEventListener::HandleInputTouchMove() {
+ nsEventStatus rv = nsEventStatus_eIgnore;
+
+ switch (mState) {
+ case GESTURE_NONE:
+ // Ignore this input signal as the corresponding events get handled by
+ // APZC
+ break;
+
+ case GESTURE_LONG_TOUCH_DOWN:
+ if (MoveDistanceIsLarge()) {
+ // So that we don't fire a long-tap-up if the user moves around after a
+ // long-tap
+ SetState(GESTURE_NONE);
+ }
+ break;
+
+ case GESTURE_FIRST_SINGLE_TOUCH_DOWN:
+ case GESTURE_FIRST_SINGLE_TOUCH_MAX_TAP_DOWN: {
+ // If we move too much, bail out of the tap.
+ if (MoveDistanceIsLarge()) {
+ CancelLongTapTimeoutTask();
+ CancelMaxTapTimeoutTask();
+ mSingleTapSent = Nothing();
+ SetState(GESTURE_NONE);
+ }
+ break;
+ }
+
+ // The user has performed a double tap, but not lifted her finger.
+ case GESTURE_SECOND_SINGLE_TOUCH_DOWN: {
+ // If touch has moved noticeably (within StaticPrefs::apz_max_tap_time()),
+ // change state.
+ if (MoveDistanceIsLarge()) {
+ CancelLongTapTimeoutTask();
+ CancelMaxTapTimeoutTask();
+ mSingleTapSent = Nothing();
+ if (!StaticPrefs::apz_one_touch_pinch_enabled()) {
+ // If the one-touch-pinch feature is disabled, bail out of the double-
+ // tap gesture instead.
+ SetState(GESTURE_NONE);
+ break;
+ }
+
+ SetState(GESTURE_ONE_TOUCH_PINCH);
+
+ ScreenCoord currentSpan = 1.0f;
+ ScreenPoint currentFocus = mTouchStartPosition;
+
+ // save the position that the one-touch-pinch gesture actually begins
+ mOneTouchPinchStartPosition = mLastTouchInput.mTouches[0].mScreenPoint;
+
+ PinchGestureInput pinchEvent(
+ PinchGestureInput::PINCHGESTURE_START, PinchGestureInput::ONE_TOUCH,
+ mLastTouchInput.mTimeStamp, mLastTouchInput.mScreenOffset,
+ currentFocus, currentSpan, currentSpan, mLastTouchInput.modifiers);
+
+ rv = mAsyncPanZoomController->HandleGestureEvent(pinchEvent);
+
+ mPreviousSpan = currentSpan;
+ mPreviousFocus = currentFocus;
+ }
+ break;
+ }
+
+ case GESTURE_MULTI_TOUCH_DOWN: {
+ if (mLastTouchInput.mTouches.Length() < 2) {
+ NS_WARNING(
+ "Wrong input: less than 2 moving points in "
+ "GESTURE_MULTI_TOUCH_DOWN state");
+ break;
+ }
+
+ ScreenCoord currentSpan = GetCurrentSpan(mLastTouchInput);
+ ScreenPoint currentFocus = GetCurrentFocus(mLastTouchInput);
+
+ mSpanChange += fabsf(currentSpan - mPreviousSpan);
+ mFocusChange += (currentFocus - mPreviousFocus).Length();
+ if (mSpanChange > PINCH_START_THRESHOLD ||
+ mFocusChange > PINCH_START_THRESHOLD) {
+ SetState(GESTURE_PINCH);
+ PinchGestureInput pinchEvent(
+ PinchGestureInput::PINCHGESTURE_START, PinchGestureInput::TOUCH,
+ mLastTouchInput.mTimeStamp, mLastTouchInput.mScreenOffset,
+ currentFocus, currentSpan, currentSpan, mLastTouchInput.modifiers);
+
+ rv = mAsyncPanZoomController->HandleGestureEvent(pinchEvent);
+ } else {
+ // Prevent APZC::OnTouchMove from processing a move event when two
+ // touches are active
+ rv = nsEventStatus_eConsumeNoDefault;
+ }
+
+ mPreviousSpan = currentSpan;
+ mPreviousFocus = currentFocus;
+ break;
+ }
+
+ case GESTURE_PINCH: {
+ if (mLastTouchInput.mTouches.Length() < 2) {
+ NS_WARNING(
+ "Wrong input: less than 2 moving points in GESTURE_PINCH state");
+ // Prevent APZC::OnTouchMove() from handling this wrong input
+ rv = nsEventStatus_eConsumeNoDefault;
+ break;
+ }
+
+ ScreenCoord currentSpan = GetCurrentSpan(mLastTouchInput);
+
+ PinchGestureInput pinchEvent(
+ PinchGestureInput::PINCHGESTURE_SCALE, PinchGestureInput::TOUCH,
+ mLastTouchInput.mTimeStamp, mLastTouchInput.mScreenOffset,
+ GetCurrentFocus(mLastTouchInput), currentSpan, mPreviousSpan,
+ mLastTouchInput.modifiers);
+
+ rv = mAsyncPanZoomController->HandleGestureEvent(pinchEvent);
+ mPreviousSpan = currentSpan;
+
+ break;
+ }
+
+ case GESTURE_ONE_TOUCH_PINCH: {
+ ScreenCoord currentSpan = GetYSpanFromGestureStartPoint();
+ float effectiveSpan =
+ 1.0f + (fabsf(currentSpan.value) * ONE_TOUCH_PINCH_SPEED);
+ ScreenPoint currentFocus = mTouchStartPosition;
+
+ // Invert zoom.
+ if (currentSpan.value < 0) {
+ effectiveSpan = 1.0f / effectiveSpan;
+ }
+
+ PinchGestureInput pinchEvent(
+ PinchGestureInput::PINCHGESTURE_SCALE, PinchGestureInput::ONE_TOUCH,
+ mLastTouchInput.mTimeStamp, mLastTouchInput.mScreenOffset,
+ currentFocus, effectiveSpan, mPreviousSpan,
+ mLastTouchInput.modifiers);
+
+ rv = mAsyncPanZoomController->HandleGestureEvent(pinchEvent);
+ mPreviousSpan = effectiveSpan;
+
+ break;
+ }
+
+ default:
+ NS_WARNING("Unhandled state upon touch move");
+ SetState(GESTURE_NONE);
+ break;
+ }
+
+ return rv;
+}
+
+nsEventStatus GestureEventListener::HandleInputTouchEnd() {
+ // We intentionally do not pass apzc return statuses up since
+ // it may cause apzc stay in the touching state even after
+ // gestures are completed (please see Bug 1013378 for reference).
+
+ nsEventStatus rv = nsEventStatus_eIgnore;
+
+ switch (mState) {
+ case GESTURE_NONE:
+ // GEL doesn't have a dedicated state for PANNING handled in APZC thus
+ // ignore.
+ break;
+
+ case GESTURE_FIRST_SINGLE_TOUCH_DOWN: {
+ CancelLongTapTimeoutTask();
+ CancelMaxTapTimeoutTask();
+ nsEventStatus tapupStatus = mAsyncPanZoomController->HandleGestureEvent(
+ CreateTapEvent(mLastTouchInput, TapGestureInput::TAPGESTURE_UP));
+ mSingleTapSent = Some(tapupStatus != nsEventStatus_eIgnore);
+ SetState(GESTURE_FIRST_SINGLE_TOUCH_UP);
+ CreateMaxTapTimeoutTask();
+ break;
+ }
+
+ case GESTURE_SECOND_SINGLE_TOUCH_DOWN: {
+ CancelMaxTapTimeoutTask();
+ MOZ_ASSERT(mSingleTapSent.isSome());
+ mAsyncPanZoomController->HandleGestureEvent(CreateTapEvent(
+ mLastTouchInput, mSingleTapSent.value()
+ ? TapGestureInput::TAPGESTURE_SECOND
+ : TapGestureInput::TAPGESTURE_DOUBLE));
+ mSingleTapSent = Nothing();
+ SetState(GESTURE_NONE);
+ break;
+ }
+
+ case GESTURE_FIRST_SINGLE_TOUCH_MAX_TAP_DOWN:
+ CancelLongTapTimeoutTask();
+ SetState(GESTURE_NONE);
+ TriggerSingleTapConfirmedEvent();
+ break;
+
+ case GESTURE_LONG_TOUCH_DOWN: {
+ SetState(GESTURE_NONE);
+ mAsyncPanZoomController->HandleGestureEvent(
+ CreateTapEvent(mLastTouchInput, TapGestureInput::TAPGESTURE_LONG_UP));
+ break;
+ }
+
+ case GESTURE_MULTI_TOUCH_DOWN:
+ if (mTouches.Length() < 2) {
+ SetState(GESTURE_NONE);
+ }
+ break;
+
+ case GESTURE_PINCH:
+ if (mTouches.Length() < 2) {
+ SetState(GESTURE_NONE);
+ PinchGestureInput::PinchGestureType type =
+ PinchGestureInput::PINCHGESTURE_END;
+ ScreenPoint point;
+ if (mTouches.Length() == 1) {
+ // As user still keeps one finger down the event's focus point should
+ // contain meaningful data.
+ type = PinchGestureInput::PINCHGESTURE_FINGERLIFTED;
+ point = mTouches[0].mScreenPoint;
+ }
+ PinchGestureInput pinchEvent(type, PinchGestureInput::TOUCH,
+ mLastTouchInput.mTimeStamp,
+ mLastTouchInput.mScreenOffset, point, 1.0f,
+ 1.0f, mLastTouchInput.modifiers);
+ mAsyncPanZoomController->HandleGestureEvent(pinchEvent);
+ }
+
+ rv = nsEventStatus_eConsumeNoDefault;
+
+ break;
+
+ case GESTURE_ONE_TOUCH_PINCH: {
+ SetState(GESTURE_NONE);
+ PinchGestureInput pinchEvent(
+ PinchGestureInput::PINCHGESTURE_END, PinchGestureInput::ONE_TOUCH,
+ mLastTouchInput.mTimeStamp, mLastTouchInput.mScreenOffset,
+ ScreenPoint(), 1.0f, 1.0f, mLastTouchInput.modifiers);
+ mAsyncPanZoomController->HandleGestureEvent(pinchEvent);
+
+ rv = nsEventStatus_eConsumeNoDefault;
+
+ break;
+ }
+
+ default:
+ NS_WARNING("Unhandled state upon touch end");
+ SetState(GESTURE_NONE);
+ break;
+ }
+
+ return rv;
+}
+
+nsEventStatus GestureEventListener::HandleInputTouchCancel() {
+ mSingleTapSent = Nothing();
+ SetState(GESTURE_NONE);
+ CancelMaxTapTimeoutTask();
+ CancelLongTapTimeoutTask();
+ return nsEventStatus_eIgnore;
+}
+
+void GestureEventListener::HandleInputTimeoutLongTap() {
+ GEL_LOG("Running long-tap timeout task in state %d\n", mState);
+
+ mLongTapTimeoutTask = nullptr;
+
+ switch (mState) {
+ case GESTURE_FIRST_SINGLE_TOUCH_DOWN:
+ // just in case MaxTapTime > ContextMenuDelay cancel MaxTap timer
+ // and fall through
+ CancelMaxTapTimeoutTask();
+ [[fallthrough]];
+ case GESTURE_FIRST_SINGLE_TOUCH_MAX_TAP_DOWN: {
+ SetState(GESTURE_LONG_TOUCH_DOWN);
+ mAsyncPanZoomController->HandleGestureEvent(
+ CreateTapEvent(mLastTouchInput, TapGestureInput::TAPGESTURE_LONG));
+ break;
+ }
+ default:
+ NS_WARNING("Unhandled state upon long tap timeout");
+ SetState(GESTURE_NONE);
+ break;
+ }
+}
+
+void GestureEventListener::HandleInputTimeoutMaxTap(bool aDuringFastFling) {
+ GEL_LOG("Running max-tap timeout task in state %d\n", mState);
+
+ mMaxTapTimeoutTask = nullptr;
+
+ if (mState == GESTURE_FIRST_SINGLE_TOUCH_DOWN) {
+ SetState(GESTURE_FIRST_SINGLE_TOUCH_MAX_TAP_DOWN);
+ } else if (mState == GESTURE_FIRST_SINGLE_TOUCH_UP ||
+ mState == GESTURE_SECOND_SINGLE_TOUCH_DOWN) {
+ MOZ_ASSERT(mSingleTapSent.isSome());
+ if (!aDuringFastFling && !mSingleTapSent.value()) {
+ TriggerSingleTapConfirmedEvent();
+ }
+ mSingleTapSent = Nothing();
+ SetState(GESTURE_NONE);
+ } else {
+ NS_WARNING("Unhandled state upon MAX_TAP timeout");
+ SetState(GESTURE_NONE);
+ }
+}
+
+void GestureEventListener::TriggerSingleTapConfirmedEvent() {
+ mAsyncPanZoomController->HandleGestureEvent(
+ CreateTapEvent(mLastTapInput, TapGestureInput::TAPGESTURE_CONFIRMED));
+}
+
+void GestureEventListener::SetState(GestureState aState) {
+ mState = aState;
+
+ if (mState == GESTURE_NONE) {
+ mSpanChange = 0.0f;
+ mPreviousSpan = 0.0f;
+ mFocusChange = 0.0f;
+ } else if (mState == GESTURE_MULTI_TOUCH_DOWN) {
+ mPreviousSpan = GetCurrentSpan(mLastTouchInput);
+ mPreviousFocus = GetCurrentFocus(mLastTouchInput);
+ }
+}
+
+void GestureEventListener::CancelLongTapTimeoutTask() {
+ if (mState == GESTURE_SECOND_SINGLE_TOUCH_DOWN) {
+ // being in this state means the task has been canceled already
+ return;
+ }
+
+ if (mLongTapTimeoutTask) {
+ mLongTapTimeoutTask->Cancel();
+ mLongTapTimeoutTask = nullptr;
+ }
+}
+
+void GestureEventListener::CreateLongTapTimeoutTask() {
+ RefPtr<CancelableRunnable> task = NewCancelableRunnableMethod(
+ "layers::GestureEventListener::HandleInputTimeoutLongTap", this,
+ &GestureEventListener::HandleInputTimeoutLongTap);
+
+ mLongTapTimeoutTask = task;
+
+ TouchBlockState* block =
+ mAsyncPanZoomController->GetInputQueue()->GetCurrentTouchBlock();
+ MOZ_ASSERT(block);
+ long alreadyElapsed =
+ static_cast<long>(block->GetTimeSinceBlockStart().ToMilliseconds());
+ long remainingDelay =
+ StaticPrefs::ui_click_hold_context_menus_delay() - alreadyElapsed;
+ mAsyncPanZoomController->PostDelayedTask(task.forget(),
+ std::max(0L, remainingDelay));
+}
+
+void GestureEventListener::CancelMaxTapTimeoutTask() {
+ if (mState == GESTURE_FIRST_SINGLE_TOUCH_MAX_TAP_DOWN) {
+ // being in this state means the timer has just been triggered
+ return;
+ }
+
+ if (mMaxTapTimeoutTask) {
+ mMaxTapTimeoutTask->Cancel();
+ mMaxTapTimeoutTask = nullptr;
+ }
+}
+
+void GestureEventListener::CreateMaxTapTimeoutTask() {
+ mLastTapInput = mLastTouchInput;
+
+ TouchBlockState* block =
+ mAsyncPanZoomController->GetInputQueue()->GetCurrentTouchBlock();
+ MOZ_ASSERT(block);
+ RefPtr<CancelableRunnable> task = NewCancelableRunnableMethod<bool>(
+ "layers::GestureEventListener::HandleInputTimeoutMaxTap", this,
+ &GestureEventListener::HandleInputTimeoutMaxTap,
+ block->IsDuringFastFling());
+
+ mMaxTapTimeoutTask = task;
+
+ long alreadyElapsed =
+ static_cast<long>(block->GetTimeSinceBlockStart().ToMilliseconds());
+ long remainingDelay = StaticPrefs::apz_max_tap_time() - alreadyElapsed;
+ mAsyncPanZoomController->PostDelayedTask(task.forget(),
+ std::max(0L, remainingDelay));
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/apz/src/GestureEventListener.h b/gfx/layers/apz/src/GestureEventListener.h
new file mode 100644
index 0000000000..aa51889fdd
--- /dev/null
+++ b/gfx/layers/apz/src/GestureEventListener.h
@@ -0,0 +1,285 @@
+/* -*- 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_GestureEventListener_h
+#define mozilla_layers_GestureEventListener_h
+
+#include "InputData.h" // for MultiTouchInput, etc
+#include "Units.h"
+#include "mozilla/EventForwards.h" // for nsEventStatus
+#include "mozilla/RefPtr.h" // for RefPtr
+#include "nsISupportsImpl.h"
+#include "nsTArray.h" // for nsTArray
+
+namespace mozilla {
+
+class CancelableRunnable;
+
+namespace layers {
+
+class AsyncPanZoomController;
+
+/**
+ * Platform-non-specific, generalized gesture event listener. This class
+ * intercepts all touches events on their way to AsyncPanZoomController and
+ * determines whether or not they are part of a gesture.
+ *
+ * For example, seeing that two fingers are on the screen means that the user
+ * wants to do a pinch gesture, so we don't forward the touches along to
+ * AsyncPanZoomController since it will think that they are just trying to pan
+ * the screen. Instead, we generate a PinchGestureInput and send that. If the
+ * touch event is not part of a gesture, we just return nsEventStatus_eIgnore
+ * and AsyncPanZoomController is expected to handle it.
+ */
+class GestureEventListener final {
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(GestureEventListener)
+
+ explicit GestureEventListener(
+ AsyncPanZoomController* aAsyncPanZoomController);
+
+ // --------------------------------------------------------------------------
+ // These methods must only be called on the controller/UI thread.
+ //
+
+ /**
+ * General input handler for a touch event. If the touch event is not a part
+ * of a gesture, then we pass it along to AsyncPanZoomController. Otherwise,
+ * it gets consumed here and never forwarded along.
+ */
+ nsEventStatus HandleInputEvent(const MultiTouchInput& aEvent);
+
+ /**
+ * Returns the identifier of the touch in the last touch event processed by
+ * this GestureEventListener. This should only be called when the last touch
+ * event contained only one touch.
+ */
+ int32_t GetLastTouchIdentifier() const;
+
+ /**
+ * Function used to disable long tap gestures.
+ *
+ * On slow running tests, drags and touch events can be misinterpreted
+ * as a long tap. This allows tests to disable long tap gesture detection.
+ */
+ static void SetLongTapEnabled(bool aLongTapEnabled);
+ static bool IsLongTapEnabled();
+
+ private:
+ // Private destructor, to discourage deletion outside of Release():
+ ~GestureEventListener();
+
+ /**
+ * States of GEL finite-state machine.
+ */
+ enum GestureState {
+ // This is the initial and final state of any gesture.
+ // In this state there's no gesture going on, and we don't think we're
+ // about to enter one.
+ // Allowed next states: GESTURE_FIRST_SINGLE_TOUCH_DOWN,
+ // GESTURE_MULTI_TOUCH_DOWN.
+ GESTURE_NONE,
+
+ // A touch start with a single touch point has just happened.
+ // After having gotten into this state we start timers for MAX_TAP_TIME and
+ // StaticPrefs::ui_click_hold_context_menus_delay().
+ // Allowed next states: GESTURE_MULTI_TOUCH_DOWN, GESTURE_NONE,
+ // GESTURE_FIRST_SINGLE_TOUCH_UP,
+ // GESTURE_LONG_TOUCH_DOWN,
+ // GESTURE_FIRST_SINGLE_TOUCH_MAX_TAP_DOWN.
+ GESTURE_FIRST_SINGLE_TOUCH_DOWN,
+
+ // While in GESTURE_FIRST_SINGLE_TOUCH_DOWN state a MAX_TAP_TIME timer got
+ // triggered. Now we'll trigger either a single tap if a user lifts her
+ // finger or a long tap if StaticPrefs::ui_click_hold_context_menus_delay()
+ // happens first.
+ // Allowed next states: GESTURE_MULTI_TOUCH_DOWN, GESTURE_NONE,
+ // GESTURE_LONG_TOUCH_DOWN.
+ GESTURE_FIRST_SINGLE_TOUCH_MAX_TAP_DOWN,
+
+ // A user put her finger down and lifted it up quickly enough.
+ // After having gotten into this state we clear the timer for MAX_TAP_TIME.
+ // Allowed next states: GESTURE_SECOND_SINGLE_TOUCH_DOWN, GESTURE_NONE,
+ // GESTURE_MULTI_TOUCH_DOWN.
+ GESTURE_FIRST_SINGLE_TOUCH_UP,
+
+ // A user put down her finger again right after a single tap thus the
+ // gesture can't be a single tap, but rather a double tap. But we're
+ // still not sure about that until the user lifts her finger again.
+ // Allowed next states: GESTURE_MULTI_TOUCH_DOWN, GESTURE_ONE_TOUCH_PINCH,
+ // GESTURE_NONE.
+ GESTURE_SECOND_SINGLE_TOUCH_DOWN,
+
+ // A long touch has happened, but the user still keeps her finger down.
+ // We'll trigger a "long tap up" event when the finger is up.
+ // Allowed next states: GESTURE_NONE, GESTURE_MULTI_TOUCH_DOWN.
+ GESTURE_LONG_TOUCH_DOWN,
+
+ // We have detected that two or more fingers are on the screen, but there
+ // hasn't been enough movement yet to make us start actually zooming the
+ // screen.
+ // Allowed next states: GESTURE_PINCH, GESTURE_NONE
+ GESTURE_MULTI_TOUCH_DOWN,
+
+ // There are two or more fingers on the screen, and the user has already
+ // pinched enough for us to start zooming the screen.
+ // Allowed next states: GESTURE_NONE
+ GESTURE_PINCH,
+
+ // The user has double tapped, but not lifted her finger, and moved her
+ // finger more than PINCH_START_THRESHOLD.
+ // Allowed next states: GESTURE_NONE.
+ GESTURE_ONE_TOUCH_PINCH
+ };
+
+ /**
+ * These HandleInput* functions comprise input alphabet of the GEL
+ * finite-state machine triggering state transitions.
+ */
+ nsEventStatus HandleInputTouchSingleStart();
+ nsEventStatus HandleInputTouchMultiStart();
+ nsEventStatus HandleInputTouchEnd();
+ nsEventStatus HandleInputTouchMove();
+ nsEventStatus HandleInputTouchCancel();
+ void HandleInputTimeoutLongTap();
+ void HandleInputTimeoutMaxTap(bool aDuringFastFling);
+
+ void EnterFirstSingleTouchDown();
+
+ void TriggerSingleTapConfirmedEvent();
+
+ bool MoveDistanceExceeds(ScreenCoord aThreshold) const;
+ bool MoveDistanceIsLarge() const;
+ bool SecondTapIsFar() const;
+
+ /**
+ * Returns current vertical span, counting from the where the gesture first
+ * began (after a brief delay detecting the gesture from first touch).
+ */
+ ScreenCoord GetYSpanFromGestureStartPoint();
+
+ /**
+ * Do actual state transition and reset substates.
+ */
+ void SetState(GestureState aState);
+
+ RefPtr<AsyncPanZoomController> mAsyncPanZoomController;
+
+ /**
+ * Array containing all active touches. When a touch happens it, gets added to
+ * this array, even if we choose not to handle it. When it ends, we remove it.
+ * We need to maintain this array in order to detect the end of the
+ * "multitouch" states because touch start events contain all current touches,
+ * but touch end events contain only those touches that have gone.
+ */
+ nsTArray<SingleTouchData> mTouches;
+
+ /**
+ * Current state we're dealing with.
+ */
+ GestureState mState;
+
+ /**
+ * Total change in span since we detected a pinch gesture. Only used when we
+ * are in the |GESTURE_WAITING_PINCH| state and need to know how far zoomed
+ * out we are compared to our original pinch span. Note that this does _not_
+ * continue to be updated once we jump into the |GESTURE_PINCH| state.
+ */
+ ScreenCoord mSpanChange;
+
+ /**
+ * Previous span calculated for the purposes of setting inside a
+ * PinchGestureInput.
+ */
+ ScreenCoord mPreviousSpan;
+
+ /* Properties similar to mSpanChange and mPreviousSpan, but for the focus */
+ ScreenCoord mFocusChange;
+ ScreenPoint mPreviousFocus;
+
+ /**
+ * Cached copy of the last touch input.
+ */
+ MultiTouchInput mLastTouchInput;
+
+ /**
+ * Cached copy of the last tap gesture input.
+ * In the situation when we have a tap followed by a pinch we lose info
+ * about tap since we keep only last input and to dispatch it correctly
+ * we save last tap copy into this variable.
+ * For more info see bug 947892.
+ */
+ MultiTouchInput mLastTapInput;
+
+ /**
+ * Position of the last touch that exceeds the GetTouchStartTolerance when
+ * performing a one-touch-pinch gesture; using the mTouchStartPosition is
+ * slightly inaccurate because by the time the touch position has passed
+ * the threshold for the gesture, there is already a span that the zoom
+ * is calculated from, instead of starting at 1.0 when the threshold gets
+ * passed.
+ */
+ ScreenPoint mOneTouchPinchStartPosition;
+
+ /**
+ * Position of the last touch starting. This is only valid during an attempt
+ * to determine if a touch is a tap. If a touch point moves away from
+ * mTouchStartPosition to the distance greater than
+ * AsyncPanZoomController::GetTouchStartTolerance() while in
+ * GESTURE_FIRST_SINGLE_TOUCH_DOWN, GESTURE_FIRST_SINGLE_TOUCH_MAX_TAP_DOWN
+ * or GESTURE_SECOND_SINGLE_TOUCH_DOWN then we're certain the gesture is
+ * not tap.
+ */
+ ScreenPoint mTouchStartPosition;
+
+ /**
+ * We store the window/GeckoView's display offset as well, so we can
+ * track the user's physical touch movements in absolute display coordinates.
+ */
+ ExternalPoint mTouchStartOffset;
+
+ /**
+ * Task used to timeout a long tap. This gets posted to the UI thread such
+ * that it runs a time when a single tap happens. We cache it so that
+ * we can cancel it if any other touch event happens.
+ *
+ * The task is supposed to be non-null if in GESTURE_FIRST_SINGLE_TOUCH_DOWN
+ * and GESTURE_FIRST_SINGLE_TOUCH_MAX_TAP_DOWN states.
+ *
+ * CancelLongTapTimeoutTask: Cancel the mLongTapTimeoutTask and also set
+ * it to null.
+ */
+ RefPtr<CancelableRunnable> mLongTapTimeoutTask;
+ void CancelLongTapTimeoutTask();
+ void CreateLongTapTimeoutTask();
+
+ /**
+ * Task used to timeout a single tap or a double tap.
+ *
+ * The task is supposed to be non-null if in GESTURE_FIRST_SINGLE_TOUCH_DOWN,
+ * GESTURE_FIRST_SINGLE_TOUCH_UP and GESTURE_SECOND_SINGLE_TOUCH_DOWN states.
+ *
+ * CancelMaxTapTimeoutTask: Cancel the mMaxTapTimeoutTask and also set
+ * it to null.
+ */
+ RefPtr<CancelableRunnable> mMaxTapTimeoutTask;
+ void CancelMaxTapTimeoutTask();
+ void CreateMaxTapTimeoutTask();
+
+ /**
+ * Tracks whether the single-tap event was already sent to content. This is
+ * needed because it affects how the double-tap gesture, if detected, is
+ * handled. The value is only valid in states GESTURE_FIRST_SINGLE_TOUCH_UP
+ * and GESTURE_SECOND_SINGLE_TOUCH_DOWN; to more easily catch violations it is
+ * stored in a Maybe which is set to Nothing() at all other times.
+ */
+ Maybe<bool> mSingleTapSent;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif
diff --git a/gfx/layers/apz/src/HitTestingTreeNode.cpp b/gfx/layers/apz/src/HitTestingTreeNode.cpp
new file mode 100644
index 0000000000..d25a9c1d5f
--- /dev/null
+++ b/gfx/layers/apz/src/HitTestingTreeNode.cpp
@@ -0,0 +1,419 @@
+/* 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 "HitTestingTreeNode.h"
+#include <stack>
+
+#include "AsyncPanZoomController.h" // for AsyncPanZoomController
+#include "mozilla/StaticPrefs_layout.h"
+#include "mozilla/gfx/Point.h" // for Point4D
+#include "mozilla/layers/APZUtils.h" // for AsyncTransform, CompleteAsyncTransform
+#include "mozilla/layers/AsyncDragMetrics.h" // for AsyncDragMetrics
+#include "mozilla/ToString.h" // for ToString
+#include "nsPrintfCString.h" // for nsPrintfCString
+#include "UnitTransforms.h" // for ViewAs
+
+static mozilla::LazyLogModule sApzMgrLog("apz.manager");
+
+namespace mozilla {
+namespace layers {
+
+using gfx::CompositorHitTestInfo;
+
+HitTestingTreeNode::HitTestingTreeNode(AsyncPanZoomController* aApzc,
+ bool aIsPrimaryHolder,
+ LayersId aLayersId)
+ : mApzc(aApzc),
+ mIsPrimaryApzcHolder(aIsPrimaryHolder),
+ mLockCount(0),
+ mLayersId(aLayersId),
+ mFixedPosTarget(ScrollableLayerGuid::NULL_SCROLL_ID),
+ mStickyPosTarget(ScrollableLayerGuid::NULL_SCROLL_ID),
+ mOverride(EventRegionsOverride::NoOverride) {
+ if (mIsPrimaryApzcHolder) {
+ MOZ_ASSERT(mApzc);
+ }
+ MOZ_ASSERT(!mApzc || mApzc->GetLayersId() == mLayersId);
+}
+
+void HitTestingTreeNode::RecycleWith(
+ const RecursiveMutexAutoLock& aProofOfTreeLock,
+ AsyncPanZoomController* aApzc, LayersId aLayersId) {
+ MOZ_ASSERT(IsRecyclable(aProofOfTreeLock));
+ Destroy(); // clear out tree pointers
+ mApzc = aApzc;
+ mLayersId = aLayersId;
+ MOZ_ASSERT(!mApzc || mApzc->GetLayersId() == mLayersId);
+ // The caller is expected to call appropriate setters (SetHitTestData,
+ // SetScrollbarData, SetFixedPosData, SetStickyPosData, etc.) to repopulate
+ // all the data fields before using this node for "real work". Otherwise
+ // those data fields may contain stale information from the previous use
+ // of this node object.
+}
+
+HitTestingTreeNode::~HitTestingTreeNode() = default;
+
+void HitTestingTreeNode::Destroy() {
+ // This runs on the updater thread, it's not worth passing around extra raw
+ // pointers just to assert it.
+
+ mPrevSibling = nullptr;
+ mLastChild = nullptr;
+ mParent = nullptr;
+
+ if (mApzc) {
+ if (mIsPrimaryApzcHolder) {
+ mApzc->Destroy();
+ }
+ mApzc = nullptr;
+ }
+}
+
+bool HitTestingTreeNode::IsRecyclable(
+ const RecursiveMutexAutoLock& aProofOfTreeLock) {
+ return !(IsPrimaryHolder() || (mLockCount > 0));
+}
+
+void HitTestingTreeNode::SetLastChild(HitTestingTreeNode* aChild) {
+ mLastChild = aChild;
+ if (aChild) {
+ aChild->mParent = this;
+
+ if (aChild->GetApzc()) {
+ AsyncPanZoomController* parent = GetNearestContainingApzc();
+ // We assume that HitTestingTreeNodes with an ancestor/descendant
+ // relationship cannot both point to the same APZC instance. This
+ // assertion only covers a subset of cases in which that might occur,
+ // but it's better than nothing.
+ MOZ_ASSERT(aChild->GetApzc() != parent);
+ aChild->SetApzcParent(parent);
+ }
+ }
+}
+
+void HitTestingTreeNode::SetScrollbarData(
+ const Maybe<uint64_t>& aScrollbarAnimationId,
+ const ScrollbarData& aScrollbarData) {
+ mScrollbarAnimationId = aScrollbarAnimationId;
+ mScrollbarData = aScrollbarData;
+}
+
+bool HitTestingTreeNode::MatchesScrollDragMetrics(
+ const AsyncDragMetrics& aDragMetrics, LayersId aLayersId) const {
+ return IsScrollThumbNode() && mLayersId == aLayersId &&
+ mScrollbarData.mDirection == aDragMetrics.mDirection &&
+ mScrollbarData.mTargetViewId == aDragMetrics.mViewId;
+}
+
+bool HitTestingTreeNode::IsScrollThumbNode() const {
+ return mScrollbarData.mScrollbarLayerType ==
+ layers::ScrollbarLayerType::Thumb;
+}
+
+bool HitTestingTreeNode::IsScrollbarNode() const {
+ return mScrollbarData.mScrollbarLayerType != layers::ScrollbarLayerType::None;
+}
+
+bool HitTestingTreeNode::IsScrollbarContainerNode() const {
+ return mScrollbarData.mScrollbarLayerType ==
+ layers::ScrollbarLayerType::Container;
+}
+
+ScrollDirection HitTestingTreeNode::GetScrollbarDirection() const {
+ MOZ_ASSERT(IsScrollbarNode());
+ MOZ_ASSERT(mScrollbarData.mDirection.isSome());
+ return *mScrollbarData.mDirection;
+}
+
+ScrollableLayerGuid::ViewID HitTestingTreeNode::GetScrollTargetId() const {
+ return mScrollbarData.mTargetViewId;
+}
+
+Maybe<uint64_t> HitTestingTreeNode::GetScrollbarAnimationId() const {
+ return mScrollbarAnimationId;
+}
+
+const ScrollbarData& HitTestingTreeNode::GetScrollbarData() const {
+ return mScrollbarData;
+}
+
+void HitTestingTreeNode::SetFixedPosData(
+ ScrollableLayerGuid::ViewID aFixedPosTarget, SideBits aFixedPosSides,
+ const Maybe<uint64_t>& aFixedPositionAnimationId) {
+ mFixedPosTarget = aFixedPosTarget;
+ mFixedPosSides = aFixedPosSides;
+ mFixedPositionAnimationId = aFixedPositionAnimationId;
+}
+
+ScrollableLayerGuid::ViewID HitTestingTreeNode::GetFixedPosTarget() const {
+ return mFixedPosTarget;
+}
+
+SideBits HitTestingTreeNode::GetFixedPosSides() const { return mFixedPosSides; }
+
+Maybe<uint64_t> HitTestingTreeNode::GetFixedPositionAnimationId() const {
+ return mFixedPositionAnimationId;
+}
+
+void HitTestingTreeNode::SetPrevSibling(HitTestingTreeNode* aSibling) {
+ mPrevSibling = aSibling;
+ if (aSibling) {
+ aSibling->mParent = mParent;
+
+ if (aSibling->GetApzc()) {
+ AsyncPanZoomController* parent =
+ mParent ? mParent->GetNearestContainingApzc() : nullptr;
+ aSibling->SetApzcParent(parent);
+ }
+ }
+}
+
+void HitTestingTreeNode::SetStickyPosData(
+ ScrollableLayerGuid::ViewID aStickyPosTarget,
+ const LayerRectAbsolute& aScrollRangeOuter,
+ const LayerRectAbsolute& aScrollRangeInner,
+ const Maybe<uint64_t>& aStickyPositionAnimationId) {
+ mStickyPosTarget = aStickyPosTarget;
+ mStickyScrollRangeOuter = aScrollRangeOuter;
+ mStickyScrollRangeInner = aScrollRangeInner;
+ mStickyPositionAnimationId = aStickyPositionAnimationId;
+}
+
+ScrollableLayerGuid::ViewID HitTestingTreeNode::GetStickyPosTarget() const {
+ return mStickyPosTarget;
+}
+
+const LayerRectAbsolute& HitTestingTreeNode::GetStickyScrollRangeOuter() const {
+ return mStickyScrollRangeOuter;
+}
+
+const LayerRectAbsolute& HitTestingTreeNode::GetStickyScrollRangeInner() const {
+ return mStickyScrollRangeInner;
+}
+
+Maybe<uint64_t> HitTestingTreeNode::GetStickyPositionAnimationId() const {
+ return mStickyPositionAnimationId;
+}
+
+void HitTestingTreeNode::MakeRoot() {
+ mParent = nullptr;
+
+ if (GetApzc()) {
+ SetApzcParent(nullptr);
+ }
+}
+
+HitTestingTreeNode* HitTestingTreeNode::GetFirstChild() const {
+ HitTestingTreeNode* child = GetLastChild();
+ while (child && child->GetPrevSibling()) {
+ child = child->GetPrevSibling();
+ }
+ return child;
+}
+
+HitTestingTreeNode* HitTestingTreeNode::GetLastChild() const {
+ return mLastChild;
+}
+
+HitTestingTreeNode* HitTestingTreeNode::GetPrevSibling() const {
+ return mPrevSibling;
+}
+
+HitTestingTreeNode* HitTestingTreeNode::GetParent() const { return mParent; }
+
+bool HitTestingTreeNode::IsAncestorOf(const HitTestingTreeNode* aOther) const {
+ for (const HitTestingTreeNode* cur = aOther; cur; cur = cur->GetParent()) {
+ if (cur == this) {
+ return true;
+ }
+ }
+ return false;
+}
+
+AsyncPanZoomController* HitTestingTreeNode::GetApzc() const { return mApzc; }
+
+AsyncPanZoomController* HitTestingTreeNode::GetNearestContainingApzc() const {
+ for (const HitTestingTreeNode* n = this; n; n = n->GetParent()) {
+ if (n->GetApzc()) {
+ return n->GetApzc();
+ }
+ }
+ return nullptr;
+}
+
+bool HitTestingTreeNode::IsPrimaryHolder() const {
+ return mIsPrimaryApzcHolder;
+}
+
+LayersId HitTestingTreeNode::GetLayersId() const { return mLayersId; }
+
+void HitTestingTreeNode::SetHitTestData(
+ const LayerIntRegion& aVisibleRegion,
+ const LayerIntSize& aRemoteDocumentSize,
+ const CSSTransformMatrix& aTransform, const EventRegionsOverride& aOverride,
+ const Maybe<ScrollableLayerGuid::ViewID>& aAsyncZoomContainerId) {
+ mVisibleRegion = aVisibleRegion;
+ mRemoteDocumentSize = aRemoteDocumentSize;
+ mTransform = aTransform;
+ mOverride = aOverride;
+ mAsyncZoomContainerId = aAsyncZoomContainerId;
+}
+
+EventRegionsOverride HitTestingTreeNode::GetEventRegionsOverride() const {
+ return mOverride;
+}
+
+const CSSTransformMatrix& HitTestingTreeNode::GetTransform() const {
+ return mTransform;
+}
+
+LayerToScreenMatrix4x4 HitTestingTreeNode::GetTransformToGecko() const {
+ if (mParent) {
+ LayerToParentLayerMatrix4x4 thisToParent =
+ mTransform * AsyncTransformMatrix();
+ if (mApzc) {
+ thisToParent =
+ thisToParent * ViewAs<ParentLayerToParentLayerMatrix4x4>(
+ mApzc->GetTransformToLastDispatchedPaint());
+ }
+ ParentLayerToScreenMatrix4x4 parentToRoot =
+ ViewAs<ParentLayerToScreenMatrix4x4>(
+ mParent->GetTransformToGecko(),
+ PixelCastJustification::MovingDownToChildren);
+ return thisToParent * parentToRoot;
+ }
+
+ return ViewAs<LayerToScreenMatrix4x4>(
+ mTransform * AsyncTransformMatrix(),
+ PixelCastJustification::ScreenIsParentLayerForRoot);
+}
+
+const LayerIntRegion& HitTestingTreeNode::GetVisibleRegion() const {
+ return mVisibleRegion;
+}
+
+ScreenRect HitTestingTreeNode::GetRemoteDocumentScreenRect() const {
+ ScreenRect result = TransformBy(
+ GetTransformToGecko(),
+ IntRectToRect(LayerIntRect(LayerIntPoint(), mRemoteDocumentSize)));
+
+ for (const HitTestingTreeNode* node = this; node; node = node->GetParent()) {
+ if (!node->GetApzc()) {
+ continue;
+ }
+
+ ParentLayerRect compositionBounds = node->GetApzc()->GetCompositionBounds();
+ if (compositionBounds.IsEmpty()) {
+ return ScreenRect();
+ }
+
+ ScreenRect scrollPortOnScreenCoordinate = TransformBy(
+ node->GetParent() ? node->GetParent()->GetTransformToGecko()
+ : LayerToScreenMatrix4x4(),
+ ViewAs<LayerPixel>(compositionBounds,
+ PixelCastJustification::MovingDownToChildren));
+ if (scrollPortOnScreenCoordinate.IsEmpty()) {
+ return ScreenRect();
+ }
+
+ result = result.Intersect(scrollPortOnScreenCoordinate);
+ if (result.IsEmpty()) {
+ return ScreenRect();
+ }
+ }
+ return result;
+}
+
+Maybe<ScrollableLayerGuid::ViewID> HitTestingTreeNode::GetAsyncZoomContainerId()
+ const {
+ return mAsyncZoomContainerId;
+}
+
+void HitTestingTreeNode::Dump(const char* aPrefix) const {
+ MOZ_LOG(
+ sApzMgrLog, LogLevel::Debug,
+ ("%sHitTestingTreeNode (%p) APZC (%p) g=(%s) %s%s%s t=(%s) "
+ "%s%s\n",
+ aPrefix, this, mApzc.get(),
+ mApzc ? ToString(mApzc->GetGuid()).c_str()
+ : nsPrintfCString("l=0x%" PRIx64, uint64_t(mLayersId)).get(),
+ (mOverride & EventRegionsOverride::ForceDispatchToContent) ? "fdtc "
+ : "",
+ (mOverride & EventRegionsOverride::ForceEmptyHitRegion) ? "fehr " : "",
+ (mFixedPosTarget != ScrollableLayerGuid::NULL_SCROLL_ID)
+ ? nsPrintfCString("fixed=%" PRIu64 " ", mFixedPosTarget).get()
+ : "",
+ ToString(mTransform).c_str(),
+ mScrollbarData.mDirection.isSome() ? " scrollbar" : "",
+ IsScrollThumbNode() ? " scrollthumb" : ""));
+
+ if (!mLastChild) {
+ return;
+ }
+
+ // Dump the children in order from first child to last child
+ std::stack<HitTestingTreeNode*> children;
+ for (HitTestingTreeNode* child = mLastChild.get(); child;
+ child = child->mPrevSibling) {
+ children.push(child);
+ }
+ nsPrintfCString childPrefix("%s ", aPrefix);
+ while (!children.empty()) {
+ children.top()->Dump(childPrefix.get());
+ children.pop();
+ }
+}
+
+void HitTestingTreeNode::SetApzcParent(AsyncPanZoomController* aParent) {
+ // precondition: GetApzc() is non-null
+ MOZ_ASSERT(GetApzc() != nullptr);
+ if (IsPrimaryHolder()) {
+ GetApzc()->SetParent(aParent);
+ } else {
+ MOZ_ASSERT(GetApzc()->GetParent() == aParent);
+ }
+}
+
+void HitTestingTreeNode::Lock(const RecursiveMutexAutoLock& aProofOfTreeLock) {
+ mLockCount++;
+}
+
+void HitTestingTreeNode::Unlock(
+ const RecursiveMutexAutoLock& aProofOfTreeLock) {
+ MOZ_ASSERT(mLockCount > 0);
+ mLockCount--;
+}
+
+HitTestingTreeNodeAutoLock::HitTestingTreeNodeAutoLock()
+ : mTreeMutex(nullptr) {}
+
+HitTestingTreeNodeAutoLock::~HitTestingTreeNodeAutoLock() { Clear(); }
+
+void HitTestingTreeNodeAutoLock::Initialize(
+ const RecursiveMutexAutoLock& aProofOfTreeLock,
+ already_AddRefed<HitTestingTreeNode> aNode, RecursiveMutex& aTreeMutex) {
+ MOZ_ASSERT(!mNode);
+
+ mNode = aNode;
+ mTreeMutex = &aTreeMutex;
+
+ mNode->Lock(aProofOfTreeLock);
+}
+
+void HitTestingTreeNodeAutoLock::Clear() {
+ if (!mNode) {
+ return;
+ }
+ MOZ_ASSERT(mTreeMutex);
+
+ { // scope lock
+ RecursiveMutexAutoLock lock(*mTreeMutex);
+ mNode->Unlock(lock);
+ }
+ mNode = nullptr;
+ mTreeMutex = nullptr;
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/apz/src/HitTestingTreeNode.h b/gfx/layers/apz/src/HitTestingTreeNode.h
new file mode 100644
index 0000000000..ed20f9f561
--- /dev/null
+++ b/gfx/layers/apz/src/HitTestingTreeNode.h
@@ -0,0 +1,270 @@
+/* -*- 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_HitTestingTreeNode_h
+#define mozilla_layers_HitTestingTreeNode_h
+
+#include "mozilla/gfx/CompositorHitTestInfo.h"
+#include "mozilla/gfx/Matrix.h" // for Matrix4x4
+#include "mozilla/layers/LayersTypes.h" // for EventRegions
+#include "mozilla/layers/ScrollableLayerGuid.h" // for ScrollableLayerGuid
+#include "mozilla/layers/ScrollbarData.h" // for ScrollbarData
+#include "mozilla/Maybe.h" // for Maybe
+#include "mozilla/RecursiveMutex.h" // for RecursiveMutexAutoLock
+#include "mozilla/RefPtr.h" // for nsRefPtr
+namespace mozilla {
+namespace layers {
+
+class AsyncDragMetrics;
+class AsyncPanZoomController;
+
+/**
+ * This class represents a node in a tree that is used by the APZCTreeManager
+ * to do hit testing. The tree is roughly a copy of the layer tree, but will
+ * contain multiple nodes in cases where the layer has multiple FrameMetrics.
+ * In other words, the structure of this tree should be identical to the
+ * WebRenderScrollDataWrapper tree (see documentation in
+ * WebRenderScrollDataWrapper.h).
+ *
+ * Not all HitTestingTreeNode instances will have an APZC associated with them;
+ * only HitTestingTreeNodes that correspond to layers with scrollable metrics
+ * have APZCs.
+ * Multiple HitTestingTreeNode instances may share the same underlying APZC
+ * instance if the layers they represent share the same scrollable metrics (i.e.
+ * are part of the same animated geometry root). If this happens, exactly one of
+ * the HitTestingTreeNode instances will be designated as the "primary holder"
+ * of the APZC. When this primary holder is destroyed, it will destroy the APZC
+ * along with it; in contrast, destroying non-primary-holder nodes will not
+ * destroy the APZC.
+ * Code should not make assumptions about which of the nodes will be the
+ * primary holder, only that that there will be exactly one for each APZC in
+ * the tree.
+ *
+ * The reason this tree exists at all is so that we can do hit-testing on the
+ * thread that we receive input on (referred to the as the controller thread in
+ * APZ terminology), which may be different from the compositor thread.
+ * Accessing the compositor layer tree can only be done on the compositor
+ * thread, and so it is simpler to make a copy of the hit-testing related
+ * properties into a separate tree.
+ *
+ * The tree pointers on the node (mLastChild, etc.) can only be manipulated
+ * while holding the APZ tree lock. Any code that wishes to use a
+ * HitTestingTreeNode outside of holding the tree lock should do so by using
+ * the HitTestingTreeNodeAutoLock wrapper, which prevents the node from
+ * being recycled (and also holds a RefPtr to the node to prevent it from
+ * getting freed).
+ */
+class HitTestingTreeNode {
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(HitTestingTreeNode);
+
+ private:
+ ~HitTestingTreeNode();
+
+ public:
+ HitTestingTreeNode(AsyncPanZoomController* aApzc, bool aIsPrimaryHolder,
+ LayersId aLayersId);
+ void RecycleWith(const RecursiveMutexAutoLock& aProofOfTreeLock,
+ AsyncPanZoomController* aApzc, LayersId aLayersId);
+ // Clears the tree pointers on the node, thereby breaking RefPtr cycles. This
+ // can trigger free'ing of this and other HitTestingTreeNode instances.
+ void Destroy();
+
+ // Returns true if and only if the node is available for recycling as part
+ // of a hit-testing tree update. Note that this node can have Destroy() called
+ // on it whether or not it is recyclable.
+ bool IsRecyclable(const RecursiveMutexAutoLock& aProofOfTreeLock);
+
+ /* Tree construction methods */
+
+ void SetLastChild(HitTestingTreeNode* aChild);
+ void SetPrevSibling(HitTestingTreeNode* aSibling);
+ void MakeRoot();
+
+ /* Tree walking methods. GetFirstChild is O(n) in the number of children. The
+ * other tree walking methods are all O(1). */
+
+ HitTestingTreeNode* GetFirstChild() const;
+ HitTestingTreeNode* GetLastChild() const;
+ HitTestingTreeNode* GetPrevSibling() const;
+ HitTestingTreeNode* GetParent() const;
+
+ bool IsAncestorOf(const HitTestingTreeNode* aOther) const;
+
+ /* APZC related methods */
+
+ AsyncPanZoomController* GetApzc() const;
+ AsyncPanZoomController* GetNearestContainingApzc() const;
+ bool IsPrimaryHolder() const;
+ LayersId GetLayersId() const;
+
+ /* Hit test related methods */
+
+ void SetHitTestData(
+ const LayerIntRegion& aVisibleRegion,
+ const LayerIntSize& aRemoteDocumentSize,
+ const CSSTransformMatrix& aTransform,
+ const EventRegionsOverride& aOverride,
+ const Maybe<ScrollableLayerGuid::ViewID>& aAsyncZoomContainerId);
+
+ /* Scrollbar info */
+
+ void SetScrollbarData(const Maybe<uint64_t>& aScrollbarAnimationId,
+ const ScrollbarData& aScrollbarData);
+ bool MatchesScrollDragMetrics(const AsyncDragMetrics& aDragMetrics,
+ LayersId aLayersId) const;
+ bool IsScrollbarNode() const; // Scroll thumb or scrollbar container layer.
+ bool IsScrollbarContainerNode() const; // Scrollbar container layer.
+ // This can only be called if IsScrollbarNode() is true
+ ScrollDirection GetScrollbarDirection() const;
+ bool IsScrollThumbNode() const; // Scroll thumb container layer.
+ ScrollableLayerGuid::ViewID GetScrollTargetId() const;
+ const ScrollbarData& GetScrollbarData() const;
+ Maybe<uint64_t> GetScrollbarAnimationId() const;
+
+ /* Fixed pos info */
+
+ void SetFixedPosData(ScrollableLayerGuid::ViewID aFixedPosTarget,
+ SideBits aFixedPosSides,
+ const Maybe<uint64_t>& aFixedPositionAnimationId);
+ ScrollableLayerGuid::ViewID GetFixedPosTarget() const;
+ SideBits GetFixedPosSides() const;
+ Maybe<uint64_t> GetFixedPositionAnimationId() const;
+
+ /* Sticky pos info */
+ void SetStickyPosData(ScrollableLayerGuid::ViewID aStickyPosTarget,
+ const LayerRectAbsolute& aScrollRangeOuter,
+ const LayerRectAbsolute& aScrollRangeInner,
+ const Maybe<uint64_t>& aStickyPositionAnimationId);
+ ScrollableLayerGuid::ViewID GetStickyPosTarget() const;
+ const LayerRectAbsolute& GetStickyScrollRangeOuter() const;
+ const LayerRectAbsolute& GetStickyScrollRangeInner() const;
+ Maybe<uint64_t> GetStickyPositionAnimationId() const;
+
+ /* Returns the mOverride flag. */
+ EventRegionsOverride GetEventRegionsOverride() const;
+ const CSSTransformMatrix& GetTransform() const;
+ /* This is similar to APZCTreeManager::GetApzcToGeckoTransform but without
+ * the async bits. It's used on the main-thread for transforming coordinates
+ * across a BrowserParent/BrowserChild interface.*/
+ LayerToScreenMatrix4x4 GetTransformToGecko() const;
+ const LayerIntRegion& GetVisibleRegion() const;
+
+ /* Returns the screen coordinate rectangle of remote iframe corresponding to
+ * this node. The rectangle is the result of clipped by ancestor async
+ * scrolling. */
+ ScreenRect GetRemoteDocumentScreenRect() const;
+
+ Maybe<ScrollableLayerGuid::ViewID> GetAsyncZoomContainerId() const;
+
+ /* Debug helpers */
+ void Dump(const char* aPrefix = "") const;
+
+ private:
+ friend class HitTestingTreeNodeAutoLock;
+ // Functions that are private but called from HitTestingTreeNodeAutoLock
+ void Lock(const RecursiveMutexAutoLock& aProofOfTreeLock);
+ void Unlock(const RecursiveMutexAutoLock& aProofOfTreeLock);
+
+ void SetApzcParent(AsyncPanZoomController* aApzc);
+
+ RefPtr<HitTestingTreeNode> mLastChild;
+ RefPtr<HitTestingTreeNode> mPrevSibling;
+ RefPtr<HitTestingTreeNode> mParent;
+
+ RefPtr<AsyncPanZoomController> mApzc;
+ bool mIsPrimaryApzcHolder;
+ int mLockCount;
+
+ LayersId mLayersId;
+
+ // This is only set if WebRender is enabled, and only for HTTNs
+ // where IsScrollThumbNode() returns true. It holds the animation id that we
+ // use to move the thumb node to reflect async scrolling.
+ Maybe<uint64_t> mScrollbarAnimationId;
+
+ // This is set for scrollbar Container and Thumb layers.
+ ScrollbarData mScrollbarData;
+
+ // This is only set if WebRender is enabled. It holds the animation id that
+ // we use to adjust fixed position content for the toolbar.
+ Maybe<uint64_t> mFixedPositionAnimationId;
+
+ ScrollableLayerGuid::ViewID mFixedPosTarget;
+ SideBits mFixedPosSides;
+
+ ScrollableLayerGuid::ViewID mStickyPosTarget;
+ LayerRectAbsolute mStickyScrollRangeOuter;
+ LayerRectAbsolute mStickyScrollRangeInner;
+ // This is only set if WebRender is enabled. It holds the animation id that
+ // we use to adjust sticky position content for the toolbar.
+ Maybe<uint64_t> mStickyPositionAnimationId;
+
+ LayerIntRegion mVisibleRegion;
+
+ /* The size of remote iframe on the corresponding layer coordinate.
+ * It's empty if this node is not for remote iframe. */
+ LayerIntSize mRemoteDocumentSize;
+
+ /* This is the transform from layer L. This does NOT include any async
+ * transforms. */
+ CSSTransformMatrix mTransform;
+
+ /* If the layer is the async zoom container layer then this will hold the id.
+ */
+ Maybe<ScrollableLayerGuid::ViewID> mAsyncZoomContainerId;
+
+ /* Indicates whether or not the event regions on this node need to be
+ * overridden in a certain way. */
+ EventRegionsOverride mOverride;
+};
+
+/**
+ * A class that allows safe usage of a HitTestingTreeNode outside of the APZ
+ * tree lock. In general, this class should be Initialize()'d inside the tree
+ * lock (enforced by the proof-of-lock to Initialize), and then can be returned
+ * to a scope outside the tree lock and used safely. Upon destruction or
+ * Clear() being called, it unlocks the underlying node at which point it can
+ * be recycled or freed.
+ */
+class HitTestingTreeNodeAutoLock final {
+ public:
+ HitTestingTreeNodeAutoLock();
+ ~HitTestingTreeNodeAutoLock();
+ // Make it move-only. Note that the default implementations of the move
+ // constructor and assignment operator are correct: they'll call the
+ // move constructor of mNode, which will null out mNode on the moved-from
+ // object, and Clear() will early-exit when the moved-from object's
+ // destructor is called.
+ HitTestingTreeNodeAutoLock(HitTestingTreeNodeAutoLock&&) = default;
+ HitTestingTreeNodeAutoLock& operator=(HitTestingTreeNodeAutoLock&&) = default;
+
+ void Initialize(const RecursiveMutexAutoLock& aProofOfTreeLock,
+ already_AddRefed<HitTestingTreeNode> aNode,
+ RecursiveMutex& aTreeMutex);
+ void Clear();
+
+ // Convenience operators to simplify the using code.
+ explicit operator bool() const { return !!mNode; }
+ bool operator!() const { return !mNode; }
+ HitTestingTreeNode* operator->() const { return mNode.get(); }
+
+ // Allow getting back a raw pointer to the node, but only inside the scope
+ // of the tree lock. The caller is responsible for ensuring that they do not
+ // use the raw pointer outside that scope.
+ HitTestingTreeNode* Get(
+ mozilla::RecursiveMutexAutoLock& aProofOfTreeLock) const {
+ return mNode.get();
+ }
+
+ private:
+ RefPtr<HitTestingTreeNode> mNode;
+ RecursiveMutex* mTreeMutex;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif // mozilla_layers_HitTestingTreeNode_h
diff --git a/gfx/layers/apz/src/IAPZHitTester.cpp b/gfx/layers/apz/src/IAPZHitTester.cpp
new file mode 100644
index 0000000000..884efe97e8
--- /dev/null
+++ b/gfx/layers/apz/src/IAPZHitTester.cpp
@@ -0,0 +1,78 @@
+/* -*- 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 "IAPZHitTester.h"
+#include "APZCTreeManager.h"
+#include "AsyncPanZoomController.h"
+
+namespace mozilla {
+namespace layers {
+
+IAPZHitTester::HitTestResult IAPZHitTester::CloneHitTestResult(
+ RecursiveMutexAutoLock& aProofOfTreeLock,
+ const IAPZHitTester::HitTestResult& aHitTestResult) const {
+ HitTestResult result;
+
+ result.mTargetApzc = aHitTestResult.mTargetApzc;
+ result.mHitResult = aHitTestResult.mHitResult;
+ result.mLayersId = aHitTestResult.mLayersId;
+ result.mFixedPosSides = aHitTestResult.mFixedPosSides;
+ result.mHitOverscrollGutter = aHitTestResult.mHitOverscrollGutter;
+
+ RefPtr<HitTestingTreeNode> scrollbarNode =
+ aHitTestResult.mScrollbarNode.Get(aProofOfTreeLock);
+ RefPtr<HitTestingTreeNode> node = aHitTestResult.mNode.Get(aProofOfTreeLock);
+
+ if (aHitTestResult.mScrollbarNode) {
+ InitializeHitTestingTreeNodeAutoLock(result.mScrollbarNode,
+ aProofOfTreeLock, scrollbarNode);
+ }
+ if (aHitTestResult.mNode) {
+ InitializeHitTestingTreeNodeAutoLock(result.mNode, aProofOfTreeLock, node);
+ }
+
+ return result;
+}
+
+LayersId IAPZHitTester::GetRootLayersId() const {
+ return mTreeManager->mRootLayersId;
+}
+
+HitTestingTreeNode* IAPZHitTester::GetRootNode() const {
+ mTreeManager->mTreeLock.AssertCurrentThreadIn();
+ return mTreeManager->mRootNode;
+}
+
+HitTestingTreeNode* IAPZHitTester::FindRootNodeForLayersId(
+ LayersId aLayersId) const {
+ return mTreeManager->FindRootNodeForLayersId(aLayersId);
+}
+
+AsyncPanZoomController* IAPZHitTester::FindRootApzcForLayersId(
+ LayersId aLayersId) const {
+ HitTestingTreeNode* resultNode = FindRootNodeForLayersId(aLayersId);
+ return resultNode ? resultNode->GetApzc() : nullptr;
+}
+
+already_AddRefed<HitTestingTreeNode> IAPZHitTester::GetTargetNode(
+ const ScrollableLayerGuid& aGuid,
+ ScrollableLayerGuid::Comparator aComparator) {
+ // Acquire the tree lock so that derived classes can call this from
+ // methods other than GetAPZCAtPoint().
+ RecursiveMutexAutoLock lock(mTreeManager->mTreeLock);
+ return mTreeManager->GetTargetNode(aGuid, aComparator);
+}
+
+void IAPZHitTester::InitializeHitTestingTreeNodeAutoLock(
+ HitTestingTreeNodeAutoLock& aAutoLock,
+ const RecursiveMutexAutoLock& aProofOfTreeLock,
+ RefPtr<HitTestingTreeNode>& aNode) const {
+ aAutoLock.Initialize(aProofOfTreeLock, aNode.forget(),
+ mTreeManager->mTreeLock);
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/apz/src/IAPZHitTester.h b/gfx/layers/apz/src/IAPZHitTester.h
new file mode 100644
index 0000000000..8822b40ea8
--- /dev/null
+++ b/gfx/layers/apz/src/IAPZHitTester.h
@@ -0,0 +1,91 @@
+/* -*- 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_IAPZHitTester_h
+#define mozilla_layers_IAPZHitTester_h
+
+#include "HitTestingTreeNode.h" // for HitTestingTreeNodeAutoLock
+#include "mozilla/RefPtr.h"
+#include "mozilla/gfx/CompositorHitTestInfo.h"
+#include "mozilla/layers/LayersTypes.h"
+
+namespace mozilla {
+namespace layers {
+
+class AsyncPanZoomController;
+class APZCTreeManager;
+
+class IAPZHitTester {
+ public:
+ virtual ~IAPZHitTester() = default;
+
+ // Not a constructor because we want external code to be able to pass a hit
+ // tester to the APZCTreeManager constructor, which will then initialize it.
+ void Initialize(APZCTreeManager* aTreeManager) {
+ mTreeManager = aTreeManager;
+ }
+
+ // Represents the results of an APZ hit test.
+ struct HitTestResult {
+ // The APZC targeted by the hit test.
+ RefPtr<AsyncPanZoomController> mTargetApzc;
+ // The applicable hit test flags.
+ gfx::CompositorHitTestInfo mHitResult;
+ // The layers id of the content that was hit.
+ // This effectively identifiers the process that was hit for
+ // Fission purposes.
+ LayersId mLayersId;
+ // If a scrollbar was hit, this will be populated with the
+ // scrollbar node. The AutoLock allows accessing the scrollbar
+ // node without having to hold the tree lock.
+ HitTestingTreeNodeAutoLock mScrollbarNode;
+ // If content that is fixed to the root-content APZC was hit,
+ // the sides of the viewport to which the content is fixed.
+ SideBits mFixedPosSides = SideBits::eNone;
+ // If a fixed/sticky position element was hit, this will be populated with
+ // the hit-testing tree node. The AutoLock allows accessing the node
+ // without having to hold the tree lock.
+ HitTestingTreeNodeAutoLock mNode;
+ // This is set to true If mTargetApzc is overscrolled and the
+ // event targeted the gap space ("gutter") created by the overscroll.
+ bool mHitOverscrollGutter = false;
+
+ HitTestResult() = default;
+ // Make it move-only.
+ HitTestResult(HitTestResult&&) = default;
+ HitTestResult& operator=(HitTestResult&&) = default;
+ };
+
+ virtual HitTestResult GetAPZCAtPoint(
+ const ScreenPoint& aHitTestPoint,
+ const RecursiveMutexAutoLock& aProofOfTreeLock) = 0;
+
+ HitTestResult CloneHitTestResult(RecursiveMutexAutoLock& aProofOfTreeLock,
+ const HitTestResult& aHitTestResult) const;
+
+ protected:
+ APZCTreeManager* mTreeManager = nullptr;
+
+ // We are a friend of APZCTreeManager but our derived classes
+ // are not. Wrap a few private members of APZCTreeManager for
+ // use by derived classes.
+ LayersId GetRootLayersId() const;
+ HitTestingTreeNode* GetRootNode() const;
+ HitTestingTreeNode* FindRootNodeForLayersId(LayersId aLayersId) const;
+ AsyncPanZoomController* FindRootApzcForLayersId(LayersId aLayersId) const;
+ already_AddRefed<HitTestingTreeNode> GetTargetNode(
+ const ScrollableLayerGuid& aGuid,
+ ScrollableLayerGuid::Comparator aComparator);
+ void InitializeHitTestingTreeNodeAutoLock(
+ HitTestingTreeNodeAutoLock& aAutoLock,
+ const RecursiveMutexAutoLock& aProofOfTreeLock,
+ RefPtr<HitTestingTreeNode>& aNode) const;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif // mozilla_layers_IAPZHitTester_h
diff --git a/gfx/layers/apz/src/InputBlockState.cpp b/gfx/layers/apz/src/InputBlockState.cpp
new file mode 100644
index 0000000000..b492af0215
--- /dev/null
+++ b/gfx/layers/apz/src/InputBlockState.cpp
@@ -0,0 +1,840 @@
+/* -*- 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 "InputBlockState.h"
+
+#include "APZUtils.h"
+#include "AsyncPanZoomController.h" // for AsyncPanZoomController
+
+#include "mozilla/MouseEvents.h"
+#include "mozilla/StaticPrefs_apz.h"
+#include "mozilla/StaticPrefs_layout.h"
+#include "mozilla/StaticPrefs_mousewheel.h"
+#include "mozilla/StaticPrefs_test.h"
+#include "mozilla/Telemetry.h" // for Telemetry
+#include "mozilla/ToString.h"
+#include "mozilla/layers/IAPZCTreeManager.h" // for AllowedTouchBehavior
+#include "OverscrollHandoffState.h"
+#include "QueuedInput.h"
+
+static mozilla::LazyLogModule sApzIbsLog("apz.inputstate");
+#define TBS_LOG(...) MOZ_LOG(sApzIbsLog, LogLevel::Debug, (__VA_ARGS__))
+
+namespace mozilla {
+namespace layers {
+
+static uint64_t sBlockCounter = InputBlockState::NO_BLOCK_ID + 1;
+
+InputBlockState::InputBlockState(
+ const RefPtr<AsyncPanZoomController>& aTargetApzc,
+ TargetConfirmationFlags aFlags)
+ : mTargetApzc(aTargetApzc),
+ mRequiresTargetConfirmation(aFlags.mRequiresTargetConfirmation),
+ mBlockId(sBlockCounter++),
+ mTransformToApzc(aTargetApzc->GetTransformToThis()) {
+ // We should never be constructed with a nullptr target.
+ MOZ_ASSERT(mTargetApzc);
+ mOverscrollHandoffChain = mTargetApzc->BuildOverscrollHandoffChain();
+ // If a new block starts on a scrollthumb and we have APZ scrollbar
+ // dragging enabled, defer confirmation until we get the drag metrics
+ // for the thumb.
+ bool startingDrag = StaticPrefs::apz_drag_enabled() && aFlags.mHitScrollThumb;
+ mTargetConfirmed = aFlags.mTargetConfirmed && !startingDrag
+ ? TargetConfirmationState::eConfirmed
+ : TargetConfirmationState::eUnconfirmed;
+}
+
+bool InputBlockState::SetConfirmedTargetApzc(
+ const RefPtr<AsyncPanZoomController>& aTargetApzc,
+ TargetConfirmationState aState, InputData* aFirstInput,
+ bool aForScrollbarDrag) {
+ MOZ_ASSERT(aState == TargetConfirmationState::eConfirmed ||
+ aState == TargetConfirmationState::eTimedOut);
+
+ if (mTargetConfirmed == TargetConfirmationState::eTimedOut &&
+ aState == TargetConfirmationState::eConfirmed) {
+ // The main thread finally responded. We had already timed out the
+ // confirmation, but we want to update the state internally so that we
+ // can record the time for telemetry purposes.
+ mTargetConfirmed = TargetConfirmationState::eTimedOutAndMainThreadResponded;
+ }
+ // Sometimes, bugs in compositor hit testing can lead to APZ confirming
+ // a different target than the main thread. If this happens for a drag
+ // block created for a scrollbar drag, the consequences can be fairly
+ // user-unfriendly, such as the scrollbar not being draggable at all,
+ // or it scrolling the contents of the wrong scrollframe. In debug
+ // builds, we assert in this situation, so that the
+ // underlying compositor hit testing bug can be fixed. In release builds,
+ // however, we just silently accept the main thread's confirmed target,
+ // which will produce the expected behaviour (apart from drag events
+ // received so far being dropped).
+ if (AsDragBlock() && aForScrollbarDrag &&
+ mTargetConfirmed == TargetConfirmationState::eConfirmed &&
+ aState == TargetConfirmationState::eConfirmed && mTargetApzc &&
+ aTargetApzc && mTargetApzc->GetGuid() != aTargetApzc->GetGuid()) {
+ MOZ_ASSERT(false,
+ "APZ and main thread confirmed scrollbar drag block with "
+ "different targets");
+ UpdateTargetApzc(aTargetApzc);
+ return true;
+ }
+
+ if (mTargetConfirmed != TargetConfirmationState::eUnconfirmed) {
+ return false;
+ }
+ mTargetConfirmed = aState;
+
+ TBS_LOG("%p got confirmed target APZC %p\n", this, mTargetApzc.get());
+ if (mTargetApzc == aTargetApzc) {
+ // The confirmed target is the same as the tentative one, so we're done.
+ return true;
+ }
+
+ TBS_LOG("%p replacing unconfirmed target %p with real target %p\n", this,
+ mTargetApzc.get(), aTargetApzc.get());
+
+ UpdateTargetApzc(aTargetApzc);
+ return true;
+}
+
+void InputBlockState::UpdateTargetApzc(
+ const RefPtr<AsyncPanZoomController>& aTargetApzc) {
+ if (mTargetApzc == aTargetApzc) {
+ MOZ_ASSERT_UNREACHABLE(
+ "The new target APZC should be different from the old one");
+ return;
+ }
+
+ if (mTargetApzc) {
+ // Restore overscroll state on the previous target APZC and ancestor APZCs
+ // in the scroll handoff chain other than the new one.
+ mTargetApzc->SnapBackIfOverscrolled();
+
+ uint32_t i = mOverscrollHandoffChain->IndexOf(mTargetApzc) + 1;
+ for (; i < mOverscrollHandoffChain->Length(); i++) {
+ AsyncPanZoomController* apzc = mOverscrollHandoffChain->GetApzcAtIndex(i);
+ if (apzc != aTargetApzc) {
+ MOZ_ASSERT(!apzc->IsOverscrolled() ||
+ apzc->IsOverscrollAnimationRunning());
+ apzc->SnapBackIfOverscrolled();
+ }
+ }
+ }
+
+ // note that aTargetApzc MAY be null here.
+ mTargetApzc = aTargetApzc;
+ mTransformToApzc = aTargetApzc ? aTargetApzc->GetTransformToThis()
+ : ScreenToParentLayerMatrix4x4();
+ mOverscrollHandoffChain =
+ (mTargetApzc ? mTargetApzc->BuildOverscrollHandoffChain() : nullptr);
+}
+
+const RefPtr<AsyncPanZoomController>& InputBlockState::GetTargetApzc() const {
+ return mTargetApzc;
+}
+
+const RefPtr<const OverscrollHandoffChain>&
+InputBlockState::GetOverscrollHandoffChain() const {
+ return mOverscrollHandoffChain;
+}
+
+uint64_t InputBlockState::GetBlockId() const { return mBlockId; }
+
+bool InputBlockState::IsTargetConfirmed() const {
+ return mTargetConfirmed != TargetConfirmationState::eUnconfirmed;
+}
+
+bool InputBlockState::HasReceivedRealConfirmedTarget() const {
+ return mTargetConfirmed == TargetConfirmationState::eConfirmed ||
+ mTargetConfirmed ==
+ TargetConfirmationState::eTimedOutAndMainThreadResponded;
+}
+
+bool InputBlockState::ShouldDropEvents() const {
+ return mRequiresTargetConfirmation &&
+ (mTargetConfirmed != TargetConfirmationState::eConfirmed);
+}
+
+bool InputBlockState::IsDownchainOf(AsyncPanZoomController* aA,
+ AsyncPanZoomController* aB) const {
+ if (aA == aB) {
+ return true;
+ }
+
+ bool seenA = false;
+ for (size_t i = 0; i < mOverscrollHandoffChain->Length(); ++i) {
+ AsyncPanZoomController* apzc = mOverscrollHandoffChain->GetApzcAtIndex(i);
+ if (apzc == aB) {
+ return seenA;
+ }
+ if (apzc == aA) {
+ seenA = true;
+ }
+ }
+ return false;
+}
+
+void InputBlockState::SetScrolledApzc(AsyncPanZoomController* aApzc) {
+ // An input block should only have one scrolled APZC.
+ MOZ_ASSERT(!mScrolledApzc || (StaticPrefs::apz_allow_immediate_handoff()
+ ? IsDownchainOf(mScrolledApzc, aApzc)
+ : mScrolledApzc == aApzc));
+
+ mScrolledApzc = aApzc;
+}
+
+AsyncPanZoomController* InputBlockState::GetScrolledApzc() const {
+ return mScrolledApzc;
+}
+
+bool InputBlockState::IsDownchainOfScrolledApzc(
+ AsyncPanZoomController* aApzc) const {
+ MOZ_ASSERT(aApzc && mScrolledApzc);
+
+ return IsDownchainOf(mScrolledApzc, aApzc);
+}
+
+void InputBlockState::DispatchEvent(const InputData& aEvent) const {
+ GetTargetApzc()->HandleInputEvent(aEvent, mTransformToApzc);
+}
+
+CancelableBlockState::CancelableBlockState(
+ const RefPtr<AsyncPanZoomController>& aTargetApzc,
+ TargetConfirmationFlags aFlags)
+ : InputBlockState(aTargetApzc, aFlags),
+ mPreventDefault(false),
+ mContentResponded(false),
+ mContentResponseTimerExpired(false) {}
+
+bool CancelableBlockState::SetContentResponse(bool aPreventDefault) {
+ if (mContentResponded) {
+ return false;
+ }
+ TBS_LOG("%p got content response %d with timer expired %d\n", this,
+ aPreventDefault, mContentResponseTimerExpired);
+ mPreventDefault = aPreventDefault;
+ mContentResponded = true;
+ return true;
+}
+
+bool CancelableBlockState::TimeoutContentResponse() {
+ if (mContentResponseTimerExpired) {
+ return false;
+ }
+ TBS_LOG("%p got content timer expired with response received %d\n", this,
+ mContentResponded);
+ if (!mContentResponded) {
+ mPreventDefault = false;
+ }
+ mContentResponseTimerExpired = true;
+ return true;
+}
+
+bool CancelableBlockState::IsContentResponseTimerExpired() const {
+ return mContentResponseTimerExpired;
+}
+
+bool CancelableBlockState::IsDefaultPrevented() const {
+ MOZ_ASSERT(mContentResponded || mContentResponseTimerExpired);
+ return mPreventDefault;
+}
+
+bool CancelableBlockState::IsReadyForHandling() const {
+ if (!IsTargetConfirmed()) {
+ return false;
+ }
+ return mContentResponded || mContentResponseTimerExpired;
+}
+
+bool CancelableBlockState::ShouldDropEvents() const {
+ return InputBlockState::ShouldDropEvents() || IsDefaultPrevented();
+}
+
+DragBlockState::DragBlockState(
+ const RefPtr<AsyncPanZoomController>& aTargetApzc,
+ TargetConfirmationFlags aFlags, const MouseInput& aInitialEvent)
+ : CancelableBlockState(aTargetApzc, aFlags), mReceivedMouseUp(false) {}
+
+bool DragBlockState::HasReceivedMouseUp() { return mReceivedMouseUp; }
+
+void DragBlockState::MarkMouseUpReceived() { mReceivedMouseUp = true; }
+
+void DragBlockState::SetInitialThumbPos(OuterCSSCoord aThumbPos) {
+ mInitialThumbPos = aThumbPos;
+}
+
+void DragBlockState::SetDragMetrics(const AsyncDragMetrics& aDragMetrics) {
+ mDragMetrics = aDragMetrics;
+}
+
+void DragBlockState::DispatchEvent(const InputData& aEvent) const {
+ MouseInput mouseInput = aEvent.AsMouseInput();
+ if (!mouseInput.TransformToLocal(mTransformToApzc)) {
+ return;
+ }
+
+ GetTargetApzc()->HandleDragEvent(mouseInput, mDragMetrics, mInitialThumbPos);
+}
+
+bool DragBlockState::MustStayActive() { return !mReceivedMouseUp; }
+
+const char* DragBlockState::Type() { return "drag"; }
+// This is used to track the current wheel transaction.
+static uint64_t sLastWheelBlockId = InputBlockState::NO_BLOCK_ID;
+
+WheelBlockState::WheelBlockState(
+ const RefPtr<AsyncPanZoomController>& aTargetApzc,
+ TargetConfirmationFlags aFlags, const ScrollWheelInput& aInitialEvent)
+ : CancelableBlockState(aTargetApzc, aFlags),
+ mScrollSeriesCounter(0),
+ mTransactionEnded(false) {
+ sLastWheelBlockId = GetBlockId();
+
+ if (aFlags.mTargetConfirmed) {
+ // Find the nearest APZC in the overscroll handoff chain that is scrollable.
+ // If we get a content confirmation later that the apzc is different, then
+ // content should have found a scrollable apzc, so we don't need to handle
+ // that case.
+ RefPtr<AsyncPanZoomController> apzc =
+ mOverscrollHandoffChain->FindFirstScrollable(aInitialEvent,
+ &mAllowedScrollDirections);
+
+ if (apzc) {
+ if (apzc != GetTargetApzc()) {
+ UpdateTargetApzc(apzc);
+ }
+ } else if (!mOverscrollHandoffChain->CanBePanned(
+ mOverscrollHandoffChain->GetApzcAtIndex(0))) {
+ // If there's absolutely nothing scrollable start a transaction and mark
+ // this as such to we know to store our EventTime.
+ mIsScrollable = false;
+ } else {
+ // Scrollable, but not in this direction.
+ EndTransaction();
+ }
+ }
+}
+
+bool WheelBlockState::SetContentResponse(bool aPreventDefault) {
+ if (aPreventDefault) {
+ EndTransaction();
+ }
+ return CancelableBlockState::SetContentResponse(aPreventDefault);
+}
+
+bool WheelBlockState::SetConfirmedTargetApzc(
+ const RefPtr<AsyncPanZoomController>& aTargetApzc,
+ TargetConfirmationState aState, InputData* aFirstInput,
+ bool aForScrollbarDrag) {
+ // The APZC that we find via APZCCallbackHelpers may not be the same APZC
+ // ESM or OverscrollHandoff would have computed. Make sure we get the right
+ // one by looking for the first apzc the next pending event can scroll.
+ RefPtr<AsyncPanZoomController> apzc = aTargetApzc;
+ if (apzc && aFirstInput) {
+ apzc = apzc->BuildOverscrollHandoffChain()->FindFirstScrollable(
+ *aFirstInput, &mAllowedScrollDirections);
+ }
+
+ InputBlockState::SetConfirmedTargetApzc(apzc, aState, aFirstInput,
+ aForScrollbarDrag);
+ return true;
+}
+
+void WheelBlockState::Update(ScrollWheelInput& aEvent) {
+ // We might not be in a transaction if the block never started in a
+ // transaction - for example, if nothing was scrollable.
+ if (!InTransaction()) {
+ return;
+ }
+
+ // The current "scroll series" is a like a sub-transaction. It has a separate
+ // timeout of 80ms. Since we need to compute wheel deltas at different phases
+ // of a transaction (for example, when it is updated, and later when the
+ // event action is taken), we affix the scroll series counter to the event.
+ // This makes GetScrollWheelDelta() consistent.
+ if (!mLastEventTime.IsNull() &&
+ (aEvent.mTimeStamp - mLastEventTime).ToMilliseconds() >
+ StaticPrefs::mousewheel_scroll_series_timeout()) {
+ mScrollSeriesCounter = 0;
+ }
+ aEvent.mScrollSeriesNumber = ++mScrollSeriesCounter;
+
+ // If we can't scroll in the direction of the wheel event, we don't update
+ // the last move time. This allows us to timeout a transaction even if the
+ // mouse isn't moving.
+ //
+ // We skip this check if the target is not yet confirmed, so that when it is
+ // confirmed, we don't timeout the transaction.
+ RefPtr<AsyncPanZoomController> apzc = GetTargetApzc();
+ if (mIsScrollable && IsTargetConfirmed() && !apzc->CanScroll(aEvent)) {
+ return;
+ }
+
+ // Update the time of the last known good event, and reset the mouse move
+ // time to null. This will reset the delays on both the general transaction
+ // timeout and the mouse-move-in-frame timeout.
+ mLastEventTime = aEvent.mTimeStamp;
+ mLastMouseMove = TimeStamp();
+}
+
+bool WheelBlockState::MustStayActive() { return !mTransactionEnded; }
+
+const char* WheelBlockState::Type() { return "scroll wheel"; }
+
+bool WheelBlockState::ShouldAcceptNewEvent() const {
+ if (!InTransaction()) {
+ // If we're not in a transaction, start a new one.
+ return false;
+ }
+
+ RefPtr<AsyncPanZoomController> apzc = GetTargetApzc();
+ if (apzc->IsDestroyed()) {
+ return false;
+ }
+
+ return true;
+}
+
+bool WheelBlockState::MaybeTimeout(const ScrollWheelInput& aEvent) {
+ MOZ_ASSERT(InTransaction());
+
+ if (MaybeTimeout(aEvent.mTimeStamp)) {
+ return true;
+ }
+
+ if (!mLastMouseMove.IsNull()) {
+ // If there's a recent mouse movement, we can time out the transaction
+ // early.
+ TimeDuration duration = TimeStamp::Now() - mLastMouseMove;
+ if (duration.ToMilliseconds() >=
+ StaticPrefs::mousewheel_transaction_ignoremovedelay()) {
+ TBS_LOG("%p wheel transaction timed out after mouse move\n", this);
+ EndTransaction();
+ return true;
+ }
+ }
+
+ return false;
+}
+
+bool WheelBlockState::MaybeTimeout(const TimeStamp& aTimeStamp) {
+ MOZ_ASSERT(InTransaction());
+
+ // End the transaction if the event occurred > 1.5s after the most recently
+ // seen wheel event.
+ TimeDuration duration = aTimeStamp - mLastEventTime;
+ if (duration.ToMilliseconds() <
+ StaticPrefs::mousewheel_transaction_timeout()) {
+ return false;
+ }
+
+ TBS_LOG("%p wheel transaction timed out\n", this);
+
+ if (StaticPrefs::test_mousescroll()) {
+ RefPtr<AsyncPanZoomController> apzc = GetTargetApzc();
+ apzc->NotifyMozMouseScrollEvent(u"MozMouseScrollTransactionTimeout"_ns);
+ }
+
+ EndTransaction();
+ return true;
+}
+
+void WheelBlockState::OnMouseMove(
+ const ScreenIntPoint& aPoint,
+ const Maybe<ScrollableLayerGuid>& aTargetGuid) {
+ MOZ_ASSERT(InTransaction());
+
+ if (!GetTargetApzc()->Contains(aPoint) ||
+ // If the mouse moved over to a different APZC, `mIsScrollable`
+ // may no longer be false and needs to be recomputed.
+ (!mIsScrollable && aTargetGuid.isSome() &&
+ aTargetGuid.value() != GetTargetApzc()->GetGuid())) {
+ EndTransaction();
+ return;
+ }
+
+ if (mLastMouseMove.IsNull()) {
+ // If the cursor is moving inside the frame, and it is more than the
+ // ignoremovedelay time since the last scroll operation, we record
+ // this as the most recent mouse movement.
+ TimeStamp now = TimeStamp::Now();
+ TimeDuration duration = now - mLastEventTime;
+ if (duration.ToMilliseconds() >=
+ StaticPrefs::mousewheel_transaction_ignoremovedelay()) {
+ mLastMouseMove = now;
+ }
+ }
+}
+
+void WheelBlockState::UpdateTargetApzc(
+ const RefPtr<AsyncPanZoomController>& aTargetApzc) {
+ InputBlockState::UpdateTargetApzc(aTargetApzc);
+
+ // If we found there was no target apzc, then we end the transaction.
+ if (!GetTargetApzc()) {
+ EndTransaction();
+ }
+}
+
+bool WheelBlockState::InTransaction() const {
+ // We consider a wheel block to be in a transaction if it has a confirmed
+ // target and is the most recent wheel input block to be created.
+ if (GetBlockId() != sLastWheelBlockId) {
+ return false;
+ }
+
+ if (mTransactionEnded) {
+ return false;
+ }
+
+ MOZ_ASSERT(GetTargetApzc());
+ return true;
+}
+
+bool WheelBlockState::AllowScrollHandoff() const {
+ // If we're in a wheel transaction, we do not allow overscroll handoff until
+ // a new event ends the wheel transaction.
+ return !IsTargetConfirmed() || !InTransaction();
+}
+
+void WheelBlockState::EndTransaction() {
+ TBS_LOG("%p ending wheel transaction\n", this);
+ mTransactionEnded = true;
+}
+
+PanGestureBlockState::PanGestureBlockState(
+ const RefPtr<AsyncPanZoomController>& aTargetApzc,
+ TargetConfirmationFlags aFlags, const PanGestureInput& aInitialEvent)
+ : CancelableBlockState(aTargetApzc, aFlags),
+ mInterrupted(false),
+ mWaitingForContentResponse(false),
+ mWaitingForBrowserGestureResponse(false),
+ mStartedBrowserGesture(false) {
+ if (aFlags.mTargetConfirmed) {
+ // Find the nearest APZC in the overscroll handoff chain that is scrollable.
+ // If we get a content confirmation later that the apzc is different, then
+ // content should have found a scrollable apzc, so we don't need to handle
+ // that case.
+ RefPtr<AsyncPanZoomController> apzc =
+ mOverscrollHandoffChain->FindFirstScrollable(aInitialEvent,
+ &mAllowedScrollDirections);
+
+ if (apzc && apzc != GetTargetApzc()) {
+ UpdateTargetApzc(apzc);
+ }
+ }
+}
+
+bool PanGestureBlockState::SetConfirmedTargetApzc(
+ const RefPtr<AsyncPanZoomController>& aTargetApzc,
+ TargetConfirmationState aState, InputData* aFirstInput,
+ bool aForScrollbarDrag) {
+ // The APZC that we find via APZCCallbackHelpers may not be the same APZC
+ // ESM or OverscrollHandoff would have computed. Make sure we get the right
+ // one by looking for the first apzc the next pending event can scroll.
+ RefPtr<AsyncPanZoomController> apzc = aTargetApzc;
+ if (apzc && aFirstInput) {
+ RefPtr<AsyncPanZoomController> scrollableApzc =
+ apzc->BuildOverscrollHandoffChain()->FindFirstScrollable(
+ *aFirstInput, &mAllowedScrollDirections);
+ if (scrollableApzc) {
+ apzc = scrollableApzc;
+ }
+ }
+
+ InputBlockState::SetConfirmedTargetApzc(apzc, aState, aFirstInput,
+ aForScrollbarDrag);
+ return true;
+}
+
+bool PanGestureBlockState::MustStayActive() { return !mInterrupted; }
+
+const char* PanGestureBlockState::Type() { return "pan gesture"; }
+
+bool PanGestureBlockState::SetContentResponse(bool aPreventDefault) {
+ if (aPreventDefault) {
+ TBS_LOG("%p setting interrupted flag\n", this);
+ mInterrupted = true;
+ }
+ bool stateChanged = CancelableBlockState::SetContentResponse(aPreventDefault);
+ if (mWaitingForContentResponse) {
+ mWaitingForContentResponse = false;
+ stateChanged = true;
+ }
+ return stateChanged;
+}
+
+bool PanGestureBlockState::IsReadyForHandling() const {
+ if (!CancelableBlockState::IsReadyForHandling()) {
+ return false;
+ }
+ return !mWaitingForBrowserGestureResponse &&
+ (!mWaitingForContentResponse || IsContentResponseTimerExpired());
+}
+
+bool PanGestureBlockState::ShouldDropEvents() const {
+ return CancelableBlockState::ShouldDropEvents() || mStartedBrowserGesture;
+}
+
+bool PanGestureBlockState::TimeoutContentResponse() {
+ // Reset mWaitingForBrowserGestureResponse here so that we will not wait for
+ // the response forever.
+ mWaitingForBrowserGestureResponse = false;
+ return CancelableBlockState::TimeoutContentResponse();
+}
+
+bool PanGestureBlockState::AllowScrollHandoff() const { return false; }
+
+void PanGestureBlockState::SetNeedsToWaitForContentResponse(
+ bool aWaitForContentResponse) {
+ mWaitingForContentResponse = aWaitForContentResponse;
+}
+
+void PanGestureBlockState::SetNeedsToWaitForBrowserGestureResponse(
+ bool aWaitForBrowserGestureResponse) {
+ mWaitingForBrowserGestureResponse = aWaitForBrowserGestureResponse;
+}
+
+void PanGestureBlockState::SetBrowserGestureResponse(
+ BrowserGestureResponse aResponse) {
+ mWaitingForBrowserGestureResponse = false;
+ mStartedBrowserGesture = bool(aResponse);
+}
+
+PinchGestureBlockState::PinchGestureBlockState(
+ const RefPtr<AsyncPanZoomController>& aTargetApzc,
+ TargetConfirmationFlags aFlags)
+ : CancelableBlockState(aTargetApzc, aFlags),
+ mInterrupted(false),
+ mWaitingForContentResponse(false) {}
+
+bool PinchGestureBlockState::MustStayActive() { return true; }
+
+const char* PinchGestureBlockState::Type() { return "pinch gesture"; }
+
+bool PinchGestureBlockState::SetContentResponse(bool aPreventDefault) {
+ if (aPreventDefault) {
+ TBS_LOG("%p setting interrupted flag\n", this);
+ mInterrupted = true;
+ }
+ bool stateChanged = CancelableBlockState::SetContentResponse(aPreventDefault);
+ if (mWaitingForContentResponse) {
+ mWaitingForContentResponse = false;
+ stateChanged = true;
+ }
+ return stateChanged;
+}
+
+bool PinchGestureBlockState::IsReadyForHandling() const {
+ if (!CancelableBlockState::IsReadyForHandling()) {
+ return false;
+ }
+ return !mWaitingForContentResponse || IsContentResponseTimerExpired();
+}
+
+void PinchGestureBlockState::SetNeedsToWaitForContentResponse(
+ bool aWaitForContentResponse) {
+ mWaitingForContentResponse = aWaitForContentResponse;
+}
+
+TouchBlockState::TouchBlockState(
+ const RefPtr<AsyncPanZoomController>& aTargetApzc,
+ TargetConfirmationFlags aFlags, TouchCounter& aCounter)
+ : CancelableBlockState(aTargetApzc, aFlags),
+ mAllowedTouchBehaviorSet(false),
+ mDuringFastFling(false),
+ mSingleTapOccurred(false),
+ mInSlop(false),
+ mTouchCounter(aCounter),
+ mStartTime(GetTargetApzc()->GetFrameTime().Time()) {
+ TBS_LOG("Creating %p\n", this);
+}
+
+bool TouchBlockState::SetAllowedTouchBehaviors(
+ const nsTArray<TouchBehaviorFlags>& aBehaviors) {
+ if (mAllowedTouchBehaviorSet) {
+ return false;
+ }
+ TBS_LOG("%p got allowed touch behaviours for %zu points\n", this,
+ aBehaviors.Length());
+ mAllowedTouchBehaviors.AppendElements(aBehaviors);
+ mAllowedTouchBehaviorSet = true;
+ return true;
+}
+
+bool TouchBlockState::GetAllowedTouchBehaviors(
+ nsTArray<TouchBehaviorFlags>& aOutBehaviors) const {
+ if (!mAllowedTouchBehaviorSet) {
+ return false;
+ }
+ aOutBehaviors.AppendElements(mAllowedTouchBehaviors);
+ return true;
+}
+
+bool TouchBlockState::HasAllowedTouchBehaviors() const {
+ return mAllowedTouchBehaviorSet;
+}
+
+void TouchBlockState::CopyPropertiesFrom(const TouchBlockState& aOther) {
+ TBS_LOG("%p copying properties from %p\n", this, &aOther);
+ MOZ_ASSERT(aOther.mAllowedTouchBehaviorSet ||
+ aOther.IsContentResponseTimerExpired());
+ SetAllowedTouchBehaviors(aOther.mAllowedTouchBehaviors);
+ mTransformToApzc = aOther.mTransformToApzc;
+}
+
+bool TouchBlockState::IsReadyForHandling() const {
+ if (!CancelableBlockState::IsReadyForHandling()) {
+ return false;
+ }
+
+ return mAllowedTouchBehaviorSet || IsContentResponseTimerExpired();
+}
+
+void TouchBlockState::SetDuringFastFling() {
+ TBS_LOG("%p setting fast-motion flag\n", this);
+ mDuringFastFling = true;
+}
+
+bool TouchBlockState::IsDuringFastFling() const { return mDuringFastFling; }
+
+void TouchBlockState::SetSingleTapOccurred() {
+ TBS_LOG("%p setting single-tap-occurred flag\n", this);
+ mSingleTapOccurred = true;
+}
+
+bool TouchBlockState::SingleTapOccurred() const { return mSingleTapOccurred; }
+
+bool TouchBlockState::MustStayActive() { return true; }
+
+const char* TouchBlockState::Type() { return "touch"; }
+
+TimeDuration TouchBlockState::GetTimeSinceBlockStart() const {
+ return GetTargetApzc()->GetFrameTime().Time() - mStartTime;
+}
+
+void TouchBlockState::DispatchEvent(const InputData& aEvent) const {
+ MOZ_ASSERT(aEvent.mInputType == MULTITOUCH_INPUT);
+ mTouchCounter.Update(aEvent.AsMultiTouchInput());
+ CancelableBlockState::DispatchEvent(aEvent);
+}
+
+bool TouchBlockState::TouchActionAllowsPinchZoom() const {
+ // Pointer events specification requires that all touch points allow zoom.
+ for (auto& behavior : mAllowedTouchBehaviors) {
+ if (!(behavior & AllowedTouchBehavior::PINCH_ZOOM)) {
+ return false;
+ }
+ }
+ return true;
+}
+
+bool TouchBlockState::TouchActionAllowsDoubleTapZoom() const {
+ for (auto& behavior : mAllowedTouchBehaviors) {
+ if (!(behavior & AllowedTouchBehavior::ANIMATING_ZOOM)) {
+ return false;
+ }
+ }
+ return true;
+}
+
+bool TouchBlockState::TouchActionAllowsPanningX() const {
+ if (mAllowedTouchBehaviors.IsEmpty()) {
+ // Default to allowed
+ return true;
+ }
+ TouchBehaviorFlags flags = mAllowedTouchBehaviors[0];
+ return (flags & AllowedTouchBehavior::HORIZONTAL_PAN);
+}
+
+bool TouchBlockState::TouchActionAllowsPanningY() const {
+ if (mAllowedTouchBehaviors.IsEmpty()) {
+ // Default to allowed
+ return true;
+ }
+ TouchBehaviorFlags flags = mAllowedTouchBehaviors[0];
+ return (flags & AllowedTouchBehavior::VERTICAL_PAN);
+}
+
+bool TouchBlockState::TouchActionAllowsPanningXY() const {
+ if (mAllowedTouchBehaviors.IsEmpty()) {
+ // Default to allowed
+ return true;
+ }
+ TouchBehaviorFlags flags = mAllowedTouchBehaviors[0];
+ return (flags & AllowedTouchBehavior::HORIZONTAL_PAN) &&
+ (flags & AllowedTouchBehavior::VERTICAL_PAN);
+}
+
+bool TouchBlockState::UpdateSlopState(const MultiTouchInput& aInput,
+ bool aApzcCanConsumeEvents) {
+ if (aInput.mType == MultiTouchInput::MULTITOUCH_START) {
+ // this is by definition the first event in this block. If it's the first
+ // touch, then we enter a slop state.
+ mInSlop = (aInput.mTouches.Length() == 1);
+ if (mInSlop) {
+ mSlopOrigin = aInput.mTouches[0].mScreenPoint;
+ TBS_LOG("%p entering slop with origin %s\n", this,
+ ToString(mSlopOrigin).c_str());
+ }
+ return false;
+ }
+ if (mInSlop) {
+ ScreenCoord threshold = 0;
+ // If the target was confirmed to null then the threshold doesn't
+ // matter anyway since the events will never be processed.
+ if (const RefPtr<AsyncPanZoomController>& apzc = GetTargetApzc()) {
+ threshold = aApzcCanConsumeEvents ? apzc->GetTouchStartTolerance()
+ : apzc->GetTouchMoveTolerance();
+ }
+ bool stayInSlop =
+ (aInput.mType == MultiTouchInput::MULTITOUCH_MOVE) &&
+ (aInput.mTouches.Length() == 1) &&
+ ((aInput.mTouches[0].mScreenPoint - mSlopOrigin).Length() < threshold);
+ if (!stayInSlop) {
+ // we're out of the slop zone, and will stay out for the remainder of
+ // this block
+ TBS_LOG("%p exiting slop\n", this);
+ mInSlop = false;
+ }
+ }
+ return mInSlop;
+}
+
+bool TouchBlockState::IsInSlop() const { return mInSlop; }
+
+Maybe<ScrollDirection> TouchBlockState::GetBestGuessPanDirection(
+ const MultiTouchInput& aInput) {
+ if (aInput.mType != MultiTouchInput::MULTITOUCH_MOVE ||
+ aInput.mTouches.Length() != 1) {
+ return Nothing();
+ }
+ ScreenPoint vector = aInput.mTouches[0].mScreenPoint - mSlopOrigin;
+ double angle = atan2(vector.y, vector.x); // range [-pi, pi]
+ angle = fabs(angle); // range [0, pi]
+
+ double angleThreshold = TouchActionAllowsPanningXY()
+ ? StaticPrefs::apz_axis_lock_lock_angle()
+ : StaticPrefs::apz_axis_lock_direct_pan_angle();
+ if (apz::IsCloseToHorizontal(angle, angleThreshold)) {
+ return Some(ScrollDirection::eHorizontal);
+ }
+ if (apz::IsCloseToVertical(angle, angleThreshold)) {
+ return Some(ScrollDirection::eVertical);
+ }
+ return Nothing();
+}
+
+uint32_t TouchBlockState::GetActiveTouchCount() const {
+ return mTouchCounter.GetActiveTouchCount();
+}
+
+KeyboardBlockState::KeyboardBlockState(
+ const RefPtr<AsyncPanZoomController>& aTargetApzc)
+ : InputBlockState(aTargetApzc, TargetConfirmationFlags{true}) {}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/apz/src/InputBlockState.h b/gfx/layers/apz/src/InputBlockState.h
new file mode 100644
index 0000000000..6320eda6d6
--- /dev/null
+++ b/gfx/layers/apz/src/InputBlockState.h
@@ -0,0 +1,544 @@
+/* -*- 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_InputBlockState_h
+#define mozilla_layers_InputBlockState_h
+
+#include "InputData.h" // for MultiTouchInput
+#include "mozilla/RefCounted.h" // for RefCounted
+#include "mozilla/RefPtr.h" // for RefPtr
+#include "mozilla/StaticPrefs_apz.h"
+#include "mozilla/gfx/Matrix.h" // for Matrix4x4
+#include "mozilla/layers/APZUtils.h"
+#include "mozilla/layers/LayersTypes.h" // for TouchBehaviorFlags
+#include "mozilla/layers/AsyncDragMetrics.h"
+#include "mozilla/layers/TouchCounter.h"
+#include "mozilla/TimeStamp.h" // for TimeStamp
+#include "nsTArray.h" // for nsTArray
+
+namespace mozilla {
+namespace layers {
+
+class AsyncPanZoomController;
+class OverscrollHandoffChain;
+class CancelableBlockState;
+class TouchBlockState;
+class WheelBlockState;
+class DragBlockState;
+class PanGestureBlockState;
+class PinchGestureBlockState;
+class KeyboardBlockState;
+enum class BrowserGestureResponse : bool;
+
+/**
+ * A base class that stores state common to various input blocks.
+ * Note that the InputBlockState constructor acquires the tree lock, so callers
+ * from inside AsyncPanZoomController should ensure that the APZC lock is not
+ * held.
+ */
+class InputBlockState : public RefCounted<InputBlockState> {
+ public:
+ MOZ_DECLARE_REFCOUNTED_TYPENAME(InputBlockState)
+
+ static const uint64_t NO_BLOCK_ID = 0;
+
+ enum class TargetConfirmationState : uint8_t {
+ eUnconfirmed,
+ eTimedOut,
+ eTimedOutAndMainThreadResponded,
+ eConfirmed
+ };
+
+ InputBlockState(const RefPtr<AsyncPanZoomController>& aTargetApzc,
+ TargetConfirmationFlags aFlags);
+ virtual ~InputBlockState() = default;
+
+ virtual CancelableBlockState* AsCancelableBlock() { return nullptr; }
+ virtual TouchBlockState* AsTouchBlock() { return nullptr; }
+ virtual WheelBlockState* AsWheelBlock() { return nullptr; }
+ virtual DragBlockState* AsDragBlock() { return nullptr; }
+ virtual PanGestureBlockState* AsPanGestureBlock() { return nullptr; }
+ virtual PinchGestureBlockState* AsPinchGestureBlock() { return nullptr; }
+ virtual KeyboardBlockState* AsKeyboardBlock() { return nullptr; }
+
+ virtual bool SetConfirmedTargetApzc(
+ const RefPtr<AsyncPanZoomController>& aTargetApzc,
+ TargetConfirmationState aState, InputData* aFirstInput,
+ bool aForScrollbarDrag);
+ const RefPtr<AsyncPanZoomController>& GetTargetApzc() const;
+ const RefPtr<const OverscrollHandoffChain>& GetOverscrollHandoffChain() const;
+ uint64_t GetBlockId() const;
+
+ bool IsTargetConfirmed() const;
+ bool HasReceivedRealConfirmedTarget() const;
+
+ virtual bool ShouldDropEvents() const;
+
+ void SetScrolledApzc(AsyncPanZoomController* aApzc);
+ AsyncPanZoomController* GetScrolledApzc() const;
+ bool IsDownchainOfScrolledApzc(AsyncPanZoomController* aApzc) const;
+
+ /**
+ * Dispatch the event to the target APZC. Mostly this is a hook for
+ * subclasses to do any per-event processing they need to.
+ */
+ virtual void DispatchEvent(const InputData& aEvent) const;
+
+ /**
+ * Return true if this input block must stay active if it would otherwise
+ * be removed as the last item in the pending queue.
+ */
+ virtual bool MustStayActive() = 0;
+
+ protected:
+ virtual void UpdateTargetApzc(
+ const RefPtr<AsyncPanZoomController>& aTargetApzc);
+
+ private:
+ // Checks whether |aA| is an ancestor of |aB| (or the same as |aB|) in
+ // |mOverscrollHandoffChain|.
+ bool IsDownchainOf(AsyncPanZoomController* aA,
+ AsyncPanZoomController* aB) const;
+
+ private:
+ RefPtr<AsyncPanZoomController> mTargetApzc;
+ TargetConfirmationState mTargetConfirmed;
+ bool mRequiresTargetConfirmation;
+ const uint64_t mBlockId;
+
+ // The APZC that was actually scrolled by events in this input block.
+ // This is used in configurations where a single input block is only
+ // allowed to scroll a single APZC (configurations where
+ // StaticPrefs::apz_allow_immediate_handoff() is false). Set the first time an
+ // input event in this block scrolls an APZC.
+ RefPtr<AsyncPanZoomController> mScrolledApzc;
+
+ protected:
+ RefPtr<const OverscrollHandoffChain> mOverscrollHandoffChain;
+
+ // Used to transform events from global screen space to |mTargetApzc|'s
+ // screen space. It's cached at the beginning of the input block so that
+ // all events in the block are in the same coordinate space.
+ ScreenToParentLayerMatrix4x4 mTransformToApzc;
+};
+
+/**
+ * This class represents a set of events that can be cancelled by web content
+ * via event listeners.
+ *
+ * Each cancelable input block can be cancelled by web content, and
+ * this information is stored in the mPreventDefault flag. Because web
+ * content runs on the Gecko main thread, we cannot always wait for web
+ * content's response. Instead, there is a timeout that sets this flag in the
+ * case where web content doesn't respond in time. The mContentResponded and
+ * mContentResponseTimerExpired flags indicate which of these scenarios
+ * occurred.
+ */
+class CancelableBlockState : public InputBlockState {
+ public:
+ CancelableBlockState(const RefPtr<AsyncPanZoomController>& aTargetApzc,
+ TargetConfirmationFlags aFlags);
+
+ CancelableBlockState* AsCancelableBlock() override { return this; }
+
+ /**
+ * Record whether or not content cancelled this block of events.
+ * @param aPreventDefault true iff the block is cancelled.
+ * @return false if this block has already received a response from
+ * web content, true if not.
+ */
+ virtual bool SetContentResponse(bool aPreventDefault);
+
+ /**
+ * Record that content didn't respond in time.
+ * @return false if this block already timed out, true if not.
+ */
+ virtual bool TimeoutContentResponse();
+
+ /**
+ * Checks if the content response timer has already expired.
+ */
+ bool IsContentResponseTimerExpired() const;
+
+ /**
+ * @return true iff web content cancelled this block of events.
+ */
+ bool IsDefaultPrevented() const;
+
+ /**
+ * @return true iff this block has received all the information needed
+ * to properly dispatch the events in the block.
+ */
+ virtual bool IsReadyForHandling() const;
+
+ /**
+ * Return a descriptive name for the block kind.
+ */
+ virtual const char* Type() = 0;
+
+ bool ShouldDropEvents() const override;
+
+ private:
+ bool mPreventDefault;
+ bool mContentResponded;
+ bool mContentResponseTimerExpired;
+};
+
+/**
+ * A single block of wheel events.
+ */
+class WheelBlockState : public CancelableBlockState {
+ public:
+ WheelBlockState(const RefPtr<AsyncPanZoomController>& aTargetApzc,
+ TargetConfirmationFlags aFlags,
+ const ScrollWheelInput& aEvent);
+
+ bool SetContentResponse(bool aPreventDefault) override;
+ bool MustStayActive() override;
+ const char* Type() override;
+ bool SetConfirmedTargetApzc(const RefPtr<AsyncPanZoomController>& aTargetApzc,
+ TargetConfirmationState aState,
+ InputData* aFirstInput,
+ bool aForScrollbarDrag) override;
+
+ WheelBlockState* AsWheelBlock() override { return this; }
+
+ /**
+ * Determine whether this wheel block is accepting new events.
+ */
+ bool ShouldAcceptNewEvent() const;
+
+ /**
+ * Call to check whether a wheel event will cause the current transaction to
+ * timeout.
+ */
+ bool MaybeTimeout(const ScrollWheelInput& aEvent);
+
+ /**
+ * Called from APZCTM when a mouse move or drag+drop event occurs, before
+ * the event has been processed.
+ */
+ void OnMouseMove(const ScreenIntPoint& aPoint,
+ const Maybe<ScrollableLayerGuid>& aTargetGuid);
+
+ /**
+ * Returns whether or not the block is participating in a wheel transaction.
+ * This means that the block is the most recent input block to be created,
+ * and no events have occurred that would require scrolling a different
+ * frame.
+ *
+ * @return True if in a transaction, false otherwise.
+ */
+ bool InTransaction() const;
+
+ /**
+ * Mark the block as no longer participating in a wheel transaction. This
+ * will force future wheel events to begin a new input block.
+ */
+ void EndTransaction();
+
+ /**
+ * @return Whether or not overscrolling is prevented for this wheel block.
+ */
+ bool AllowScrollHandoff() const;
+
+ /**
+ * Called to check and possibly end the transaction due to a timeout.
+ *
+ * @return True if the transaction ended, false otherwise.
+ */
+ bool MaybeTimeout(const TimeStamp& aTimeStamp);
+
+ /**
+ * Update the wheel transaction state for a new event.
+ */
+ void Update(ScrollWheelInput& aEvent);
+
+ ScrollDirections GetAllowedScrollDirections() const {
+ return mAllowedScrollDirections;
+ }
+
+ protected:
+ void UpdateTargetApzc(
+ const RefPtr<AsyncPanZoomController>& aTargetApzc) override;
+
+ private:
+ TimeStamp mLastEventTime;
+ TimeStamp mLastMouseMove;
+ uint32_t mScrollSeriesCounter;
+ bool mTransactionEnded;
+ bool mIsScrollable = true;
+ ScrollDirections mAllowedScrollDirections;
+};
+
+/**
+ * A block of mouse events that are part of a drag
+ */
+class DragBlockState : public CancelableBlockState {
+ public:
+ DragBlockState(const RefPtr<AsyncPanZoomController>& aTargetApzc,
+ TargetConfirmationFlags aFlags, const MouseInput& aEvent);
+
+ bool MustStayActive() override;
+ const char* Type() override;
+
+ bool HasReceivedMouseUp();
+ void MarkMouseUpReceived();
+
+ DragBlockState* AsDragBlock() override { return this; }
+
+ void SetInitialThumbPos(OuterCSSCoord aThumbPos);
+ void SetDragMetrics(const AsyncDragMetrics& aDragMetrics);
+
+ void DispatchEvent(const InputData& aEvent) const override;
+
+ private:
+ AsyncDragMetrics mDragMetrics;
+ OuterCSSCoord mInitialThumbPos;
+ bool mReceivedMouseUp;
+};
+
+/**
+ * A single block of pan gesture events.
+ */
+class PanGestureBlockState : public CancelableBlockState {
+ public:
+ PanGestureBlockState(const RefPtr<AsyncPanZoomController>& aTargetApzc,
+ TargetConfirmationFlags aFlags,
+ const PanGestureInput& aEvent);
+
+ bool SetContentResponse(bool aPreventDefault) override;
+ bool IsReadyForHandling() const override;
+ bool MustStayActive() override;
+ const char* Type() override;
+ bool SetConfirmedTargetApzc(const RefPtr<AsyncPanZoomController>& aTargetApzc,
+ TargetConfirmationState aState,
+ InputData* aFirstInput,
+ bool aForScrollbarDrag) override;
+
+ PanGestureBlockState* AsPanGestureBlock() override { return this; }
+
+ bool ShouldDropEvents() const override;
+
+ bool TimeoutContentResponse() override;
+
+ /**
+ * @return Whether or not overscrolling is prevented for this block.
+ */
+ bool AllowScrollHandoff() const;
+
+ bool WasInterrupted() const { return mInterrupted; }
+
+ void SetNeedsToWaitForContentResponse(bool aWaitForContentResponse);
+ void SetNeedsToWaitForBrowserGestureResponse(
+ bool aWaitForBrowserGestureResponse);
+ void SetBrowserGestureResponse(BrowserGestureResponse aResponse);
+
+ ScrollDirections GetAllowedScrollDirections() const {
+ return mAllowedScrollDirections;
+ }
+
+ private:
+ bool mInterrupted;
+ bool mWaitingForContentResponse;
+ // A pan gesture may be used for browser's swipe gestures so APZ needs to wait
+ // for the response from the browser whether the gesture has been used for
+ // swipe or not. This `mWaitingForBrowserGestureResponse` flag represents the
+ // waiting state. And below `mStartedBrowserGesture` represents the response
+ // from the browser.
+ bool mWaitingForBrowserGestureResponse;
+ bool mStartedBrowserGesture;
+ ScrollDirections mAllowedScrollDirections;
+};
+
+/**
+ * A single block of pinch gesture events.
+ */
+class PinchGestureBlockState : public CancelableBlockState {
+ public:
+ PinchGestureBlockState(const RefPtr<AsyncPanZoomController>& aTargetApzc,
+ TargetConfirmationFlags aFlags);
+
+ bool SetContentResponse(bool aPreventDefault) override;
+ bool IsReadyForHandling() const override;
+ bool MustStayActive() override;
+ const char* Type() override;
+
+ PinchGestureBlockState* AsPinchGestureBlock() override { return this; }
+
+ bool WasInterrupted() const { return mInterrupted; }
+
+ void SetNeedsToWaitForContentResponse(bool aWaitForContentResponse);
+
+ private:
+ bool mInterrupted;
+ bool mWaitingForContentResponse;
+};
+
+/**
+ * This class represents a single touch block. A touch block is
+ * a set of touch events that can be cancelled by web content via
+ * touch event listeners.
+ *
+ * Every touch-start event creates a new touch block. In this case, the
+ * touch block consists of the touch-start, followed by all touch events
+ * up to but not including the next touch-start (except in the case where
+ * a long-tap happens, see below). Note that in particular we cannot know
+ * when a touch block ends until the next one is started. Most touch
+ * blocks are created by receipt of a touch-start event.
+ *
+ * Every long-tap event also creates a new touch block, since it can also
+ * be consumed by web content. In this case, when the long-tap event is
+ * dispatched to web content, a new touch block is started to hold the remaining
+ * touch events, up to but not including the next touch start (or long-tap).
+ *
+ * Additionally, if touch-action is enabled, each touch block should
+ * have a set of allowed touch behavior flags; one for each touch point.
+ * This also requires running code on the Gecko main thread, and so may
+ * be populated with some latency. The mAllowedTouchBehaviorSet and
+ * mAllowedTouchBehaviors variables track this information.
+ */
+class TouchBlockState : public CancelableBlockState {
+ public:
+ explicit TouchBlockState(const RefPtr<AsyncPanZoomController>& aTargetApzc,
+ TargetConfirmationFlags aFlags,
+ TouchCounter& aTouchCounter);
+
+ TouchBlockState* AsTouchBlock() override { return this; }
+
+ /**
+ * Set the allowed touch behavior flags for this block.
+ * @return false if this block already has these flags set, true if not.
+ */
+ bool SetAllowedTouchBehaviors(const nsTArray<TouchBehaviorFlags>& aBehaviors);
+ /**
+ * If the allowed touch behaviors have been set, populate them into
+ * |aOutBehaviors| and return true. Else, return false.
+ */
+ bool GetAllowedTouchBehaviors(
+ nsTArray<TouchBehaviorFlags>& aOutBehaviors) const;
+
+ /**
+ * Returns true if the allowed touch behaviours have been set, or if touch
+ * action is disabled.
+ */
+ bool HasAllowedTouchBehaviors() const;
+
+ /**
+ * Copy various properties from another block.
+ */
+ void CopyPropertiesFrom(const TouchBlockState& aOther);
+
+ /**
+ * @return true iff this block has received all the information needed
+ * to properly dispatch the events in the block.
+ */
+ bool IsReadyForHandling() const override;
+
+ /**
+ * Sets a flag that indicates this input block occurred while the APZ was
+ * in a state of fast flinging. This affects gestures that may be produced
+ * from input events in this block.
+ */
+ void SetDuringFastFling();
+ /**
+ * @return true iff SetDuringFastFling was called on this block.
+ */
+ bool IsDuringFastFling() const;
+ /**
+ * Set the single-tap-occurred flag that indicates that this touch block
+ * triggered a single tap event.
+ */
+ void SetSingleTapOccurred();
+ /**
+ * @return true iff the single-tap-occurred flag is set on this block.
+ */
+ bool SingleTapOccurred() const;
+
+ /**
+ * @return false iff touch-action is enabled and the allowed touch behaviors
+ * for this touch block do not allow pinch-zooming.
+ */
+ bool TouchActionAllowsPinchZoom() const;
+ /**
+ * @return false iff touch-action is enabled and the allowed touch behaviors
+ * for this touch block do not allow double-tap zooming.
+ */
+ bool TouchActionAllowsDoubleTapZoom() const;
+ /**
+ * @return false iff touch-action is enabled and the allowed touch behaviors
+ * for the first touch point do not allow panning in the specified
+ * direction(s).
+ */
+ bool TouchActionAllowsPanningX() const;
+ bool TouchActionAllowsPanningY() const;
+ bool TouchActionAllowsPanningXY() const;
+
+ /**
+ * Notifies the input block of an incoming touch event so that the block can
+ * update its internal slop state. "Slop" refers to the area around the
+ * initial touchstart where we drop touchmove events so that content doesn't
+ * see them. The |aApzcCanConsumeEvents| parameter is factored into how large
+ * the slop area is - if this is true the slop area is larger.
+ * @return true iff the provided event is a touchmove in the slop area and
+ * so should not be sent to content.
+ */
+ bool UpdateSlopState(const MultiTouchInput& aInput,
+ bool aApzcCanConsumeEvents);
+ bool IsInSlop() const;
+
+ /**
+ * Based on the slop origin and the given input event, return a best guess
+ * as to the pan direction of this touch block. Returns Nothing() if no guess
+ * can be made.
+ */
+ Maybe<ScrollDirection> GetBestGuessPanDirection(
+ const MultiTouchInput& aInput);
+
+ /**
+ * Returns the number of touch points currently active.
+ */
+ uint32_t GetActiveTouchCount() const;
+
+ void DispatchEvent(const InputData& aEvent) const override;
+ bool MustStayActive() override;
+ const char* Type() override;
+ TimeDuration GetTimeSinceBlockStart() const;
+
+ private:
+ nsTArray<TouchBehaviorFlags> mAllowedTouchBehaviors;
+ bool mAllowedTouchBehaviorSet;
+ bool mDuringFastFling;
+ bool mSingleTapOccurred;
+ bool mInSlop;
+ ScreenIntPoint mSlopOrigin;
+ // A reference to the InputQueue's touch counter
+ TouchCounter& mTouchCounter;
+ TimeStamp mStartTime;
+};
+
+/**
+ * This class represents a set of keyboard inputs targeted at the same Apzc.
+ */
+class KeyboardBlockState : public InputBlockState {
+ public:
+ explicit KeyboardBlockState(
+ const RefPtr<AsyncPanZoomController>& aTargetApzc);
+
+ KeyboardBlockState* AsKeyboardBlock() override { return this; }
+
+ bool MustStayActive() override { return false; }
+
+ /**
+ * @return Whether or not overscrolling is prevented for this keyboard block.
+ */
+ bool AllowScrollHandoff() const { return false; }
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif // mozilla_layers_InputBlockState_h
diff --git a/gfx/layers/apz/src/InputQueue.cpp b/gfx/layers/apz/src/InputQueue.cpp
new file mode 100644
index 0000000000..b6c7a05830
--- /dev/null
+++ b/gfx/layers/apz/src/InputQueue.cpp
@@ -0,0 +1,1090 @@
+/* -*- 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 "InputQueue.h"
+
+#include "AsyncPanZoomController.h"
+
+#include "GestureEventListener.h"
+#include "InputBlockState.h"
+#include "mozilla/EventForwards.h"
+#include "mozilla/layers/APZInputBridge.h"
+#include "mozilla/layers/APZThreadUtils.h"
+#include "mozilla/ToString.h"
+#include "OverscrollHandoffState.h"
+#include "QueuedInput.h"
+#include "mozilla/StaticPrefs_apz.h"
+#include "mozilla/StaticPrefs_layout.h"
+#include "mozilla/StaticPrefs_ui.h"
+
+static mozilla::LazyLogModule sApzInpLog("apz.inputqueue");
+#define INPQ_LOG(...) MOZ_LOG(sApzInpLog, LogLevel::Debug, (__VA_ARGS__))
+
+namespace mozilla {
+namespace layers {
+
+InputQueue::InputQueue() = default;
+
+InputQueue::~InputQueue() { mQueuedInputs.Clear(); }
+
+APZEventResult InputQueue::ReceiveInputEvent(
+ const RefPtr<AsyncPanZoomController>& aTarget,
+ TargetConfirmationFlags aFlags, InputData& aEvent,
+ const Maybe<nsTArray<TouchBehaviorFlags>>& aTouchBehaviors) {
+ APZThreadUtils::AssertOnControllerThread();
+
+ AutoRunImmediateTimeout timeoutRunner{this};
+
+ switch (aEvent.mInputType) {
+ case MULTITOUCH_INPUT: {
+ const MultiTouchInput& event = aEvent.AsMultiTouchInput();
+ return ReceiveTouchInput(aTarget, aFlags, event, aTouchBehaviors);
+ }
+
+ case SCROLLWHEEL_INPUT: {
+ const ScrollWheelInput& event = aEvent.AsScrollWheelInput();
+ return ReceiveScrollWheelInput(aTarget, aFlags, event);
+ }
+
+ case PANGESTURE_INPUT: {
+ const PanGestureInput& event = aEvent.AsPanGestureInput();
+ return ReceivePanGestureInput(aTarget, aFlags, event);
+ }
+
+ case PINCHGESTURE_INPUT: {
+ const PinchGestureInput& event = aEvent.AsPinchGestureInput();
+ return ReceivePinchGestureInput(aTarget, aFlags, event);
+ }
+
+ case MOUSE_INPUT: {
+ MouseInput& event = aEvent.AsMouseInput();
+ return ReceiveMouseInput(aTarget, aFlags, event);
+ }
+
+ case KEYBOARD_INPUT: {
+ // Every keyboard input must have a confirmed target
+ MOZ_ASSERT(aTarget && aFlags.mTargetConfirmed);
+
+ const KeyboardInput& event = aEvent.AsKeyboardInput();
+ return ReceiveKeyboardInput(aTarget, aFlags, event);
+ }
+
+ default: {
+ // The `mStatus` for other input type is only used by tests, so just
+ // pass through the return value of HandleInputEvent() for now.
+ APZEventResult result(aTarget, aFlags);
+ nsEventStatus status =
+ aTarget->HandleInputEvent(aEvent, aTarget->GetTransformToThis());
+ switch (status) {
+ case nsEventStatus_eIgnore:
+ result.SetStatusAsIgnore();
+ break;
+ case nsEventStatus_eConsumeNoDefault:
+ result.SetStatusAsConsumeNoDefault();
+ break;
+ case nsEventStatus_eConsumeDoDefault:
+ result.SetStatusAsConsumeDoDefault(aTarget);
+ break;
+ default:
+ MOZ_ASSERT_UNREACHABLE("An invalid status");
+ break;
+ }
+ return result;
+ }
+ }
+}
+
+APZEventResult InputQueue::ReceiveTouchInput(
+ const RefPtr<AsyncPanZoomController>& aTarget,
+ TargetConfirmationFlags aFlags, const MultiTouchInput& aEvent,
+ const Maybe<nsTArray<TouchBehaviorFlags>>& aTouchBehaviors) {
+ APZEventResult result(aTarget, aFlags);
+
+ RefPtr<TouchBlockState> block;
+ bool waitingForContentResponse = false;
+ if (aEvent.mType == MultiTouchInput::MULTITOUCH_START) {
+ nsTArray<TouchBehaviorFlags> currentBehaviors;
+ bool haveBehaviors = false;
+ if (mActiveTouchBlock) {
+ haveBehaviors =
+ mActiveTouchBlock->GetAllowedTouchBehaviors(currentBehaviors);
+ // If the behaviours aren't set, but the main-thread response timer on
+ // the block is expired we still treat it as though it has behaviors,
+ // because in that case we still want to interrupt the fast-fling and
+ // use the default behaviours.
+ haveBehaviors |= mActiveTouchBlock->IsContentResponseTimerExpired();
+ }
+
+ block = StartNewTouchBlock(aTarget, aFlags, false);
+ INPQ_LOG("started new touch block %p id %" PRIu64 " for target %p\n",
+ block.get(), block->GetBlockId(), aTarget.get());
+
+ // XXX using the chain from |block| here may be wrong in cases where the
+ // target isn't confirmed and the real target turns out to be something
+ // else. For now assume this is rare enough that it's not an issue.
+ if (mQueuedInputs.IsEmpty() && aEvent.mTouches.Length() == 1 &&
+ block->GetOverscrollHandoffChain()->HasFastFlungApzc() &&
+ haveBehaviors) {
+ // If we're already in a fast fling, and a single finger goes down, then
+ // we want special handling for the touch event, because it shouldn't get
+ // delivered to content. Note that we don't set this flag when going
+ // from a fast fling to a pinch state (i.e. second finger goes down while
+ // the first finger is moving).
+ block->SetDuringFastFling();
+ block->SetConfirmedTargetApzc(
+ aTarget, InputBlockState::TargetConfirmationState::eConfirmed,
+ nullptr /* the block was just created so it has no events */,
+ false /* not a scrollbar drag */);
+ block->SetAllowedTouchBehaviors(currentBehaviors);
+ INPQ_LOG("block %p tagged as fast-motion\n", block.get());
+ } else if (aTouchBehaviors) {
+ // If this block isn't started during a fast-fling, and APZCTM has
+ // provided touch behavior information, then put it on the block so
+ // that the ArePointerEventsConsumable call below can use it.
+ block->SetAllowedTouchBehaviors(*aTouchBehaviors);
+ }
+
+ CancelAnimationsForNewBlock(block);
+
+ waitingForContentResponse = MaybeRequestContentResponse(aTarget, block);
+ } else {
+ // for touch inputs that don't start a block, APZCTM shouldn't be giving
+ // us any touch behaviors.
+ MOZ_ASSERT(aTouchBehaviors.isNothing());
+
+ block = mActiveTouchBlock.get();
+ if (!block) {
+ NS_WARNING(
+ "Received a non-start touch event while no touch blocks active!");
+ return result;
+ }
+
+ INPQ_LOG("received new touch event (type=%d) in block %p\n", aEvent.mType,
+ block.get());
+ }
+
+ result.mInputBlockId = block->GetBlockId();
+
+ // Note that the |aTarget| the APZCTM sent us may contradict the confirmed
+ // target set on the block. In this case the confirmed target (which may be
+ // null) should take priority. This is equivalent to just always using the
+ // target (confirmed or not) from the block.
+ RefPtr<AsyncPanZoomController> target = block->GetTargetApzc();
+
+ // XXX calling ArePointerEventsConsumable on |target| may be wrong here if
+ // the target isn't confirmed and the real target turns out to be something
+ // else. For now assume this is rare enough that it's not an issue.
+ PointerEventsConsumableFlags consumableFlags;
+ if (target) {
+ consumableFlags = target->ArePointerEventsConsumable(block, aEvent);
+ }
+ if (block->IsDuringFastFling()) {
+ INPQ_LOG("dropping event due to block %p being in fast motion\n",
+ block.get());
+ result.SetStatusForFastFling(*block, aFlags, consumableFlags, target);
+ } else { // handling depends on ArePointerEventsConsumable()
+ bool consumable = consumableFlags.IsConsumable();
+ if (block->UpdateSlopState(aEvent, consumable)) {
+ INPQ_LOG("dropping event due to block %p being in %sslop\n", block.get(),
+ consumable ? "" : "mini-");
+ result.SetStatusAsConsumeNoDefault();
+ } else {
+ result.SetStatusForTouchEvent(*block, aFlags, consumableFlags, target);
+ }
+ }
+ mQueuedInputs.AppendElement(MakeUnique<QueuedInput>(aEvent, *block));
+ ProcessQueue();
+
+ // If this block just started and is waiting for a content response, but
+ // also in a slop state (i.e. touchstart gets delivered to content but
+ // not any touchmoves), then we might end up in a situation where we don't
+ // get the content response until the timeout is hit because we never exit
+ // the slop state. But if that timeout is longer than the long-press timeout,
+ // then the long-press gets delayed too. Avoid that by scheduling a callback
+ // with the long-press timeout that will force the block to get processed.
+ int32_t longTapTimeout = StaticPrefs::ui_click_hold_context_menus_delay();
+ int32_t contentTimeout = StaticPrefs::apz_content_response_timeout();
+ if (waitingForContentResponse && longTapTimeout < contentTimeout &&
+ block->IsInSlop() && GestureEventListener::IsLongTapEnabled()) {
+ MOZ_ASSERT(aEvent.mType == MultiTouchInput::MULTITOUCH_START);
+ MOZ_ASSERT(!block->IsDuringFastFling());
+ RefPtr<Runnable> maybeLongTap = NewRunnableMethod<uint64_t>(
+ "layers::InputQueue::MaybeLongTapTimeout", this,
+ &InputQueue::MaybeLongTapTimeout, block->GetBlockId());
+ INPQ_LOG("scheduling maybe-long-tap timeout for target %p\n",
+ aTarget.get());
+ aTarget->PostDelayedTask(maybeLongTap.forget(), longTapTimeout);
+ }
+
+ return result;
+}
+
+APZEventResult InputQueue::ReceiveMouseInput(
+ const RefPtr<AsyncPanZoomController>& aTarget,
+ TargetConfirmationFlags aFlags, MouseInput& aEvent) {
+ APZEventResult result(aTarget, aFlags);
+
+ // On a new mouse down we can have a new target so we must force a new block
+ // with a new target.
+ bool newBlock = DragTracker::StartsDrag(aEvent);
+
+ RefPtr<DragBlockState> block = newBlock ? nullptr : mActiveDragBlock.get();
+ if (block && block->HasReceivedMouseUp()) {
+ block = nullptr;
+ }
+
+ if (!block && mDragTracker.InDrag()) {
+ // If there's no current drag block, but we're getting a move with a button
+ // down, we need to start a new drag block because we're obviously already
+ // in the middle of a drag (it probably got interrupted by something else).
+ INPQ_LOG(
+ "got a drag event outside a drag block, need to create a block to hold "
+ "it\n");
+ newBlock = true;
+ }
+
+ mDragTracker.Update(aEvent);
+
+ if (!newBlock && !block) {
+ // This input event is not in a drag block, so we're not doing anything
+ // with it, return eIgnore.
+ return result;
+ }
+
+ if (!block) {
+ MOZ_ASSERT(newBlock);
+ block = new DragBlockState(aTarget, aFlags, aEvent);
+
+ INPQ_LOG(
+ "started new drag block %p id %" PRIu64
+ "for %sconfirmed target %p; on scrollbar: %d; on scrollthumb: %d\n",
+ block.get(), block->GetBlockId(), aFlags.mTargetConfirmed ? "" : "un",
+ aTarget.get(), aFlags.mHitScrollbar, aFlags.mHitScrollThumb);
+
+ mActiveDragBlock = block;
+
+ if (aFlags.mHitScrollThumb || !aFlags.mHitScrollbar) {
+ // If we're running autoscroll, we'll always cancel it during the
+ // following call of CancelAnimationsForNewBlock. At this time,
+ // we don't want to fire `click` event on the web content for web-compat
+ // with Chrome. Therefore, we notify widget of it with the flag.
+ if ((aEvent.mType == MouseInput::MOUSE_DOWN ||
+ aEvent.mType == MouseInput::MOUSE_UP) &&
+ block->GetOverscrollHandoffChain()->HasAutoscrollApzc()) {
+ aEvent.mPreventClickEvent = true;
+ }
+ CancelAnimationsForNewBlock(block);
+ }
+ MaybeRequestContentResponse(aTarget, block);
+ }
+
+ result.mInputBlockId = block->GetBlockId();
+
+ mQueuedInputs.AppendElement(MakeUnique<QueuedInput>(aEvent, *block));
+ ProcessQueue();
+
+ if (DragTracker::EndsDrag(aEvent)) {
+ block->MarkMouseUpReceived();
+ }
+
+ // The event is part of a drag block and could potentially cause
+ // scrolling, so return DoDefault.
+ result.SetStatusAsConsumeDoDefault(*block);
+ return result;
+}
+
+APZEventResult InputQueue::ReceiveScrollWheelInput(
+ const RefPtr<AsyncPanZoomController>& aTarget,
+ TargetConfirmationFlags aFlags, const ScrollWheelInput& aEvent) {
+ APZEventResult result(aTarget, aFlags);
+
+ RefPtr<WheelBlockState> block = mActiveWheelBlock.get();
+ // If the block is not accepting new events we'll create a new input block
+ // (and therefore a new wheel transaction).
+ if (block &&
+ (!block->ShouldAcceptNewEvent() || block->MaybeTimeout(aEvent))) {
+ block = nullptr;
+ }
+
+ MOZ_ASSERT(!block || block->InTransaction());
+
+ if (!block) {
+ block = new WheelBlockState(aTarget, aFlags, aEvent);
+ INPQ_LOG("started new scroll wheel block %p id %" PRIu64
+ " for %starget %p\n",
+ block.get(), block->GetBlockId(),
+ aFlags.mTargetConfirmed ? "confirmed " : "", aTarget.get());
+
+ mActiveWheelBlock = block;
+
+ CancelAnimationsForNewBlock(block, ExcludeWheel);
+ MaybeRequestContentResponse(aTarget, block);
+ } else {
+ INPQ_LOG("received new wheel event in block %p\n", block.get());
+ }
+
+ result.mInputBlockId = block->GetBlockId();
+
+ // Note that the |aTarget| the APZCTM sent us may contradict the confirmed
+ // target set on the block. In this case the confirmed target (which may be
+ // null) should take priority. This is equivalent to just always using the
+ // target (confirmed or not) from the block, which is what
+ // ProcessQueue() does.
+ mQueuedInputs.AppendElement(MakeUnique<QueuedInput>(aEvent, *block));
+
+ // The WheelBlockState needs to affix a counter to the event before we process
+ // it. Note that the counter is affixed to the copy in the queue rather than
+ // |aEvent|.
+ block->Update(mQueuedInputs.LastElement()->Input()->AsScrollWheelInput());
+
+ ProcessQueue();
+
+ result.SetStatusAsConsumeDoDefault(*block);
+ return result;
+}
+
+APZEventResult InputQueue::ReceiveKeyboardInput(
+ const RefPtr<AsyncPanZoomController>& aTarget,
+ TargetConfirmationFlags aFlags, const KeyboardInput& aEvent) {
+ APZEventResult result(aTarget, aFlags);
+
+ RefPtr<KeyboardBlockState> block = mActiveKeyboardBlock.get();
+
+ // If the block is targeting a different Apzc than this keyboard event then
+ // we'll create a new input block
+ if (block && block->GetTargetApzc() != aTarget) {
+ block = nullptr;
+ }
+
+ if (!block) {
+ block = new KeyboardBlockState(aTarget);
+ INPQ_LOG("started new keyboard block %p id %" PRIu64 " for target %p\n",
+ block.get(), block->GetBlockId(), aTarget.get());
+
+ mActiveKeyboardBlock = block;
+ } else {
+ INPQ_LOG("received new keyboard event in block %p\n", block.get());
+ }
+
+ result.mInputBlockId = block->GetBlockId();
+
+ mQueuedInputs.AppendElement(MakeUnique<QueuedInput>(aEvent, *block));
+
+ ProcessQueue();
+
+ // If APZ is allowing passive listeners then we must dispatch the event to
+ // content, otherwise we can consume the event.
+ if (StaticPrefs::apz_keyboard_passive_listeners()) {
+ result.SetStatusAsConsumeDoDefault(*block);
+ } else {
+ result.SetStatusAsConsumeNoDefault();
+ }
+ return result;
+}
+
+static bool CanScrollTargetHorizontally(const PanGestureInput& aInitialEvent,
+ PanGestureBlockState* aBlock) {
+ PanGestureInput horizontalComponent = aInitialEvent;
+ horizontalComponent.mPanDisplacement.y = 0;
+ ScrollDirections allowedScrollDirections;
+ RefPtr<AsyncPanZoomController> horizontallyScrollableAPZC =
+ aBlock->GetOverscrollHandoffChain()->FindFirstScrollable(
+ horizontalComponent, &allowedScrollDirections,
+ OverscrollHandoffChain::IncludeOverscroll::No);
+ return horizontallyScrollableAPZC &&
+ horizontallyScrollableAPZC == aBlock->GetTargetApzc() &&
+ allowedScrollDirections.contains(ScrollDirection::eHorizontal);
+}
+
+APZEventResult InputQueue::ReceivePanGestureInput(
+ const RefPtr<AsyncPanZoomController>& aTarget,
+ TargetConfirmationFlags aFlags, const PanGestureInput& aEvent) {
+ APZEventResult result(aTarget, aFlags);
+
+ if (aEvent.mType == PanGestureInput::PANGESTURE_MAYSTART ||
+ aEvent.mType == PanGestureInput::PANGESTURE_CANCELLED) {
+ // Ignore these events for now.
+ result.SetStatusAsConsumeDoDefault(aTarget);
+ return result;
+ }
+
+ if (aEvent.mType == PanGestureInput::PANGESTURE_INTERRUPTED) {
+ if (RefPtr<PanGestureBlockState> block = mActivePanGestureBlock.get()) {
+ mQueuedInputs.AppendElement(MakeUnique<QueuedInput>(aEvent, *block));
+ ProcessQueue();
+ }
+ result.SetStatusAsIgnore();
+ return result;
+ }
+
+ RefPtr<PanGestureBlockState> block;
+ if (aEvent.mType != PanGestureInput::PANGESTURE_START) {
+ block = mActivePanGestureBlock.get();
+ }
+
+ PanGestureInput event = aEvent;
+
+ // Below `SetStatusAsConsumeDoDefault()` preserves `mHandledResult` of
+ // `result` which was set in the ctor of APZEventResult at the top of this
+ // function based on `aFlag` so that the `mHandledResult` value is reliable to
+ // tell whether the event will be handled by the root content APZC at least
+ // for swipe-navigation stuff. E.g. if a pan-start event scrolled the root
+ // scroll container, we don't need to anything for swipe-navigation.
+ result.SetStatusAsConsumeDoDefault();
+
+ if (!block || block->WasInterrupted()) {
+ if (event.mType == PanGestureInput::PANGESTURE_MOMENTUMSTART ||
+ event.mType == PanGestureInput::PANGESTURE_MOMENTUMPAN ||
+ event.mType == PanGestureInput::PANGESTURE_MOMENTUMEND) {
+ // If there are momentum events after an interruption, discard them.
+ // However, if there is a non-momentum event (indicating the user
+ // continued scrolling on the touchpad), a new input block is started
+ // by turning the event into a pan-start below.
+ return result;
+ }
+ if (event.mType != PanGestureInput::PANGESTURE_START) {
+ // Only PANGESTURE_START events are allowed to start a new pan gesture
+ // block, but we really want to start a new block here, so we magically
+ // turn this input into a PANGESTURE_START.
+ INPQ_LOG(
+ "transmogrifying pan input %d to PANGESTURE_START for new block\n",
+ event.mType);
+ event.mType = PanGestureInput::PANGESTURE_START;
+ }
+ block = new PanGestureBlockState(aTarget, aFlags, event);
+ INPQ_LOG("started new pan gesture block %p id %" PRIu64 " for target %p\n",
+ block.get(), block->GetBlockId(), aTarget.get());
+
+ mActivePanGestureBlock = block;
+
+ CancelAnimationsForNewBlock(block);
+ const bool waitingForContentResponse =
+ MaybeRequestContentResponse(aTarget, block);
+
+ if (event.AllowsSwipe() && !CanScrollTargetHorizontally(event, block)) {
+ // We will ask the browser whether this pan event is going to be used for
+ // swipe or not, so we need to wait the response.
+ block->SetNeedsToWaitForBrowserGestureResponse(true);
+ if (!waitingForContentResponse) {
+ ScheduleMainThreadTimeout(aTarget, block);
+ }
+ if (aFlags.mTargetConfirmed) {
+ // This event may trigger a swipe gesture, depending on what our caller
+ // wants to do it. We need to suspend handling of this block until we
+ // get a content response which will tell us whether to proceed or abort
+ // the block.
+ block->SetNeedsToWaitForContentResponse(true);
+
+ // Inform our caller that we haven't scrolled in response to the event
+ // and that a swipe can be started from this event if desired.
+ result.SetStatusAsIgnore();
+ }
+ }
+ } else {
+ INPQ_LOG("received new pan event (type=%d) in block %p\n", aEvent.mType,
+ block.get());
+ }
+
+ result.mInputBlockId = block->GetBlockId();
+
+ // Note that the |aTarget| the APZCTM sent us may contradict the confirmed
+ // target set on the block. In this case the confirmed target (which may be
+ // null) should take priority. This is equivalent to just always using the
+ // target (confirmed or not) from the block, which is what
+ // ProcessQueue() does.
+ mQueuedInputs.AppendElement(MakeUnique<QueuedInput>(event, *block));
+ ProcessQueue();
+
+ return result;
+}
+
+APZEventResult InputQueue::ReceivePinchGestureInput(
+ const RefPtr<AsyncPanZoomController>& aTarget,
+ TargetConfirmationFlags aFlags, const PinchGestureInput& aEvent) {
+ APZEventResult result(aTarget, aFlags);
+
+ RefPtr<PinchGestureBlockState> block;
+ if (aEvent.mType != PinchGestureInput::PINCHGESTURE_START) {
+ block = mActivePinchGestureBlock.get();
+ }
+
+ result.SetStatusAsConsumeDoDefault(aTarget);
+
+ if (!block || block->WasInterrupted()) {
+ if (aEvent.mType != PinchGestureInput::PINCHGESTURE_START) {
+ // Only PINCHGESTURE_START events are allowed to start a new pinch gesture
+ // block.
+ INPQ_LOG("pinchgesture block %p was interrupted %d\n", block.get(),
+ block ? block->WasInterrupted() : 0);
+ return result;
+ }
+ block = new PinchGestureBlockState(aTarget, aFlags);
+ INPQ_LOG("started new pinch gesture block %p id %" PRIu64
+ " for target %p\n",
+ block.get(), block->GetBlockId(), aTarget.get());
+
+ mActivePinchGestureBlock = block;
+ block->SetNeedsToWaitForContentResponse(true);
+
+ CancelAnimationsForNewBlock(block);
+ MaybeRequestContentResponse(aTarget, block);
+ } else {
+ INPQ_LOG("received new pinch event (type=%d) in block %p\n", aEvent.mType,
+ block.get());
+ }
+
+ result.mInputBlockId = block->GetBlockId();
+
+ // Note that the |aTarget| the APZCTM sent us may contradict the confirmed
+ // target set on the block. In this case the confirmed target (which may be
+ // null) should take priority. This is equivalent to just always using the
+ // target (confirmed or not) from the block, which is what
+ // ProcessQueue() does.
+ mQueuedInputs.AppendElement(MakeUnique<QueuedInput>(aEvent, *block));
+ ProcessQueue();
+
+ return result;
+}
+
+void InputQueue::CancelAnimationsForNewBlock(InputBlockState* aBlock,
+ CancelAnimationFlags aExtraFlags) {
+ // We want to cancel animations here as soon as possible (i.e. without waiting
+ // for content responses) because a finger has gone down and we don't want to
+ // keep moving the content under the finger. However, to prevent "future"
+ // touchstart events from interfering with "past" animations (i.e. from a
+ // previous touch block that is still being processed) we only do this
+ // animation-cancellation if there are no older touch blocks still in the
+ // queue.
+ if (mQueuedInputs.IsEmpty()) {
+ aBlock->GetOverscrollHandoffChain()->CancelAnimations(
+ aExtraFlags | ExcludeOverscroll | ScrollSnap);
+ }
+}
+
+bool InputQueue::MaybeRequestContentResponse(
+ const RefPtr<AsyncPanZoomController>& aTarget,
+ CancelableBlockState* aBlock) {
+ bool waitForMainThread = false;
+ if (aBlock->IsTargetConfirmed()) {
+ // Content won't prevent-default this, so we can just set the flag directly.
+ INPQ_LOG("not waiting for content response on block %p\n", aBlock);
+ aBlock->SetContentResponse(false);
+ } else {
+ waitForMainThread = true;
+ }
+ if (aBlock->AsTouchBlock() &&
+ !aBlock->AsTouchBlock()->HasAllowedTouchBehaviors()) {
+ INPQ_LOG("waiting for main thread touch-action info on block %p\n", aBlock);
+ waitForMainThread = true;
+ }
+ if (waitForMainThread) {
+ // We either don't know for sure if aTarget is the right APZC, or we may
+ // need to wait to give content the opportunity to prevent-default the
+ // touch events. Either way we schedule a timeout so the main thread stuff
+ // can run.
+ ScheduleMainThreadTimeout(aTarget, aBlock);
+ }
+ return waitForMainThread;
+}
+
+uint64_t InputQueue::InjectNewTouchBlock(AsyncPanZoomController* aTarget) {
+ AutoRunImmediateTimeout timeoutRunner{this};
+ TouchBlockState* block =
+ StartNewTouchBlock(aTarget, TargetConfirmationFlags{true},
+ /* aCopyPropertiesFromCurrent = */ true);
+ INPQ_LOG("injecting new touch block %p with id %" PRIu64 " and target %p\n",
+ block, block->GetBlockId(), aTarget);
+ ScheduleMainThreadTimeout(aTarget, block);
+ return block->GetBlockId();
+}
+
+TouchBlockState* InputQueue::StartNewTouchBlock(
+ const RefPtr<AsyncPanZoomController>& aTarget,
+ TargetConfirmationFlags aFlags, bool aCopyPropertiesFromCurrent) {
+ TouchBlockState* newBlock =
+ new TouchBlockState(aTarget, aFlags, mTouchCounter);
+ if (aCopyPropertiesFromCurrent) {
+ // We should never enter here without a current touch block, because this
+ // codepath is invoked from the OnLongPress handler in
+ // AsyncPanZoomController, which should bail out if there is no current
+ // touch block.
+ MOZ_ASSERT(GetCurrentTouchBlock());
+ newBlock->CopyPropertiesFrom(*GetCurrentTouchBlock());
+ }
+
+ mActiveTouchBlock = newBlock;
+ return newBlock;
+}
+
+InputBlockState* InputQueue::GetCurrentBlock() const {
+ APZThreadUtils::AssertOnControllerThread();
+ return mQueuedInputs.IsEmpty() ? nullptr : mQueuedInputs[0]->Block();
+}
+
+TouchBlockState* InputQueue::GetCurrentTouchBlock() const {
+ InputBlockState* block = GetCurrentBlock();
+ return block ? block->AsTouchBlock() : mActiveTouchBlock.get();
+}
+
+WheelBlockState* InputQueue::GetCurrentWheelBlock() const {
+ InputBlockState* block = GetCurrentBlock();
+ return block ? block->AsWheelBlock() : mActiveWheelBlock.get();
+}
+
+DragBlockState* InputQueue::GetCurrentDragBlock() const {
+ InputBlockState* block = GetCurrentBlock();
+ return block ? block->AsDragBlock() : mActiveDragBlock.get();
+}
+
+PanGestureBlockState* InputQueue::GetCurrentPanGestureBlock() const {
+ InputBlockState* block = GetCurrentBlock();
+ return block ? block->AsPanGestureBlock() : mActivePanGestureBlock.get();
+}
+
+PinchGestureBlockState* InputQueue::GetCurrentPinchGestureBlock() const {
+ InputBlockState* block = GetCurrentBlock();
+ return block ? block->AsPinchGestureBlock() : mActivePinchGestureBlock.get();
+}
+
+KeyboardBlockState* InputQueue::GetCurrentKeyboardBlock() const {
+ InputBlockState* block = GetCurrentBlock();
+ return block ? block->AsKeyboardBlock() : mActiveKeyboardBlock.get();
+}
+
+WheelBlockState* InputQueue::GetActiveWheelTransaction() const {
+ WheelBlockState* block = mActiveWheelBlock.get();
+ if (!block || !block->InTransaction()) {
+ return nullptr;
+ }
+ return block;
+}
+
+bool InputQueue::HasReadyTouchBlock() const {
+ return !mQueuedInputs.IsEmpty() &&
+ mQueuedInputs[0]->Block()->AsTouchBlock() &&
+ mQueuedInputs[0]->Block()->AsTouchBlock()->IsReadyForHandling();
+}
+
+bool InputQueue::AllowScrollHandoff() const {
+ if (GetCurrentWheelBlock()) {
+ return GetCurrentWheelBlock()->AllowScrollHandoff();
+ }
+ if (GetCurrentPanGestureBlock()) {
+ return GetCurrentPanGestureBlock()->AllowScrollHandoff();
+ }
+ if (GetCurrentKeyboardBlock()) {
+ return GetCurrentKeyboardBlock()->AllowScrollHandoff();
+ }
+ return true;
+}
+
+bool InputQueue::IsDragOnScrollbar(bool aHitScrollbar) {
+ if (!mDragTracker.InDrag()) {
+ return false;
+ }
+ // Now that we know we are in a drag, get the info from the drag tracker.
+ // We keep it in the tracker rather than the block because the block can get
+ // interrupted by something else (like a wheel event) and then a new block
+ // will get created without the info we want. The tracker will persist though.
+ return mDragTracker.IsOnScrollbar(aHitScrollbar);
+}
+
+void InputQueue::ScheduleMainThreadTimeout(
+ const RefPtr<AsyncPanZoomController>& aTarget,
+ CancelableBlockState* aBlock) {
+ INPQ_LOG("scheduling main thread timeout for target %p\n", aTarget.get());
+ RefPtr<Runnable> timeoutTask = NewRunnableMethod<uint64_t>(
+ "layers::InputQueue::MainThreadTimeout", this,
+ &InputQueue::MainThreadTimeout, aBlock->GetBlockId());
+ int32_t timeout = StaticPrefs::apz_content_response_timeout();
+ if (timeout == 0) {
+ // If the timeout is zero, treat it as a request to ignore any main
+ // thread confirmation and unconditionally use fallback behaviour for
+ // when a timeout is reached. This codepath is used by tests that
+ // want to exercise the fallback behaviour.
+ // To ensure the fallback behaviour is used unconditionally, the timeout
+ // is run right away instead of using PostDelayedTask(). However,
+ // we can't run it right here, because MainThreadTimeout() expects that
+ // the input block has at least one input event in mQueuedInputs, and
+ // the event that triggered this call may not have been added to
+ // mQueuedInputs yet.
+ mImmediateTimeout = std::move(timeoutTask);
+ } else {
+ aTarget->PostDelayedTask(timeoutTask.forget(), timeout);
+ }
+}
+
+InputBlockState* InputQueue::GetBlockForId(uint64_t aInputBlockId) {
+ return FindBlockForId(aInputBlockId, nullptr);
+}
+
+void InputQueue::AddInputBlockCallback(uint64_t aInputBlockId,
+ InputBlockCallbackInfo&& aCallbackInfo) {
+ mInputBlockCallbacks.insert(InputBlockCallbackMap::value_type(
+ aInputBlockId, std::move(aCallbackInfo)));
+}
+
+InputBlockState* InputQueue::FindBlockForId(uint64_t aInputBlockId,
+ InputData** aOutFirstInput) {
+ for (const auto& queuedInput : mQueuedInputs) {
+ if (queuedInput->Block()->GetBlockId() == aInputBlockId) {
+ if (aOutFirstInput) {
+ *aOutFirstInput = queuedInput->Input();
+ }
+ return queuedInput->Block();
+ }
+ }
+
+ InputBlockState* block = nullptr;
+ if (mActiveTouchBlock && mActiveTouchBlock->GetBlockId() == aInputBlockId) {
+ block = mActiveTouchBlock.get();
+ } else if (mActiveWheelBlock &&
+ mActiveWheelBlock->GetBlockId() == aInputBlockId) {
+ block = mActiveWheelBlock.get();
+ } else if (mActiveDragBlock &&
+ mActiveDragBlock->GetBlockId() == aInputBlockId) {
+ block = mActiveDragBlock.get();
+ } else if (mActivePanGestureBlock &&
+ mActivePanGestureBlock->GetBlockId() == aInputBlockId) {
+ block = mActivePanGestureBlock.get();
+ } else if (mActivePinchGestureBlock &&
+ mActivePinchGestureBlock->GetBlockId() == aInputBlockId) {
+ block = mActivePinchGestureBlock.get();
+ } else if (mActiveKeyboardBlock &&
+ mActiveKeyboardBlock->GetBlockId() == aInputBlockId) {
+ block = mActiveKeyboardBlock.get();
+ }
+ // Since we didn't encounter this block while iterating through mQueuedInputs,
+ // it must have no events associated with it at the moment.
+ if (aOutFirstInput) {
+ *aOutFirstInput = nullptr;
+ }
+ return block;
+}
+
+void InputQueue::MainThreadTimeout(uint64_t aInputBlockId) {
+ // It's possible that this function gets called after the controller thread
+ // was discarded during shutdown.
+ if (!APZThreadUtils::IsControllerThreadAlive()) {
+ return;
+ }
+ APZThreadUtils::AssertOnControllerThread();
+
+ INPQ_LOG("got a main thread timeout; block=%" PRIu64 "\n", aInputBlockId);
+ bool success = false;
+ InputData* firstInput = nullptr;
+ InputBlockState* inputBlock = FindBlockForId(aInputBlockId, &firstInput);
+ if (inputBlock && inputBlock->AsCancelableBlock()) {
+ CancelableBlockState* block = inputBlock->AsCancelableBlock();
+ // time out the touch-listener response and also confirm the existing
+ // target apzc in the case where the main thread doesn't get back to us
+ // fast enough.
+ success = block->TimeoutContentResponse();
+ success |= block->SetConfirmedTargetApzc(
+ block->GetTargetApzc(),
+ InputBlockState::TargetConfirmationState::eTimedOut, firstInput,
+ // This actually could be a scrollbar drag, but we pass
+ // aForScrollbarDrag=false because for scrollbar drags,
+ // SetConfirmedTargetApzc() will also be called by ConfirmDragBlock(),
+ // and we pass aForScrollbarDrag=true there.
+ false);
+ } else if (inputBlock) {
+ NS_WARNING("input block is not a cancelable block");
+ }
+ if (success) {
+ ProcessQueue();
+ }
+}
+
+void InputQueue::MaybeLongTapTimeout(uint64_t aInputBlockId) {
+ // It's possible that this function gets called after the controller thread
+ // was discarded during shutdown.
+ if (!APZThreadUtils::IsControllerThreadAlive()) {
+ return;
+ }
+ APZThreadUtils::AssertOnControllerThread();
+
+ INPQ_LOG("got a maybe-long-tap timeout; block=%" PRIu64 "\n", aInputBlockId);
+
+ InputBlockState* inputBlock = FindBlockForId(aInputBlockId, nullptr);
+ MOZ_ASSERT(!inputBlock || inputBlock->AsTouchBlock());
+ if (inputBlock && inputBlock->AsTouchBlock()->IsInSlop()) {
+ // If the block is still in slop, it won't have sent a touchmove to content
+ // and so content will not have sent a content response. But also it means
+ // the touchstart should trigger a long-press gesture so let's force the
+ // block to get processed now.
+ MainThreadTimeout(aInputBlockId);
+ }
+}
+
+void InputQueue::ContentReceivedInputBlock(uint64_t aInputBlockId,
+ bool aPreventDefault) {
+ APZThreadUtils::AssertOnControllerThread();
+
+ INPQ_LOG("got a content response; block=%" PRIu64 " preventDefault=%d\n",
+ aInputBlockId, aPreventDefault);
+ bool success = false;
+ InputBlockState* inputBlock = FindBlockForId(aInputBlockId, nullptr);
+ if (inputBlock && inputBlock->AsCancelableBlock()) {
+ CancelableBlockState* block = inputBlock->AsCancelableBlock();
+ success = block->SetContentResponse(aPreventDefault);
+ } else if (inputBlock) {
+ NS_WARNING("input block is not a cancelable block");
+ }
+ if (success) {
+ ProcessQueue();
+ }
+}
+
+void InputQueue::SetConfirmedTargetApzc(
+ uint64_t aInputBlockId, const RefPtr<AsyncPanZoomController>& aTargetApzc) {
+ APZThreadUtils::AssertOnControllerThread();
+
+ INPQ_LOG("got a target apzc; block=%" PRIu64 " guid=%s\n", aInputBlockId,
+ aTargetApzc ? ToString(aTargetApzc->GetGuid()).c_str() : "");
+ bool success = false;
+ InputData* firstInput = nullptr;
+ InputBlockState* inputBlock = FindBlockForId(aInputBlockId, &firstInput);
+ if (inputBlock && inputBlock->AsCancelableBlock()) {
+ CancelableBlockState* block = inputBlock->AsCancelableBlock();
+ success = block->SetConfirmedTargetApzc(
+ aTargetApzc, InputBlockState::TargetConfirmationState::eConfirmed,
+ firstInput,
+ // This actually could be a scrollbar drag, but we pass
+ // aForScrollbarDrag=false because for scrollbar drags,
+ // SetConfirmedTargetApzc() will also be called by ConfirmDragBlock(),
+ // and we pass aForScrollbarDrag=true there.
+ false);
+ } else if (inputBlock) {
+ NS_WARNING("input block is not a cancelable block");
+ }
+ if (success) {
+ ProcessQueue();
+ }
+}
+
+void InputQueue::ConfirmDragBlock(
+ uint64_t aInputBlockId, const RefPtr<AsyncPanZoomController>& aTargetApzc,
+ const AsyncDragMetrics& aDragMetrics) {
+ APZThreadUtils::AssertOnControllerThread();
+
+ INPQ_LOG("got a target apzc; block=%" PRIu64 " guid=%s dragtarget=%" PRIu64
+ "\n",
+ aInputBlockId,
+ aTargetApzc ? ToString(aTargetApzc->GetGuid()).c_str() : "",
+ aDragMetrics.mViewId);
+ bool success = false;
+ InputData* firstInput = nullptr;
+ InputBlockState* inputBlock = FindBlockForId(aInputBlockId, &firstInput);
+ if (inputBlock && inputBlock->AsDragBlock()) {
+ DragBlockState* block = inputBlock->AsDragBlock();
+ block->SetDragMetrics(aDragMetrics);
+ success = block->SetConfirmedTargetApzc(
+ aTargetApzc, InputBlockState::TargetConfirmationState::eConfirmed,
+ firstInput,
+ /* aForScrollbarDrag = */ true);
+ }
+ if (success) {
+ ProcessQueue();
+ }
+}
+
+void InputQueue::SetAllowedTouchBehavior(
+ uint64_t aInputBlockId, const nsTArray<TouchBehaviorFlags>& aBehaviors) {
+ APZThreadUtils::AssertOnControllerThread();
+
+ INPQ_LOG("got allowed touch behaviours; block=%" PRIu64 "\n", aInputBlockId);
+ bool success = false;
+ InputBlockState* inputBlock = FindBlockForId(aInputBlockId, nullptr);
+ if (inputBlock && inputBlock->AsTouchBlock()) {
+ TouchBlockState* block = inputBlock->AsTouchBlock();
+ success = block->SetAllowedTouchBehaviors(aBehaviors);
+ } else if (inputBlock) {
+ NS_WARNING("input block is not a touch block");
+ }
+ if (success) {
+ ProcessQueue();
+ }
+}
+
+void InputQueue::SetBrowserGestureResponse(uint64_t aInputBlockId,
+ BrowserGestureResponse aResponse) {
+ InputBlockState* inputBlock = FindBlockForId(aInputBlockId, nullptr);
+
+ if (inputBlock && inputBlock->AsPanGestureBlock()) {
+ PanGestureBlockState* block = inputBlock->AsPanGestureBlock();
+ block->SetBrowserGestureResponse(aResponse);
+ } else if (inputBlock) {
+ NS_WARNING("input block is not a pan gesture block");
+ }
+ ProcessQueue();
+}
+
+static APZHandledResult GetHandledResultFor(
+ const AsyncPanZoomController* aApzc,
+ const InputBlockState& aCurrentInputBlock, nsEventStatus aEagerStatus) {
+ if (aCurrentInputBlock.ShouldDropEvents()) {
+ return APZHandledResult{APZHandledPlace::HandledByContent, aApzc};
+ }
+
+ if (!aApzc) {
+ return APZHandledResult{APZHandledPlace::HandledByContent, aApzc};
+ }
+
+ if (aApzc->IsRootContent()) {
+ // If the eager status was eIgnore, we would have returned an eager result
+ // of Unhandled if there had been no event handler. Now that we know the
+ // event handler did not preventDefault() the input block, return Unhandled
+ // as the delayed result.
+ // FIXME: A more accurate implementation would be to re-do the entire
+ // computation that determines the status (i.e. calling
+ // ArePointerEventsConsumable()) with the confirmed target APZC.
+ return (aEagerStatus == nsEventStatus_eConsumeDoDefault &&
+ aApzc->CanVerticalScrollWithDynamicToolbar())
+ ? APZHandledResult{APZHandledPlace::HandledByRoot, aApzc}
+ : APZHandledResult{APZHandledPlace::Unhandled, aApzc};
+ }
+
+ auto [result, rootApzc] = aCurrentInputBlock.GetOverscrollHandoffChain()
+ ->ScrollingDownWillMoveDynamicToolbar(aApzc);
+ if (!result) {
+ return APZHandledResult{APZHandledPlace::HandledByContent, aApzc};
+ }
+
+ // Return `HandledByRoot` if scroll positions in all relevant APZC are at the
+ // bottom edge and if there are contents covered by the dynamic toolbar.
+ MOZ_ASSERT(rootApzc && rootApzc->IsRootContent());
+ return APZHandledResult{APZHandledPlace::HandledByRoot, rootApzc};
+}
+
+void InputQueue::ProcessQueue() {
+ APZThreadUtils::AssertOnControllerThread();
+
+ while (!mQueuedInputs.IsEmpty()) {
+ InputBlockState* curBlock = mQueuedInputs[0]->Block();
+ CancelableBlockState* cancelable = curBlock->AsCancelableBlock();
+ if (cancelable && !cancelable->IsReadyForHandling()) {
+ break;
+ }
+
+ INPQ_LOG(
+ "processing input from block %p; preventDefault %d shouldDropEvents %d "
+ "target %p\n",
+ curBlock, cancelable && cancelable->IsDefaultPrevented(),
+ curBlock->ShouldDropEvents(), curBlock->GetTargetApzc().get());
+ RefPtr<AsyncPanZoomController> target = curBlock->GetTargetApzc();
+
+ // If there is an input block callback registered for this
+ // input block, invoke it.
+ auto it = mInputBlockCallbacks.find(curBlock->GetBlockId());
+ if (it != mInputBlockCallbacks.end()) {
+ APZHandledResult handledResult =
+ GetHandledResultFor(target, *curBlock, it->second.mEagerStatus);
+ it->second.mCallback(curBlock->GetBlockId(), handledResult);
+ // The callback is one-shot; discard it after calling it.
+ mInputBlockCallbacks.erase(it);
+ }
+
+ // target may be null here if the initial target was unconfirmed and then
+ // we later got a confirmed null target. in that case drop the events.
+ if (target) {
+ // If the event is targeting a different APZC than the previous one,
+ // we want to clear the previous APZC's gesture state regardless of
+ // whether we're actually dispatching the event or not.
+ if (mLastActiveApzc && mLastActiveApzc != target &&
+ mTouchCounter.GetActiveTouchCount() > 0) {
+ mLastActiveApzc->ResetTouchInputState();
+ }
+ if (curBlock->ShouldDropEvents()) {
+ if (curBlock->AsTouchBlock()) {
+ target->ResetTouchInputState();
+ } else if (curBlock->AsPanGestureBlock()) {
+ target->ResetPanGestureInputState();
+ }
+ } else {
+ UpdateActiveApzc(target);
+ curBlock->DispatchEvent(*(mQueuedInputs[0]->Input()));
+ }
+ }
+ mQueuedInputs.RemoveElementAt(0);
+ }
+
+ if (CanDiscardBlock(mActiveTouchBlock)) {
+ mActiveTouchBlock = nullptr;
+ }
+ if (CanDiscardBlock(mActiveWheelBlock)) {
+ mActiveWheelBlock = nullptr;
+ }
+ if (CanDiscardBlock(mActiveDragBlock)) {
+ mActiveDragBlock = nullptr;
+ }
+ if (CanDiscardBlock(mActivePanGestureBlock)) {
+ mActivePanGestureBlock = nullptr;
+ }
+ if (CanDiscardBlock(mActivePinchGestureBlock)) {
+ mActivePinchGestureBlock = nullptr;
+ }
+ if (CanDiscardBlock(mActiveKeyboardBlock)) {
+ mActiveKeyboardBlock = nullptr;
+ }
+}
+
+bool InputQueue::CanDiscardBlock(InputBlockState* aBlock) {
+ if (!aBlock ||
+ (aBlock->AsCancelableBlock() &&
+ !aBlock->AsCancelableBlock()->IsReadyForHandling()) ||
+ aBlock->MustStayActive()) {
+ return false;
+ }
+ InputData* firstInput = nullptr;
+ FindBlockForId(aBlock->GetBlockId(), &firstInput);
+ if (firstInput) {
+ // The block has at least one input event still in the queue, so it's
+ // not depleted
+ return false;
+ }
+ return true;
+}
+
+void InputQueue::UpdateActiveApzc(
+ const RefPtr<AsyncPanZoomController>& aNewActive) {
+ mLastActiveApzc = aNewActive;
+}
+
+void InputQueue::Clear() {
+ // On Android, where the controller thread is the Android UI thread,
+ // it's possible for this to be called after the main thread has
+ // already run the shutdown task that clears the state used to
+ // implement APZThreadUtils::AssertOnControllerThread().
+ // In such cases, we still want to perform the cleanup.
+ if (APZThreadUtils::IsControllerThreadAlive()) {
+ APZThreadUtils::AssertOnControllerThread();
+ }
+
+ mQueuedInputs.Clear();
+ mActiveTouchBlock = nullptr;
+ mActiveWheelBlock = nullptr;
+ mActiveDragBlock = nullptr;
+ mActivePanGestureBlock = nullptr;
+ mActivePinchGestureBlock = nullptr;
+ mActiveKeyboardBlock = nullptr;
+ mLastActiveApzc = nullptr;
+}
+
+InputQueue::AutoRunImmediateTimeout::AutoRunImmediateTimeout(InputQueue* aQueue)
+ : mQueue(aQueue) {
+ MOZ_ASSERT(!mQueue->mImmediateTimeout);
+}
+
+InputQueue::AutoRunImmediateTimeout::~AutoRunImmediateTimeout() {
+ if (mQueue->mImmediateTimeout) {
+ mQueue->mImmediateTimeout->Run();
+ mQueue->mImmediateTimeout = nullptr;
+ }
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/apz/src/InputQueue.h b/gfx/layers/apz/src/InputQueue.h
new file mode 100644
index 0000000000..8a015c24f3
--- /dev/null
+++ b/gfx/layers/apz/src/InputQueue.h
@@ -0,0 +1,277 @@
+/* -*- 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_InputQueue_h
+#define mozilla_layers_InputQueue_h
+
+#include "APZUtils.h"
+#include "DragTracker.h"
+#include "InputData.h"
+#include "mozilla/EventForwards.h"
+#include "mozilla/layers/TouchCounter.h"
+#include "mozilla/RefPtr.h"
+#include "mozilla/UniquePtr.h"
+#include "nsTArray.h"
+
+#include <unordered_map>
+
+namespace mozilla {
+
+class InputData;
+class MultiTouchInput;
+class ScrollWheelInput;
+
+namespace layers {
+
+class AsyncPanZoomController;
+class InputBlockState;
+class CancelableBlockState;
+class TouchBlockState;
+class WheelBlockState;
+class DragBlockState;
+class PanGestureBlockState;
+class PinchGestureBlockState;
+class KeyboardBlockState;
+class AsyncDragMetrics;
+class QueuedInput;
+struct APZEventResult;
+struct APZHandledResult;
+enum class BrowserGestureResponse : bool;
+
+using InputBlockCallback = std::function<void(uint64_t aInputBlockId,
+ APZHandledResult aHandledResult)>;
+
+struct InputBlockCallbackInfo {
+ nsEventStatus mEagerStatus;
+ InputBlockCallback mCallback;
+};
+
+/**
+ * This class stores incoming input events, associated with "input blocks",
+ * until they are ready for handling.
+ */
+class InputQueue {
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(InputQueue)
+
+ public:
+ InputQueue();
+
+ /**
+ * Notifies the InputQueue of a new incoming input event. The APZC that the
+ * input event was targeted to should be provided in the |aTarget| parameter.
+ * See the documentation on APZCTreeManager::ReceiveInputEvent for info on
+ * return values from this function.
+ */
+ APZEventResult ReceiveInputEvent(
+ const RefPtr<AsyncPanZoomController>& aTarget,
+ TargetConfirmationFlags aFlags, InputData& aEvent,
+ const Maybe<nsTArray<TouchBehaviorFlags>>& aTouchBehaviors = Nothing());
+ /**
+ * This function should be invoked to notify the InputQueue when web content
+ * decides whether or not it wants to cancel a block of events. The block
+ * id to which this applies should be provided in |aInputBlockId|.
+ */
+ void ContentReceivedInputBlock(uint64_t aInputBlockId, bool aPreventDefault);
+ /**
+ * This function should be invoked to notify the InputQueue once the target
+ * APZC to handle an input block has been confirmed. In practice this should
+ * generally be decidable upon receipt of the input event, but in some cases
+ * we may need to query the layout engine to know for sure. The input block
+ * this applies to should be specified via the |aInputBlockId| parameter.
+ */
+ void SetConfirmedTargetApzc(
+ uint64_t aInputBlockId,
+ const RefPtr<AsyncPanZoomController>& aTargetApzc);
+ /**
+ * This function is invoked to confirm that the drag block should be handled
+ * by the APZ.
+ */
+ void ConfirmDragBlock(uint64_t aInputBlockId,
+ const RefPtr<AsyncPanZoomController>& aTargetApzc,
+ const AsyncDragMetrics& aDragMetrics);
+ /**
+ * This function should be invoked to notify the InputQueue of the touch-
+ * action properties for the different touch points in an input block. The
+ * input block this applies to should be specified by the |aInputBlockId|
+ * parameter. If touch-action is not enabled on the platform, this function
+ * does nothing and need not be called.
+ */
+ void SetAllowedTouchBehavior(uint64_t aInputBlockId,
+ const nsTArray<TouchBehaviorFlags>& aBehaviors);
+ /**
+ * Adds a new touch block at the end of the input queue that has the same
+ * allowed touch behaviour flags as the the touch block currently being
+ * processed. This should only be called when processing of a touch block
+ * triggers the creation of a new touch block. Returns the input block id
+ * of the the newly-created block.
+ */
+ uint64_t InjectNewTouchBlock(AsyncPanZoomController* aTarget);
+ /**
+ * Returns the pending input block at the head of the queue, if there is one.
+ * This may return null if there all input events have been processed.
+ */
+ InputBlockState* GetCurrentBlock() const;
+ /*
+ * Returns the current pending input block as a specific kind of block. If
+ * GetCurrentBlock() returns null, these functions additionally check the
+ * mActiveXXXBlock field of the corresponding input type to see if there is
+ * a depleted but still active input block, and returns that if found. These
+ * functions may return null if no block is found.
+ */
+ TouchBlockState* GetCurrentTouchBlock() const;
+ WheelBlockState* GetCurrentWheelBlock() const;
+ DragBlockState* GetCurrentDragBlock() const;
+ PanGestureBlockState* GetCurrentPanGestureBlock() const;
+ PinchGestureBlockState* GetCurrentPinchGestureBlock() const;
+ KeyboardBlockState* GetCurrentKeyboardBlock() const;
+ /**
+ * Returns true iff the pending block at the head of the queue is a touch
+ * block and is ready for handling.
+ */
+ bool HasReadyTouchBlock() const;
+ /**
+ * If there is an active wheel transaction, returns the WheelBlockState
+ * representing the transaction. Otherwise, returns null. "Active" in this
+ * function name is the same kind of "active" as in mActiveWheelBlock - that
+ * is, new incoming wheel events will go into the "active" block.
+ */
+ WheelBlockState* GetActiveWheelTransaction() const;
+ /**
+ * Remove all input blocks from the input queue.
+ */
+ void Clear();
+ /**
+ * Whether the current pending block allows scroll handoff.
+ */
+ bool AllowScrollHandoff() const;
+ /**
+ * If there is currently a drag in progress, return whether or not it was
+ * targeted at a scrollbar. If the drag was newly-created and doesn't know,
+ * use the provided |aOnScrollbar| to populate that information.
+ */
+ bool IsDragOnScrollbar(bool aOnScrollbar);
+
+ InputBlockState* GetBlockForId(uint64_t aInputBlockId);
+
+ void AddInputBlockCallback(uint64_t aInputBlockId,
+ InputBlockCallbackInfo&& aCallback);
+
+ void SetBrowserGestureResponse(uint64_t aInputBlockId,
+ BrowserGestureResponse aResponse);
+
+ private:
+ ~InputQueue();
+
+ // RAII class for automatically running a timeout task that may
+ // need to be run immediately after an event has been queued.
+ class AutoRunImmediateTimeout final {
+ public:
+ explicit AutoRunImmediateTimeout(InputQueue* aQueue);
+ ~AutoRunImmediateTimeout();
+
+ private:
+ InputQueue* mQueue;
+ };
+
+ TouchBlockState* StartNewTouchBlock(
+ const RefPtr<AsyncPanZoomController>& aTarget,
+ TargetConfirmationFlags aFlags, bool aCopyPropertiesFromCurrent);
+
+ /**
+ * If animations are present for the current pending input block, cancel
+ * them as soon as possible.
+ */
+ void CancelAnimationsForNewBlock(InputBlockState* aBlock,
+ CancelAnimationFlags aExtraFlags = Default);
+
+ /**
+ * If we need to wait for a content response, schedule that now. Returns true
+ * if the timeout was scheduled, false otherwise.
+ */
+ bool MaybeRequestContentResponse(
+ const RefPtr<AsyncPanZoomController>& aTarget,
+ CancelableBlockState* aBlock);
+
+ APZEventResult ReceiveTouchInput(
+ const RefPtr<AsyncPanZoomController>& aTarget,
+ TargetConfirmationFlags aFlags, const MultiTouchInput& aEvent,
+ const Maybe<nsTArray<TouchBehaviorFlags>>& aTouchBehaviors);
+ APZEventResult ReceiveMouseInput(
+ const RefPtr<AsyncPanZoomController>& aTarget,
+ TargetConfirmationFlags aFlags, MouseInput& aEvent);
+ APZEventResult ReceiveScrollWheelInput(
+ const RefPtr<AsyncPanZoomController>& aTarget,
+ TargetConfirmationFlags aFlags, const ScrollWheelInput& aEvent);
+ APZEventResult ReceivePanGestureInput(
+ const RefPtr<AsyncPanZoomController>& aTarget,
+ TargetConfirmationFlags aFlags, const PanGestureInput& aEvent);
+ APZEventResult ReceivePinchGestureInput(
+ const RefPtr<AsyncPanZoomController>& aTarget,
+ TargetConfirmationFlags aFlags, const PinchGestureInput& aEvent);
+ APZEventResult ReceiveKeyboardInput(
+ const RefPtr<AsyncPanZoomController>& aTarget,
+ TargetConfirmationFlags aFlags, const KeyboardInput& aEvent);
+
+ /**
+ * Helper function that searches mQueuedInputs for the first block matching
+ * the given id, and returns it. If |aOutFirstInput| is non-null, it is
+ * populated with a pointer to the first input in mQueuedInputs that
+ * corresponds to the block, or null if no such input was found. Note that
+ * even if there are no inputs in mQueuedInputs, this function can return
+ * non-null if the block id provided matches one of the depleted-but-still-
+ * active blocks (mActiveTouchBlock, mActiveWheelBlock, etc.).
+ */
+ InputBlockState* FindBlockForId(uint64_t aInputBlockId,
+ InputData** aOutFirstInput);
+ void ScheduleMainThreadTimeout(const RefPtr<AsyncPanZoomController>& aTarget,
+ CancelableBlockState* aBlock);
+ void MainThreadTimeout(uint64_t aInputBlockId);
+ void MaybeLongTapTimeout(uint64_t aInputBlockId);
+ void ProcessQueue();
+ bool CanDiscardBlock(InputBlockState* aBlock);
+ void UpdateActiveApzc(const RefPtr<AsyncPanZoomController>& aNewActive);
+
+ private:
+ // The queue of input events that have not yet been fully processed.
+ // This member must only be accessed on the controller/UI thread.
+ nsTArray<UniquePtr<QueuedInput>> mQueuedInputs;
+
+ // These are the most recently created blocks of each input type. They are
+ // "active" in the sense that new inputs of that type are associated with
+ // them. Note that these pointers may be null if no inputs of the type have
+ // arrived, or if the inputs for the type formed a complete block that was
+ // then discarded.
+ RefPtr<TouchBlockState> mActiveTouchBlock;
+ RefPtr<WheelBlockState> mActiveWheelBlock;
+ RefPtr<DragBlockState> mActiveDragBlock;
+ RefPtr<PanGestureBlockState> mActivePanGestureBlock;
+ RefPtr<PinchGestureBlockState> mActivePinchGestureBlock;
+ RefPtr<KeyboardBlockState> mActiveKeyboardBlock;
+
+ // The APZC to which the last event was delivered
+ RefPtr<AsyncPanZoomController> mLastActiveApzc;
+
+ // Track touches so we know when to clear mLastActiveApzc
+ TouchCounter mTouchCounter;
+
+ // Track mouse inputs so we know if we're in a drag or not
+ DragTracker mDragTracker;
+
+ // Temporarily stores a timeout task that needs to be run as soon as
+ // as the event that triggered it has been queued.
+ RefPtr<Runnable> mImmediateTimeout;
+
+ // Maps input block ids to callbacks that will be invoked when the input block
+ // is ready for handling.
+ using InputBlockCallbackMap =
+ std::unordered_map<uint64_t, InputBlockCallbackInfo>;
+ InputBlockCallbackMap mInputBlockCallbacks;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif // mozilla_layers_InputQueue_h
diff --git a/gfx/layers/apz/src/KeyboardMap.cpp b/gfx/layers/apz/src/KeyboardMap.cpp
new file mode 100644
index 0000000000..9444037be6
--- /dev/null
+++ b/gfx/layers/apz/src/KeyboardMap.cpp
@@ -0,0 +1,170 @@
+/* -*- 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 "mozilla/layers/KeyboardMap.h"
+
+#include "mozilla/TextEvents.h" // for IgnoreModifierState, ShortcutKeyCandidate
+
+namespace mozilla {
+namespace layers {
+
+KeyboardShortcut::KeyboardShortcut()
+ : mKeyCode(0),
+ mCharCode(0),
+ mModifiers(0),
+ mModifiersMask(0),
+ mEventType(KeyboardInput::KeyboardEventType::KEY_OTHER),
+ mDispatchToContent(false) {}
+
+KeyboardShortcut::KeyboardShortcut(KeyboardInput::KeyboardEventType aEventType,
+ uint32_t aKeyCode, uint32_t aCharCode,
+ Modifiers aModifiers,
+ Modifiers aModifiersMask,
+ const KeyboardScrollAction& aAction)
+ : mAction(aAction),
+ mKeyCode(aKeyCode),
+ mCharCode(aCharCode),
+ mModifiers(aModifiers),
+ mModifiersMask(aModifiersMask),
+ mEventType(aEventType),
+ mDispatchToContent(false) {}
+
+KeyboardShortcut::KeyboardShortcut(KeyboardInput::KeyboardEventType aEventType,
+ uint32_t aKeyCode, uint32_t aCharCode,
+ Modifiers aModifiers,
+ Modifiers aModifiersMask)
+ : mKeyCode(aKeyCode),
+ mCharCode(aCharCode),
+ mModifiers(aModifiers),
+ mModifiersMask(aModifiersMask),
+ mEventType(aEventType),
+ mDispatchToContent(true) {}
+
+/* static */
+void KeyboardShortcut::AppendHardcodedShortcuts(
+ nsTArray<KeyboardShortcut>& aShortcuts) {
+ // Tab
+ KeyboardShortcut tab1;
+ tab1.mDispatchToContent = true;
+ tab1.mKeyCode = NS_VK_TAB;
+ tab1.mCharCode = 0;
+ tab1.mModifiers = 0;
+ tab1.mModifiersMask = 0;
+ tab1.mEventType = KeyboardInput::KEY_PRESS;
+ aShortcuts.AppendElement(tab1);
+
+ // F6
+ KeyboardShortcut tab2;
+ tab2.mDispatchToContent = true;
+ tab2.mKeyCode = NS_VK_F6;
+ tab2.mCharCode = 0;
+ tab2.mModifiers = 0;
+ tab2.mModifiersMask = 0;
+ tab2.mEventType = KeyboardInput::KEY_PRESS;
+ aShortcuts.AppendElement(tab2);
+}
+
+bool KeyboardShortcut::Matches(const KeyboardInput& aInput,
+ const IgnoreModifierState& aIgnore,
+ uint32_t aOverrideCharCode) const {
+ return mEventType == aInput.mType && MatchesKey(aInput, aOverrideCharCode) &&
+ MatchesModifiers(aInput, aIgnore);
+}
+
+bool KeyboardShortcut::MatchesKey(const KeyboardInput& aInput,
+ uint32_t aOverrideCharCode) const {
+ // Compare by the key code if we have one
+ if (!mCharCode) {
+ return mKeyCode == aInput.mKeyCode;
+ }
+
+ // We are comparing by char code
+ uint32_t charCode;
+
+ // If we are comparing against a shortcut candidate then we might
+ // have an override char code
+ if (aOverrideCharCode) {
+ charCode = aOverrideCharCode;
+ } else {
+ charCode = aInput.mCharCode;
+ }
+
+ // Both char codes must be in lowercase to compare correctly
+ if (IS_IN_BMP(charCode)) {
+ charCode = ToLowerCase(static_cast<char16_t>(charCode));
+ }
+
+ return mCharCode == charCode;
+}
+
+bool KeyboardShortcut::MatchesModifiers(
+ const KeyboardInput& aInput, const IgnoreModifierState& aIgnore) const {
+ Modifiers modifiersMask = mModifiersMask;
+
+ // If we are ignoring Shift or OS, then unset that part of the mask
+ if (aIgnore.mOS) {
+ modifiersMask &= ~MODIFIER_OS;
+ }
+ if (aIgnore.mShift) {
+ modifiersMask &= ~MODIFIER_SHIFT;
+ }
+
+ // Mask off the modifiers we are ignoring from the keyboard input
+ return (aInput.modifiers & modifiersMask) == mModifiers;
+}
+
+KeyboardMap::KeyboardMap(nsTArray<KeyboardShortcut>&& aShortcuts)
+ : mShortcuts(aShortcuts) {}
+
+KeyboardMap::KeyboardMap() = default;
+
+Maybe<KeyboardShortcut> KeyboardMap::FindMatch(
+ const KeyboardInput& aEvent) const {
+ // If there are no shortcut candidates, then just search with with the
+ // keyboard input
+ if (aEvent.mShortcutCandidates.IsEmpty()) {
+ return FindMatchInternal(aEvent, IgnoreModifierState());
+ }
+
+ // Otherwise do a search with each shortcut candidate in order
+ for (auto& key : aEvent.mShortcutCandidates) {
+ IgnoreModifierState ignoreModifierState;
+ ignoreModifierState.mShift = key.mIgnoreShift;
+
+ auto match = FindMatchInternal(aEvent, ignoreModifierState, key.mCharCode);
+ if (match) {
+ return match;
+ }
+ }
+ return Nothing();
+}
+
+Maybe<KeyboardShortcut> KeyboardMap::FindMatchInternal(
+ const KeyboardInput& aEvent, const IgnoreModifierState& aIgnore,
+ uint32_t aOverrideCharCode) const {
+ for (auto& shortcut : mShortcuts) {
+ if (shortcut.Matches(aEvent, aIgnore, aOverrideCharCode)) {
+ return Some(shortcut);
+ }
+ }
+
+#ifdef XP_WIN
+ // Windows native applications ignore Windows-Logo key state when checking
+ // shortcut keys even if the key is pressed. Therefore, if there is no
+ // shortcut key which exactly matches current modifier state, we should
+ // retry to look for a shortcut key without the Windows-Logo key press.
+ if (!aIgnore.mOS && (aEvent.modifiers & MODIFIER_OS)) {
+ IgnoreModifierState ignoreModifierState(aIgnore);
+ ignoreModifierState.mOS = true;
+ return FindMatchInternal(aEvent, ignoreModifierState, aOverrideCharCode);
+ }
+#endif
+
+ return Nothing();
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/apz/src/KeyboardMap.h b/gfx/layers/apz/src/KeyboardMap.h
new file mode 100644
index 0000000000..32ec8ea61d
--- /dev/null
+++ b/gfx/layers/apz/src/KeyboardMap.h
@@ -0,0 +1,118 @@
+/* -*- 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_KeyboardMap_h
+#define mozilla_layers_KeyboardMap_h
+
+#include <stdint.h> // for uint32_t
+
+#include "InputData.h" // for KeyboardInput
+#include "nsIScrollableFrame.h" // for nsIScrollableFrame::ScrollUnit
+#include "nsTArray.h" // for nsTArray
+#include "mozilla/Maybe.h" // for mozilla::Maybe
+#include "KeyboardScrollAction.h" // for KeyboardScrollAction
+
+namespace mozilla {
+
+struct IgnoreModifierState;
+
+namespace layers {
+
+class KeyboardMap;
+
+/**
+ * This class is an off main-thread <xul:handler> for scrolling commands.
+ */
+class KeyboardShortcut final {
+ public:
+ KeyboardShortcut();
+
+ /**
+ * Create a keyboard shortcut that when matched can be handled by executing
+ * the specified keyboard action.
+ */
+ KeyboardShortcut(KeyboardInput::KeyboardEventType aEventType,
+ uint32_t aKeyCode, uint32_t aCharCode, Modifiers aModifiers,
+ Modifiers aModifiersMask,
+ const KeyboardScrollAction& aAction);
+
+ /**
+ * Create a keyboard shortcut that when matched should be handled by ignoring
+ * the keyboard event and dispatching it to content.
+ */
+ KeyboardShortcut(KeyboardInput::KeyboardEventType aEventType,
+ uint32_t aKeyCode, uint32_t aCharCode, Modifiers aModifiers,
+ Modifiers aModifiersMask);
+
+ /**
+ * There are some default actions for keyboard inputs that are hardcoded in
+ * EventStateManager instead of being represented as XBL handlers. This adds
+ * keyboard shortcuts to match these inputs and dispatch them to content.
+ */
+ static void AppendHardcodedShortcuts(nsTArray<KeyboardShortcut>& aShortcuts);
+
+ protected:
+ friend mozilla::layers::KeyboardMap;
+
+ bool Matches(const KeyboardInput& aInput, const IgnoreModifierState& aIgnore,
+ uint32_t aOverrideCharCode = 0) const;
+
+ private:
+ bool MatchesKey(const KeyboardInput& aInput,
+ uint32_t aOverrideCharCode) const;
+ bool MatchesModifiers(const KeyboardInput& aInput,
+ const IgnoreModifierState& aIgnore) const;
+
+ public:
+ // The action to perform when this shortcut is matched,
+ // and not flagged to be dispatched to content
+ KeyboardScrollAction mAction;
+
+ // Only one of mKeyCode or mCharCode may be non-zero
+ // whichever one is non-zero is the one to compare when matching
+ uint32_t mKeyCode;
+ uint32_t mCharCode;
+
+ // The modifiers that must be active for this shortcut
+ Modifiers mModifiers;
+ // The modifiers to compare when matching this shortcut
+ Modifiers mModifiersMask;
+
+ // The type of keyboard event to match against
+ KeyboardInput::KeyboardEventType mEventType;
+
+ // Whether events matched by this must be dispatched to content
+ bool mDispatchToContent;
+};
+
+/**
+ * A keyboard map is an off main-thread <xul:binding> for scrolling commands.
+ */
+class KeyboardMap final {
+ public:
+ KeyboardMap();
+ explicit KeyboardMap(nsTArray<KeyboardShortcut>&& aShortcuts);
+
+ const nsTArray<KeyboardShortcut>& Shortcuts() const { return mShortcuts; }
+
+ /**
+ * Search through the internal list of shortcuts for a match for the input
+ * event
+ */
+ Maybe<KeyboardShortcut> FindMatch(const KeyboardInput& aEvent) const;
+
+ private:
+ Maybe<KeyboardShortcut> FindMatchInternal(
+ const KeyboardInput& aEvent, const IgnoreModifierState& aIgnore,
+ uint32_t aOverrideCharCode = 0) const;
+
+ CopyableTArray<KeyboardShortcut> mShortcuts;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif // mozilla_layers_KeyboardMap_h
diff --git a/gfx/layers/apz/src/KeyboardScrollAction.cpp b/gfx/layers/apz/src/KeyboardScrollAction.cpp
new file mode 100644
index 0000000000..42d9a8bff2
--- /dev/null
+++ b/gfx/layers/apz/src/KeyboardScrollAction.cpp
@@ -0,0 +1,37 @@
+/* -*- 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 "mozilla/layers/KeyboardScrollAction.h"
+
+namespace mozilla {
+namespace layers {
+
+/* static */ ScrollUnit KeyboardScrollAction::GetScrollUnit(
+ KeyboardScrollAction::KeyboardScrollActionType aDeltaType) {
+ switch (aDeltaType) {
+ case KeyboardScrollAction::eScrollCharacter:
+ return ScrollUnit::LINES;
+ case KeyboardScrollAction::eScrollLine:
+ return ScrollUnit::LINES;
+ case KeyboardScrollAction::eScrollPage:
+ return ScrollUnit::PAGES;
+ case KeyboardScrollAction::eScrollComplete:
+ return ScrollUnit::WHOLE;
+ }
+
+ // Silence an overzealous warning
+ return ScrollUnit::WHOLE;
+}
+
+KeyboardScrollAction::KeyboardScrollAction()
+ : mType(KeyboardScrollAction::eScrollCharacter), mForward(false) {}
+
+KeyboardScrollAction::KeyboardScrollAction(KeyboardScrollActionType aType,
+ bool aForward)
+ : mType(aType), mForward(aForward) {}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/apz/src/KeyboardScrollAction.h b/gfx/layers/apz/src/KeyboardScrollAction.h
new file mode 100644
index 0000000000..780006c1b3
--- /dev/null
+++ b/gfx/layers/apz/src/KeyboardScrollAction.h
@@ -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/. */
+
+#ifndef mozilla_layers_KeyboardScrollAction_h
+#define mozilla_layers_KeyboardScrollAction_h
+
+#include <cstdint> // for uint8_t
+
+#include "mozilla/ScrollTypes.h"
+#include "mozilla/DefineEnum.h" // for MOZ_DEFINE_ENUM
+
+namespace mozilla {
+namespace layers {
+
+/**
+ * This class represents a scrolling action to be performed on a scrollable
+ * layer.
+ */
+struct KeyboardScrollAction final {
+ public:
+ // clang-format off
+ MOZ_DEFINE_ENUM_WITH_BASE_AT_CLASS_SCOPE(
+ KeyboardScrollActionType, uint8_t, (
+ eScrollCharacter,
+ eScrollLine,
+ eScrollPage,
+ eScrollComplete
+ ));
+ // clang-format on
+
+ static ScrollUnit GetScrollUnit(KeyboardScrollActionType aDeltaType);
+
+ KeyboardScrollAction();
+ KeyboardScrollAction(KeyboardScrollActionType aType, bool aForward);
+
+ // The type of scroll to perform for this action
+ KeyboardScrollActionType mType;
+ // Whether to scroll forward or backward along the axis of this action type
+ bool mForward;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif // mozilla_layers_KeyboardScrollAction_h
diff --git a/gfx/layers/apz/src/Overscroll.h b/gfx/layers/apz/src/Overscroll.h
new file mode 100644
index 0000000000..1fb7c3e487
--- /dev/null
+++ b/gfx/layers/apz/src/Overscroll.h
@@ -0,0 +1,250 @@
+/* -*- 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_Overscroll_h
+#define mozilla_layers_Overscroll_h
+
+#include "AsyncPanZoomAnimation.h"
+#include "AsyncPanZoomController.h"
+#include "mozilla/TimeStamp.h"
+#include "nsThreadUtils.h"
+
+namespace mozilla {
+namespace layers {
+
+// Animation used by GenericOverscrollEffect.
+class OverscrollAnimation : public AsyncPanZoomAnimation {
+ public:
+ OverscrollAnimation(AsyncPanZoomController& aApzc,
+ const ParentLayerPoint& aVelocity,
+ SideBits aOverscrollSideBits)
+ : mApzc(aApzc), mOverscrollSideBits(aOverscrollSideBits) {
+ MOZ_ASSERT(
+ (mOverscrollSideBits & SideBits::eTopBottom) != SideBits::eTopBottom &&
+ (mOverscrollSideBits & SideBits::eLeftRight) !=
+ SideBits::eLeftRight,
+ "Don't allow overscrolling on both sides at the same time");
+ if ((aOverscrollSideBits & SideBits::eLeftRight) != SideBits::eNone) {
+ mApzc.mX.StartOverscrollAnimation(aVelocity.x);
+ }
+ if ((aOverscrollSideBits & SideBits::eTopBottom) != SideBits::eNone) {
+ mApzc.mY.StartOverscrollAnimation(aVelocity.y);
+ }
+ }
+ virtual ~OverscrollAnimation() {
+ mApzc.mX.EndOverscrollAnimation();
+ mApzc.mY.EndOverscrollAnimation();
+ }
+
+ virtual bool DoSample(FrameMetrics& aFrameMetrics,
+ const TimeDuration& aDelta) override {
+ // Can't inline these variables due to short-circuit evaluation.
+ bool continueX = mApzc.mX.IsOverscrollAnimationAlive() &&
+ mApzc.mX.SampleOverscrollAnimation(
+ aDelta, mOverscrollSideBits & SideBits::eLeftRight);
+ bool continueY = mApzc.mY.IsOverscrollAnimationAlive() &&
+ mApzc.mY.SampleOverscrollAnimation(
+ aDelta, mOverscrollSideBits & SideBits::eTopBottom);
+ if (!continueX && !continueY) {
+ // If we got into overscroll from a fling, that fling did not request a
+ // fling snap to avoid a resulting scrollTo from cancelling the overscroll
+ // animation too early. We do still want to request a fling snap, though,
+ // in case the end of the axis at which we're overscrolled is not a valid
+ // snap point, so we request one now. If there are no snap points, this
+ // will do nothing. If there are snap points, we'll get a scrollTo that
+ // snaps us back to the nearest valid snap point. The scroll snapping is
+ // done in a deferred task, otherwise the state change to NOTHING caused
+ // by the overscroll animation ending would clobber a possible state
+ // change to SMOOTH_SCROLL in ScrollSnap().
+ mDeferredTasks.AppendElement(NewRunnableMethod<ScrollSnapFlags>(
+ "layers::AsyncPanZoomController::ScrollSnap", &mApzc,
+ &AsyncPanZoomController::ScrollSnap,
+ ScrollSnapFlags::IntendedDirection |
+ ScrollSnapFlags::IntendedEndPosition));
+ return false;
+ }
+ return true;
+ }
+
+ virtual bool WantsRepaints() override { return false; }
+
+ // Tell the overscroll animation about the pan momentum event. For each axis,
+ // the overscroll animation may start, stop, or continue managing that axis in
+ // response to the pan momentum event
+ void HandlePanMomentum(const ParentLayerPoint& aDisplacement) {
+ float xOverscroll = mApzc.mX.GetOverscroll();
+ if ((xOverscroll > 0 && aDisplacement.x > 0) ||
+ (xOverscroll < 0 && aDisplacement.x < 0)) {
+ if (!mApzc.mX.IsOverscrollAnimationRunning()) {
+ // Start a new overscroll animation on this axis, if there is no
+ // overscroll animation running and if the pan momentum displacement
+ // the pan momentum displacement is the same direction of the current
+ // overscroll.
+ mApzc.mX.StartOverscrollAnimation(mApzc.mX.GetVelocity());
+ mOverscrollSideBits |=
+ xOverscroll > 0 ? SideBits::eRight : SideBits::eLeft;
+ }
+ } else if ((xOverscroll > 0 && aDisplacement.x < 0) ||
+ (xOverscroll < 0 && aDisplacement.x > 0)) {
+ // Otherwise, stop the animation in the direction so that it won't clobber
+ // subsequent pan momentum scrolling.
+ mApzc.mX.EndOverscrollAnimation();
+ }
+
+ // Same as above but for Y axis.
+ float yOverscroll = mApzc.mY.GetOverscroll();
+ if ((yOverscroll > 0 && aDisplacement.y > 0) ||
+ (yOverscroll < 0 && aDisplacement.y < 0)) {
+ if (!mApzc.mY.IsOverscrollAnimationRunning()) {
+ mApzc.mY.StartOverscrollAnimation(mApzc.mY.GetVelocity());
+ mOverscrollSideBits |=
+ yOverscroll > 0 ? SideBits::eBottom : SideBits::eTop;
+ }
+ } else if ((yOverscroll > 0 && aDisplacement.y < 0) ||
+ (yOverscroll < 0 && aDisplacement.y > 0)) {
+ mApzc.mY.EndOverscrollAnimation();
+ }
+ }
+
+ ScrollDirections GetDirections() const {
+ ScrollDirections directions;
+ if (mApzc.mX.IsOverscrollAnimationRunning()) {
+ directions += ScrollDirection::eHorizontal;
+ }
+ if (mApzc.mY.IsOverscrollAnimationRunning()) {
+ directions += ScrollDirection::eVertical;
+ }
+ return directions;
+ };
+
+ OverscrollAnimation* AsOverscrollAnimation() override { return this; }
+
+ bool IsManagingXAxis() const {
+ return mApzc.mX.IsOverscrollAnimationRunning();
+ }
+ bool IsManagingYAxis() const {
+ return mApzc.mY.IsOverscrollAnimationRunning();
+ }
+
+ private:
+ AsyncPanZoomController& mApzc;
+ SideBits mOverscrollSideBits;
+};
+
+// Base class for different overscroll effects;
+class OverscrollEffectBase {
+ public:
+ virtual ~OverscrollEffectBase() = default;
+
+ // Try to increase the amount of overscroll by |aOverscroll|. Limited to
+ // directions contained in |aOverscrollableDirections|. Components of
+ // |aOverscroll| in directions that are successfully consumed are dropped.
+ virtual void ConsumeOverscroll(
+ ParentLayerPoint& aOverscroll,
+ ScrollDirections aOverscrollableDirections) = 0;
+
+ // Relieve overscroll. Depending on the implementation, the relief may
+ // be immediate, or gradual (e.g. after an animation) but this starts
+ // the process. |aVelocity| is the current velocity of the APZC, and
+ // |aOverscrollSideBits| contains the side(s) at which the APZC is
+ // overscrolled.
+ virtual void RelieveOverscroll(const ParentLayerPoint& aVelocity,
+ SideBits aOverscrollSideBits) = 0;
+
+ virtual bool IsOverscrolled() const = 0;
+
+ // Similarly to RelieveOverscroll(), but has immediate effect
+ // (no animation).
+ virtual void ClearOverscroll() = 0;
+};
+
+// A generic overscroll effect, implemented by AsyncPanZoomController itself.
+class GenericOverscrollEffect : public OverscrollEffectBase {
+ public:
+ explicit GenericOverscrollEffect(AsyncPanZoomController& aApzc)
+ : mApzc(aApzc) {}
+
+ void ConsumeOverscroll(ParentLayerPoint& aOverscroll,
+ ScrollDirections aOverscrollableDirections) override {
+ if (aOverscrollableDirections.contains(ScrollDirection::eHorizontal)) {
+ mApzc.mX.OverscrollBy(aOverscroll.x);
+ aOverscroll.x = 0;
+ }
+
+ if (aOverscrollableDirections.contains(ScrollDirection::eVertical)) {
+ mApzc.mY.OverscrollBy(aOverscroll.y);
+ aOverscroll.y = 0;
+ }
+
+ if (!aOverscrollableDirections.isEmpty()) {
+ mApzc.ScheduleComposite();
+ }
+ }
+
+ void RelieveOverscroll(const ParentLayerPoint& aVelocity,
+ SideBits aOverscrollSideBits) override {
+ mApzc.StartOverscrollAnimation(aVelocity, aOverscrollSideBits);
+ }
+
+ bool IsOverscrolled() const override {
+ return mApzc.IsPhysicallyOverscrolled();
+ }
+
+ void ClearOverscroll() override { mApzc.ClearPhysicalOverscroll(); }
+
+ private:
+ AsyncPanZoomController& mApzc;
+};
+
+// A widget-specific overscroll effect, implemented by the widget via
+// GeckoContentController.
+class WidgetOverscrollEffect : public OverscrollEffectBase {
+ public:
+ explicit WidgetOverscrollEffect(AsyncPanZoomController& aApzc)
+ : mApzc(aApzc), mIsOverscrolled(false) {}
+
+ void ConsumeOverscroll(ParentLayerPoint& aOverscroll,
+ ScrollDirections aOverscrollableDirections) override {
+ RefPtr<GeckoContentController> controller =
+ mApzc.GetGeckoContentController();
+ if (controller && !aOverscrollableDirections.isEmpty()) {
+ mIsOverscrolled = true;
+ controller->UpdateOverscrollOffset(mApzc.GetGuid(), aOverscroll.x,
+ aOverscroll.y, mApzc.IsRootContent());
+ aOverscroll = ParentLayerPoint();
+ }
+ }
+
+ void RelieveOverscroll(const ParentLayerPoint& aVelocity,
+ SideBits aOverscrollSideBits) override {
+ RefPtr<GeckoContentController> controller =
+ mApzc.GetGeckoContentController();
+ // From APZC's point of view, consider it to no longer be overscrolled
+ // as soon as RelieveOverscroll() is called. The widget may use a
+ // delay or animation until the relieving of the overscroll is complete,
+ // but we don't have any insight into that.
+ mIsOverscrolled = false;
+ if (controller) {
+ controller->UpdateOverscrollVelocity(mApzc.GetGuid(), aVelocity.x,
+ aVelocity.y, mApzc.IsRootContent());
+ }
+ }
+
+ bool IsOverscrolled() const override { return mIsOverscrolled; }
+
+ void ClearOverscroll() override {
+ RelieveOverscroll(ParentLayerPoint(), SideBits() /* ignored */);
+ }
+
+ private:
+ AsyncPanZoomController& mApzc;
+ bool mIsOverscrolled;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif // mozilla_layers_Overscroll_h
diff --git a/gfx/layers/apz/src/OverscrollHandoffState.cpp b/gfx/layers/apz/src/OverscrollHandoffState.cpp
new file mode 100644
index 0000000000..ed38f6fa7a
--- /dev/null
+++ b/gfx/layers/apz/src/OverscrollHandoffState.cpp
@@ -0,0 +1,228 @@
+/* -*- 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 "OverscrollHandoffState.h"
+
+#include <algorithm> // for std::stable_sort
+#include "mozilla/Assertions.h"
+#include "mozilla/FloatingPoint.h"
+#include "AsyncPanZoomController.h"
+
+namespace mozilla {
+namespace layers {
+
+OverscrollHandoffChain::~OverscrollHandoffChain() = default;
+
+void OverscrollHandoffChain::Add(AsyncPanZoomController* aApzc) {
+ mChain.push_back(aApzc);
+}
+
+struct CompareByScrollPriority {
+ bool operator()(const RefPtr<AsyncPanZoomController>& a,
+ const RefPtr<AsyncPanZoomController>& b) const {
+ return a->HasScrollgrab() && !b->HasScrollgrab();
+ }
+};
+
+void OverscrollHandoffChain::SortByScrollPriority() {
+ // The sorting being stable ensures that the relative order between
+ // non-scrollgrabbing APZCs remains child -> parent.
+ // (The relative order between scrollgrabbing APZCs will also remain
+ // child -> parent, though that's just an artefact of the implementation
+ // and users of 'scrollgrab' should not rely on this.)
+ std::stable_sort(mChain.begin(), mChain.end(), CompareByScrollPriority());
+}
+
+const RefPtr<AsyncPanZoomController>& OverscrollHandoffChain::GetApzcAtIndex(
+ uint32_t aIndex) const {
+ MOZ_ASSERT(aIndex < Length());
+ return mChain[aIndex];
+}
+
+uint32_t OverscrollHandoffChain::IndexOf(
+ const AsyncPanZoomController* aApzc) const {
+ uint32_t i;
+ for (i = 0; i < Length(); ++i) {
+ if (mChain[i] == aApzc) {
+ break;
+ }
+ }
+ return i;
+}
+
+void OverscrollHandoffChain::ForEachApzc(APZCMethod aMethod) const {
+ for (uint32_t i = 0; i < Length(); ++i) {
+ (mChain[i]->*aMethod)();
+ }
+}
+
+bool OverscrollHandoffChain::AnyApzc(APZCPredicate aPredicate) const {
+ MOZ_ASSERT(Length() > 0);
+ for (uint32_t i = 0; i < Length(); ++i) {
+ if ((mChain[i]->*aPredicate)()) {
+ return true;
+ }
+ }
+ return false;
+}
+
+void OverscrollHandoffChain::FlushRepaints() const {
+ ForEachApzc(&AsyncPanZoomController::FlushRepaintForOverscrollHandoff);
+}
+
+void OverscrollHandoffChain::CancelAnimations(
+ CancelAnimationFlags aFlags) const {
+ MOZ_ASSERT(Length() > 0);
+ for (uint32_t i = 0; i < Length(); ++i) {
+ mChain[i]->CancelAnimation(aFlags);
+ }
+}
+
+void OverscrollHandoffChain::ClearOverscroll() const {
+ ForEachApzc(&AsyncPanZoomController::ClearOverscroll);
+}
+
+void OverscrollHandoffChain::SnapBackOverscrolledApzc(
+ const AsyncPanZoomController* aStart) const {
+ uint32_t i = IndexOf(aStart);
+ for (; i < Length(); ++i) {
+ AsyncPanZoomController* apzc = mChain[i];
+ if (!apzc->IsDestroyed()) {
+ apzc->SnapBackIfOverscrolled();
+ }
+ }
+}
+
+void OverscrollHandoffChain::SnapBackOverscrolledApzcForMomentum(
+ const AsyncPanZoomController* aStart,
+ const ParentLayerPoint& aVelocity) const {
+ uint32_t i = IndexOf(aStart);
+ for (; i < Length(); ++i) {
+ AsyncPanZoomController* apzc = mChain[i];
+ if (!apzc->IsDestroyed()) {
+ apzc->SnapBackIfOverscrolledForMomentum(aVelocity);
+ }
+ }
+}
+
+bool OverscrollHandoffChain::CanBePanned(
+ const AsyncPanZoomController* aApzc) const {
+ // Find |aApzc| in the handoff chain.
+ uint32_t i = IndexOf(aApzc);
+
+ // See whether any APZC in the handoff chain starting from |aApzc|
+ // has room to be panned.
+ for (uint32_t j = i; j < Length(); ++j) {
+ if (mChain[j]->IsPannable()) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+bool OverscrollHandoffChain::CanScrollInDirection(
+ const AsyncPanZoomController* aApzc, ScrollDirection aDirection) const {
+ // Find |aApzc| in the handoff chain.
+ uint32_t i = IndexOf(aApzc);
+
+ // See whether any APZC in the handoff chain starting from |aApzc|
+ // has room to scroll in the given direction.
+ for (uint32_t j = i; j < Length(); ++j) {
+ if (mChain[j]->CanScroll(aDirection)) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+bool OverscrollHandoffChain::HasOverscrolledApzc() const {
+ return AnyApzc(&AsyncPanZoomController::IsOverscrolled);
+}
+
+bool OverscrollHandoffChain::HasFastFlungApzc() const {
+ return AnyApzc(&AsyncPanZoomController::IsFlingingFast);
+}
+
+bool OverscrollHandoffChain::HasAutoscrollApzc() const {
+ return AnyApzc(&AsyncPanZoomController::IsAutoscroll);
+}
+
+RefPtr<AsyncPanZoomController> OverscrollHandoffChain::FindFirstScrollable(
+ const InputData& aInput, ScrollDirections* aOutAllowedScrollDirections,
+ IncludeOverscroll aIncludeOverscroll) const {
+ // Start by allowing scrolling in both directions. As we do handoff
+ // overscroll-behavior may restrict one or both of the directions.
+ *aOutAllowedScrollDirections += ScrollDirection::eVertical;
+ *aOutAllowedScrollDirections += ScrollDirection::eHorizontal;
+
+ for (size_t i = 0; i < Length(); i++) {
+ if (mChain[i]->CanScroll(aInput)) {
+ return mChain[i];
+ }
+
+ // If there is any directions we allow overscroll effects on the root
+ // content APZC (i.e. the overscroll-behavior of the root one is not
+ // `none`), we consider the APZC can be scrollable in terms of pan gestures
+ // because it causes overscrolling even if it's not able to scroll to the
+ // direction.
+ if (StaticPrefs::apz_overscroll_enabled() && bool(aIncludeOverscroll) &&
+ // FIXME: Bug 1707491: Drop this pan gesture input check.
+ aInput.mInputType == PANGESTURE_INPUT && mChain[i]->IsRootContent()) {
+ // Check whether the root content APZC is also overscrollable governed by
+ // overscroll-behavior in the same directions where we allow scrolling
+ // handoff and where we are going to scroll, if it matches we do handoff
+ // to the root content APZC.
+ // In other words, if the root content is not scrollable, we don't
+ // handoff.
+ ScrollDirections allowedOverscrollDirections =
+ mChain[i]->GetOverscrollableDirections();
+ ParentLayerPoint delta = mChain[i]->GetDeltaForEvent(aInput);
+ if (mChain[i]->IsZero(delta.x)) {
+ allowedOverscrollDirections -= ScrollDirection::eHorizontal;
+ }
+ if (mChain[i]->IsZero(delta.y)) {
+ allowedOverscrollDirections -= ScrollDirection::eVertical;
+ }
+
+ allowedOverscrollDirections &= *aOutAllowedScrollDirections;
+ if (!allowedOverscrollDirections.isEmpty()) {
+ *aOutAllowedScrollDirections = allowedOverscrollDirections;
+ return mChain[i];
+ }
+ }
+
+ *aOutAllowedScrollDirections &= mChain[i]->GetAllowedHandoffDirections();
+ if (aOutAllowedScrollDirections->isEmpty()) {
+ return nullptr;
+ }
+ }
+ return nullptr;
+}
+
+std::tuple<bool, const AsyncPanZoomController*>
+OverscrollHandoffChain::ScrollingDownWillMoveDynamicToolbar(
+ const AsyncPanZoomController* aApzc) const {
+ MOZ_ASSERT(aApzc && !aApzc->IsRootContent(),
+ "Should be used for non-root APZC");
+
+ for (uint32_t i = IndexOf(aApzc); i < Length(); i++) {
+ if (mChain[i]->IsRootContent()) {
+ bool scrollable = mChain[i]->CanVerticalScrollWithDynamicToolbar();
+ return {scrollable, scrollable ? mChain[i].get() : nullptr};
+ }
+
+ if (mChain[i]->CanScrollDownwards()) {
+ return {false, nullptr};
+ }
+ }
+
+ return {false, nullptr};
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/apz/src/OverscrollHandoffState.h b/gfx/layers/apz/src/OverscrollHandoffState.h
new file mode 100644
index 0000000000..90a22f259c
--- /dev/null
+++ b/gfx/layers/apz/src/OverscrollHandoffState.h
@@ -0,0 +1,203 @@
+/* -*- 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_OverscrollHandoffChain_h
+#define mozilla_layers_OverscrollHandoffChain_h
+
+#include <vector>
+#include "mozilla/RefPtr.h" // for RefPtr
+#include "nsISupportsImpl.h" // for NS_INLINE_DECL_THREADSAFE_REFCOUNTING
+#include "APZUtils.h" // for CancelAnimationFlags
+#include "mozilla/layers/LayersTypes.h" // for Layer::ScrollDirection
+#include "Units.h" // for ScreenPoint
+
+namespace mozilla {
+
+class InputData;
+
+namespace layers {
+
+class AsyncPanZoomController;
+
+/**
+ * This class represents the chain of APZCs along which overscroll is handed
+ * off. It is created by APZCTreeManager by starting from an initial APZC which
+ * is the target for input events, and following the scroll parent ID links
+ * (often but not always corresponding to parent pointers in the APZC tree),
+ * then adjusting for scrollgrab.
+ */
+class OverscrollHandoffChain {
+ protected:
+ // Reference-counted classes cannot have public destructors.
+ ~OverscrollHandoffChain();
+
+ public:
+ // Threadsafe so that the controller and sampler threads can both maintain
+ // nsRefPtrs to the same handoff chain.
+ // Mutable so that we can pass around the class by
+ // RefPtr<const OverscrollHandoffChain> and thus enforce that, once built,
+ // the chain is not modified.
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(OverscrollHandoffChain)
+
+ /*
+ * Methods for building the handoff chain.
+ * These should be used only by
+ * AsyncPanZoomController::BuildOverscrollHandoffChain().
+ */
+ void Add(AsyncPanZoomController* aApzc);
+ void SortByScrollPriority();
+
+ /*
+ * Methods for accessing the handoff chain.
+ */
+ uint32_t Length() const { return mChain.size(); }
+ const RefPtr<AsyncPanZoomController>& GetApzcAtIndex(uint32_t aIndex) const;
+ // Returns Length() if |aApzc| is not on this chain.
+ uint32_t IndexOf(const AsyncPanZoomController* aApzc) const;
+
+ /*
+ * Convenience methods for performing operations on APZCs in the chain.
+ */
+
+ // Flush repaints all the way up the chain.
+ void FlushRepaints() const;
+
+ // Cancel animations all the way up the chain.
+ void CancelAnimations(CancelAnimationFlags aFlags = Default) const;
+
+ // Clear overscroll all the way up the chain.
+ void ClearOverscroll() const;
+
+ // Snap back the APZC that is overscrolled on the subset of the chain from
+ // |aStart| onwards, if any.
+ void SnapBackOverscrolledApzc(const AsyncPanZoomController* aStart) const;
+
+ // Similar to above SnapbackOverscrolledApzc but for pan gestures with
+ // momentum events, this function doesn't end up calling each APZC's
+ // ScrollSnap.
+ // |aVelocity| is the initial velocity of |aStart|.
+ void SnapBackOverscrolledApzcForMomentum(
+ const AsyncPanZoomController* aStart,
+ const ParentLayerPoint& aVelocity) const;
+
+ // Determine whether the given APZC, or any APZC further in the chain,
+ // has room to be panned.
+ bool CanBePanned(const AsyncPanZoomController* aApzc) const;
+
+ // Determine whether the given APZC, or any APZC further in the chain,
+ // can scroll in the given direction.
+ bool CanScrollInDirection(const AsyncPanZoomController* aApzc,
+ ScrollDirection aDirection) const;
+
+ // Determine whether any APZC along this handoff chain is overscrolled.
+ bool HasOverscrolledApzc() const;
+
+ // Determine whether any APZC along this handoff chain has been flung fast.
+ bool HasFastFlungApzc() const;
+
+ // Determine whether any APZC along this handoff chain is autoscroll.
+ bool HasAutoscrollApzc() const;
+
+ // Find the first APZC in this handoff chain that can be scrolled by |aInput|.
+ // Since overscroll-behavior can restrict handoff in some directions,
+ // |aOutAllowedScrollDirections| is populated with the scroll directions
+ // in which scrolling of the returned APZC is allowed.
+ // |aIncludeOverscroll| is an optional flag whether to consider overscrollable
+ // as scrollable or not.
+ enum class IncludeOverscroll : bool { No, Yes };
+ RefPtr<AsyncPanZoomController> FindFirstScrollable(
+ const InputData& aInput, ScrollDirections* aOutAllowedScrollDirections,
+ IncludeOverscroll aIncludeOverscroll = IncludeOverscroll::Yes) const;
+
+ // Return a pair of true and the root content APZC if all non-root APZCs in
+ // this handoff chain starting from |aApzc| are not able to scroll downwards
+ // (i.e. there is no room to scroll downwards in each APZC respectively) and
+ // there is any contents covered by the dynamic toolbar, otherwise return a
+ // pair of false and nullptr.
+ std::tuple<bool, const AsyncPanZoomController*>
+ ScrollingDownWillMoveDynamicToolbar(
+ const AsyncPanZoomController* aApzc) const;
+
+ private:
+ std::vector<RefPtr<AsyncPanZoomController>> mChain;
+
+ typedef void (AsyncPanZoomController::*APZCMethod)();
+ typedef bool (AsyncPanZoomController::*APZCPredicate)() const;
+ void ForEachApzc(APZCMethod aMethod) const;
+ bool AnyApzc(APZCPredicate aPredicate) const;
+};
+
+/**
+ * This class groups the state maintained during overscroll handoff.
+ */
+struct OverscrollHandoffState {
+ OverscrollHandoffState(const OverscrollHandoffChain& aChain,
+ const ScreenPoint& aPanDistance,
+ ScrollSource aScrollSource)
+ : mChain(aChain),
+ mChainIndex(0),
+ mPanDistance(aPanDistance),
+ mScrollSource(aScrollSource) {}
+
+ // The chain of APZCs along which we hand off scroll.
+ // This is const to indicate that the chain does not change over the
+ // course of handoff.
+ const OverscrollHandoffChain& mChain;
+
+ // The index of the APZC in the chain that we are currently giving scroll to.
+ // This is non-const to indicate that this changes over the course of handoff.
+ uint32_t mChainIndex;
+
+ // The total distance since touch-start of the pan that triggered the
+ // handoff. This is const to indicate that it does not change over the
+ // course of handoff.
+ // The x/y components of this are non-negative.
+ const ScreenPoint mPanDistance;
+
+ ScrollSource mScrollSource;
+
+ // The total amount of actual movement that this scroll caused, including
+ // scrolling and changes to overscroll. This starts at zero and is accumulated
+ // over the course of the handoff.
+ ScreenPoint mTotalMovement;
+};
+
+/*
+ * This class groups the state maintained during fling handoff.
+ */
+struct FlingHandoffState {
+ // The velocity of the fling being handed off.
+ ParentLayerPoint mVelocity;
+
+ // The chain of APZCs along which we hand off the fling.
+ // Unlike in OverscrollHandoffState, this is stored by RefPtr because
+ // otherwise it may not stay alive for the entire handoff.
+ RefPtr<const OverscrollHandoffChain> mChain;
+
+ // The time duration between the touch start and the touch move that started
+ // the pan gesture which triggered this fling. In other words, the time it
+ // took for the finger to move enough to cross the touch slop threshold.
+ // Nothing if this fling was not immediately caused by a touch pan.
+ Maybe<TimeDuration> mTouchStartRestingTime;
+
+ // The slowest panning velocity encountered during the pan that triggered this
+ // fling.
+ ParentLayerCoord mMinPanVelocity;
+
+ // Whether handoff has happened by this point, or we're still process
+ // the original fling.
+ bool mIsHandoff;
+
+ // The single APZC that was scrolled by the pan that started this fling.
+ // The fling is only allowed to scroll this APZC, too.
+ // Used only if immediate scroll handoff is disallowed.
+ RefPtr<const AsyncPanZoomController> mScrolledApzc;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif /* mozilla_layers_OverscrollHandoffChain_h */
diff --git a/gfx/layers/apz/src/PotentialCheckerboardDurationTracker.cpp b/gfx/layers/apz/src/PotentialCheckerboardDurationTracker.cpp
new file mode 100644
index 0000000000..f3d5538ba0
--- /dev/null
+++ b/gfx/layers/apz/src/PotentialCheckerboardDurationTracker.cpp
@@ -0,0 +1,74 @@
+/* -*- 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 "PotentialCheckerboardDurationTracker.h"
+
+#include "mozilla/Telemetry.h" // for Telemetry
+
+namespace mozilla {
+namespace layers {
+
+PotentialCheckerboardDurationTracker::PotentialCheckerboardDurationTracker()
+ : mInCheckerboard(false), mInTransform(false) {}
+
+void PotentialCheckerboardDurationTracker::CheckerboardSeen() {
+ // This might get called while mInCheckerboard is already true
+ if (!Tracking()) {
+ mCurrentPeriodStart = TimeStamp::Now();
+ }
+ mInCheckerboard = true;
+}
+
+void PotentialCheckerboardDurationTracker::CheckerboardDone(
+ bool aRecordTelemetry) {
+ MOZ_ASSERT(Tracking());
+ mInCheckerboard = false;
+ if (!Tracking()) {
+ if (aRecordTelemetry) {
+ mozilla::Telemetry::AccumulateTimeDelta(
+ mozilla::Telemetry::CHECKERBOARD_POTENTIAL_DURATION,
+ mCurrentPeriodStart);
+ }
+ }
+}
+
+void PotentialCheckerboardDurationTracker::InTransform(bool aInTransform,
+ bool aRecordTelemetry) {
+ if (aInTransform == mInTransform) {
+ // no-op
+ return;
+ }
+
+ if (!Tracking()) {
+ // Because !Tracking(), mInTransform must be false, and so aInTransform
+ // must be true (or we would have early-exited this function already).
+ // Therefore, we are starting a potential checkerboard period.
+ mInTransform = aInTransform;
+ mCurrentPeriodStart = TimeStamp::Now();
+ return;
+ }
+
+ mInTransform = aInTransform;
+
+ if (!Tracking()) {
+ // Tracking() must have been true at the start of this function, or we
+ // would have taken the other !Tracking branch above. If it's false now,
+ // it means we just stopped tracking, so we are ending a potential
+ // checkerboard period.
+ if (aRecordTelemetry) {
+ mozilla::Telemetry::AccumulateTimeDelta(
+ mozilla::Telemetry::CHECKERBOARD_POTENTIAL_DURATION,
+ mCurrentPeriodStart);
+ }
+ }
+}
+
+bool PotentialCheckerboardDurationTracker::Tracking() const {
+ return mInTransform || mInCheckerboard;
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/apz/src/PotentialCheckerboardDurationTracker.h b/gfx/layers/apz/src/PotentialCheckerboardDurationTracker.h
new file mode 100644
index 0000000000..58786f32af
--- /dev/null
+++ b/gfx/layers/apz/src/PotentialCheckerboardDurationTracker.h
@@ -0,0 +1,61 @@
+/* -*- 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_PotentialCheckerboardDurationTracker_h
+#define mozilla_layers_PotentialCheckerboardDurationTracker_h
+
+#include "mozilla/TimeStamp.h"
+
+namespace mozilla {
+namespace layers {
+
+/**
+ * This class allows the owner to track the duration of time considered
+ * "potentially checkerboarding". This is the union of two possibly-intersecting
+ * sets of time periods. The first set is that in which checkerboarding was
+ * actually happening, since by definition it could potentially be happening.
+ * The second set is that in which the APZC is actively transforming content
+ * in the compositor, since it could potentially transform it so as to display
+ * checkerboarding to the user.
+ * The caller of this class calls the appropriate methods to indicate the start
+ * and stop of these two sets, and this class manages accumulating the union
+ * of the various durations.
+ */
+class PotentialCheckerboardDurationTracker {
+ public:
+ PotentialCheckerboardDurationTracker();
+
+ /**
+ * This should be called if checkerboarding is encountered. It can be called
+ * multiple times during a checkerboard event.
+ */
+ void CheckerboardSeen();
+ /**
+ * This should be called when checkerboarding is done. It must have been
+ * preceded by one or more calls to CheckerboardSeen().
+ */
+ void CheckerboardDone(bool aRecordTelemetry);
+
+ /**
+ * This should be called at composition time, to indicate if the APZC is in
+ * a transforming state or not.
+ */
+ void InTransform(bool aInTransform, bool aRecordTelemetry);
+
+ private:
+ bool Tracking() const;
+
+ private:
+ bool mInCheckerboard;
+ bool mInTransform;
+
+ TimeStamp mCurrentPeriodStart;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif // mozilla_layers_PotentialCheckerboardDurationTracker_h
diff --git a/gfx/layers/apz/src/QueuedInput.cpp b/gfx/layers/apz/src/QueuedInput.cpp
new file mode 100644
index 0000000000..87ffe7250e
--- /dev/null
+++ b/gfx/layers/apz/src/QueuedInput.cpp
@@ -0,0 +1,44 @@
+/* -*- 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 "QueuedInput.h"
+
+#include "AsyncPanZoomController.h"
+#include "InputBlockState.h"
+#include "InputData.h"
+#include "OverscrollHandoffState.h"
+
+namespace mozilla {
+namespace layers {
+
+QueuedInput::QueuedInput(const MultiTouchInput& aInput, TouchBlockState& aBlock)
+ : mInput(MakeUnique<MultiTouchInput>(aInput)), mBlock(&aBlock) {}
+
+QueuedInput::QueuedInput(const ScrollWheelInput& aInput,
+ WheelBlockState& aBlock)
+ : mInput(MakeUnique<ScrollWheelInput>(aInput)), mBlock(&aBlock) {}
+
+QueuedInput::QueuedInput(const MouseInput& aInput, DragBlockState& aBlock)
+ : mInput(MakeUnique<MouseInput>(aInput)), mBlock(&aBlock) {}
+
+QueuedInput::QueuedInput(const PanGestureInput& aInput,
+ PanGestureBlockState& aBlock)
+ : mInput(MakeUnique<PanGestureInput>(aInput)), mBlock(&aBlock) {}
+
+QueuedInput::QueuedInput(const PinchGestureInput& aInput,
+ PinchGestureBlockState& aBlock)
+ : mInput(MakeUnique<PinchGestureInput>(aInput)), mBlock(&aBlock) {}
+
+QueuedInput::QueuedInput(const KeyboardInput& aInput,
+ KeyboardBlockState& aBlock)
+ : mInput(MakeUnique<KeyboardInput>(aInput)), mBlock(&aBlock) {}
+
+InputData* QueuedInput::Input() { return mInput.get(); }
+
+InputBlockState* QueuedInput::Block() { return mBlock.get(); }
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/apz/src/QueuedInput.h b/gfx/layers/apz/src/QueuedInput.h
new file mode 100644
index 0000000000..fcc2f2090a
--- /dev/null
+++ b/gfx/layers/apz/src/QueuedInput.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_QueuedInput_h
+#define mozilla_layers_QueuedInput_h
+
+#include "mozilla/RefPtr.h"
+#include "mozilla/UniquePtr.h"
+
+namespace mozilla {
+
+class InputData;
+class MultiTouchInput;
+class ScrollWheelInput;
+class MouseInput;
+class PanGestureInput;
+class PinchGestureInput;
+class KeyboardInput;
+
+namespace layers {
+
+class InputBlockState;
+class TouchBlockState;
+class WheelBlockState;
+class DragBlockState;
+class PanGestureBlockState;
+class PinchGestureBlockState;
+class KeyboardBlockState;
+
+/**
+ * This lightweight class holds a pointer to an input event that has not yet
+ * been completely processed, along with the input block that the input event
+ * is associated with.
+ */
+class QueuedInput {
+ public:
+ QueuedInput(const MultiTouchInput& aInput, TouchBlockState& aBlock);
+ QueuedInput(const ScrollWheelInput& aInput, WheelBlockState& aBlock);
+ QueuedInput(const MouseInput& aInput, DragBlockState& aBlock);
+ QueuedInput(const PanGestureInput& aInput, PanGestureBlockState& aBlock);
+ QueuedInput(const PinchGestureInput& aInput, PinchGestureBlockState& aBlock);
+ QueuedInput(const KeyboardInput& aInput, KeyboardBlockState& aBlock);
+
+ InputData* Input();
+ InputBlockState* Block();
+
+ private:
+ // A copy of the input event that is provided to the constructor. This must
+ // be non-null, and is owned by this QueuedInput instance (hence the
+ // UniquePtr).
+ UniquePtr<InputData> mInput;
+ // A pointer to the block that the input event is associated with. This must
+ // be non-null.
+ RefPtr<InputBlockState> mBlock;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif // mozilla_layers_QueuedInput_h
diff --git a/gfx/layers/apz/src/RecentEventsBuffer.h b/gfx/layers/apz/src/RecentEventsBuffer.h
new file mode 100644
index 0000000000..d1ae5797af
--- /dev/null
+++ b/gfx/layers/apz/src/RecentEventsBuffer.h
@@ -0,0 +1,83 @@
+/* -*- 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_RecentEventsBuffer_h
+#define mozilla_layers_RecentEventsBuffer_h
+
+#include <deque>
+
+#include "mozilla/TimeStamp.h"
+
+namespace mozilla {
+namespace layers {
+/**
+ * RecentEventsBuffer: maintains an age constrained buffer of events
+ *
+ * Intended for use with elements of type InputData, but the only requirement
+ * is a member "mTimeStamp" of type TimeStamp
+ */
+template <typename Event>
+class RecentEventsBuffer {
+ public:
+ explicit RecentEventsBuffer(TimeDuration maxAge);
+
+ void push(Event event);
+ void clear();
+
+ typedef typename std::deque<Event>::size_type size_type;
+ size_type size() { return mBuffer.size(); }
+
+ // Delegate to container for iterators
+ typedef typename std::deque<Event>::iterator iterator;
+ typedef typename std::deque<Event>::const_iterator const_iterator;
+ iterator begin() { return mBuffer.begin(); }
+ iterator end() { return mBuffer.end(); }
+ const_iterator cbegin() const { return mBuffer.cbegin(); }
+ const_iterator cend() const { return mBuffer.cend(); }
+
+ // Also delegate for front/back
+ typedef typename std::deque<Event>::reference reference;
+ typedef typename std::deque<Event>::const_reference const_reference;
+ reference front() { return mBuffer.front(); }
+ reference back() { return mBuffer.back(); }
+ const_reference front() const { return mBuffer.front(); }
+ const_reference back() const { return mBuffer.back(); }
+
+ private:
+ TimeDuration mMaxAge;
+ std::deque<Event> mBuffer;
+};
+
+template <typename Event>
+RecentEventsBuffer<Event>::RecentEventsBuffer(TimeDuration maxAge)
+ : mMaxAge(maxAge), mBuffer() {}
+
+template <typename Event>
+void RecentEventsBuffer<Event>::push(Event event) {
+ // Events must be pushed in chronological order
+ MOZ_ASSERT(mBuffer.empty() || mBuffer.back().mTimeStamp <= event.mTimeStamp);
+
+ mBuffer.push_back(event);
+
+ // Flush all events older than the given lifetime
+ TimeStamp bound = event.mTimeStamp - mMaxAge;
+ while (!mBuffer.empty()) {
+ if (mBuffer.front().mTimeStamp >= bound) {
+ break;
+ }
+ mBuffer.pop_front();
+ }
+}
+
+template <typename Event>
+void RecentEventsBuffer<Event>::clear() {
+ mBuffer.clear();
+}
+
+} // namespace layers
+} // namespace mozilla
+
+#endif // mozilla_layers_RecentEventsBuffer_h
diff --git a/gfx/layers/apz/src/SampledAPZCState.cpp b/gfx/layers/apz/src/SampledAPZCState.cpp
new file mode 100644
index 0000000000..712a46a3b1
--- /dev/null
+++ b/gfx/layers/apz/src/SampledAPZCState.cpp
@@ -0,0 +1,111 @@
+/* -*- 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 "SampledAPZCState.h"
+#include "APZUtils.h"
+
+namespace mozilla {
+namespace layers {
+
+SampledAPZCState::SampledAPZCState() {}
+
+SampledAPZCState::SampledAPZCState(const FrameMetrics& aMetrics)
+ : mLayoutViewport(aMetrics.GetLayoutViewport()),
+ mVisualScrollOffset(aMetrics.GetVisualScrollOffset()),
+ mZoom(aMetrics.GetZoom()) {
+ RemoveFractionalAsyncDelta();
+}
+
+SampledAPZCState::SampledAPZCState(const FrameMetrics& aMetrics,
+ Maybe<CompositionPayload>&& aPayload,
+ APZScrollGeneration aGeneration)
+ : mLayoutViewport(aMetrics.GetLayoutViewport()),
+ mVisualScrollOffset(aMetrics.GetVisualScrollOffset()),
+ mZoom(aMetrics.GetZoom()),
+ mScrollPayload(std::move(aPayload)),
+ mGeneration(aGeneration) {
+ RemoveFractionalAsyncDelta();
+}
+
+bool SampledAPZCState::operator==(const SampledAPZCState& aOther) const {
+ // The payload doesn't factor into equality, that just comes along for
+ // the ride.
+ return mLayoutViewport.IsEqualEdges(aOther.mLayoutViewport) &&
+ mVisualScrollOffset == aOther.mVisualScrollOffset &&
+ mZoom == aOther.mZoom;
+}
+
+bool SampledAPZCState::operator!=(const SampledAPZCState& aOther) const {
+ return !(*this == aOther);
+}
+
+Maybe<CompositionPayload> SampledAPZCState::TakeScrollPayload() {
+ return std::move(mScrollPayload);
+}
+
+void SampledAPZCState::UpdateScrollProperties(const FrameMetrics& aMetrics) {
+ mLayoutViewport = aMetrics.GetLayoutViewport();
+ mVisualScrollOffset = aMetrics.GetVisualScrollOffset();
+}
+
+void SampledAPZCState::UpdateScrollPropertiesWithRelativeDelta(
+ const FrameMetrics& aMetrics, const CSSPoint& aRelativeDelta) {
+ mVisualScrollOffset += aRelativeDelta;
+ KeepLayoutViewportEnclosingVisualViewport(aMetrics);
+}
+
+void SampledAPZCState::UpdateZoomProperties(const FrameMetrics& aMetrics) {
+ mZoom = aMetrics.GetZoom();
+}
+
+void SampledAPZCState::ClampVisualScrollOffset(const FrameMetrics& aMetrics) {
+ // Make sure that we use the local mZoom to do these calculations, because the
+ // one on aMetrics might be newer.
+ CSSRect scrollRange = FrameMetrics::CalculateScrollRange(
+ aMetrics.GetScrollableRect(), aMetrics.GetCompositionBounds(), mZoom);
+ mVisualScrollOffset = scrollRange.ClampPoint(mVisualScrollOffset);
+
+ KeepLayoutViewportEnclosingVisualViewport(aMetrics);
+}
+
+void SampledAPZCState::ZoomBy(float aScale) { mZoom.scale *= aScale; }
+
+void SampledAPZCState::RemoveFractionalAsyncDelta() {
+ // This function is a performance hack. With non-WebRender, having small
+ // fractional deltas between the layout offset and scroll offset on
+ // container layers can trigger the creation of a temporary surface during
+ // composition, because it produces a non-integer translation that doesn't
+ // play well with layer clips. So we detect the case where the delta is
+ // uselessly small (0.01 parentlayer pixels or less) and tweak the sampled
+ // scroll offset to eliminate it. By doing this here at sample time rather
+ // than elsewhere in the pipeline we are least likely to break assumptions
+ // and invariants elsewhere in the code, since sampling effectively takes
+ // a snapshot of APZ state (decoupling it from APZ assumptions) and provides
+ // it as an input to the compositor (so all compositor state should be
+ // internally consistent based on this input).
+ if (mLayoutViewport.TopLeft() == mVisualScrollOffset) {
+ return;
+ }
+ const ParentLayerCoord EPSILON = 0.01;
+ ParentLayerPoint paintedOffset = mLayoutViewport.TopLeft() * mZoom;
+ ParentLayerPoint asyncOffset = mVisualScrollOffset * mZoom;
+ if (FuzzyEqualsAdditive(paintedOffset.x, asyncOffset.x, EPSILON) &&
+ FuzzyEqualsAdditive(paintedOffset.y, asyncOffset.y, EPSILON)) {
+ mVisualScrollOffset = mLayoutViewport.TopLeft();
+ }
+}
+
+void SampledAPZCState::KeepLayoutViewportEnclosingVisualViewport(
+ const FrameMetrics& aMetrics) {
+ FrameMetrics::KeepLayoutViewportEnclosingVisualViewport(
+ CSSRect(mVisualScrollOffset,
+ FrameMetrics::CalculateCompositedSizeInCssPixels(
+ aMetrics.GetCompositionBounds(), mZoom)),
+ aMetrics.GetScrollableRect(), mLayoutViewport);
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/apz/src/SampledAPZCState.h b/gfx/layers/apz/src/SampledAPZCState.h
new file mode 100644
index 0000000000..a521eeaf87
--- /dev/null
+++ b/gfx/layers/apz/src/SampledAPZCState.h
@@ -0,0 +1,72 @@
+/* -*- 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_SampledAPZCState_h
+#define mozilla_layers_SampledAPZCState_h
+
+#include "FrameMetrics.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/ScrollGeneration.h"
+
+namespace mozilla {
+namespace layers {
+
+class SampledAPZCState {
+ public:
+ SampledAPZCState();
+ explicit SampledAPZCState(const FrameMetrics& aMetrics);
+ SampledAPZCState(const FrameMetrics& aMetrics,
+ Maybe<CompositionPayload>&& aPayload,
+ APZScrollGeneration aGeneration);
+
+ bool operator==(const SampledAPZCState& aOther) const;
+ bool operator!=(const SampledAPZCState& aOther) const;
+
+ CSSRect GetLayoutViewport() const { return mLayoutViewport; }
+ CSSPoint GetVisualScrollOffset() const { return mVisualScrollOffset; }
+ CSSToParentLayerScale GetZoom() const { return mZoom; }
+ Maybe<CompositionPayload> TakeScrollPayload();
+ const APZScrollGeneration& Generation() const { return mGeneration; }
+
+ void UpdateScrollProperties(const FrameMetrics& aMetrics);
+ void UpdateScrollPropertiesWithRelativeDelta(const FrameMetrics& aMetrics,
+ const CSSPoint& aRelativeDelta);
+
+ void UpdateZoomProperties(const FrameMetrics& aMetrics);
+
+ /**
+ * Re-clamp mVisualScrollOffset to the scroll range specified by the provided
+ * metrics. This only needs to be called if the scroll offset changes
+ * outside of AsyncPanZoomController::SampleCompositedAsyncTransform().
+ * It also recalculates mLayoutViewport so that it continues to enclose
+ * the visual viewport. This only needs to be called if the
+ * layout viewport changes outside of SampleCompositedAsyncTransform().
+ */
+ void ClampVisualScrollOffset(const FrameMetrics& aMetrics);
+
+ void ZoomBy(float aScale);
+
+ private:
+ // These variables cache the layout viewport, scroll offset, and zoom stored
+ // in |Metrics()| at the time this class was constructed.
+ CSSRect mLayoutViewport;
+ CSSPoint mVisualScrollOffset;
+ CSSToParentLayerScale mZoom;
+ // An optional payload that rides along with the sampled state.
+ Maybe<CompositionPayload> mScrollPayload;
+ APZScrollGeneration mGeneration;
+
+ void RemoveFractionalAsyncDelta();
+ // A handy wrapper to call
+ // FrameMetrics::KeepLayoutViewportEnclosingVisualViewport with this
+ // SampledAPZCState and the given |aMetrics|.
+ void KeepLayoutViewportEnclosingVisualViewport(const FrameMetrics& aMetrics);
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif // mozilla_layers_SampledAPZCState_h
diff --git a/gfx/layers/apz/src/ScrollThumbUtils.cpp b/gfx/layers/apz/src/ScrollThumbUtils.cpp
new file mode 100644
index 0000000000..814fa59759
--- /dev/null
+++ b/gfx/layers/apz/src/ScrollThumbUtils.cpp
@@ -0,0 +1,341 @@
+/* -*- 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 "ScrollThumbUtils.h"
+#include "AsyncPanZoomController.h"
+#include "FrameMetrics.h"
+#include "UnitTransforms.h"
+#include "Units.h"
+#include "gfxPlatform.h"
+#include "mozilla/gfx/Matrix.h"
+#include "mozilla/StaticPrefs_toolkit.h"
+
+namespace mozilla {
+namespace layers {
+namespace apz {
+
+struct AsyncScrollThumbTransformer {
+ // Inputs
+ const LayerToParentLayerMatrix4x4& mCurrentTransform;
+ const gfx::Matrix4x4& mScrollableContentTransform;
+ AsyncPanZoomController* mApzc;
+ const FrameMetrics& mMetrics;
+ const ScrollbarData& mScrollbarData;
+ bool mScrollbarIsDescendant;
+
+ // Intermediate results
+ AsyncTransformComponentMatrix mAsyncTransform;
+ AsyncTransformComponentMatrix mScrollbarTransform;
+
+ LayerToParentLayerMatrix4x4 ComputeTransform();
+
+ private:
+ // Helper functions for ComputeTransform().
+
+ // If the thumb's orientation is along |aAxis|, add transformations
+ // of the thumb into |mScrollbarTransform|.
+ void ApplyTransformForAxis(const Axis& aAxis);
+
+ enum class ScrollThumbExtent { Start, End };
+
+ // Scale the thumb by |aScale| along |aAxis|, while keeping constant the
+ // position of the top denoted by |aExtent|.
+ void ScaleThumbBy(const Axis& aAxis, float aScale, ScrollThumbExtent aExtent);
+
+ // Translate the thumb along |aAxis| by |aTranslation| in "scrollbar space"
+ // (CSS pixels along the scrollbar track, similar to e.g.
+ // |mScrollbarData.mThumbStart|).
+ void TranslateThumb(const Axis& aAxis, OuterCSSCoord aTranslation);
+};
+
+void AsyncScrollThumbTransformer::TranslateThumb(const Axis& aAxis,
+ OuterCSSCoord aTranslation) {
+ aAxis.PostTranslate(
+ mScrollbarTransform,
+ ViewAs<CSSPixel>(aTranslation,
+ PixelCastJustification::CSSPixelsOfSurroundingContent) *
+ mMetrics.GetDevPixelsPerCSSPixel() *
+ LayoutDeviceToParentLayerScale(1.0));
+}
+
+void AsyncScrollThumbTransformer::ScaleThumbBy(const Axis& aAxis, float aScale,
+ ScrollThumbExtent aExtent) {
+ // To keep the position of the top of the thumb constant, the thumb needs to
+ // translated to compensate for the scale applied. The origin with respect to
+ // which the scale is applied is the origin of the layer tree, rather than
+ // the origin of the scroll thumb. This means that the space between the
+ // origin and the top of thumb (including the part of the scrollbar track
+ // above the thumb, the part of the scrollbar above the track (i.e. a
+ // scrollbar button if present), plus whatever content is above the scroll
+ // frame) is scaled too, effectively translating the thumb. We undo that
+ // translation here. (One can think of the adjustment being done to the
+ // translation here as a change of basis. We have a method to help with that,
+ // Matrix4x4::ChangeBasis(), but it wouldn't necessarily make the code cleaner
+ // in this case).
+ const OuterCSSCoord scrollTrackOrigin =
+ aAxis.GetPointOffset(
+ mMetrics.CalculateCompositionBoundsInOuterCssPixels().TopLeft()) +
+ mScrollbarData.mScrollTrackStart;
+ OuterCSSCoord thumbExtent = scrollTrackOrigin + mScrollbarData.mThumbStart;
+ if (aExtent == ScrollThumbExtent::End) {
+ thumbExtent += mScrollbarData.mThumbLength;
+ }
+ const OuterCSSCoord thumbExtentScaled = thumbExtent * aScale;
+ const OuterCSSCoord thumbExtentDelta = thumbExtentScaled - thumbExtent;
+
+ aAxis.PostScale(mScrollbarTransform, aScale);
+ TranslateThumb(aAxis, -thumbExtentDelta);
+}
+
+void AsyncScrollThumbTransformer::ApplyTransformForAxis(const Axis& aAxis) {
+ ParentLayerCoord asyncScroll = aAxis.GetTransformTranslation(mAsyncTransform);
+ const float asyncZoom = aAxis.GetTransformScale(mAsyncTransform);
+ const ParentLayerCoord overscroll =
+ aAxis.GetPointOffset(mApzc->GetOverscrollAmount());
+
+ bool haveAsyncZoom = !FuzzyEqualsAdditive(asyncZoom, 1.f);
+ if (!haveAsyncZoom && mApzc->IsZero(asyncScroll) &&
+ mApzc->IsZero(overscroll)) {
+ return;
+ }
+
+ OuterCSSCoord translation;
+ float scale = 1.0;
+
+ bool recalcMode = StaticPrefs::apz_scrollthumb_recalc();
+ if (recalcMode) {
+ // In this branch (taken when apz.scrollthumb.recalc=true), |translation|
+ // and |scale| are computed using the approach implemented in bug 1554795
+ // of fully recalculating the desired position and size using the logic
+ // that attempts to closely match the main-thread calculation.
+
+ const CSSRect visualViewportRect = mApzc->GetCurrentAsyncVisualViewport(
+ AsyncPanZoomController::eForCompositing);
+ const CSSCoord visualViewportLength =
+ aAxis.GetRectLength(visualViewportRect);
+
+ const CSSCoord maxMinPosDifference =
+ CSSCoord(
+ aAxis.GetRectLength(mMetrics.GetScrollableRect()).Truncated()) -
+ visualViewportLength;
+
+ OuterCSSCoord effectiveThumbLength = mScrollbarData.mThumbLength;
+
+ if (haveAsyncZoom) {
+ // The calculations here closely follow the main thread calculations at
+ // https://searchfox.org/mozilla-central/rev/0bf957f909ae1f3d19b43fd4edfc277342554836/layout/generic/nsGfxScrollFrame.cpp#6902-6927
+ // and
+ // https://searchfox.org/mozilla-central/rev/0bf957f909ae1f3d19b43fd4edfc277342554836/layout/xul/nsSliderFrame.cpp#587-614
+ // Any modifications there should be reflected here as well.
+ const CSSCoord pageIncrementMin =
+ static_cast<int>(visualViewportLength * 0.8);
+ CSSCoord pageIncrement;
+
+ CSSToLayoutDeviceScale deviceScale = mMetrics.GetDevPixelsPerCSSPixel();
+ if (*mScrollbarData.mDirection == ScrollDirection::eVertical) {
+ const CSSCoord lineScrollAmount =
+ (mApzc->GetScrollMetadata().GetLineScrollAmount() / deviceScale)
+ .height;
+ const double kScrollMultiplier =
+ StaticPrefs::toolkit_scrollbox_verticalScrollDistance();
+ CSSCoord increment = lineScrollAmount * kScrollMultiplier;
+
+ pageIncrement =
+ std::max(visualViewportLength - increment, pageIncrementMin);
+ } else {
+ pageIncrement = pageIncrementMin;
+ }
+
+ float ratio = pageIncrement / (maxMinPosDifference + pageIncrement);
+
+ OuterCSSCoord desiredThumbLength{
+ std::max(mScrollbarData.mThumbMinLength,
+ mScrollbarData.mScrollTrackLength * ratio)};
+
+ // Round the thumb length to an integer number of LayoutDevice pixels, to
+ // match the main-thread behaviour.
+ auto outerDeviceScale = ViewAs<OuterCSSToLayoutDeviceScale>(
+ deviceScale, PixelCastJustification::CSSPixelsOfSurroundingContent);
+ desiredThumbLength =
+ LayoutDeviceCoord((desiredThumbLength * outerDeviceScale).Rounded()) /
+ outerDeviceScale;
+
+ effectiveThumbLength = desiredThumbLength;
+
+ scale = desiredThumbLength / mScrollbarData.mThumbLength;
+ }
+
+ // Subtracting the offset of the scrollable rect is needed for right-to-left
+ // pages.
+ const CSSCoord curPos = aAxis.GetRectOffset(visualViewportRect) -
+ aAxis.GetRectOffset(mMetrics.GetScrollableRect());
+
+ const CSSToOuterCSSScale thumbPosRatio(
+ (maxMinPosDifference != 0)
+ ? float((mScrollbarData.mScrollTrackLength - effectiveThumbLength) /
+ maxMinPosDifference)
+ : 1.f);
+
+ const OuterCSSCoord desiredThumbPos = curPos * thumbPosRatio;
+
+ translation = desiredThumbPos - mScrollbarData.mThumbStart;
+ } else {
+ // In this branch (taken when apz.scrollthumb.recalc=false), |translation|
+ // and |scale| are computed using the pre-bug1554795 approach of turning
+ // the async scroll and zoom deltas into transforms to apply to the
+ // main-thread thumb position and size.
+
+ // The scroll thumb needs to be scaled in the direction of scrolling by the
+ // inverse of the async zoom. This is because zooming in decreases the
+ // fraction of the whole srollable rect that is in view.
+ scale = 1.f / asyncZoom;
+
+ // Note: |metrics.GetZoom()| doesn't yet include the async zoom.
+ CSSToParentLayerScale effectiveZoom =
+ CSSToParentLayerScale(mMetrics.GetZoom().scale * asyncZoom);
+
+ if (gfxPlatform::UseDesktopZoomingScrollbars()) {
+ // As computed by GetCurrentAsyncTransform, asyncScrollY is
+ // asyncScrollY = -(GetEffectiveScrollOffset -
+ // mLastContentPaintMetrics.GetLayoutScrollOffset()) *
+ // effectiveZoom
+ // where GetEffectiveScrollOffset includes the visual viewport offset that
+ // the main thread knows about plus any async scrolling to the visual
+ // viewport offset that the main thread does not (yet) know about. We want
+ // asyncScrollY to be
+ // asyncScrollY = -(GetEffectiveScrollOffset -
+ // mLastContentPaintMetrics.GetVisualScrollOffset()) * effectiveZoom
+ // because the main thread positions the scrollbars at the visual viewport
+ // offset that it knows about. (aMetrics is mLastContentPaintMetrics)
+
+ asyncScroll -= aAxis.GetPointOffset((mMetrics.GetLayoutScrollOffset() -
+ mMetrics.GetVisualScrollOffset()) *
+ effectiveZoom);
+ }
+
+ // Here we convert the scrollbar thumb ratio into a true unitless ratio by
+ // dividing out the conversion factor from the scrollframe's parent's space
+ // to the scrollframe's space.
+ float unitlessThumbRatio = mScrollbarData.mThumbRatio /
+ (mMetrics.GetPresShellResolution() * asyncZoom);
+
+ // The scroll thumb needs to be translated in opposite direction of the
+ // async scroll. This is because scrolling down, which translates the layer
+ // content up, should result in moving the scroll thumb down.
+ ParentLayerCoord translationPL = -asyncScroll * unitlessThumbRatio;
+
+ // The translation we computed is in the scroll frame's ParentLayer space.
+ // This includes the full cumulative resolution, even if we are a subframe.
+ // However, the resulting transform is used in a context where the scrollbar
+ // is already subject to the resolutions of enclosing scroll frames. To
+ // avoid double application of these enclosing resolutions, divide them out,
+ // leaving only the local resolution if any.
+ translationPL /= (mMetrics.GetCumulativeResolution().scale /
+ mMetrics.GetPresShellResolution());
+
+ // Convert translation to CSS pixels as this is what TranslateThumb expects.
+ translation = ViewAs<OuterCSSPixel>(
+ translationPL / (mMetrics.GetDevPixelsPerCSSPixel() *
+ LayoutDeviceToParentLayerScale(1.0)),
+ PixelCastJustification::CSSPixelsOfSurroundingContent);
+ }
+
+ // When scaling the thumb to account for the async zoom, keep the position
+ // of the start of the thumb (which corresponds to the scroll offset)
+ // constant.
+ if (haveAsyncZoom) {
+ ScaleThumbBy(aAxis, scale, ScrollThumbExtent::Start);
+ }
+
+ // If the page is overscrolled, additionally squish the thumb in accordance
+ // with the overscroll amount.
+ if (overscroll != 0) {
+ float overscrollScale =
+ 1.0f - (std::abs(overscroll.value) /
+ aAxis.GetRectLength(mMetrics.GetCompositionBounds()));
+ MOZ_ASSERT(overscrollScale > 0.0f && overscrollScale <= 1.0f);
+ // If we're overscrolled at the top, keep the top of the thumb in place
+ // as we squish it. If we're overscrolled at the bottom, keep the bottom of
+ // the thumb in place.
+ ScaleThumbBy(
+ aAxis, overscrollScale,
+ overscroll < 0 ? ScrollThumbExtent::Start : ScrollThumbExtent::End);
+ }
+
+ TranslateThumb(aAxis, translation);
+}
+
+LayerToParentLayerMatrix4x4 AsyncScrollThumbTransformer::ComputeTransform() {
+ // We only apply the transform if the scroll-target layer has non-container
+ // children (i.e. when it has some possibly-visible content). This is to
+ // avoid moving scroll-bars in the situation that only a scroll information
+ // layer has been built for a scroll frame, as this would result in a
+ // disparity between scrollbars and visible content.
+ if (mMetrics.IsScrollInfoLayer()) {
+ return LayerToParentLayerMatrix4x4{};
+ }
+
+ MOZ_RELEASE_ASSERT(mApzc);
+
+ mAsyncTransform =
+ mApzc->GetCurrentAsyncTransform(AsyncPanZoomController::eForCompositing);
+
+ // |mAsyncTransform| represents the amount by which we have scrolled and
+ // zoomed since the last paint. Because the scrollbar was sized and positioned
+ // based on the painted content, we need to adjust it based on asyncTransform
+ // so that it reflects what the user is actually seeing now.
+ if (*mScrollbarData.mDirection == ScrollDirection::eVertical) {
+ ApplyTransformForAxis(mApzc->mY);
+ }
+ if (*mScrollbarData.mDirection == ScrollDirection::eHorizontal) {
+ ApplyTransformForAxis(mApzc->mX);
+ }
+
+ LayerToParentLayerMatrix4x4 transform =
+ mCurrentTransform * mScrollbarTransform;
+
+ AsyncTransformComponentMatrix compensation;
+ // If the scrollbar layer is a child of the content it is a scrollbar for,
+ // then we need to adjust for any async transform (including an overscroll
+ // transform) on the content. This needs to be cancelled out because layout
+ // positions and sizes the scrollbar on the assumption that there is no async
+ // transform, and without this adjustment the scrollbar will end up in the
+ // wrong place.
+ //
+ // Note that since the async transform is applied on top of the content's
+ // regular transform, we need to make sure to unapply the async transform in
+ // the same coordinate space. This requires applying the content transform
+ // and then unapplying it after unapplying the async transform.
+ if (mScrollbarIsDescendant) {
+ AsyncTransformComponentMatrix overscroll =
+ mApzc->GetOverscrollTransform(AsyncPanZoomController::eForCompositing);
+ gfx::Matrix4x4 asyncUntransform =
+ (mAsyncTransform * overscroll).Inverse().ToUnknownMatrix();
+ const gfx::Matrix4x4& contentTransform = mScrollableContentTransform;
+ gfx::Matrix4x4 contentUntransform = contentTransform.Inverse();
+
+ compensation *= ViewAs<AsyncTransformComponentMatrix>(
+ contentTransform * asyncUntransform * contentUntransform);
+ }
+ transform = transform * compensation;
+
+ return transform;
+}
+
+LayerToParentLayerMatrix4x4 ComputeTransformForScrollThumb(
+ const LayerToParentLayerMatrix4x4& aCurrentTransform,
+ const gfx::Matrix4x4& aScrollableContentTransform,
+ AsyncPanZoomController* aApzc, const FrameMetrics& aMetrics,
+ const ScrollbarData& aScrollbarData, bool aScrollbarIsDescendant) {
+ return AsyncScrollThumbTransformer{
+ aCurrentTransform, aScrollableContentTransform, aApzc, aMetrics,
+ aScrollbarData, aScrollbarIsDescendant}
+ .ComputeTransform();
+}
+
+} // namespace apz
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/apz/src/ScrollThumbUtils.h b/gfx/layers/apz/src/ScrollThumbUtils.h
new file mode 100644
index 0000000000..49a45de3a7
--- /dev/null
+++ b/gfx/layers/apz/src/ScrollThumbUtils.h
@@ -0,0 +1,51 @@
+/* -*- 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_ScrollThumbUtils_h
+#define mozilla_layers_ScrollThumbUtils_h
+
+#include "LayersTypes.h"
+#include "Units.h"
+
+namespace mozilla {
+namespace layers {
+
+class AsyncPanZoomController;
+
+struct FrameMetrics;
+struct ScrollbarData;
+
+namespace apz {
+/**
+ * Compute the updated shadow transform for a scroll thumb layer that
+ * reflects async scrolling of the associated scroll frame.
+ *
+ * @param aCurrentTransform The current shadow transform on the scroll thumb
+ * layer, as returned by Layer::GetLocalTransform() or similar.
+ * @param aScrollableContentTransform The current content transform on the
+ * scrollable content, as returned by Layer::GetTransform().
+ * @param aApzc The APZC that scrolls the scroll frame.
+ * @param aMetrics The metrics associated with the scroll frame, reflecting
+ * the last paint of the associated content. Note: this metrics should
+ * NOT reflect async scrolling or zooming, i.e. they should be the layer
+ * tree's copy of the metrics, or APZC's last-content-paint metrics.
+ * @param aScrollbarData The scrollbar data for the the scroll thumb layer.
+ * @param aScrollbarIsDescendant True iff. the scroll thumb layer is a
+ * descendant of the layer bearing the scroll frame's metrics.
+ * @return The new shadow transform for the scroll thumb layer, including
+ * any pre- or post-scales.
+ */
+LayerToParentLayerMatrix4x4 ComputeTransformForScrollThumb(
+ const LayerToParentLayerMatrix4x4& aCurrentTransform,
+ const gfx::Matrix4x4& aScrollableContentTransform,
+ AsyncPanZoomController* aApzc, const FrameMetrics& aMetrics,
+ const ScrollbarData& aScrollbarData, bool aScrollbarIsDescendant);
+
+} // namespace apz
+} // namespace layers
+} // namespace mozilla
+
+#endif // mozilla_layers_ScrollThumbUtils_h
diff --git a/gfx/layers/apz/src/SimpleVelocityTracker.cpp b/gfx/layers/apz/src/SimpleVelocityTracker.cpp
new file mode 100644
index 0000000000..87cae10d51
--- /dev/null
+++ b/gfx/layers/apz/src/SimpleVelocityTracker.cpp
@@ -0,0 +1,135 @@
+/* -*- 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 "SimpleVelocityTracker.h"
+
+#include "mozilla/ServoStyleConsts.h" // for StyleComputedTimingFunction
+#include "mozilla/StaticPrefs_apz.h"
+#include "mozilla/StaticPtr.h" // for StaticAutoPtr
+
+static mozilla::LazyLogModule sApzSvtLog("apz.simplevelocitytracker");
+#define SVT_LOG(...) MOZ_LOG(sApzSvtLog, LogLevel::Debug, (__VA_ARGS__))
+
+namespace mozilla {
+namespace layers {
+
+// When we compute the velocity we do so by taking two input events and
+// dividing the distance delta over the time delta. In some cases the time
+// delta can be really small, which can make the velocity computation very
+// volatile. To avoid this we impose a minimum time delta below which we do
+// not recompute the velocity.
+const TimeDuration MIN_VELOCITY_SAMPLE_TIME = TimeDuration::FromMilliseconds(5);
+
+extern StaticAutoPtr<StyleComputedTimingFunction> gVelocityCurveFunction;
+
+SimpleVelocityTracker::SimpleVelocityTracker(Axis* aAxis)
+ : mAxis(aAxis), mVelocitySamplePos(0) {}
+
+void SimpleVelocityTracker::StartTracking(ParentLayerCoord aPos,
+ TimeStamp aTimestamp) {
+ Clear();
+ mVelocitySampleTime = aTimestamp;
+ mVelocitySamplePos = aPos;
+}
+
+Maybe<float> SimpleVelocityTracker::AddPosition(ParentLayerCoord aPos,
+ TimeStamp aTimestamp) {
+ if (aTimestamp <= mVelocitySampleTime + MIN_VELOCITY_SAMPLE_TIME) {
+ // See also the comment on MIN_VELOCITY_SAMPLE_TIME.
+ // We don't update either mVelocitySampleTime or mVelocitySamplePos so that
+ // eventually when we do get an event with the required time delta we use
+ // the corresponding distance delta as well.
+ SVT_LOG("%p|%s skipping velocity computation for small time delta %f ms\n",
+ mAxis->OpaqueApzcPointer(), mAxis->Name(),
+ (aTimestamp - mVelocitySampleTime).ToMilliseconds());
+ return Nothing();
+ }
+
+ float newVelocity =
+ (float)(mVelocitySamplePos - aPos) /
+ (float)(aTimestamp - mVelocitySampleTime).ToMilliseconds();
+
+ newVelocity = ApplyFlingCurveToVelocity(newVelocity);
+
+ SVT_LOG("%p|%s updating velocity to %f with touch\n",
+ mAxis->OpaqueApzcPointer(), mAxis->Name(), newVelocity);
+ mVelocitySampleTime = aTimestamp;
+ mVelocitySamplePos = aPos;
+
+ AddVelocityToQueue(aTimestamp, newVelocity);
+
+ return Some(newVelocity);
+}
+
+Maybe<float> SimpleVelocityTracker::ComputeVelocity(TimeStamp aTimestamp) {
+ float velocity = 0;
+ int count = 0;
+ for (const auto& e : mVelocityQueue) {
+ TimeDuration timeDelta = (aTimestamp - e.first);
+ if (timeDelta < TimeDuration::FromMilliseconds(
+ StaticPrefs::apz_velocity_relevance_time_ms())) {
+ count++;
+ velocity += e.second;
+ }
+ }
+ mVelocityQueue.Clear();
+ if (count > 1) {
+ velocity /= count;
+ }
+ return Some(velocity);
+}
+
+void SimpleVelocityTracker::Clear() { mVelocityQueue.Clear(); }
+
+void SimpleVelocityTracker::AddVelocityToQueue(TimeStamp aTimestamp,
+ float aVelocity) {
+ mVelocityQueue.AppendElement(std::make_pair(aTimestamp, aVelocity));
+ if (mVelocityQueue.Length() >
+ StaticPrefs::apz_max_velocity_queue_size_AtStartup()) {
+ mVelocityQueue.RemoveElementAt(0);
+ }
+}
+
+float SimpleVelocityTracker::ApplyFlingCurveToVelocity(float aVelocity) const {
+ float newVelocity = aVelocity;
+ if (StaticPrefs::apz_max_velocity_inches_per_ms() > 0.0f) {
+ bool velocityIsNegative = (newVelocity < 0);
+ newVelocity = fabs(newVelocity);
+
+ float maxVelocity =
+ mAxis->ToLocalVelocity(StaticPrefs::apz_max_velocity_inches_per_ms());
+ newVelocity = std::min(newVelocity, maxVelocity);
+
+ if (StaticPrefs::apz_fling_curve_threshold_inches_per_ms() > 0.0f &&
+ StaticPrefs::apz_fling_curve_threshold_inches_per_ms() <
+ StaticPrefs::apz_max_velocity_inches_per_ms()) {
+ float curveThreshold = mAxis->ToLocalVelocity(
+ StaticPrefs::apz_fling_curve_threshold_inches_per_ms());
+ if (newVelocity > curveThreshold) {
+ // here, 0 < curveThreshold < newVelocity <= maxVelocity, so we apply
+ // the curve
+ float scale = maxVelocity - curveThreshold;
+ float funcInput = (newVelocity - curveThreshold) / scale;
+ float funcOutput =
+ gVelocityCurveFunction->At(funcInput, /* aBeforeFlag = */ false);
+ float curvedVelocity = (funcOutput * scale) + curveThreshold;
+ SVT_LOG("%p|%s curving up velocity from %f to %f\n",
+ mAxis->OpaqueApzcPointer(), mAxis->Name(), newVelocity,
+ curvedVelocity);
+ newVelocity = curvedVelocity;
+ }
+ }
+
+ if (velocityIsNegative) {
+ newVelocity = -newVelocity;
+ }
+ }
+
+ return newVelocity;
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/apz/src/SimpleVelocityTracker.h b/gfx/layers/apz/src/SimpleVelocityTracker.h
new file mode 100644
index 0000000000..1778dee065
--- /dev/null
+++ b/gfx/layers/apz/src/SimpleVelocityTracker.h
@@ -0,0 +1,54 @@
+/* -*- 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_VelocityTracker_h
+#define mozilla_layers_VelocityTracker_h
+
+#include <utility>
+#include <cstdint>
+
+#include "Axis.h"
+#include "mozilla/Attributes.h"
+#include "nsTArray.h"
+
+namespace mozilla {
+namespace layers {
+
+class SimpleVelocityTracker : public VelocityTracker {
+ public:
+ explicit SimpleVelocityTracker(Axis* aAxis);
+ void StartTracking(ParentLayerCoord aPos, TimeStamp aTimestamp) override;
+ Maybe<float> AddPosition(ParentLayerCoord aPos,
+ TimeStamp aTimestamp) override;
+ Maybe<float> ComputeVelocity(TimeStamp aTimestamp) override;
+ void Clear() override;
+
+ private:
+ void AddVelocityToQueue(TimeStamp aTimestamp, float aVelocity);
+ float ApplyFlingCurveToVelocity(float aVelocity) const;
+
+ // The Axis that uses this velocity tracker.
+ // This is a raw pointer because the Axis owns the velocity tracker
+ // by UniquePtr, so the velocity tracker cannot outlive the Axis.
+ Axis* MOZ_NON_OWNING_REF mAxis;
+
+ // A queue of (timestamp, velocity) pairs; these are the historical
+ // velocities at the given timestamps. Velocities are in screen pixels per ms.
+ // This member can only be accessed on the controller/UI thread.
+ nsTArray<std::pair<TimeStamp, float>> mVelocityQueue;
+
+ // mVelocitySampleTime and mVelocitySamplePos are the time and position
+ // used in the last velocity sampling. They get updated when a new sample is
+ // taken (which may not happen on every input event, if the time delta is too
+ // small).
+ TimeStamp mVelocitySampleTime;
+ ParentLayerCoord mVelocitySamplePos;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif
diff --git a/gfx/layers/apz/src/SmoothMsdScrollAnimation.cpp b/gfx/layers/apz/src/SmoothMsdScrollAnimation.cpp
new file mode 100644
index 0000000000..8342dc157f
--- /dev/null
+++ b/gfx/layers/apz/src/SmoothMsdScrollAnimation.cpp
@@ -0,0 +1,139 @@
+/* -*- 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 "SmoothMsdScrollAnimation.h"
+#include "AsyncPanZoomController.h"
+
+namespace mozilla {
+namespace layers {
+
+SmoothMsdScrollAnimation::SmoothMsdScrollAnimation(
+ AsyncPanZoomController& aApzc, const CSSPoint& aInitialPosition,
+ const CSSPoint& aInitialVelocity, const CSSPoint& aDestination,
+ double aSpringConstant, double aDampingRatio,
+ ScrollSnapTargetIds&& aSnapTargetIds,
+ ScrollTriggeredByScript aTriggeredByScript)
+ : mApzc(aApzc),
+ mXAxisModel(aInitialPosition.x, aDestination.x, aInitialVelocity.x,
+ aSpringConstant, aDampingRatio),
+ mYAxisModel(aInitialPosition.y, aDestination.y, aInitialVelocity.y,
+ aSpringConstant, aDampingRatio),
+ mSnapTargetIds(std::move(aSnapTargetIds)),
+ mTriggeredByScript(aTriggeredByScript) {}
+
+bool SmoothMsdScrollAnimation::DoSample(FrameMetrics& aFrameMetrics,
+ const TimeDuration& aDelta) {
+ CSSToParentLayerScale zoom(aFrameMetrics.GetZoom());
+ if (zoom == CSSToParentLayerScale(0)) {
+ return false;
+ }
+ CSSPoint oneParentLayerPixel =
+ ParentLayerPoint(1, 1) / aFrameMetrics.GetZoom();
+ if (mXAxisModel.IsFinished(oneParentLayerPixel.x) &&
+ mYAxisModel.IsFinished(oneParentLayerPixel.y)) {
+ // Set the scroll offset to the exact destination. If we allow the scroll
+ // offset to end up being a bit off from the destination, we can get
+ // artefacts like "scroll to the next snap point in this direction"
+ // scrolling to the snap point we're already supposed to be at.
+ mApzc.ClampAndSetVisualScrollOffset(
+ CSSPoint(mXAxisModel.GetDestination(), mYAxisModel.GetDestination()));
+ return false;
+ }
+
+ mXAxisModel.Simulate(aDelta);
+ mYAxisModel.Simulate(aDelta);
+
+ CSSPoint position =
+ CSSPoint(mXAxisModel.GetPosition(), mYAxisModel.GetPosition());
+ CSSPoint css_velocity =
+ CSSPoint(mXAxisModel.GetVelocity(), mYAxisModel.GetVelocity());
+
+ // Convert from pixels/second to pixels/ms
+ ParentLayerPoint velocity =
+ ParentLayerPoint(css_velocity.x, css_velocity.y) / 1000.0f;
+
+ // Keep the velocity updated for the Axis class so that any animations
+ // chained off of the smooth scroll will inherit it.
+ if (mXAxisModel.IsFinished(oneParentLayerPixel.x)) {
+ mApzc.mX.SetVelocity(0);
+ } else {
+ mApzc.mX.SetVelocity(velocity.x);
+ }
+ if (mYAxisModel.IsFinished(oneParentLayerPixel.y)) {
+ mApzc.mY.SetVelocity(0);
+ } else {
+ mApzc.mY.SetVelocity(velocity.y);
+ }
+ // If we overscroll, hand off to a fling animation that will complete the
+ // spring back.
+ ParentLayerPoint displacement =
+ (position - aFrameMetrics.GetVisualScrollOffset()) * zoom;
+
+ ParentLayerPoint overscroll;
+ ParentLayerPoint adjustedOffset;
+ mApzc.mX.AdjustDisplacement(displacement.x, adjustedOffset.x, overscroll.x);
+ mApzc.mY.AdjustDisplacement(displacement.y, adjustedOffset.y, overscroll.y);
+ mApzc.ScrollBy(adjustedOffset / zoom);
+ // The smooth scroll may have caused us to reach the end of our scroll
+ // range. This can happen if either the
+ // layout.css.scroll-behavior.damping-ratio preference is set to less than 1
+ // (underdamped) or if a smooth scroll inherits velocity from a fling
+ // gesture.
+ if (!IsZero(overscroll / zoom)) {
+ // Hand off a fling with the remaining momentum to the next APZC in the
+ // overscroll handoff chain.
+
+ // We may have reached the end of the scroll range along one axis but
+ // not the other. In such a case we only want to hand off the relevant
+ // component of the fling.
+ if (mApzc.IsZero(overscroll.x)) {
+ velocity.x = 0;
+ } else if (mApzc.IsZero(overscroll.y)) {
+ velocity.y = 0;
+ }
+
+ // To hand off the fling, we attempt to find a target APZC and start a new
+ // fling with the same velocity on that APZC. For simplicity, the actual
+ // overscroll of the current sample is discarded rather than being handed
+ // off. The compositor should sample animations sufficiently frequently
+ // that this is not noticeable. The target APZC is chosen by seeing if
+ // there is an APZC further in the handoff chain which is pannable; if
+ // there isn't, we take the new fling ourselves, entering an overscrolled
+ // state.
+ // Note: APZC is holding mRecursiveMutex, so directly calling
+ // HandleSmoothScrollOverscroll() (which acquires the tree lock) would
+ // violate the lock ordering. Instead we schedule
+ // HandleSmoothScrollOverscroll() to be called after mRecursiveMutex is
+ // released.
+ mDeferredTasks.AppendElement(NewRunnableMethod<ParentLayerPoint, SideBits>(
+ "layers::AsyncPanZoomController::HandleSmoothScrollOverscroll", &mApzc,
+ &AsyncPanZoomController::HandleSmoothScrollOverscroll, velocity,
+ apz::GetOverscrollSideBits(overscroll)));
+ return false;
+ }
+
+ return true;
+}
+
+void SmoothMsdScrollAnimation::SetDestination(
+ const CSSPoint& aNewDestination, ScrollSnapTargetIds&& aSnapTargetIds,
+ ScrollTriggeredByScript aTriggeredByScript) {
+ mXAxisModel.SetDestination(aNewDestination.x);
+ mYAxisModel.SetDestination(aNewDestination.y);
+ mSnapTargetIds = std::move(aSnapTargetIds);
+ mTriggeredByScript = aTriggeredByScript;
+}
+
+CSSPoint SmoothMsdScrollAnimation::GetDestination() const {
+ return CSSPoint(mXAxisModel.GetDestination(), mYAxisModel.GetDestination());
+}
+
+SmoothMsdScrollAnimation*
+SmoothMsdScrollAnimation::AsSmoothMsdScrollAnimation() {
+ return this;
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/apz/src/SmoothMsdScrollAnimation.h b/gfx/layers/apz/src/SmoothMsdScrollAnimation.h
new file mode 100644
index 0000000000..1f2247c473
--- /dev/null
+++ b/gfx/layers/apz/src/SmoothMsdScrollAnimation.h
@@ -0,0 +1,61 @@
+/* -*- 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_SmoothMsdScrollAnimation_h_
+#define mozilla_layers_SmoothMsdScrollAnimation_h_
+
+#include "AsyncPanZoomAnimation.h"
+#include "mozilla/layers/AxisPhysicsMSDModel.h"
+#include "mozilla/ScrollPositionUpdate.h"
+
+namespace mozilla {
+namespace layers {
+
+class AsyncPanZoomController;
+
+class SmoothMsdScrollAnimation final : public AsyncPanZoomAnimation {
+ public:
+ SmoothMsdScrollAnimation(AsyncPanZoomController& aApzc,
+ const CSSPoint& aInitialPosition,
+ const CSSPoint& aInitialVelocity,
+ const CSSPoint& aDestination, double aSpringConstant,
+ double aDampingRatio,
+ ScrollSnapTargetIds&& aSnapTargetIds,
+ ScrollTriggeredByScript aTriggeredByScript);
+
+ /**
+ * Advances a smooth scroll simulation based on the time passed in |aDelta|.
+ * This should be called whenever sampling the content transform for this
+ * frame. Returns true if the smooth scroll should be advanced by one frame,
+ * or false if the smooth scroll has ended.
+ */
+ bool DoSample(FrameMetrics& aFrameMetrics,
+ const TimeDuration& aDelta) override;
+
+ void SetDestination(const CSSPoint& aNewDestination,
+ ScrollSnapTargetIds&& aSnapTargetIds,
+ ScrollTriggeredByScript aTriggeredByScript);
+ CSSPoint GetDestination() const;
+ SmoothMsdScrollAnimation* AsSmoothMsdScrollAnimation() override;
+
+ bool WasTriggeredByScript() const override {
+ return mTriggeredByScript == ScrollTriggeredByScript::Yes;
+ }
+
+ ScrollSnapTargetIds TakeSnapTargetIds() { return std::move(mSnapTargetIds); }
+
+ private:
+ AsyncPanZoomController& mApzc;
+ AxisPhysicsMSDModel mXAxisModel;
+ AxisPhysicsMSDModel mYAxisModel;
+ ScrollSnapTargetIds mSnapTargetIds;
+ ScrollTriggeredByScript mTriggeredByScript;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif
diff --git a/gfx/layers/apz/src/SmoothScrollAnimation.cpp b/gfx/layers/apz/src/SmoothScrollAnimation.cpp
new file mode 100644
index 0000000000..266c027a55
--- /dev/null
+++ b/gfx/layers/apz/src/SmoothScrollAnimation.cpp
@@ -0,0 +1,46 @@
+/* -*- 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 "SmoothScrollAnimation.h"
+#include "ScrollAnimationBezierPhysics.h"
+#include "mozilla/layers/APZPublicUtils.h"
+
+namespace mozilla {
+namespace layers {
+
+SmoothScrollAnimation::SmoothScrollAnimation(AsyncPanZoomController& aApzc,
+ const nsPoint& aInitialPosition,
+ ScrollOrigin aOrigin)
+ : GenericScrollAnimation(
+ aApzc, aInitialPosition,
+ apz::ComputeBezierAnimationSettingsForOrigin(aOrigin)),
+ mOrigin(aOrigin) {}
+
+SmoothScrollAnimation* SmoothScrollAnimation::AsSmoothScrollAnimation() {
+ return this;
+}
+
+ScrollOrigin SmoothScrollAnimation::GetScrollOrigin() const { return mOrigin; }
+
+ScrollOrigin SmoothScrollAnimation::GetScrollOriginForAction(
+ KeyboardScrollAction::KeyboardScrollActionType aAction) {
+ switch (aAction) {
+ case KeyboardScrollAction::eScrollCharacter:
+ case KeyboardScrollAction::eScrollLine: {
+ return ScrollOrigin::Lines;
+ }
+ case KeyboardScrollAction::eScrollPage:
+ return ScrollOrigin::Pages;
+ case KeyboardScrollAction::eScrollComplete:
+ return ScrollOrigin::Other;
+ default:
+ MOZ_ASSERT(false, "Unknown keyboard scroll action type");
+ return ScrollOrigin::Other;
+ }
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/apz/src/SmoothScrollAnimation.h b/gfx/layers/apz/src/SmoothScrollAnimation.h
new file mode 100644
index 0000000000..1143744cc1
--- /dev/null
+++ b/gfx/layers/apz/src/SmoothScrollAnimation.h
@@ -0,0 +1,37 @@
+/* -*- 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_SmoothScrollAnimation_h_
+#define mozilla_layers_SmoothScrollAnimation_h_
+
+#include "GenericScrollAnimation.h"
+#include "mozilla/ScrollOrigin.h"
+#include "mozilla/layers/KeyboardScrollAction.h"
+
+namespace mozilla {
+namespace layers {
+
+class AsyncPanZoomController;
+
+class SmoothScrollAnimation : public GenericScrollAnimation {
+ public:
+ SmoothScrollAnimation(AsyncPanZoomController& aApzc,
+ const nsPoint& aInitialPosition,
+ ScrollOrigin aScrollOrigin);
+
+ SmoothScrollAnimation* AsSmoothScrollAnimation() override;
+ ScrollOrigin GetScrollOrigin() const;
+ static ScrollOrigin GetScrollOriginForAction(
+ KeyboardScrollAction::KeyboardScrollActionType aAction);
+
+ private:
+ ScrollOrigin mOrigin;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif // mozilla_layers_SmoothScrollAnimation_h_
diff --git a/gfx/layers/apz/src/WRHitTester.cpp b/gfx/layers/apz/src/WRHitTester.cpp
new file mode 100644
index 0000000000..873400976f
--- /dev/null
+++ b/gfx/layers/apz/src/WRHitTester.cpp
@@ -0,0 +1,247 @@
+/* -*- 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 "WRHitTester.h"
+#include "AsyncPanZoomController.h"
+#include "APZCTreeManager.h"
+#include "TreeTraversal.h" // for BreadthFirstSearch
+#include "mozilla/gfx/CompositorHitTestInfo.h"
+#include "mozilla/webrender/WebRenderAPI.h"
+#include "nsDebug.h" // for NS_ASSERTION
+#include "nsIXULRuntime.h" // for FissionAutostart
+#include "mozilla/gfx/Matrix.h"
+
+#define APZCTM_LOG(...) \
+ MOZ_LOG(APZCTreeManager::sLog, LogLevel::Debug, (__VA_ARGS__))
+
+namespace mozilla {
+namespace layers {
+
+using mozilla::gfx::CompositorHitTestFlags;
+using mozilla::gfx::CompositorHitTestInvisibleToHit;
+
+static bool CheckCloseToIdentity(const gfx::Matrix4x4& aMatrix) {
+ // We allow a factor of 1/2048 in the multiply part of the matrix, so that if
+ // we multiply by a point on a screen of size 2048 we would be off by at most
+ // 1 pixel approximately.
+ const float multiplyEps = 1 / 2048.f;
+ // We allow 1 pixel in the translate part of the matrix.
+ const float translateEps = 1.f;
+
+ if (!FuzzyEqualsAdditive(aMatrix._11, 1.f, multiplyEps) ||
+ !FuzzyEqualsAdditive(aMatrix._12, 0.f, multiplyEps) ||
+ !FuzzyEqualsAdditive(aMatrix._13, 0.f, multiplyEps) ||
+ !FuzzyEqualsAdditive(aMatrix._14, 0.f, multiplyEps) ||
+ !FuzzyEqualsAdditive(aMatrix._21, 0.f, multiplyEps) ||
+ !FuzzyEqualsAdditive(aMatrix._22, 1.f, multiplyEps) ||
+ !FuzzyEqualsAdditive(aMatrix._23, 0.f, multiplyEps) ||
+ !FuzzyEqualsAdditive(aMatrix._24, 0.f, multiplyEps) ||
+ !FuzzyEqualsAdditive(aMatrix._31, 0.f, multiplyEps) ||
+ !FuzzyEqualsAdditive(aMatrix._32, 0.f, multiplyEps) ||
+ !FuzzyEqualsAdditive(aMatrix._33, 1.f, multiplyEps) ||
+ !FuzzyEqualsAdditive(aMatrix._34, 0.f, multiplyEps) ||
+ !FuzzyEqualsAdditive(aMatrix._41, 0.f, translateEps) ||
+ !FuzzyEqualsAdditive(aMatrix._42, 0.f, translateEps) ||
+ !FuzzyEqualsAdditive(aMatrix._43, 0.f, translateEps) ||
+ !FuzzyEqualsAdditive(aMatrix._44, 1.f, multiplyEps)) {
+ return false;
+ }
+ return true;
+}
+
+// Checks that within the constraints of floating point math we can invert it
+// reasonably enough that multiplying by the computed inverse is close to the
+// identity.
+static bool CheckInvertibleWithFinitePrecision(const gfx::Matrix4x4& aMatrix) {
+ auto inverse = aMatrix.MaybeInverse();
+ if (inverse.isNothing()) {
+ // Should we return false?
+ return true;
+ }
+ if (!CheckCloseToIdentity(aMatrix * *inverse)) {
+ return false;
+ }
+ if (!CheckCloseToIdentity(*inverse * aMatrix)) {
+ return false;
+ }
+ return true;
+}
+
+IAPZHitTester::HitTestResult WRHitTester::GetAPZCAtPoint(
+ const ScreenPoint& aHitTestPoint,
+ const RecursiveMutexAutoLock& aProofOfTreeLock) {
+ HitTestResult hit;
+ RefPtr<wr::WebRenderAPI> wr = mTreeManager->GetWebRenderAPI();
+ if (!wr) {
+ // If WebRender isn't running, fall back to the root APZC.
+ // This is mostly for the benefit of GTests which do not
+ // run a WebRender instance, but gracefully falling back
+ // here allows those tests which are not specifically
+ // testing the hit-test algorithm to still work.
+ hit.mTargetApzc = FindRootApzcForLayersId(GetRootLayersId());
+ hit.mHitResult = CompositorHitTestFlags::eVisibleToHitTest;
+ return hit;
+ }
+
+ APZCTM_LOG("Hit-testing point %s with WR\n", ToString(aHitTestPoint).c_str());
+ std::vector<wr::WrHitResult> results =
+ wr->HitTest(wr::ToWorldPoint(aHitTestPoint));
+
+ Maybe<wr::WrHitResult> chosenResult;
+ for (const wr::WrHitResult& result : results) {
+ ScrollableLayerGuid guid{result.mLayersId, 0, result.mScrollId};
+ APZCTM_LOG("Examining result with guid %s hit info 0x%x... ",
+ ToString(guid).c_str(), result.mHitInfo.serialize());
+ if (result.mHitInfo == CompositorHitTestInvisibleToHit) {
+ APZCTM_LOG("skipping due to invisibility.\n");
+ continue;
+ }
+ RefPtr<HitTestingTreeNode> node =
+ GetTargetNode(guid, &ScrollableLayerGuid::EqualsIgnoringPresShell);
+ if (!node) {
+ APZCTM_LOG("no corresponding node found, falling back to root.\n");
+
+#ifdef DEBUG
+ // We can enter here during normal codepaths for cases where the
+ // nsDisplayCompositorHitTestInfo item emitted a scrollId of
+ // NULL_SCROLL_ID to the webrender display list. The semantics of that
+ // is to fall back to the root APZC for the layers id, so that's what
+ // we do here.
+ // If we enter this codepath and scrollId is not NULL_SCROLL_ID, then
+ // that's more likely to be due to a race condition between rebuilding
+ // the APZ tree and updating the WR scene/hit-test information, resulting
+ // in WR giving us a hit result for a scene that is not active in APZ.
+ // Such a scenario would need debugging and fixing.
+ // In non-Fission mode, make this assertion non-fatal because there is
+ // a known issue related to inactive scroll frames that can cause this
+ // to fire (see bug 1634763), which is fixed in Fission mode and not
+ // worth fixing in non-Fission mode.
+ if (FissionAutostart()) {
+ MOZ_ASSERT(result.mScrollId == ScrollableLayerGuid::NULL_SCROLL_ID);
+ } else {
+ NS_ASSERTION(
+ result.mScrollId == ScrollableLayerGuid::NULL_SCROLL_ID,
+ "Inconsistency between WebRender display list and APZ scroll data");
+ }
+#endif
+ node = FindRootNodeForLayersId(result.mLayersId);
+ if (!node) {
+ // Should never happen, but handle gracefully in release builds just
+ // in case.
+ MOZ_ASSERT(false);
+ chosenResult = Some(result);
+ break;
+ }
+ }
+ MOZ_ASSERT(node->GetApzc()); // any node returned must have an APZC
+ EventRegionsOverride flags = node->GetEventRegionsOverride();
+ if (flags & EventRegionsOverride::ForceEmptyHitRegion) {
+ // This result is inside a subtree that is invisible to hit-testing.
+ APZCTM_LOG("skipping due to FEHR subtree.\n");
+ continue;
+ }
+
+ if (!CheckInvertibleWithFinitePrecision(
+ mTreeManager->GetScreenToApzcTransform(node->GetApzc())
+ .ToUnknownMatrix())) {
+ APZCTM_LOG("skipping due to check inverse accuracy\n");
+ continue;
+ }
+
+ APZCTM_LOG("selecting as chosen result.\n");
+ chosenResult = Some(result);
+ hit.mTargetApzc = node->GetApzc();
+ if (flags & EventRegionsOverride::ForceDispatchToContent) {
+ chosenResult->mHitInfo += CompositorHitTestFlags::eApzAwareListeners;
+ }
+ break;
+ }
+ if (!chosenResult) {
+ return hit;
+ }
+
+ MOZ_ASSERT(hit.mTargetApzc);
+ hit.mLayersId = chosenResult->mLayersId;
+ ScrollableLayerGuid::ViewID scrollId = chosenResult->mScrollId;
+ gfx::CompositorHitTestInfo hitInfo = chosenResult->mHitInfo;
+ Maybe<uint64_t> animationId = chosenResult->mAnimationId;
+ SideBits sideBits = chosenResult->mSideBits;
+
+ APZCTM_LOG("Successfully matched APZC %p (hit result 0x%x)\n",
+ hit.mTargetApzc.get(), hitInfo.serialize());
+
+ const bool isScrollbar =
+ hitInfo.contains(gfx::CompositorHitTestFlags::eScrollbar);
+ const bool isScrollbarThumb =
+ hitInfo.contains(gfx::CompositorHitTestFlags::eScrollbarThumb);
+ const ScrollDirection direction =
+ hitInfo.contains(gfx::CompositorHitTestFlags::eScrollbarVertical)
+ ? ScrollDirection::eVertical
+ : ScrollDirection::eHorizontal;
+ HitTestingTreeNode* scrollbarNode = nullptr;
+ if (isScrollbar || isScrollbarThumb) {
+ scrollbarNode = BreadthFirstSearch<ReverseIterator>(
+ GetRootNode(), [&](HitTestingTreeNode* aNode) {
+ return (aNode->GetLayersId() == hit.mLayersId) &&
+ (aNode->IsScrollbarNode() == isScrollbar) &&
+ (aNode->IsScrollThumbNode() == isScrollbarThumb) &&
+ (aNode->GetScrollbarDirection() == direction) &&
+ (aNode->GetScrollTargetId() == scrollId);
+ });
+ }
+
+ hit.mHitResult = hitInfo;
+
+ if (scrollbarNode) {
+ RefPtr<HitTestingTreeNode> scrollbarRef = scrollbarNode;
+ InitializeHitTestingTreeNodeAutoLock(hit.mScrollbarNode, aProofOfTreeLock,
+ scrollbarRef);
+ }
+
+ hit.mFixedPosSides = sideBits;
+ if (animationId.isSome()) {
+ RefPtr<HitTestingTreeNode> positionedNode = nullptr;
+
+ positionedNode = BreadthFirstSearch<ReverseIterator>(
+ GetRootNode(), [&](HitTestingTreeNode* aNode) {
+ return (aNode->GetFixedPositionAnimationId() == animationId ||
+ aNode->GetStickyPositionAnimationId() == animationId);
+ });
+
+ if (positionedNode) {
+ MOZ_ASSERT(positionedNode->GetLayersId() == chosenResult->mLayersId,
+ "Found node layers id does not match the hit result");
+ MOZ_ASSERT((positionedNode->GetFixedPositionAnimationId().isSome() ||
+ positionedNode->GetStickyPositionAnimationId().isSome()),
+ "A a matching fixed/sticky position node should be found");
+ InitializeHitTestingTreeNodeAutoLock(hit.mNode, aProofOfTreeLock,
+ positionedNode);
+ }
+
+#if defined(MOZ_WIDGET_ANDROID)
+ if (hit.mNode && hit.mNode->GetFixedPositionAnimationId().isSome()) {
+ // If the hit element is a fixed position element, the side bits from
+ // the hit-result item tag are used. For now just ensure that these
+ // match what is found in the hit-testing tree node.
+ MOZ_ASSERT(sideBits == hit.mNode->GetFixedPosSides(),
+ "Fixed position side bits do not match");
+ } else if (hit.mTargetApzc && hit.mTargetApzc->IsRootContent()) {
+ // If the hit element is not a fixed position element, then the hit test
+ // result item's side bits should not be populated.
+ MOZ_ASSERT(sideBits == SideBits::eNone,
+ "Hit test results have side bits only for pos:fixed");
+ }
+#endif
+ }
+
+ hit.mHitOverscrollGutter =
+ hit.mTargetApzc && hit.mTargetApzc->IsInOverscrollGutter(aHitTestPoint);
+
+ return hit;
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/apz/src/WRHitTester.h b/gfx/layers/apz/src/WRHitTester.h
new file mode 100644
index 0000000000..abb9de1a66
--- /dev/null
+++ b/gfx/layers/apz/src/WRHitTester.h
@@ -0,0 +1,26 @@
+/* -*- 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_WRHitTester_h
+#define mozilla_layers_WRHitTester_h
+
+#include "IAPZHitTester.h"
+
+namespace mozilla {
+namespace layers {
+
+// IAPZHitTester implementation for WebRender.
+class WRHitTester : public IAPZHitTester {
+ public:
+ virtual HitTestResult GetAPZCAtPoint(
+ const ScreenPoint& aHitTestPoint,
+ const RecursiveMutexAutoLock& aProofOfTreeLock) override;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif // define mozilla_layers_WRHitTester_h
diff --git a/gfx/layers/apz/src/WheelScrollAnimation.cpp b/gfx/layers/apz/src/WheelScrollAnimation.cpp
new file mode 100644
index 0000000000..6203bcb8fa
--- /dev/null
+++ b/gfx/layers/apz/src/WheelScrollAnimation.cpp
@@ -0,0 +1,64 @@
+/* -*- 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 "WheelScrollAnimation.h"
+
+#include <tuple>
+#include "AsyncPanZoomController.h"
+#include "mozilla/StaticPrefs_general.h"
+#include "nsPoint.h"
+#include "ScrollAnimationBezierPhysics.h"
+
+namespace mozilla {
+namespace layers {
+
+static ScrollAnimationBezierPhysicsSettings SettingsForDeltaType(
+ ScrollWheelInput::ScrollDeltaType aDeltaType) {
+ int32_t minMS = 0;
+ int32_t maxMS = 0;
+
+ switch (aDeltaType) {
+ case ScrollWheelInput::SCROLLDELTA_PAGE:
+ maxMS = clamped(StaticPrefs::general_smoothScroll_pages_durationMaxMS(),
+ 0, 10000);
+ minMS = clamped(StaticPrefs::general_smoothScroll_pages_durationMinMS(),
+ 0, maxMS);
+ break;
+ case ScrollWheelInput::SCROLLDELTA_PIXEL:
+ maxMS = clamped(StaticPrefs::general_smoothScroll_pixels_durationMaxMS(),
+ 0, 10000);
+ minMS = clamped(StaticPrefs::general_smoothScroll_pixels_durationMinMS(),
+ 0, maxMS);
+ break;
+ case ScrollWheelInput::SCROLLDELTA_LINE:
+ maxMS =
+ clamped(StaticPrefs::general_smoothScroll_mouseWheel_durationMaxMS(),
+ 0, 10000);
+ minMS =
+ clamped(StaticPrefs::general_smoothScroll_mouseWheel_durationMinMS(),
+ 0, maxMS);
+ break;
+ }
+
+ // The pref is 100-based int percentage, while mIntervalRatio is 1-based ratio
+ double intervalRatio =
+ ((double)StaticPrefs::general_smoothScroll_durationToIntervalRatio()) /
+ 100.0;
+ intervalRatio = std::max(1.0, intervalRatio);
+ return ScrollAnimationBezierPhysicsSettings{minMS, maxMS, intervalRatio};
+}
+
+WheelScrollAnimation::WheelScrollAnimation(
+ AsyncPanZoomController& aApzc, const nsPoint& aInitialPosition,
+ ScrollWheelInput::ScrollDeltaType aDeltaType)
+ : GenericScrollAnimation(aApzc, aInitialPosition,
+ SettingsForDeltaType(aDeltaType)) {
+ mDirectionForcedToOverscroll =
+ mApzc.mScrollMetadata.GetDisregardedDirection();
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/apz/src/WheelScrollAnimation.h b/gfx/layers/apz/src/WheelScrollAnimation.h
new file mode 100644
index 0000000000..7c039ef3fd
--- /dev/null
+++ b/gfx/layers/apz/src/WheelScrollAnimation.h
@@ -0,0 +1,30 @@
+/* -*- 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_WheelScrollAnimation_h_
+#define mozilla_layers_WheelScrollAnimation_h_
+
+#include "GenericScrollAnimation.h"
+#include "InputData.h"
+
+namespace mozilla {
+namespace layers {
+
+class AsyncPanZoomController;
+
+class WheelScrollAnimation : public GenericScrollAnimation {
+ public:
+ WheelScrollAnimation(AsyncPanZoomController& aApzc,
+ const nsPoint& aInitialPosition,
+ ScrollWheelInput::ScrollDeltaType aDeltaType);
+
+ WheelScrollAnimation* AsWheelScrollAnimation() override { return this; }
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif // mozilla_layers_WheelScrollAnimation_h_