Bug 1367765 - Implement scrollbar touch-dragging in APZ. r=rhunt draft
authorBotond Ballo <botond@mozilla.com>
Tue, 12 Sep 2017 19:38:26 -0400
changeset 663395 2e1420a9ae2b9b16c54b89a9cafa0aa9926434b2
parent 662630 bc5c153f2b2ca09ea3253b50efbca629763305e3
child 663396 94ac60bc8b38dad3d8abaa39b5a94de88ec0f6b0
push id79426
push userbballo@mozilla.com
push dateTue, 12 Sep 2017 23:39:18 +0000
reviewersrhunt
bugs1367765
milestone57.0a1
Bug 1367765 - Implement scrollbar touch-dragging in APZ. r=rhunt The general implementation strategy is to detect whether a touch block is a scrollbar drag, and if so, convert the touch events to mouse events before sending them to InputQueue. This is done so that code for handling mouse-drags in InputQueue and APZC can be re-used. Content will receive the original touch events, not the synthesized mouse events. This is done for two reasons: 1) We don't really have a mechanism for APZ to send content synthesized events. 2) Content already deals with touch events that drag a scroll thumb correctly. MozReview-Commit-ID: 7cksE5FMGoS
gfx/layers/apz/src/APZCTreeManager.cpp
gfx/layers/apz/src/APZCTreeManager.h
--- a/gfx/layers/apz/src/APZCTreeManager.cpp
+++ b/gfx/layers/apz/src/APZCTreeManager.cpp
@@ -216,16 +216,17 @@ APZCTreeManager::CalculatePendingDisplay
     aFrameMetrics, aVelocity);
 }
 
 APZCTreeManager::APZCTreeManager()
     : mInputQueue(new InputQueue()),
       mTreeLock("APZCTreeLock"),
       mHitResultForInputBlock(HitNothing),
       mRetainedTouchIdentifier(-1),
