End APZ wheel transactions when the mouse moves out of frame. (bug 1142866 part 2, r=kats,botond)
authorDavid Anderson <danderson@mozilla.com>
Sun, 22 Mar 2015 00:42:25 -0700
changeset 234983 841bf908250131cad069d06fc5562138d073809d
parent 234982 e24bb39a0c1b8322943a6f22833238123dc84ed8
child 234984 78a291fd62b34995870f6b1ddcb030ed68c94478
push id28462
push usercbook@mozilla.com
push dateMon, 23 Mar 2015 12:19:34 +0000
treeherdermozilla-central@bc85c479668a [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerskats, botond
bugs1142866
milestone39.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
End APZ wheel transactions when the mouse moves out of frame. (bug 1142866 part 2, r=kats,botond)
gfx/layers/apz/src/APZCTreeManager.cpp
gfx/layers/apz/src/APZCTreeManager.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/thebes/gfxPrefs.h
layout/base/UnitTransforms.h
--- a/gfx/layers/apz/src/APZCTreeManager.cpp
+++ b/gfx/layers/apz/src/APZCTreeManager.cpp
@@ -801,24 +801,68 @@ APZCTreeManager::TransformCoordinateToGe
   if (apzc && aOutTransformedPoint) {
     Matrix4x4 transformToApzc = GetScreenToApzcTransform(apzc);
     Matrix4x4 transformToGecko = GetApzcToGeckoTransform(apzc);
     Matrix4x4 outTransform = transformToApzc * transformToGecko;
     *aOutTransformedPoint = TransformTo<LayoutDevicePixel>(outTransform, aPoint);
   }
 }
 
