Bug 951793 - Obey overscroll-behavior for wheel and pan gesture events. r=kats draft
authorBotond Ballo <botond@mozilla.com>
Fri, 17 Nov 2017 18:52:58 -0500
changeset 703290 f681f55f274757be4965e6c912e514e81e8b1c8f
parent 703289 429438b26c2ab0b75690f87bd6b9b0b9187a4567
child 703291 43f8d69a8e10f778179e77679cb7581b95d25b87
push id90783
push userbballo@mozilla.com
push dateFri, 24 Nov 2017 21:22:10 +0000
reviewerskats
bugs951793
milestone59.0a1
Bug 951793 - Obey overscroll-behavior for wheel and pan gesture events. r=kats MozReview-Commit-ID: EmbsMu9Esww
gfx/layers/apz/src/APZUtils.h
gfx/layers/apz/src/AsyncPanZoomController.cpp
gfx/layers/apz/src/AsyncPanZoomController.h
gfx/layers/apz/src/InputBlockState.cpp
gfx/layers/apz/src/InputBlockState.h
gfx/layers/apz/src/InputQueue.cpp
gfx/layers/apz/src/OverscrollHandoffState.cpp
gfx/layers/apz/src/OverscrollHandoffState.h
--- 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;