From d00bc034b2e0aaa68971a3a05a21aa2e00615c33 Mon Sep 17 00:00:00 2001 From: jlogandavison Date: Sat, 18 Nov 2017 19:48:25 +0000 Subject: [PATCH] Bug 1180865 - Implement pinch locking in APZC. r=botond Mechanism for restricting pinch zooming when gesture is a two finger pan. If the pinch span is below a given threshold and the scroll distance above a given threshold then the zoom level is maintained to allow for smooth panning with two fingers. MozReview-Commit-ID: 62Fv0WeplOo --HG-- extra : rebase_source : 71d7da4c4b4cc3a5adde13ad1a7c1fbf49856c35 --- gfx/layers/apz/src/AsyncPanZoomController.cpp | 73 ++++++++++++++++--- gfx/layers/apz/src/AsyncPanZoomController.h | 17 +++++ gfx/thebes/gfxPrefs.h | 4 + modules/libpref/init/all.js | 4 + 4 files changed, 88 insertions(+), 10 deletions(-) diff --git a/gfx/layers/apz/src/AsyncPanZoomController.cpp b/gfx/layers/apz/src/AsyncPanZoomController.cpp index ad52709451d8..fda57b671a9a 100644 --- a/gfx/layers/apz/src/AsyncPanZoomController.cpp +++ b/gfx/layers/apz/src/AsyncPanZoomController.cpp @@ -366,6 +366,23 @@ typedef GenericFlingAnimation FlingAnimation; * 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.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 @@ -759,6 +776,7 @@ AsyncPanZoomController::AsyncPanZoomController(uint64_t aLayersId, mX(this), mY(this), mPanDirRestricted(false), + mPinchLocked(false), mZoomConstraints(false, false, mFrameMetrics.GetDevPixelsPerCSSPixel() * kViewportMinScale / ParentLayerToScreenScale(1), mFrameMetrics.GetDevPixelsPerCSSPixel() * kViewportMaxScale / ParentLayerToScreenScale(1)), @@ -864,6 +882,11 @@ AsyncPanZoomController::GetSecondTapTolerance() return static_cast(gfxPrefs::APZAxisLockMode()); } +/* static */AsyncPanZoomController::PinchLockMode AsyncPanZoomController::GetPinchLockMode() +{ + return static_cast(gfxPrefs::APZPinchLockMode()); +} + bool AsyncPanZoomController::ArePointerEventsConsumable(TouchBlockState* aBlock, uint32_t aTouchPoints) { if (aTouchPoints == 0) { @@ -1328,6 +1351,7 @@ nsEventStatus AsyncPanZoomController::OnTouchCancel(const MultiTouchInput& aEven nsEventStatus AsyncPanZoomController::OnScaleBegin(const PinchGestureInput& aEvent) { APZC_LOG("%p got a scale-begin in state %d\n", this, mState); + 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. @@ -1369,9 +1393,26 @@ nsEventStatus AsyncPanZoomController::OnScale(const PinchGestureInput& aEvent) { return nsEventStatus_eConsumeNoDefault; } + ParentLayerCoord spanDistance = fabsf(aEvent.mPreviousSpan - aEvent.mCurrentSpan); + ParentLayerPoint focusPoint, focusChange; + { + RecursiveMutexAutoLock lock(mRecursiveMutex); + + focusPoint = aEvent.mLocalFocusPoint - mFrameMetrics.GetCompositionBounds().TopLeft(); + focusChange = mLastZoomFocus - focusPoint; + mLastZoomFocus = focusPoint; + } + + HandlePinchLocking( + ToScreenCoordinates(ParentLayerPoint(0, spanDistance), focusPoint).Length(), + ToScreenCoordinates(focusChange, focusPoint)); + bool allowZoom = mZoomConstraints.mAllowZoom && !mPinchLocked; + // If zooming is not allowed, this is a two-finger pan. // Tracking panning distance and velocity. - if (!mZoomConstraints.mAllowZoom) { + // UpdateWithTouchAtDevicePoint() acquires the tree lock, so + // it cannot be called while the mRecursiveMutex lock is held. + if (!allowZoom) { mX.UpdateWithTouchAtDevicePoint(aEvent.mLocalFocusPoint.x, 0, aEvent.mTime); mY.UpdateWithTouchAtDevicePoint(aEvent.mLocalFocusPoint.y, 0, aEvent.mTime); } @@ -1397,11 +1438,8 @@ nsEventStatus AsyncPanZoomController::OnScale(const PinchGestureInput& aEvent) { RecursiveMutexAutoLock lock(mRecursiveMutex); CSSToParentLayerScale userZoom = mFrameMetrics.GetZoom().ToScaleFactor(); - ParentLayerPoint focusPoint = aEvent.mLocalFocusPoint - mFrameMetrics.GetCompositionBounds().TopLeft(); CSSPoint cssFocusPoint = focusPoint / mFrameMetrics.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); @@ -1439,12 +1477,9 @@ nsEventStatus AsyncPanZoomController::OnScale(const PinchGestureInput& aEvent) { realMaxZoom = realMinZoom; } - bool doScale = (spanRatio > 1.0 && userZoom < realMaxZoom) || - (spanRatio < 1.0 && userZoom > realMinZoom); - - if (!mZoomConstraints.mAllowZoom) { - doScale = false; - } + bool doScale = allowZoom && ( + (spanRatio > 1.0 && userZoom < realMaxZoom) || + (spanRatio < 1.0 && userZoom > realMinZoom)); if (doScale) { spanRatio = clamped(spanRatio, @@ -2601,6 +2636,24 @@ void AsyncPanZoomController::HandlePanningUpdate(const ScreenPoint& aPanDistance } } +void AsyncPanZoomController::HandlePinchLocking(ScreenCoord spanDistance, ScreenPoint focusChange) { + if (mPinchLocked) { + if (GetPinchLockMode() == PINCH_STICKY) { + ScreenCoord spanBreakoutThreshold = gfxPrefs::APZPinchLockSpanBreakoutThreshold() * APZCTreeManager::GetDPI(); + mPinchLocked = !(spanDistance > spanBreakoutThreshold); + } + } else { + if (GetPinchLockMode() != PINCH_FREE) { + ScreenCoord spanLockThreshold = gfxPrefs::APZPinchLockSpanLockThreshold() * APZCTreeManager::GetDPI(); + ScreenCoord scrollLockThreshold = gfxPrefs::APZPinchLockScrollLockThreshold() * APZCTreeManager::GetDPI(); + + if (spanDistance < spanLockThreshold && focusChange.Length() > scrollLockThreshold) { + mPinchLocked = true; + } + } + } +} + nsEventStatus AsyncPanZoomController::StartPanning(const ParentLayerPoint& aStartPoint) { RecursiveMutexAutoLock lock(mRecursiveMutex); diff --git a/gfx/layers/apz/src/AsyncPanZoomController.h b/gfx/layers/apz/src/AsyncPanZoomController.h index 54e7b5a82ddd..78788f903c29 100644 --- a/gfx/layers/apz/src/AsyncPanZoomController.h +++ b/gfx/layers/apz/src/AsyncPanZoomController.h @@ -655,6 +655,11 @@ protected: */ void HandlePanningUpdate(const ScreenPoint& aDelta); + /** + * Set and update the pinch lock + */ + void HandlePinchLocking(ScreenCoord spanDistance, ScreenPoint focusChange); + /** * Sets up anything needed for panning. This takes us out of the "TOUCHING" * state and starts actually panning us. @@ -724,6 +729,14 @@ protected: static AxisLockMode GetAxisLockMode(); + 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, @@ -800,6 +813,10 @@ private: // 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; + // 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. diff --git a/gfx/thebes/gfxPrefs.h b/gfx/thebes/gfxPrefs.h index b90dd058f7a3..4c924dc6a84f 100644 --- a/gfx/thebes/gfxPrefs.h +++ b/gfx/thebes/gfxPrefs.h @@ -333,6 +333,10 @@ private: DECL_GFX_PREF(Live, "apz.overscroll.stop_distance_threshold", APZOverscrollStopDistanceThreshold, float, 5.0f); DECL_GFX_PREF(Live, "apz.paint_skipping.enabled", APZPaintSkipping, bool, true); DECL_GFX_PREF(Live, "apz.peek_messages.enabled", APZPeekMessages, bool, true); + DECL_GFX_PREF(Live, "apz.pinch_lock.mode", APZPinchLockMode, int32_t, 1); + DECL_GFX_PREF(Live, "apz.pinch_lock.scroll_lock_threshold", APZPinchLockScrollLockThreshold, float, 1.0f / 32.0f); + DECL_GFX_PREF(Live, "apz.pinch_lock.span_breakout_threshold", APZPinchLockSpanBreakoutThreshold, float, 1.0f / 32.0f); + DECL_GFX_PREF(Live, "apz.pinch_lock.span_lock_threshold", APZPinchLockSpanLockThreshold, float, 1.0f / 32.0f); DECL_GFX_PREF(Live, "apz.popups.enabled", APZPopupsEnabled, bool, false); DECL_GFX_PREF(Live, "apz.printtree", APZPrintTree, bool, false); DECL_GFX_PREF(Live, "apz.record_checkerboarding", APZRecordCheckerboarding, bool, false); diff --git a/modules/libpref/init/all.js b/modules/libpref/init/all.js index 4649a9a4570c..99f4d309a355 100644 --- a/modules/libpref/init/all.js +++ b/modules/libpref/init/all.js @@ -710,6 +710,10 @@ pref("apz.overscroll.stretch_factor", "0.35"); pref("apz.paint_skipping.enabled", true); // Fetch displayport updates early from the message queue pref("apz.peek_messages.enabled", true); +pref("apz.pinch_lock.mode", 1); +pref("apz.pinch_lock.scoll_lock_threshold", "0.03125"); // 1/32 inches +pref("apz.pinch_lock.span_breakout_threshold", "0.03125"); // 1/32 inches +pref("apz.pinch_lock.span_lock_threshold", "0.03125"); // 1/32 inches pref("apz.popups.enabled", false); // Whether to print the APZC tree for debugging