+      mInScrollbarTouchDrag(false),
       mApzcTreeLog("apzctree")
 {
   RefPtr<APZCTreeManager> self(this);
   NS_DispatchToMainThread(
     NS_NewRunnableFunction("layers::APZCTreeManager::APZCTreeManager", [self] {
       self->mFlushObserver = new CheckerboardFlushObserver(self);
     }));
   AsyncPanZoomController::InitializeGlobalState();
@@ -1343,37 +1344,42 @@ ConvertToTouchBehavior(HitTestResult res
   }
   MOZ_ASSERT_UNREACHABLE("Invalid value");
   return AllowedTouchBehavior::UNKNOWN;
 }
 
 already_AddRefed<AsyncPanZoomController>
 APZCTreeManager::GetTouchInputBlockAPZC(const MultiTouchInput& aEvent,
                                         nsTArray<TouchBehaviorFlags>* aOutTouchBehaviors,
-                                        HitTestResult* aOutHitResult)
+                                        HitTestResult* aOutHitResult,
+                                        RefPtr<HitTestingTreeNode>* aOutHitScrollbarNode)
 {
   RefPtr<AsyncPanZoomController> apzc;
   if (aEvent.mTouches.Length() == 0) {
     return apzc.forget();
   }
 
   FlushRepaintsToClearScreenToGeckoTransform();
 
   HitTestResult hitResult;
-  apzc = GetTargetAPZC(aEvent.mTouches[0].mScreenPoint, &hitResult);
+  apzc = GetTargetAPZC(aEvent.mTouches[0].mScreenPoint, &hitResult,
+      aOutHitScrollbarNode);
   if (aOutTouchBehaviors) {
     aOutTouchBehaviors->AppendElement(ConvertToTouchBehavior(hitResult));
   }
   for (size_t i = 1; i < aEvent.mTouches.Length(); i++) {
     RefPtr<AsyncPanZoomController> apzc2 = GetTargetAPZC(aEvent.mTouches[i].mScreenPoint, &hitResult);
     if (aOutTouchBehaviors) {
       aOutTouchBehaviors->AppendElement(ConvertToTouchBehavior(hitResult));
     }
     apzc = GetMultitouchTarget(apzc, apzc2);
     APZCTM_LOG("Using APZC %p as the root APZC for multi-touch\n", apzc.get());
+    // A multi-touch gesture will not be a scrollbar drag, even if the
+    // first touch point happened to hit a scrollbar.
+    *aOutHitScrollbarNode = nullptr;
   }
 
   if (aOutHitResult) {
     // XXX we should probably be combining the hit results from the different
     // touch points somehow, instead of just using the last one.
     *aOutHitResult = hitResult;
   }
   return apzc.forget();
@@ -1381,16 +1387,17 @@ APZCTreeManager::GetTouchInputBlockAPZC(
 
 nsEventStatus
 APZCTreeManager::ProcessTouchInput(MultiTouchInput& aInput,
                                    ScrollableLayerGuid* aOutTargetGuid,
                                    uint64_t* aOutInputBlockId)
 {
   aInput.mHandledByAPZ = true;
   nsTArray<TouchBehaviorFlags> touchBehaviors;
+  RefPtr<HitTestingTreeNode> hitScrollbarNode = nullptr;
   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.)
@@ -1399,104 +1406,189 @@ APZCTreeManager::ProcessTouchInput(Multi
         BuildOverscrollHandoffChain(mApzcForInputBlock)->HasOverscrolledApzc()) {
       if (mRetainedTouchIdentifier == -1) {
         mRetainedTouchIdentifier = mApzcForInputBlock->GetLastTouchIdentifier();
       }
       return nsEventStatus_eConsumeNoDefault;
     }
 
     mHitResultForInputBlock = HitNothing;
-    mApzcForInputBlock = GetTouchInputBlockAPZC(aInput, &touchBehaviors, &mHitResultForInputBlock);
+    mApzcForInputBlock = GetTouchInputBlockAPZC(aInput, &touchBehaviors,
+        &mHitResultForInputBlock, &hitScrollbarNode);
+
+    // 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 = gfxPrefs::APZDragEnabled() && hitScrollbarNode &&
+                            hitScrollbarNode->IsScrollThumbNode() &&
+                            hitScrollbarNode->GetScrollThumbData().mIsAsyncDraggable;
+
     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 (mApzcForInputBlock) {
     APZCTM_LOG("Re-using APZC %p as continuation of event block\n", mApzcForInputBlock.get());
   }
 
-  // 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;
-  }
+  nsEventStatus result = nsEventStatus_eIgnore;
+
+  if (mInScrollbarTouchDrag) {
+    result = ProcessTouchInputForScrollbarDrag(aInput, hitScrollbarNode.get(),
+        aOutTargetGuid, aOutInputBlockId);
+  } 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);
+    // 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;
         }
-        --j;
+      }
+      if (aInput.mTouches.IsEmpty()) {
+        return nsEventStatus_eConsumeNoDefault;
       }
     }
