Bug 951793 - Obey overscroll-behavior for wheel and pan gesture events. r=kats
MozReview-Commit-ID: EmbsMu9Esww
--- a/gfx/layers/apz/src/APZUtils.h
+++ b/gfx/layers/apz/src/APZUtils.h
@@ -6,16 +6,17 @@
#ifndef mozilla_layers_APZUtils_h
#define mozilla_layers_APZUtils_h
#include <stdint.h> // for uint32_t
#include "LayersTypes.h"
#include "UnitTransforms.h"
#include "mozilla/gfx/Point.h"
+#include "mozilla/EnumSet.h"
#include "mozilla/FloatingPoint.h"
namespace mozilla {
namespace layers {
enum HitTestResult {
HitNothing,
HitLayer,
@@ -37,16 +38,18 @@ enum CancelAnimationFlags : uint32_t {
inline CancelAnimationFlags
operator|(CancelAnimationFlags a, CancelAnimationFlags b)
{
return static_cast<CancelAnimationFlags>(static_cast<int>(a)
| static_cast<int>(b));
}
+typedef EnumSet<ScrollDirection> ScrollDirections;
+
enum class ScrollSource {
// scrollTo() or something similar.
DOM,
// Touch-screen or trackpad with gesture support.
Touch,
// Mouse wheel.
--- a/gfx/layers/apz/src/AsyncPanZoomController.cpp
+++ b/gfx/layers/apz/src/AsyncPanZoomController.cpp
@@ -1909,34 +1909,55 @@ AsyncPanZoomController::GetKeyboardDesti
}
break;
}
}
return scrollDestination;
}
-// Return whether or not the underlying layer can be scrolled on either axis.
-bool
-AsyncPanZoomController::CanScroll(const InputData& aEvent) const
+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;
+}
+
+// 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;
}
return CanScrollWithWheel(delta);
}
+ScrollDirections
+AsyncPanZoomController::GetAllowedHandoffDirections() const
+{
+ ScrollDirections result;
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ if (mX.OverscrollBehaviorAllowsHandoff()) {
+ result += ScrollDirection::eHorizontal;
+ }
+ if (mY.OverscrollBehaviorAllowsHandoff()) {
+ result += ScrollDirection::eVertical;
+ }
+ return result;
+}
+
bool
AsyncPanZoomController::CanScrollWithWheel(const ParentLayerPoint& aDelta) const
{
RecursiveMutexAutoLock lock(mRecursiveMutex);
if (mX.CanScroll(aDelta.x)) {
return true;
}
if (mY.CanScroll(aDelta.y) && mScrollMetadata.AllowVerticalScrollWithWheel()) {
@@ -1994,16 +2015,29 @@ ScrollInputMethodForWheelDeltaType(Scrol
case ScrollWheelInput::SCROLLDELTA_PIXEL: {
return ScrollInputMethod::ApzWheelPixel;
}
}
MOZ_ASSERT_UNREACHABLE("Invalid value");
return ScrollInputMethod::ApzWheelLine;
}
+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)
{
ParentLayerPoint delta = GetScrollWheelDelta(aEvent);
APZC_LOG("%p got a scroll-wheel with delta %s\n", this, Stringify(delta).c_str());
if ((delta.x || delta.y) && !CanScrollWithWheel(delta)) {
// We can't scroll this apz anymore, so we simply drop the event.
if (mInputQueue->GetActiveWheelTransaction() &&
@@ -2012,16 +2046,20 @@ nsEventStatus AsyncPanZoomController::On
controller->NotifyMozMouseScrollEvent(
mFrameMetrics.GetScrollId(),
NS_LITERAL_STRING("MozMouseScrollFailed"));
}
}
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;
}
mozilla::Telemetry::Accumulate(mozilla::Telemetry::SCROLL_INPUT_METHODS,
(uint32_t) ScrollInputMethodForWheelDeltaType(aEvent.mDeltaType));
@@ -2035,17 +2073,16 @@ nsEventStatus AsyncPanZoomController::On
CSSPoint startPosition = mFrameMetrics.GetScrollOffset();
MaybeAdjustDeltaForScrollSnapping(aEvent, delta, startPosition);
ScreenPoint distance = ToScreenCoordinates(
ParentLayerPoint(fabs(delta.x), fabs(delta.y)), aEvent.mLocalOrigin);
CancelAnimation();
- MOZ_ASSERT(mInputQueue->GetCurrentWheelBlock());
OverscrollHandoffState handoffState(
*mInputQueue->GetCurrentWheelBlock()->GetOverscrollHandoffChain(),
distance,
ScrollSource::Wheel);
ParentLayerPoint startPoint = aEvent.mLocalOrigin;
ParentLayerPoint endPoint = aEvent.mLocalOrigin - delta;
CallDispatchScroll(startPoint, endPoint, handoffState);
@@ -2212,30 +2249,33 @@ nsEventStatus AsyncPanZoomController::On
// 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();
+ MOZ_ASSERT(GetCurrentPanGestureBlock());
+ AdjustDeltaForAllowedScrollDirections(logicalPanDisplacement,
+ GetCurrentPanGestureBlock()->GetAllowedScrollDirections());
+
// 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 == false); in that case the "with touch" part is not
// really appropriate, so we may want to rethink this at some point.
mX.UpdateWithTouchAtDevicePoint(aEvent.mLocalPanStartPoint.x, logicalPanDisplacement.x, aEvent.mTime);
mY.UpdateWithTouchAtDevicePoint(aEvent.mLocalPanStartPoint.y, logicalPanDisplacement.y, aEvent.mTime);
HandlePanningUpdate(physicalPanDisplacement);
mozilla::Telemetry::Accumulate(mozilla::Telemetry::SCROLL_INPUT_METHODS,
(uint32_t) ScrollInputMethod::ApzPanGesture);
ScreenPoint panDistance(fabs(physicalPanDisplacement.x), fabs(physicalPanDisplacement.y));
- MOZ_ASSERT(GetCurrentPanGestureBlock());
OverscrollHandoffState handoffState(
*GetCurrentPanGestureBlock()->GetOverscrollHandoffChain(),
panDistance,
ScrollSource::Wheel);
// 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.
--- a/gfx/layers/apz/src/AsyncPanZoomController.h
+++ b/gfx/layers/apz/src/AsyncPanZoomController.h
@@ -415,16 +415,20 @@ public:
*/
ParentLayerPoint ToParentLayerCoordinates(const ScreenPoint& aVector,
const ScreenPoint& aAnchor) 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 whether or not a scroll delta will be able to scroll in either
// direction.
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;
@@ -1177,16 +1181,18 @@ private:
/**
* 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);
+ // Helper function for CanScroll().
+ ParentLayerPoint GetDeltaForEvent(const InputData& aEvent) const;
/* ===================================================================
* 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 Matrix4x4& aTransformToLayer) {
--- a/gfx/layers/apz/src/InputBlockState.cpp
+++ b/gfx/layers/apz/src/InputBlockState.cpp
@@ -317,17 +317,18 @@ WheelBlockState::WheelBlockState(const R
sLastWheelBlockId = GetBlockId();
if (aTargetConfirmed) {
// 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);
+ mOverscrollHandoffChain->FindFirstScrollable(
+ aInitialEvent, &mAllowedScrollDirections);
// If nothing is scrollable, we don't consider this block as starting a
// transaction.
if (!apzc) {
EndTransaction();
return;
}
@@ -351,17 +352,18 @@ WheelBlockState::SetConfirmedTargetApzc(
TargetConfirmationState aState,
InputData* aFirstInput)
{
// 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);
+ apzc = apzc->BuildOverscrollHandoffChain()->FindFirstScrollable(
+ *aFirstInput, &mAllowedScrollDirections);
}
InputBlockState::SetConfirmedTargetApzc(apzc, aState, aFirstInput);
return true;
}
void
WheelBlockState::Update(ScrollWheelInput& aEvent)
@@ -548,17 +550,18 @@ PanGestureBlockState::PanGestureBlockSta
, mWaitingForContentResponse(false)
{
if (aTargetConfirmed) {
// 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);
+ mOverscrollHandoffChain->FindFirstScrollable(
+ aInitialEvent, &mAllowedScrollDirections);
if (apzc && apzc != GetTargetApzc()) {
UpdateTargetApzc(apzc);
}
}
}
bool
@@ -567,17 +570,18 @@ PanGestureBlockState::SetConfirmedTarget
InputData* aFirstInput)
{
// 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);
+ apzc->BuildOverscrollHandoffChain()->FindFirstScrollable(
+ *aFirstInput, &mAllowedScrollDirections);
if (scrollableApzc) {
apzc = scrollableApzc;
}
}
InputBlockState::SetConfirmedTargetApzc(apzc, aState, aFirstInput);
return true;
}
--- a/gfx/layers/apz/src/InputBlockState.h
+++ b/gfx/layers/apz/src/InputBlockState.h
@@ -277,24 +277,27 @@ public:
*/
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;
+ ScrollDirections mAllowedScrollDirections;
};
/**
* A block of mouse events that are part of a drag
*/
class DragBlockState : public CancelableBlockState
{
public:
@@ -349,19 +352,22 @@ public:
* @return Whether or not overscrolling is prevented for this block.
*/
bool AllowScrollHandoff() const;
bool WasInterrupted() const { return mInterrupted; }
void SetNeedsToWaitForContentResponse(bool aWaitForContentResponse);
+ ScrollDirections GetAllowedScrollDirections() const { return mAllowedScrollDirections; }
+
private:
bool mInterrupted;
bool mWaitingForContentResponse;
+ ScrollDirections mAllowedScrollDirections;
};
/**
* 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
--- a/gfx/layers/apz/src/InputQueue.cpp
+++ b/gfx/layers/apz/src/InputQueue.cpp
@@ -313,19 +313,23 @@ InputQueue::ReceiveKeyboardInput(const R
}
static bool
CanScrollTargetHorizontally(const PanGestureInput& aInitialEvent,
PanGestureBlockState* aBlock)
{
PanGestureInput horizontalComponent = aInitialEvent;
horizontalComponent.mPanDisplacement.y = 0;
+ ScrollDirections allowedScrollDirections;
RefPtr<AsyncPanZoomController> horizontallyScrollableAPZC =
- aBlock->GetOverscrollHandoffChain()->FindFirstScrollable(horizontalComponent);
- return horizontallyScrollableAPZC && horizontallyScrollableAPZC == aBlock->GetTargetApzc();
+ aBlock->GetOverscrollHandoffChain()->FindFirstScrollable(
+ horizontalComponent, &allowedScrollDirections);
+ return horizontallyScrollableAPZC &&
+ horizontallyScrollableAPZC == aBlock->GetTargetApzc() &&
+ allowedScrollDirections.contains(ScrollDirection::eHorizontal);
}
nsEventStatus
InputQueue::ReceivePanGestureInput(const RefPtr<AsyncPanZoomController>& aTarget,
bool aTargetConfirmed,
const PanGestureInput& aEvent,
uint64_t* aOutInputBlockId) {
if (aEvent.mType == PanGestureInput::PANGESTURE_MAYSTART ||
--- a/gfx/layers/apz/src/OverscrollHandoffState.cpp
+++ b/gfx/layers/apz/src/OverscrollHandoffState.cpp
@@ -156,20 +156,32 @@ OverscrollHandoffChain::HasOverscrolledA
bool
OverscrollHandoffChain::HasFastFlungApzc() const
{
return AnyApzc(&AsyncPanZoomController::IsFlingingFast);
}
RefPtr<AsyncPanZoomController>
-OverscrollHandoffChain::FindFirstScrollable(const InputData& aInput) const
+OverscrollHandoffChain::FindFirstScrollable(
+ const InputData& aInput,
+ ScrollDirections* aOutAllowedScrollDirections) 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];
}
+
+ *aOutAllowedScrollDirections &= mChain[i]->GetAllowedHandoffDirections();
+ if (aOutAllowedScrollDirections->isEmpty()) {
+ return nullptr;
+ }
}
return nullptr;
}
} // namespace layers
} // namespace mozilla
--- a/gfx/layers/apz/src/OverscrollHandoffState.h
+++ b/gfx/layers/apz/src/OverscrollHandoffState.h
@@ -84,17 +84,23 @@ public:
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;
- RefPtr<AsyncPanZoomController> FindFirstScrollable(const InputData& aInput) 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.
+ RefPtr<AsyncPanZoomController> FindFirstScrollable(
+ const InputData& aInput,
+ ScrollDirections* aOutAllowedScrollDirections) 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;