+void
+APZCTreeManager::UpdateWheelTransaction(WidgetInputEvent& aEvent)
+{
+  WheelBlockState* txn = mInputQueue->GetCurrentWheelTransaction();
+  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 (aEvent.message) {
+   case NS_MOUSE_MOVE:
+   case NS_DRAGDROP_OVER: {
+     WidgetMouseEvent* mouseEvent = aEvent.AsMouseEvent();
+     if (!mouseEvent->IsReal()) {
+       return;
+     }
+
+     ScreenIntPoint point =
+       ViewAs<ScreenPixel>(aEvent.refPoint, PixelCastJustification::LayoutDeviceToScreenForUntransformedEvent);
+     txn->OnMouseMove(point);
+     return;
+   }
+   case NS_KEY_PRESS:
+   case NS_KEY_UP:
+   case NS_KEY_DOWN:
+   case NS_MOUSE_BUTTON_UP:
+   case NS_MOUSE_BUTTON_DOWN:
+   case NS_MOUSE_DOUBLECLICK:
+   case NS_MOUSE_CLICK:
+   case NS_CONTEXTMENU:
+   case NS_DRAGDROP_DROP:
+     txn->EndTransaction();
+     return;
+  }
+}
+
 nsEventStatus
 APZCTreeManager::ProcessEvent(WidgetInputEvent& aEvent,
                               ScrollableLayerGuid* aOutTargetGuid,
                               uint64_t* aOutInputBlockId)
 {
   MOZ_ASSERT(NS_IsMainThread());
   nsEventStatus result = nsEventStatus_eIgnore;
 
+  // Note, we call this before having transformed the reference point.
+  UpdateWheelTransaction(aEvent);
+
   // Transform the refPoint.
   // If the event hits an overscrolled APZC, instruct the caller to ignore it.
   HitTestResult hitResult = HitNothing;
   nsRefPtr<AsyncPanZoomController> apzc = GetTargetAPZC(ScreenPoint(aEvent.refPoint.x, aEvent.refPoint.y),
                                                         &hitResult);
   if (apzc) {
     MOZ_ASSERT(hitResult == HitLayer || hitResult == HitDispatchToContentRegion);
     apzc->GetGuid(aOutTargetGuid);
--- a/gfx/layers/apz/src/APZCTreeManager.h
+++ b/gfx/layers/apz/src/APZCTreeManager.h
@@ -431,16 +431,17 @@ private:
                                   ScrollableLayerGuid* aOutTargetGuid,
                                   uint64_t* aOutInputBlockId);
   nsEventStatus ProcessWheelEvent(WidgetWheelEvent& aEvent,
                                   ScrollableLayerGuid* aOutTargetGuid,
                                   uint64_t* aOutInputBlockId);
   nsEventStatus ProcessEvent(WidgetInputEvent& inputEvent,
                              ScrollableLayerGuid* aOutTargetGuid,
                              uint64_t* aOutInputBlockId);
+  void UpdateWheelTransaction(WidgetInputEvent& aEvent);
   void UpdateZoomConstraintsRecursively(HitTestingTreeNode* aNode,
                                         const ZoomConstraints& aConstraints);
   void FlushRepaintsRecursively(HitTestingTreeNode* aNode);
 
   already_AddRefed<HitTestingTreeNode> RecycleOrCreateNode(TreeBuildingState& aState,
                                                            AsyncPanZoomController* aApzc);
   HitTestingTreeNode* PrepareNodeForLayer(const LayerMetricsWrapper& aLayer,
                                           const FrameMetrics& aMetrics,
--- a/gfx/layers/apz/src/AsyncPanZoomController.cpp
+++ b/gfx/layers/apz/src/AsyncPanZoomController.cpp
@@ -1762,16 +1762,29 @@ ScreenPoint AsyncPanZoomController::ToSc
   return TransformVector<ScreenPixel>(GetTransformToThis().Inverse(), aVector, aAnchor);
 }
 
 ParentLayerPoint AsyncPanZoomController::ToParentLayerCoordinates(const ScreenPoint& aVector,
                                                                   const ScreenPoint& aAnchor) const {
   return TransformVector<ParentLayerPixel>(GetTransformToThis(), aVector, aAnchor);
 }
 
+bool AsyncPanZoomController::Contains(const ScreenIntPoint& aPoint) const
+{
+  Matrix4x4 transformToThis = GetTransformToThis();
+  ParentLayerIntPoint point = TransformTo<ParentLayerPixel>(transformToThis, aPoint);
+
+  ParentLayerIntRect cb;
+  {
+    ReentrantMonitorAutoEnter lock(mMonitor);
+    GetFrameMetrics().mCompositionBounds.ToIntRect(&cb);
+  }
+  return cb.Contains(point);
+}
+
 ScreenCoord AsyncPanZoomController::PanDistance() const {
   ParentLayerPoint panVector;
   ParentLayerPoint panStart;
   {
     ReentrantMonitorAutoEnter lock(mMonitor);
     panVector = ParentLayerPoint(mX.PanDistance(), mY.PanDistance());
     panStart = PanStart();
   }
--- a/gfx/layers/apz/src/AsyncPanZoomController.h
+++ b/gfx/layers/apz/src/AsyncPanZoomController.h
@@ -994,16 +994,20 @@ public:
   void SetAncestorTransform(const Matrix4x4& aTransformToLayer) {
     mAncestorTransform = aTransformToLayer;
   }
 
   Matrix4x4 GetAncestorTransform() const {
     return mAncestorTransform;
   }
 
+  // Returns whether or not this apzc contains the given screen point within
+  // its composition bounds.
+  bool Contains(const ScreenIntPoint& aPoint) const;
+
   bool IsOverscrolled() const {
     return mX.IsOverscrolled() || mY.IsOverscrolled();
   }
 
 private:
   /* This is the cumulative CSS transform for all the layers from (and including)
    * the parent APZC down to (but excluding) this one. */
   Matrix4x4 mAncestorTransform;
--- a/gfx/layers/apz/src/InputBlockState.cpp
+++ b/gfx/layers/apz/src/InputBlockState.cpp
@@ -136,68 +136,88 @@ bool
 CancelableBlockState::IsReadyForHandling() const
 {
   if (!IsTargetConfirmed())
     return false;
   return mContentResponded || mContentResponseTimerExpired;
 }
 
 void
-CancelableBlockState::DispatchImmediate(const InputData& aEvent)
+CancelableBlockState::DispatchImmediate(const InputData& aEvent) const
 {
   MOZ_ASSERT(!HasEvents());
   MOZ_ASSERT(GetTargetApzc());
   GetTargetApzc()->HandleInputEvent(aEvent, mTransformToApzc);
 }
 
 // This is used to track the current wheel transaction.
 static uint64_t sLastWheelBlockId = InputBlockState::NO_BLOCK_ID;
 
 WheelBlockState::WheelBlockState(const nsRefPtr<AsyncPanZoomController>& aTargetApzc,
                                  bool aTargetConfirmed,
                                  const ScrollWheelInput& aInitialEvent)
   : CancelableBlockState(aTargetApzc, aTargetConfirmed)
   , mTransactionEnded(false)
 {
   sLastWheelBlockId = GetBlockId();
-  Update(aInitialEvent);
 
   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.
     nsRefPtr<AsyncPanZoomController> apzc =
       mOverscrollHandoffChain->FindFirstScrollable(aInitialEvent);
-    if (apzc && apzc != GetTargetApzc()) {
+
+    // If nothing is scrollable, we don't consider this block as starting a
+    // transaction.
+    if (!apzc) {
+      EndTransaction();
+      return;
+    }
+
+    if (apzc != GetTargetApzc()) {
       UpdateTargetApzc(apzc);
     }
   }
 }
 
 void
 WheelBlockState::Update(const 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;
+  }
+
+  // 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.
+  nsRefPtr<AsyncPanZoomController> apzc = GetTargetApzc();
+  if (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();
 }
 
 void
 WheelBlockState::AddEvent(const ScrollWheelInput& aEvent)
 {
-  Update(aEvent);
   mEvents.AppendElement(aEvent);
 }
 
-void
-WheelBlockState::DispatchImmediate(const InputData& aEvent)
-{
-  Update(aEvent.AsScrollWheelInput());
-  CancelableBlockState::DispatchImmediate(aEvent);
-}
-
 bool
 WheelBlockState::IsReadyForHandling() const
 {
   if (!CancelableBlockState::IsReadyForHandling()) {
     return false;
   }
   return true;
 }
@@ -234,38 +254,86 @@ WheelBlockState::MustStayActive()
 
 const char*
 WheelBlockState::Type()
 {
   return "scroll wheel";
 }
 
 bool
-WheelBlockState::ShouldAcceptNewEvent(const ScrollWheelInput& aEvent) const
+WheelBlockState::ShouldAcceptNewEvent() const
 {
   if (!InTransaction()) {
     // If we're not in a transaction, start a new one.
     return false;
   }
   nsRefPtr<AsyncPanZoomController> apzc = GetTargetApzc();
   if (!apzc || apzc->IsDestroyed()) {
     return false;
   }
 
+  return true;
+}
+
+bool
+WheelBlockState::MaybeTimeout(const ScrollWheelInput& aEvent)
+{
+  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() >= gfxPrefs::MouseWheelIgnoreMoveDelayMs()) {
+      TBS_LOG("%p wheel transaction timed out after mouse move\n", this);
+      EndTransaction();
+      return true;
+    }
+  }
+
+  return false;
+}
+
+bool
+WheelBlockState::MaybeTimeout(const TimeStamp& aTimeStamp)
+{
   // End the transaction if the event occurred > 1.5s after the most recently
   // seen wheel event.
-  TimeDuration duration = aEvent.mTimeStamp - mLastEventTime;
-  if (duration.ToMilliseconds() >= gfxPrefs::MouseWheelTransactionTimeoutMs()) {
+  TimeDuration duration = aTimeStamp - mLastEventTime;
+  if (duration.ToMilliseconds() < gfxPrefs::MouseWheelTransactionTimeoutMs()) {
     return false;
   }
 
-  // Accept the event, even if we can't scroll anymore.
+  TBS_LOG("%p wheel transaction timed out\n", this);
+
+  EndTransaction();
   return true;
 }
 
+void
+WheelBlockState::OnMouseMove(const ScreenIntPoint& aPoint)
+{
+  if (!GetTargetApzc()->Contains(aPoint)) {
+    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() >= gfxPrefs::MouseWheelIgnoreMoveDelayMs()) {
+      mLastMouseMove = now;
+    }
+  }
+}
+
 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;
   }
