Skip to content
This repository was archived by the owner on Jul 9, 2025. It is now read-only.

Commit 6123e50

Browse files
committed
Bug 1451461 - Make pinch locking unaffected by input sensitivity. r=botond
Implements a 50ms buffer of scale events in APZC, so pinch locking code can consider gesture movement over a fixed length of time. Previously, pinch locking was sensitive to event frequency (which is determined by the sensitivity of the input device). * New class InputBuffer wraps std::deque * New field APZC::mPinchEventBuffer * New gfxPref APZPinchLockBufferMaxAge --HG-- extra : rebase_source : a44da4c3ce971fee1b0f401357f47f994a9145df
1 parent 1b790f3 commit 6123e50

File tree

9 files changed

+251
-89
lines changed

9 files changed

+251
-89
lines changed

gfx/layers/apz/src/AsyncPanZoomController.cpp

Lines changed: 51 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -415,6 +415,13 @@ typedef PlatformSpecificStateBase
415415
* and scrolls more than apz.pinch_lock.scroll_lock_threshold.\n
416416
* Units: (real-world, i.e. screen) inches measured between two touch points
417417
*
418+
* \li\b apz.pinch_lock.buffer_max_age
419+
* To ensure that pinch locking threshold calculations are not affected by
420+
* variations in touch screen sensitivity, calculations draw from a buffer of
421+
* recent events. This preference specifies the maximum time that events are
422+
* held in this buffer.
423+
* Units: milliseconds
424+
*
418425
* \li\b apz.popups.enabled
419426
* Determines whether APZ is used for XUL popup widgets with remote content.
420427
* Ideally, this should always be true, but it is currently not well tested, and
@@ -800,6 +807,8 @@ AsyncPanZoomController::AsyncPanZoomController(
800807
mY(this),
801808
mPanDirRestricted(false),
802809
mPinchLocked(false),
810+
mPinchEventBuffer(
811+
TimeDuration::FromMilliseconds(gfxPrefs::APZPinchLockBufferMaxAge())),
803812
mZoomConstraints(false, false,
804813
Metrics().GetDevPixelsPerCSSPixel() * kViewportMinScale /
805814
ParentLayerToScreenScale(1),
@@ -1515,6 +1524,8 @@ nsEventStatus AsyncPanZoomController::OnScaleBegin(
15151524
mLastZoomFocus =
15161525
aEvent.mLocalFocusPoint - Metrics().GetCompositionBounds().TopLeft();
15171526

1527+
mPinchEventBuffer.push(aEvent);
1528+
15181529
return nsEventStatus_eConsumeNoDefault;
15191530
}
15201531

@@ -1530,22 +1541,8 @@ nsEventStatus AsyncPanZoomController::OnScale(const PinchGestureInput& aEvent) {
15301541
return nsEventStatus_eConsumeNoDefault;
15311542
}
15321543

1533-
ParentLayerCoord spanDistance =
1534-
fabsf(aEvent.mPreviousSpan - aEvent.mCurrentSpan);
1535-
ParentLayerPoint focusPoint, focusChange;
1536-
{
1537-
RecursiveMutexAutoLock lock(mRecursiveMutex);
1538-
1539-
focusPoint =
1540-
aEvent.mLocalFocusPoint - Metrics().GetCompositionBounds().TopLeft();
1541-
focusChange = mLastZoomFocus - focusPoint;
1542-
mLastZoomFocus = focusPoint;
1543-
}
1544-
1545-
HandlePinchLocking(
1546-
ToScreenCoordinates(ParentLayerPoint(0, spanDistance), focusPoint)
1547-
.Length(),
1548-
ToScreenCoordinates(focusChange, focusPoint));
1544+
mPinchEventBuffer.push(aEvent);
1545+
HandlePinchLocking();
15491546
bool allowZoom = mZoomConstraints.mAllowZoom && !mPinchLocked;
15501547

15511548
// If zooming is not allowed, this is a two-finger pan.
@@ -1581,8 +1578,12 @@ nsEventStatus AsyncPanZoomController::OnScale(const PinchGestureInput& aEvent) {
15811578
RecursiveMutexAutoLock lock(mRecursiveMutex);
15821579

15831580
CSSToParentLayerScale userZoom = Metrics().GetZoom().ToScaleFactor();
1581+
ParentLayerPoint focusPoint =
1582+
aEvent.mLocalFocusPoint - Metrics().GetCompositionBounds().TopLeft();
15841583
CSSPoint cssFocusPoint = focusPoint / Metrics().GetZoom();
15851584

1585+
ParentLayerPoint focusChange = mLastZoomFocus - focusPoint;
1586+
mLastZoomFocus = focusPoint;
15861587
// If displacing by the change in focus point will take us off page bounds,
15871588
// then reduce the displacement such that it doesn't.
15881589
focusChange.x -= mX.DisplacementWillOverscrollAmount(focusChange.x);
@@ -1701,6 +1702,8 @@ nsEventStatus AsyncPanZoomController::OnScaleEnd(
17011702
UpdateSharedCompositorFrameMetrics();
17021703
}
17031704

1705+
mPinchEventBuffer.clear();
1706+
17041707
// Non-negative focus point would indicate that one finger is still down
17051708
if (aEvent.mLocalFocusPoint != PinchGestureInput::BothFingersLifted()) {
17061709
if (mZoomConstraints.mAllowZoom) {
@@ -2932,8 +2935,38 @@ void AsyncPanZoomController::HandlePanningUpdate(
29322935
}
29332936
}
29342937

2935-
void AsyncPanZoomController::HandlePinchLocking(ScreenCoord spanDistance,
2936-
ScreenPoint focusChange) {
2938+
void AsyncPanZoomController::HandlePinchLocking() {
2939+
// Focus change and span distance calculated from an event buffer
2940+
// Used to handle pinch locking irrespective of touch screen sensitivity
2941+
// Note: both values fall back to the same value as
2942+
// their un-buffered counterparts if there is only one (the latest)
2943+
// event in the buffer. ie: when the touch screen is dispatching
2944+
// events slower than the lifetime of the buffer
2945+
ParentLayerCoord bufferedSpanDistance;
2946+
ParentLayerPoint focusPoint, bufferedFocusChange;
2947+
{
2948+
RecursiveMutexAutoLock lock(mRecursiveMutex);
2949+
2950+
focusPoint = mPinchEventBuffer.back().mLocalFocusPoint -
2951+
Metrics().GetCompositionBounds().TopLeft();
2952+
ParentLayerPoint bufferedLastZoomFocus =
2953+
(mPinchEventBuffer.size() > 1)
2954+
? mPinchEventBuffer.front().mLocalFocusPoint -
2955+
Metrics().GetCompositionBounds().TopLeft()
2956+
: mLastZoomFocus;
2957+
2958+
bufferedFocusChange = bufferedLastZoomFocus - focusPoint;
2959+
bufferedSpanDistance = fabsf(mPinchEventBuffer.front().mPreviousSpan -
2960+
mPinchEventBuffer.back().mCurrentSpan);
2961+
}
2962+
2963+
// Convert to screen coordinates
2964+
ScreenCoord spanDistance =
2965+
ToScreenCoordinates(ParentLayerPoint(0, bufferedSpanDistance), focusPoint)
2966+
.Length();
2967+
ScreenPoint focusChange =
2968+
ToScreenCoordinates(bufferedFocusChange, focusPoint);
2969+
29372970
if (mPinchLocked) {
29382971
if (GetPinchLockMode() == PINCH_STICKY) {
29392972
ScreenCoord spanBreakoutThreshold =

gfx/layers/apz/src/AsyncPanZoomController.h

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
#include "nsRegion.h"
2929
#include "nsTArray.h"
3030
#include "PotentialCheckerboardDurationTracker.h"
31+
#include "RecentEventsBuffer.h" // for RecentEventsBuffer
3132

3233
#include "base/message_loop.h"
3334

@@ -795,7 +796,7 @@ class AsyncPanZoomController {
795796
/**
796797
* Set and update the pinch lock
797798
*/
798-
void HandlePinchLocking(ScreenCoord spanDistance, ScreenPoint focusChange);
799+
void HandlePinchLocking();
799800

800801
/**
801802
* Sets up anything needed for panning. This takes us out of the "TOUCHING"
@@ -983,6 +984,12 @@ class AsyncPanZoomController {
983984
// is performing a two-finger pan rather than a pinch gesture
984985
bool mPinchLocked;
985986

987+
// Stores the pinch events that occured within a given timeframe. Used to
988+
// calculate the focusChange and spanDistance within a fixed timeframe.
989+
// RecentEventsBuffer is not threadsafe. Should only be accessed on the
990+
// controller thread.
991+
RecentEventsBuffer<PinchGestureInput> mPinchEventBuffer;
992+
986993
// Most up-to-date constraints on zooming. These should always be reasonable
987994
// values; for example, allowing a min zoom of 0.0 can cause very bad things
988995
// to happen.
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2+
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
3+
/* This Source Code Form is subject to the terms of the Mozilla Public
4+
* License, v. 2.0. If a copy of the MPL was not distributed with this
5+
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6+
7+
#ifndef mozilla_layers_RecentEventsBuffer_h
8+
#define mozilla_layers_RecentEventsBuffer_h
9+
10+
#include <deque>
11+
12+
#include "mozilla/TimeStamp.h"
13+
14+
namespace mozilla {
15+
namespace layers {
16+
/**
17+
* RecentEventsBuffer: maintains an age constrained buffer of events
18+
*
19+
* Intended for use with elements of type InputData, but the only requirement
20+
* is a member "mTimeStamp" of type TimeStamp
21+
*/
22+
template <typename Event>
23+
class RecentEventsBuffer {
24+
public:
25+
explicit RecentEventsBuffer(TimeDuration maxAge);
26+
27+
void push(Event event);
28+
void clear();
29+
30+
typedef typename std::deque<Event>::size_type size_type;
31+
size_type size() { return mBuffer.size(); }
32+
33+
// Delegate to container for iterators
34+
typedef typename std::deque<Event>::iterator iterator;
35+
typedef typename std::deque<Event>::const_iterator const_iterator;
36+
iterator begin() { return mBuffer.begin(); }
37+
iterator end() { return mBuffer.end(); }
38+
const_iterator cbegin() const { return mBuffer.cbegin(); }
39+
const_iterator cend() const { return mBuffer.cend(); }
40+
41+
// Also delegate for front/back
42+
typedef typename std::deque<Event>::reference reference;
43+
typedef typename std::deque<Event>::const_reference const_reference;
44+
reference front() { return mBuffer.front(); }
45+
reference back() { return mBuffer.back(); }
46+
const_reference front() const { return mBuffer.front(); }
47+
const_reference back() const { return mBuffer.back(); }
48+
49+
private:
50+
TimeDuration mMaxAge;
51+
std::deque<Event> mBuffer;
52+
};
53+
54+
template <typename Event>
55+
RecentEventsBuffer<Event>::RecentEventsBuffer(TimeDuration maxAge)
56+
: mMaxAge(maxAge), mBuffer() {}
57+
58+
template <typename Event>
59+
void RecentEventsBuffer<Event>::push(Event event) {
60+
// Events must be pushed in chronological order
61+
MOZ_ASSERT(mBuffer.empty() || mBuffer.back().mTimeStamp <= event.mTimeStamp);
62+
63+
mBuffer.push_back(event);
64+
65+
// Flush all events older than the given lifetime
66+
TimeStamp bound = event.mTimeStamp - mMaxAge;
67+
while (!mBuffer.empty()) {
68+
if (mBuffer.front().mTimeStamp >= bound) {
69+
break;
70+
}
71+
mBuffer.pop_front();
72+
}
73+
}
74+
75+
template <typename Event>
76+
void RecentEventsBuffer<Event>::clear() {
77+
mBuffer.clear();
78+
}
79+
80+
} // namespace layers
81+
} // namespace mozilla
82+
83+
#endif // mozilla_layers_RecentEventsBuffer_h

gfx/layers/apz/test/gtest/APZTestCommon.h

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,15 @@ inline SingleTouchData CreateSingleTouchData(int32_t aIdentifier,
6262
return CreateSingleTouchData(aIdentifier, ScreenIntPoint(aX, aY));
6363
}
6464

65+
inline PinchGestureInput CreatePinchGestureInput(
66+
PinchGestureInput::PinchGestureType aType, const ScreenPoint& aFocus,
67+
float aCurrentSpan, float aPreviousSpan, TimeStamp timestamp) {
68+
ParentLayerPoint localFocus(aFocus.x, aFocus.y);
69+
PinchGestureInput result(aType, 0, timestamp, ExternalPoint(0, 0), aFocus,
70+
aCurrentSpan, aPreviousSpan, 0);
71+
return result;
72+
}
73+
6574
template <class SetArg, class Storage>
6675
class ScopedGfxSetting {
6776
public:
@@ -450,6 +459,18 @@ class APZCTesterBase : public ::testing::Test {
450459
float aScale, int& inputId, bool aShouldTriggerPinch,
451460
nsTArray<uint32_t>* aAllowedTouchBehaviors);
452461

462+
template <class InputReceiver>
463+
void PinchWithPinchInput(const RefPtr<InputReceiver>& aTarget,
464+
const ScreenIntPoint& aFocus,
465+
const ScreenIntPoint& aSecondFocus, float aScale,
466+
nsEventStatus (*aOutEventStatuses)[3] = nullptr);
467+
468+
template <class InputReceiver>
469+
void PinchWithPinchInputAndCheckStatus(const RefPtr<InputReceiver>& aTarget,
470+
const ScreenIntPoint& aFocus,
471+
float aScale,
472+
bool aShouldTriggerPinch);
473+
453474
protected:
454475
RefPtr<MockContentControllerDelayed> mcc;
455476
};
@@ -798,6 +819,57 @@ void APZCTesterBase::PinchWithTouchInputAndCheckStatus(
798819
EXPECT_EQ(expectedMoveStatus, statuses[2]);
799820
}
800821

822+
template <class InputReceiver>
823+
void APZCTesterBase::PinchWithPinchInput(
824+
const RefPtr<InputReceiver>& aTarget, const ScreenIntPoint& aFocus,
825+
const ScreenIntPoint& aSecondFocus, float aScale,
826+
nsEventStatus (*aOutEventStatuses)[3]) {
827+
const TimeDuration TIME_BETWEEN_PINCH_INPUT =
828+
TimeDuration::FromMilliseconds(50);
829+
830+
nsEventStatus actualStatus = aTarget->ReceiveInputEvent(
831+
CreatePinchGestureInput(PinchGestureInput::PINCHGESTURE_START, aFocus,
832+
10.0, 10.0, mcc->Time()),
833+
nullptr);
834+
if (aOutEventStatuses) {
835+
(*aOutEventStatuses)[0] = actualStatus;
836+
}
837+
mcc->AdvanceBy(TIME_BETWEEN_PINCH_INPUT);
838+
839+
actualStatus = aTarget->ReceiveInputEvent(
840+
CreatePinchGestureInput(PinchGestureInput::PINCHGESTURE_SCALE,
841+
aSecondFocus, 10.0 * aScale, 10.0, mcc->Time()),
842+
nullptr);
843+
if (aOutEventStatuses) {
844+
(*aOutEventStatuses)[1] = actualStatus;
845+
}
846+
mcc->AdvanceBy(TIME_BETWEEN_PINCH_INPUT);
847+
848+
actualStatus = aTarget->ReceiveInputEvent(
849+
CreatePinchGestureInput(
850+
PinchGestureInput::PINCHGESTURE_END,
851+
PinchGestureInput::BothFingersLifted<ScreenPixel>(), 10.0 * aScale,
852+
10.0 * aScale, mcc->Time()),
853+
nullptr);
854+
if (aOutEventStatuses) {
855+
(*aOutEventStatuses)[2] = actualStatus;
856+
}
857+
}
858+
859+
template <class InputReceiver>
860+
void APZCTesterBase::PinchWithPinchInputAndCheckStatus(
861+
const RefPtr<InputReceiver>& aTarget, const ScreenIntPoint& aFocus,
862+
float aScale, bool aShouldTriggerPinch) {
863+
nsEventStatus statuses[3]; // scalebegin, scale, scaleend
864+
PinchWithPinchInput(aTarget, aFocus, aFocus, aScale, &statuses);
865+
866+
nsEventStatus expectedStatus = aShouldTriggerPinch
867+
? nsEventStatus_eConsumeNoDefault
868+
: nsEventStatus_eIgnore;
869+
EXPECT_EQ(expectedStatus, statuses[0]);
870+
EXPECT_EQ(expectedStatus, statuses[1]);
871+
}
872+
801873
AsyncPanZoomController* TestAPZCTreeManager::NewAPZCInstance(
802874
LayersId aLayersId, GeckoContentController* aController) {
803875
MockContentControllerDelayed* mcc =

gfx/layers/apz/test/gtest/InputUtils.h

Lines changed: 0 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -28,15 +28,6 @@
2828
* code to dispatch input events.
2929
*/
3030

31-
inline PinchGestureInput CreatePinchGestureInput(
32-
PinchGestureInput::PinchGestureType aType, const ScreenPoint& aFocus,
33-
float aCurrentSpan, float aPreviousSpan) {
34-
ParentLayerPoint localFocus(aFocus.x, aFocus.y);
35-
PinchGestureInput result(aType, 0, TimeStamp(), ExternalPoint(0, 0), aFocus,
36-
aCurrentSpan, aPreviousSpan, 0);
37-
return result;
38-
}
39-
4031
template <class InputReceiver>
4132
void SetDefaultAllowedTouchBehavior(const RefPtr<InputReceiver>& aTarget,
4233
uint64_t aInputBlockId,
@@ -86,50 +77,6 @@ nsEventStatus TouchUp(const RefPtr<InputReceiver>& aTarget,
8677
return aTarget->ReceiveInputEvent(mti, nullptr, nullptr);
8778
}
8879

89-
template <class InputReceiver>
90-
void PinchWithPinchInput(const RefPtr<InputReceiver>& aTarget,
91-
const ScreenIntPoint& aFocus,
92-
const ScreenIntPoint& aSecondFocus, float aScale,
93-
nsEventStatus (*aOutEventStatuses)[3] = nullptr) {
94-
nsEventStatus actualStatus = aTarget->ReceiveInputEvent(
95-
CreatePinchGestureInput(PinchGestureInput::PINCHGESTURE_START, aFocus,
96-
10.0, 10.0),
97-
nullptr);
98-
if (aOutEventStatuses) {
99-
(*aOutEventStatuses)[0] = actualStatus;
100-
}
101-
actualStatus = aTarget->ReceiveInputEvent(
102-
CreatePinchGestureInput(PinchGestureInput::PINCHGESTURE_SCALE,
103-
aSecondFocus, 10.0 * aScale, 10.0),
104-
nullptr);
105-
if (aOutEventStatuses) {
106-
(*aOutEventStatuses)[1] = actualStatus;
107-
}
108-
actualStatus = aTarget->ReceiveInputEvent(
109-
CreatePinchGestureInput(
110-
PinchGestureInput::PINCHGESTURE_END,
111-
PinchGestureInput::BothFingersLifted<ScreenPixel>(), 10.0 * aScale,
112-
10.0 * aScale),
113-
nullptr);
114-
if (aOutEventStatuses) {
115-
(*aOutEventStatuses)[2] = actualStatus;
116-
}
117-
}
118-
119-
template <class InputReceiver>
120-
void PinchWithPinchInputAndCheckStatus(const RefPtr<InputReceiver>& aTarget,
121-
const ScreenIntPoint& aFocus,
122-
float aScale, bool aShouldTriggerPinch) {
123-
nsEventStatus statuses[3]; // scalebegin, scale, scaleend
124-
PinchWithPinchInput(aTarget, aFocus, aFocus, aScale, &statuses);
125-
126-
nsEventStatus expectedStatus = aShouldTriggerPinch
127-
? nsEventStatus_eConsumeNoDefault
128-
: nsEventStatus_eIgnore;
129-
EXPECT_EQ(expectedStatus, statuses[0]);
130-
EXPECT_EQ(expectedStatus, statuses[1]);
131-
}
132-
13380
template <class InputReceiver>
13481
nsEventStatus Wheel(const RefPtr<InputReceiver>& aTarget,
13582
const ScreenIntPoint& aPoint, const ScreenPoint& aDelta,

0 commit comments

Comments
 (0)