-    if (aInput.mTouches.IsEmpty()) {
-      return nsEventStatus_eConsumeNoDefault;
-    }
-  }
+
+    if (mApzcForInputBlock) {
+      MOZ_ASSERT(mHitResultForInputBlock != HitNothing);
 
-  nsEventStatus result = nsEventStatus_eIgnore;
-  if (mApzcForInputBlock) {
-    MOZ_ASSERT(mHitResultForInputBlock != HitNothing);
+      mApzcForInputBlock->GetGuid(aOutTargetGuid);
+      uint64_t inputBlockId = 0;
+      result = mInputQueue->ReceiveInputEvent(mApzcForInputBlock,
+          /* aTargetConfirmed = */ mHitResultForInputBlock != HitDispatchToContentRegion,
+          aInput, &inputBlockId);
+      if (aOutInputBlockId) {
+        *aOutInputBlockId = inputBlockId;
+      }
+      if (!touchBehaviors.IsEmpty()) {
+        mInputQueue->SetAllowedTouchBehavior(inputBlockId, touchBehaviors);
+      }
 
-    mApzcForInputBlock->GetGuid(aOutTargetGuid);
-    uint64_t inputBlockId = 0;
-    result = mInputQueue->ReceiveInputEvent(mApzcForInputBlock,
-        /* aTargetConfirmed = */ mHitResultForInputBlock != HitDispatchToContentRegion,
-        aInput, &inputBlockId);
-    if (aOutInputBlockId) {
-      *aOutInputBlockId = inputBlockId;
-    }
-    if (!touchBehaviors.IsEmpty()) {
-      mInputQueue->SetAllowedTouchBehavior(inputBlockId, 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.
+      ScreenToParentLayerMatrix4x4 transformToApzc = GetScreenToApzcTransform(mApzcForInputBlock);
+      ParentLayerToScreenMatrix4x4 transformToGecko = GetApzcToGeckoTransform(mApzcForInputBlock);
+      ScreenToScreenMatrix4x4 outTransform = transformToApzc * transformToGecko;
 
-    // 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.
-    ScreenToParentLayerMatrix4x4 transformToApzc = GetScreenToApzcTransform(mApzcForInputBlock);
-    ParentLayerToScreenMatrix4x4 transformToGecko = GetApzcToGeckoTransform(mApzcForInputBlock);
-    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) {
-        return nsEventStatus_eIgnore;
+      for (size_t i = 0; i < aInput.mTouches.Length(); i++) {
+        SingleTouchData& touchData = aInput.mTouches[i];
+        Maybe<ScreenIntPoint> untransformedScreenPoint = UntransformBy(
+            outTransform, touchData.mScreenPoint);
+        if (!untransformedScreenPoint) {
+          return nsEventStatus_eIgnore;
+        }
+        touchData.mScreenPoint = *untransformedScreenPoint;
       }
-      touchData.mScreenPoint = *untransformedScreenPoint;
     }
   }
 
   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) {
     mApzcForInputBlock = nullptr;
     mHitResultForInputBlock = HitNothing;
     mRetainedTouchIdentifier = -1;
+    mInScrollbarTouchDrag = false;
   }
 
   return result;
 }
 