@@ -278,16 +346,17 @@ WheelBlockState::AllowScrollHandoff() co
   // 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;
 }
 
 TouchBlockState::TouchBlockState(const nsRefPtr<AsyncPanZoomController>& aTargetApzc,
                                  bool aTargetConfirmed)
   : CancelableBlockState(aTargetApzc, aTargetConfirmed)
   , mAllowedTouchBehaviorSet(false)
   , mDuringFastMotion(false)
--- a/gfx/layers/apz/src/InputBlockState.h
+++ b/gfx/layers/apz/src/InputBlockState.h
@@ -105,17 +105,17 @@ public:
    */
   bool IsDefaultPrevented() const;
 
   /**
    * Process the given event using this input block's target apzc.
    * This input block must not have pending events, and its apzc must not be
    * nullptr.
    */
-  virtual void DispatchImmediate(const InputData& aEvent);
+  void DispatchImmediate(const InputData& aEvent) const;
 
   /**
    * @return true iff this block has received all the information needed
    *         to properly dispatch the events in the block.
    */
   virtual bool IsReadyForHandling() const;
 
   /**
@@ -162,32 +162,39 @@ public:
                   const ScrollWheelInput& aEvent);
 
   bool IsReadyForHandling() const override;
   bool HasEvents() const override;
   void DropEvents() override;
   void HandleEvents() override;
   bool MustStayActive() override;
   const char* Type() override;
-  void DispatchImmediate(const InputData& aEvent) override;
 
   void AddEvent(const ScrollWheelInput& aEvent);
 
   WheelBlockState *AsWheelBlock() override {
     return this;
   }
 
   /**
-   * Returns whether or not the block should accept new events. If the APZC
-   * has been destroyed, or the block is not part of a wheel transaction, then
-   * this will return false.
-   *
-   * @return True if the event should be accepted, false otherwise.
+   * 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 ShouldAcceptNewEvent(const ScrollWheelInput& aEvent) const;
+  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);
 
   /**
    * 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.
@@ -200,25 +207,32 @@ public:
    */
   void EndTransaction();
 
   /**
    * @return Whether or not overscrolling is prevented for this wheel block.
    */
   bool AllowScrollHandoff() const;
 
