diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 09:22:09 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 09:22:09 +0000 |
commit | 43a97878ce14b72f0981164f87f2e35e14151312 (patch) | |
tree | 620249daf56c0258faa40cbdcf9cfba06de2a846 /gfx/layers/apz/src | |
parent | Initial commit. (diff) | |
download | firefox-43a97878ce14b72f0981164f87f2e35e14151312.tar.xz firefox-43a97878ce14b72f0981164f87f2e35e14151312.zip |
Adding upstream version 110.0.1.upstream/110.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'gfx/layers/apz/src')
75 files changed, 26312 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..8534112afe --- /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->GetCurrentAsyncScrollOffsetInCssPixels( + AsyncPanZoomController::eForCompositing), + (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::DOUBLE_TAP_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::eTouchActionDoubleTapZoomDisabled)) { + result &= ~AllowedTouchBehavior::DOUBLE_TAP_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()); + CSSCoord 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. + CSSCoord 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); + + if (apzc->GetScrollHandoffParentId() == + ScrollableLayerGuid::NULL_SCROLL_ID) { + if (!apzc->IsRootForLayersId()) { + // This probably indicates a bug or missed case in layout code + NS_WARNING("Found a non-root APZ with no handoff parent"); + } + } + + APZCTreeManager::TargetApzcForNodeResult handoffResult = + FindHandoffParent(apzc); + + // 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..bb5b42d262 --- /dev/null +++ b/gfx/layers/apz/src/APZInputBridge.cpp @@ -0,0 +1,411 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "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. + bool consumable = aConsumableFlags.IsConsumable(); + mStatus = + consumable ? nsEventStatus_eConsumeDoDefault : nsEventStatus_eIgnore; + + // 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 && !aFlags.mDispatchToContent && + !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 = aFlags.mDispatchToContent + ? Nothing() + : Some(APZHandledResult{ + consumable ? APZHandledPlace::HandledByRoot + : APZHandledPlace::Unhandled, + rootApzc}); + } + } +} + +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..2f9bb0f0ee --- /dev/null +++ b/gfx/layers/apz/src/APZSampler.cpp @@ -0,0 +1,215 @@ +/* -*- 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->GetCurrentAsyncScrollOffsetInCssPixels( + AsyncPanZoomController::AsyncTransformConsumer::eForCompositing), + 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..f7f24c3781 --- /dev/null +++ b/gfx/layers/apz/src/AsyncDragMetrics.h @@ -0,0 +1,53 @@ +/* -*- 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, + CSSCoord aScrollbarDragOffset, ScrollDirection aDirection) + : mViewId(aViewId), + mPresShellId(aPresShellId), + mDragStartSequenceNumber(aDragStartSequenceNumber), + mScrollbarDragOffset(aScrollbarDragOffset), + mDirection(Some(aDirection)) {} + + ScrollableLayerGuid::ViewID mViewId; + uint32_t mPresShellId; + uint64_t mDragStartSequenceNumber; + CSSCoord 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..9ea0226392 --- /dev/null +++ b/gfx/layers/apz/src/AsyncPanZoomController.cpp @@ -0,0 +1,6624 @@ +/* -*- 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, + CSSCoord 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); + CSSCoord thumbPosition; + if (isMouseAwayFromThumb) { + thumbPosition = aInitialThumbPos; + } else { + thumbPosition = ConvertScrollbarPoint(aEvent.mLocalOrigin, scrollbarData) - + aDragMetrics.mScrollbarDragOffset; + } + + CSSCoord 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)); + } + 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 == nullptr); + 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(); +} + +CSSCoord 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. + scrollbarPoint = scrollbarPoint * Metrics().GetPresShellResolution(); + + // Now, get it to be relative to the beginning of the scroll track. + CSSRect cssCompositionBound = + Metrics().CalculateCompositionBoundsInCssPixelsOfSurroundingContent(); + return GetAxisStart(*aThumbData.mDirection, scrollbarPoint) - + 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(mState).c_str()); + + if (mState == 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 (mState == 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); + + MOZ_ASSERT_IF(mState == OVERSCROLL_ANIMATION, mAnimation); + if (mState == 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() && mState != 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. + // This has been observed on some Wayland versions. + if (mState == OVERSCROLL_ANIMATION || mState == NOTHING) { + 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. + if (mState != 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, + mState), + 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()); + } +} + +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); +} + +CSSPoint AsyncPanZoomController::GetCurrentAsyncScrollOffsetInCssPixels( + AsyncTransformConsumer aMode) const { + RecursiveMutexAutoLock lock(mRecursiveMutex); + AutoApplyAsyncTestAttributes testAttributeApplier(this, lock); + + return GetEffectiveScrollOffset(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); +} + +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 (IsNaN(aConstraints.mMinZoom.scale) || + 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; +} + +} // 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..c116c12e88 --- /dev/null +++ b/gfx/layers/apz/src/AsyncPanZoomController.h @@ -0,0 +1,1927 @@ +/* -*- 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" + +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; } +}; + +/** + * 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, + CSSCoord 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 CSS coordinates relative to the beginning of the scroll track. + * Only the component in the direction of scrolling is returned. + */ + CSSCoord 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; + + 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 scroll offset of the scrollable frame corresponding + * to this APZC, including the effects of any asynchronous panning, in + * CSS pixels. + */ + CSSPoint GetCurrentAsyncScrollOffsetInCssPixels( + 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 the monitor 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); + /** + * 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; + + // 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..a1f03309c2 --- /dev/null +++ b/gfx/layers/apz/src/Axis.cpp @@ -0,0 +1,713 @@ +/* -*- 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; +} + +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(); +} + +ParentLayerCoord AxisX::GetRectOffset(const ParentLayerRect& 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; +} + +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(); +} + +ParentLayerCoord AxisY::GetRectOffset(const ParentLayerRect& 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..94195b621e --- /dev/null +++ b/gfx/layers/apz/src/Axis.h @@ -0,0 +1,453 @@ +/* -*- 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 ParentLayerCoord GetPointOffset( + const ParentLayerPoint& aPoint) const = 0; + virtual ParentLayerCoord GetRectLength( + const ParentLayerRect& aRect) const = 0; + virtual ParentLayerCoord GetRectOffset( + const ParentLayerRect& 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; + ParentLayerCoord GetPointOffset( + const ParentLayerPoint& aPoint) const override; + ParentLayerCoord GetRectLength(const ParentLayerRect& aRect) const override; + ParentLayerCoord GetRectOffset(const ParentLayerRect& 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; + ParentLayerCoord GetPointOffset( + const ParentLayerPoint& aPoint) const override; + CSSToParentLayerScale GetAxisScale( + const CSSToParentLayerScale2D& aScale) const override; + ParentLayerCoord GetRectLength(const ParentLayerRect& aRect) const override; + ParentLayerCoord GetRectOffset(const ParentLayerRect& 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..8dbdcfb2ce --- /dev/null +++ b/gfx/layers/apz/src/InputBlockState.cpp @@ -0,0 +1,819 @@ +/* -*- 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 "ScrollAnimationPhysics.h" // for kScrollSeriesTimeoutMs + +#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) { + // 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(CSSCoord 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() > + kScrollSeriesTimeoutMs) { + 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::DOUBLE_TAP_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..cac54e41f8 --- /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(CSSCoord aThumbPos); + void SetDragMetrics(const AsyncDragMetrics& aDragMetrics); + + void DispatchEvent(const InputData& aEvent) const override; + + private: + AsyncDragMetrics mDragMetrics; + CSSCoord 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..229709aece --- /dev/null +++ b/gfx/layers/apz/src/InputQueue.cpp @@ -0,0 +1,1079 @@ +/* -*- 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. + if (block->IsDuringFastFling()) { + INPQ_LOG("dropping event due to block %p being in fast motion\n", + block.get()); + result.SetStatusAsConsumeNoDefault(); + } else { // handling depends on ArePointerEventsConsumable() + PointerEventsConsumableFlags consumableFlags; + if (target) { + consumableFlags = target->ArePointerEventsConsumable(block, aEvent); + } + 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; + result.SetStatusAsConsumeDoDefault(aTarget); + + 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); + 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 (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..48b36b9278 --- /dev/null +++ b/gfx/layers/apz/src/ScrollThumbUtils.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 "ScrollThumbUtils.h" +#include "AsyncPanZoomController.h" +#include "FrameMetrics.h" +#include "gfxPlatform.h" +#include "mozilla/gfx/Matrix.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; + CSSToParentLayerScale mEffectiveZoom; + float mUnitlessThumbRatio; + + 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); +}; + +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, 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). + CSSCoord thumbExtentRelativeToCompBounds = + (aAxis.GetPointOffset(mMetrics.GetVisualScrollOffset()) * + mUnitlessThumbRatio); + CSSCoord compBoundsOrigin = aAxis.GetPointOffset( + mMetrics.CalculateCompositionBoundsInCssPixelsOfSurroundingContent() + .TopLeft()); + CSSCoord thumbExtent = compBoundsOrigin + thumbExtentRelativeToCompBounds; + if (aExtent == ScrollThumbExtent::End) { + thumbExtent += mScrollbarData.mThumbLength; + } + const CSSCoord thumbExtentScaled = thumbExtent * aScale; + const CSSCoord thumbExtentDelta = thumbExtentScaled - thumbExtent; + const ParentLayerCoord thumbExtentDeltaPL = thumbExtentDelta * mEffectiveZoom; + + aAxis.PostScale(mScrollbarTransform, aScale); + aAxis.PostTranslate(mScrollbarTransform, -thumbExtentDeltaPL); +} + +void AsyncScrollThumbTransformer::ApplyTransformForAxis(const Axis& aAxis) { + ParentLayerCoord asyncScroll = aAxis.GetTransformTranslation(mAsyncTransform); + const float asyncZoom = aAxis.GetTransformScale(mAsyncTransform); + + // 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. + const float scale = 1.f / asyncZoom; + + // Note: |metrics.GetZoom()| doesn't yet include the async zoom. + mEffectiveZoom = 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()) * + mEffectiveZoom); + } + + // 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. + mUnitlessThumbRatio = 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 translation = -asyncScroll * mUnitlessThumbRatio; + + // 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. + translation /= (mMetrics.GetCumulativeResolution().scale / + mMetrics.GetPresShellResolution()); + + // 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. + ScaleThumbBy(aAxis, scale, ScrollThumbExtent::Start); + + // If the page is overscrolled, additionally squish the thumb in accordance + // with the overscroll amount. + ParentLayerCoord overscroll = + aAxis.GetPointOffset(mApzc->GetOverscrollAmount()); + 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); + } + + aAxis.PostTranslate(mScrollbarTransform, 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..421b25579e --- /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, 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_ |