+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;
+}
+
+nsEventStatus
+APZCTreeManager::ProcessTouchInputForScrollbarDrag(MultiTouchInput& aTouchInput,
+                                                   const HitTestingTreeNode* aScrollThumbNode,
+                                                   ScrollableLayerGuid* aOutTargetGuid,
+                                                   uint64_t* aOutInputBlockId)
+{
+  MOZ_ASSERT(mRetainedTouchIdentifier == -1);
+  MOZ_ASSERT(mApzcForInputBlock);
+  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::LEFT_BUTTON,
+                        nsIDOMMouseEvent::MOZ_SOURCE_TOUCH,
+                        WidgetMouseEvent::eLeftButtonFlag,
+                        aTouchInput.mTouches[0].mScreenPoint,
+                        aTouchInput.mTime,
+                        aTouchInput.mTimeStamp,
+                        aTouchInput.modifiers};
+  mouseInput.mHandledByAPZ = true;
+
+  // The value of |targetConfirmed| passed to InputQueue::ReceiveInputEvent()
+  // only matters for the first event, which creates the drag block. For
+  // that event, the correct value is false, since the drag block will, at the
+  // earliest, be confirmed in the subsequent SetupScrollbarDrag() call.
+  bool targetConfirmed = false;
+
+  nsEventStatus result = mInputQueue->ReceiveInputEvent(mApzcForInputBlock,
+      targetConfirmed, mouseInput, aOutInputBlockId);
+
+  // |aScrollThumbNode| is non-null iff. this is the event that starts the drag.
+  // If so, set up the drag.
+  if (aScrollThumbNode) {
+    SetupScrollbarDrag(mouseInput, aScrollThumbNode, mApzcForInputBlock.get());
+  }
+
+  mApzcForInputBlock->GetGuid(aOutTargetGuid);
+
+  // 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.
+  aOutTargetGuid->mScrollId = FrameMetrics::NULL_SCROLL_ID;
+
+  return result;
+}
+
 void
 APZCTreeManager::SetupScrollbarDrag(MouseInput& aMouseInput,
                                     const HitTestingTreeNode* aScrollThumbNode,
                                     AsyncPanZoomController* aApzc)
 {
   DragBlockState* dragBlock = mInputQueue->GetCurrentDragBlock();
   if (!dragBlock) {
     return;
--- a/gfx/layers/apz/src/APZCTreeManager.h
+++ b/gfx/layers/apz/src/APZCTreeManager.h
@@ -522,19 +522,38 @@ private:
                                          const ParentLayerPoint& aHitTestPoint,
                                          HitTestResult* aOutHitResult,
                                          HitTestingTreeNode** aOutScrollbarNode);
   AsyncPanZoomController* FindRootApzcForLayersId(uint64_t aLayersId) const;
   AsyncPanZoomController* FindRootContentApzcForLayersId(uint64_t aLayersId) const;
   AsyncPanZoomController* FindRootContentOrRootApzc() const;
   already_AddRefed<AsyncPanZoomController> GetMultitouchTarget(AsyncPanZoomController* aApzc1, AsyncPanZoomController* aApzc2) const;
   already_AddRefed<AsyncPanZoomController> CommonAncestor(AsyncPanZoomController* aApzc1, AsyncPanZoomController* aApzc2) 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.
+   * @param aOutHitResult The hit test result.
+   * @param aOutHitScrollbarNode
+   *     If the touch event contains a single touch point (so that it may
+   *     potentially start a scrollbar drag), and a scrollbar node was hit,
+   *     that scrollbar node, otherwise nullptr.
+   *
+   * @return The APZC that was hit.
+   */
   already_AddRefed<AsyncPanZoomController> GetTouchInputBlockAPZC(const MultiTouchInput& aEvent,
                                                                   nsTArray<TouchBehaviorFlags>* aOutTouchBehaviors,
-                                                                  HitTestResult* aOutHitResult);
+                                                                  HitTestResult* aOutHitResult,
+                                                                  RefPtr<HitTestingTreeNode>* aOutHitScrollbarNode);
   nsEventStatus ProcessTouchInput(MultiTouchInput& aInput,
                                   ScrollableLayerGuid* aOutTargetGuid,
                                   uint64_t* aOutInputBlockId);
   /**
    * 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.
@@ -544,16 +563,34 @@ private:
    * @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 HitTestingTreeNode* 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 aOutTargetGuid
+   *     The guid of the APZC for the scroll frame whose scroll thumb is
+   *     being dragged.
+   * @param aOutInputBlockId
+   *     The ID of the input block for the touch-drag gesture.
+   * @return See ReceiveInputEvent() for what the return value means.
+   */
+  nsEventStatus ProcessTouchInputForScrollbarDrag(MultiTouchInput& aInput,
+                                                  const HitTestingTreeNode* aScrollThumbNode,
+                                                  ScrollableLayerGuid* aOutTargetGuid,
+                                                  uint64_t* aOutInputBlockId);
   void FlushRepaintsToClearScreenToGeckoTransform();
 
   already_AddRefed<HitTestingTreeNode> RecycleOrCreateNode(TreeBuildingState& aState,
                                                            AsyncPanZoomController* aApzc,
                                                            uint64_t aLayersId);
   template<class ScrollNode>
   HitTestingTreeNode* PrepareNodeForLayer(const ScrollNode& aLayer,
                                           const FrameMetrics& aMetrics,
@@ -609,16 +646,23 @@ private:
    * sync with mApzcForInputBlock.
    */
   HitTestResult mHitResultForInputBlock;
   /* 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;
   /* Stores the current mouse position in screen coordinates.
    */
   ScreenPoint mCurrentMousePosition;
   /* For logging the APZC tree for debugging (enabled by the apz.printtree
    * pref). */