-private:
+  /**
+   * 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(const ScrollWheelInput& aEvent);
 
 private:
   nsTArray<ScrollWheelInput> mEvents;
   TimeStamp mLastEventTime;
+  TimeStamp mLastMouseMove;
   bool mTransactionEnded;
 };
 
 /**
  * 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.
  *
--- a/gfx/layers/apz/src/InputQueue.cpp
+++ b/gfx/layers/apz/src/InputQueue.cpp
@@ -157,17 +157,20 @@ InputQueue::ReceiveScrollWheelInput(cons
                                     const ScrollWheelInput& aEvent,
                                     uint64_t* aOutInputBlockId) {
   WheelBlockState* block = nullptr;
   if (!mInputBlockQueue.IsEmpty()) {
     block = mInputBlockQueue.LastElement()->AsWheelBlock();
 
     // 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(aEvent)) {
+    if (block &&
+        (!block->ShouldAcceptNewEvent() ||
+         block->MaybeTimeout(aEvent)))
+    {
       block = nullptr;
     }
   }
 
   // If we have a block, it should be in a wheel transaction.
   MOZ_ASSERT(!block || block->InTransaction());
 
   if (!block) {
@@ -182,16 +185,18 @@ InputQueue::ReceiveScrollWheelInput(cons
   } else {
     INPQ_LOG("received new event in block %p\n", block);
   }
 
   if (aOutInputBlockId) {
     *aOutInputBlockId = block->GetBlockId();
   }
 
+  block->Update(aEvent);
+
   // 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
   // MaybeHandleCurrentBlock() does.
   if (!MaybeHandleCurrentBlock(block, aEvent)) {
     block->AddEvent(aEvent.AsScrollWheelInput());
   }
--- a/gfx/thebes/gfxPrefs.h
+++ b/gfx/thebes/gfxPrefs.h
@@ -332,16 +332,17 @@ private:
   DECL_GFX_PREF(Once, "layout.paint_rects_separately",         LayoutPaintRectsSeparately, bool, true);
 
   // This and code dependent on it should be removed once containerless scrolling looks stable.
   DECL_GFX_PREF(Once, "layout.scroll.root-frame-containers",   LayoutUseContainersForRootFrames, bool, true);
 
   // This affects whether events will be routed through APZ or not.
   DECL_GFX_PREF(Once, "mousewheel.system_scroll_override_on_root_content.enabled",
                                                                MouseWheelHasScrollDeltaOverride, bool, false);
+  DECL_GFX_PREF(Live, "mousewheel.transaction.ignoremovedelay",MouseWheelIgnoreMoveDelayMs, int32_t, (int32_t)100);
   DECL_GFX_PREF(Live, "mousewheel.transaction.timeout",        MouseWheelTransactionTimeoutMs, int32_t, (int32_t)1500);
 
 
   DECL_GFX_PREF(Live, "nglayout.debug.widget_update_flashing", WidgetUpdateFlashing, bool, false);
   DECL_GFX_PREF(Live, "ui.click_hold_context_menus.delay",     UiClickHoldContextMenusDelay, int32_t, 500);
   DECL_GFX_PREF(Once, "webgl.angle.force-d3d11",               WebGLANGLEForceD3D11, bool, false);
   DECL_GFX_PREF(Once, "webgl.angle.try-d3d11",                 WebGLANGLETryD3D11, bool, false);
   DECL_GFX_PREF(Once, "webgl.force-layers-readback",           WebGLForceLayersReadback, bool, false);
--- a/layout/base/UnitTransforms.h
+++ b/layout/base/UnitTransforms.h
@@ -25,31 +25,40 @@ enum class PixelCastJustification : uint
   // For the root composition size we want to view it as layer pixels in any layer
   ParentLayerToLayerForRootComposition,
   // The Layer coordinate space for one layer is the ParentLayer coordinate
   // space for its children
   MovingDownToChildren,
   // The transform that is usually used to convert between two coordinate
   // systems is not available (for example, because the object that stores it
   // is being destroyed), so fall back to the identity.
-  TransformNotAvailable
+  TransformNotAvailable,
+  // When an OS event is initially constructed, its reference point is
+  // technically in screen pixels, as it has not yet accounted for any
+  // asynchronous transforms. This justification is for viewing the initial
+  // reference point as a screen point.
+  LayoutDeviceToScreenForUntransformedEvent
 };
 
 template <class TargetUnits, class SourceUnits>
 gfx::SizeTyped<TargetUnits> ViewAs(const gfx::SizeTyped<SourceUnits>& aSize, PixelCastJustification) {
   return gfx::SizeTyped<TargetUnits>(aSize.width, aSize.height);
 }
 template <class TargetUnits, class SourceUnits>
 gfx::IntSizeTyped<TargetUnits> ViewAs(const gfx::IntSizeTyped<SourceUnits>& aSize, PixelCastJustification) {
   return gfx::IntSizeTyped<TargetUnits>(aSize.width, aSize.height);
 }
 template <class TargetUnits, class SourceUnits>
 gfx::PointTyped<TargetUnits> ViewAs(const gfx::PointTyped<SourceUnits>& aPoint, PixelCastJustification) {
   return gfx::PointTyped<TargetUnits>(aPoint.x, aPoint.y);
 }
+template <class TargetUnits, class SourceUnits>
+gfx::IntPointTyped<TargetUnits> ViewAs(const gfx::IntPointTyped<SourceUnits>& aPoint, PixelCastJustification) {
+  return gfx::IntPointTyped<TargetUnits>(aPoint.x, aPoint.y);
+}
 template <class NewTargetUnits, class OldTargetUnits, class SourceUnits>
 gfx::ScaleFactor<SourceUnits, NewTargetUnits> ViewTargetAs(
     const gfx::ScaleFactor<SourceUnits, OldTargetUnits>& aScaleFactor,
     PixelCastJustification) {
   return gfx::ScaleFactor<SourceUnits, NewTargetUnits>(aScaleFactor.scale);
 }
 
 // Convenience functions for casting untyped entities to typed entities.