summaryrefslogtreecommitdiffstats
path: root/gfx/layers/apz
diff options
context:
space:
mode:
Diffstat (limited to 'gfx/layers/apz')
-rw-r--r--gfx/layers/apz/public/CompositorController.h33
-rw-r--r--gfx/layers/apz/public/GeckoContentController.h179
-rw-r--r--gfx/layers/apz/public/IAPZCTreeManager.cpp164
-rw-r--r--gfx/layers/apz/public/IAPZCTreeManager.h223
-rw-r--r--gfx/layers/apz/public/MetricsSharingController.h40
-rw-r--r--gfx/layers/apz/src/APZCTreeManager.cpp2099
-rw-r--r--gfx/layers/apz/src/APZCTreeManager.h531
-rw-r--r--gfx/layers/apz/src/APZUtils.h83
-rw-r--r--gfx/layers/apz/src/AndroidAPZ.cpp274
-rw-r--r--gfx/layers/apz/src/AndroidAPZ.h61
-rw-r--r--gfx/layers/apz/src/AsyncDragMetrics.h65
-rw-r--r--gfx/layers/apz/src/AsyncPanZoomAnimation.h80
-rw-r--r--gfx/layers/apz/src/AsyncPanZoomController.cpp4030
-rw-r--r--gfx/layers/apz/src/AsyncPanZoomController.h1224
-rw-r--r--gfx/layers/apz/src/Axis.cpp681
-rw-r--r--gfx/layers/apz/src/Axis.h336
-rw-r--r--gfx/layers/apz/src/CheckerboardEvent.cpp230
-rw-r--r--gfx/layers/apz/src/CheckerboardEvent.h221
-rw-r--r--gfx/layers/apz/src/DragTracker.cpp70
-rw-r--r--gfx/layers/apz/src/DragTracker.h39
-rw-r--r--gfx/layers/apz/src/GenericFlingAnimation.h207
-rw-r--r--gfx/layers/apz/src/GestureEventListener.cpp552
-rw-r--r--gfx/layers/apz/src/GestureEventListener.h252
-rw-r--r--gfx/layers/apz/src/HitTestingTreeNode.cpp336
-rw-r--r--gfx/layers/apz/src/HitTestingTreeNode.h166
-rw-r--r--gfx/layers/apz/src/InputBlockState.cpp868
-rw-r--r--gfx/layers/apz/src/InputBlockState.h483
-rw-r--r--gfx/layers/apz/src/InputQueue.cpp731
-rw-r--r--gfx/layers/apz/src/InputQueue.h217
-rw-r--r--gfx/layers/apz/src/Overscroll.h137
-rw-r--r--gfx/layers/apz/src/OverscrollHandoffState.cpp175
-rw-r--r--gfx/layers/apz/src/OverscrollHandoffState.h159
-rw-r--r--gfx/layers/apz/src/PotentialCheckerboardDurationTracker.cpp79
-rw-r--r--gfx/layers/apz/src/PotentialCheckerboardDurationTracker.h60
-rw-r--r--gfx/layers/apz/src/QueuedInput.cpp54
-rw-r--r--gfx/layers/apz/src/QueuedInput.h58
-rw-r--r--gfx/layers/apz/src/TouchCounter.cpp50
-rw-r--r--gfx/layers/apz/src/TouchCounter.h33
-rw-r--r--gfx/layers/apz/src/WheelScrollAnimation.cpp119
-rw-r--r--gfx/layers/apz/src/WheelScrollAnimation.h51
-rw-r--r--gfx/layers/apz/test/gtest/APZCBasicTester.h120
-rw-r--r--gfx/layers/apz/test/gtest/APZCTreeManagerTester.h194
-rw-r--r--gfx/layers/apz/test/gtest/APZTestCommon.h609
-rw-r--r--gfx/layers/apz/test/gtest/InputUtils.h297
-rw-r--r--gfx/layers/apz/test/gtest/TestBasic.cpp356
-rw-r--r--gfx/layers/apz/test/gtest/TestEventRegions.cpp272
-rw-r--r--gfx/layers/apz/test/gtest/TestGestureDetector.cpp638
-rw-r--r--gfx/layers/apz/test/gtest/TestHitTesting.cpp578
-rw-r--r--gfx/layers/apz/test/gtest/TestInputQueue.cpp44
-rw-r--r--gfx/layers/apz/test/gtest/TestPanning.cpp128
-rw-r--r--gfx/layers/apz/test/gtest/TestPinching.cpp294
-rw-r--r--gfx/layers/apz/test/gtest/TestScrollHandoff.cpp521
-rw-r--r--gfx/layers/apz/test/gtest/TestSnapping.cpp64
-rw-r--r--gfx/layers/apz/test/gtest/TestTreeManager.cpp112
-rw-r--r--gfx/layers/apz/test/gtest/moz.build33
-rw-r--r--gfx/layers/apz/test/mochitest/apz_test_native_event_utils.js261
-rw-r--r--gfx/layers/apz/test/mochitest/apz_test_utils.js403
-rw-r--r--gfx/layers/apz/test/mochitest/chrome.ini9
-rw-r--r--gfx/layers/apz/test/mochitest/helper_basic_pan.html38
-rw-r--r--gfx/layers/apz/test/mochitest/helper_bug1151663.html83
-rw-r--r--gfx/layers/apz/test/mochitest/helper_bug1162771.html104
-rw-r--r--gfx/layers/apz/test/mochitest/helper_bug1271432.html574
-rw-r--r--gfx/layers/apz/test/mochitest/helper_bug1280013.html74
-rw-r--r--gfx/layers/apz/test/mochitest/helper_bug1285070.html44
-rw-r--r--gfx/layers/apz/test/mochitest/helper_bug1299195.html47
-rw-r--r--gfx/layers/apz/test/mochitest/helper_bug982141.html149
-rw-r--r--gfx/layers/apz/test/mochitest/helper_click.html41
-rw-r--r--gfx/layers/apz/test/mochitest/helper_div_pan.html44
-rw-r--r--gfx/layers/apz/test/mochitest/helper_drag_click.html43
-rw-r--r--gfx/layers/apz/test/mochitest/helper_drag_scroll.html603
-rw-r--r--gfx/layers/apz/test/mochitest/helper_iframe1.html14
-rw-r--r--gfx/layers/apz/test/mochitest/helper_iframe2.html14
-rw-r--r--gfx/layers/apz/test/mochitest/helper_iframe_pan.html41
-rw-r--r--gfx/layers/apz/test/mochitest/helper_long_tap.html81
-rw-r--r--gfx/layers/apz/test/mochitest/helper_scroll_inactive_perspective.html46
-rw-r--r--gfx/layers/apz/test/mochitest/helper_scroll_inactive_zindex.html47
-rw-r--r--gfx/layers/apz/test/mochitest/helper_scroll_on_position_fixed.html62
-rw-r--r--gfx/layers/apz/test/mochitest/helper_scrollto_tap.html61
-rw-r--r--gfx/layers/apz/test/mochitest/helper_subframe_style.css15
-rw-r--r--gfx/layers/apz/test/mochitest/helper_tall.html504
-rw-r--r--gfx/layers/apz/test/mochitest/helper_tap.html32
-rw-r--r--gfx/layers/apz/test/mochitest/helper_tap_fullzoom.html33
-rw-r--r--gfx/layers/apz/test/mochitest/helper_tap_passive.html64
-rw-r--r--gfx/layers/apz/test/mochitest/helper_touch_action.html115
-rw-r--r--gfx/layers/apz/test/mochitest/helper_touch_action_complex.html143
-rw-r--r--gfx/layers/apz/test/mochitest/helper_touch_action_regions.html246
-rw-r--r--gfx/layers/apz/test/mochitest/mochitest.ini67
-rw-r--r--gfx/layers/apz/test/mochitest/test_bug1151663.html38
-rw-r--r--gfx/layers/apz/test/mochitest/test_bug1151667.html65
-rw-r--r--gfx/layers/apz/test/mochitest/test_bug1253683.html59
-rw-r--r--gfx/layers/apz/test/mochitest/test_bug1277814.html106
-rw-r--r--gfx/layers/apz/test/mochitest/test_bug1304689-2.html131
-rw-r--r--gfx/layers/apz/test/mochitest/test_bug1304689.html135
-rw-r--r--gfx/layers/apz/test/mochitest/test_bug982141.html38
-rw-r--r--gfx/layers/apz/test/mochitest/test_frame_reconstruction.html218
-rw-r--r--gfx/layers/apz/test/mochitest/test_group_mouseevents.html35
-rw-r--r--gfx/layers/apz/test/mochitest/test_group_pointerevents.html31
-rw-r--r--gfx/layers/apz/test/mochitest/test_group_touchevents.html104
-rw-r--r--gfx/layers/apz/test/mochitest/test_group_wheelevents.html41
-rw-r--r--gfx/layers/apz/test/mochitest/test_group_zoom.html44
-rw-r--r--gfx/layers/apz/test/mochitest/test_interrupted_reflow.html719
-rw-r--r--gfx/layers/apz/test/mochitest/test_layerization.html214
-rw-r--r--gfx/layers/apz/test/mochitest/test_scroll_inactive_bug1190112.html541
-rw-r--r--gfx/layers/apz/test/mochitest/test_scroll_inactive_flattened_frame.html49
-rw-r--r--gfx/layers/apz/test/mochitest/test_scroll_subframe_scrollbar.html117
-rw-r--r--gfx/layers/apz/test/mochitest/test_smoothness.html77
-rw-r--r--gfx/layers/apz/test/mochitest/test_touch_listeners_impacting_wheel.html114
-rw-r--r--gfx/layers/apz/test/mochitest/test_wheel_scroll.html106
-rw-r--r--gfx/layers/apz/test/mochitest/test_wheel_transactions.html137
-rw-r--r--gfx/layers/apz/test/reftest/async-scrollbar-1-h-ref.html9
-rw-r--r--gfx/layers/apz/test/reftest/async-scrollbar-1-h-rtl-ref.html10
-rw-r--r--gfx/layers/apz/test/reftest/async-scrollbar-1-h-rtl.html14
-rw-r--r--gfx/layers/apz/test/reftest/async-scrollbar-1-h.html13
-rw-r--r--gfx/layers/apz/test/reftest/async-scrollbar-1-v-ref.html9
-rw-r--r--gfx/layers/apz/test/reftest/async-scrollbar-1-v-rtl-ref.html10
-rw-r--r--gfx/layers/apz/test/reftest/async-scrollbar-1-v-rtl.html14
-rw-r--r--gfx/layers/apz/test/reftest/async-scrollbar-1-v.html13
-rw-r--r--gfx/layers/apz/test/reftest/async-scrollbar-1-vh-ref.html9
-rw-r--r--gfx/layers/apz/test/reftest/async-scrollbar-1-vh-rtl-ref.html10
-rw-r--r--gfx/layers/apz/test/reftest/async-scrollbar-1-vh-rtl.html14
-rw-r--r--gfx/layers/apz/test/reftest/async-scrollbar-1-vh.html13
-rw-r--r--gfx/layers/apz/test/reftest/async-scrollbar-zoom-1-ref.html9
-rw-r--r--gfx/layers/apz/test/reftest/async-scrollbar-zoom-1.html14
-rw-r--r--gfx/layers/apz/test/reftest/async-scrollbar-zoom-2-ref.html9
-rw-r--r--gfx/layers/apz/test/reftest/async-scrollbar-zoom-2.html14
-rw-r--r--gfx/layers/apz/test/reftest/frame-reconstruction-scroll-clamping-ref.html27
-rw-r--r--gfx/layers/apz/test/reftest/frame-reconstruction-scroll-clamping.html53
-rw-r--r--gfx/layers/apz/test/reftest/initial-scale-1-ref.html10
-rw-r--r--gfx/layers/apz/test/reftest/initial-scale-1.html10
-rw-r--r--gfx/layers/apz/test/reftest/reftest-stylo.list20
-rw-r--r--gfx/layers/apz/test/reftest/reftest.list19
-rw-r--r--gfx/layers/apz/testutil/APZTestData.cpp66
-rw-r--r--gfx/layers/apz/testutil/APZTestData.h168
-rw-r--r--gfx/layers/apz/util/APZCCallbackHelper.cpp942
-rw-r--r--gfx/layers/apz/util/APZCCallbackHelper.h209
-rw-r--r--gfx/layers/apz/util/APZEventState.cpp512
-rw-r--r--gfx/layers/apz/util/APZEventState.h103
-rw-r--r--gfx/layers/apz/util/APZThreadUtils.cpp96
-rw-r--r--gfx/layers/apz/util/APZThreadUtils.h104
-rw-r--r--gfx/layers/apz/util/ActiveElementManager.cpp237
-rw-r--r--gfx/layers/apz/util/ActiveElementManager.h104
-rw-r--r--gfx/layers/apz/util/CheckerboardReportService.cpp228
-rw-r--r--gfx/layers/apz/util/CheckerboardReportService.h144
-rw-r--r--gfx/layers/apz/util/ChromeProcessController.cpp276
-rw-r--r--gfx/layers/apz/util/ChromeProcessController.h83
-rw-r--r--gfx/layers/apz/util/ContentProcessController.cpp207
-rw-r--r--gfx/layers/apz/util/ContentProcessController.h90
-rw-r--r--gfx/layers/apz/util/DoubleTapToZoom.cpp178
-rw-r--r--gfx/layers/apz/util/DoubleTapToZoom.h29
-rw-r--r--gfx/layers/apz/util/InputAPZContext.cpp69
-rw-r--r--gfx/layers/apz/util/InputAPZContext.h50
-rw-r--r--gfx/layers/apz/util/ScrollInputMethods.h62
-rw-r--r--gfx/layers/apz/util/ScrollLinkedEffectDetector.cpp49
-rw-r--r--gfx/layers/apz/util/ScrollLinkedEffectDetector.h43
-rw-r--r--gfx/layers/apz/util/TouchActionHelper.cpp96
-rw-r--r--gfx/layers/apz/util/TouchActionHelper.h42
156 files changed, 31441 insertions, 0 deletions
diff --git a/gfx/layers/apz/public/CompositorController.h b/gfx/layers/apz/public/CompositorController.h
new file mode 100644
index 000000000..2560240e2
--- /dev/null
+++ b/gfx/layers/apz/public/CompositorController.h
@@ -0,0 +1,33 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set sw=4 ts=8 et tw=80 : */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_layers_CompositorController_h
+#define mozilla_layers_CompositorController_h
+
+#include "mozilla/RefCountType.h" // for MozExternalRefCountType
+#include "nscore.h" // for NS_IMETHOD_
+
+namespace mozilla {
+namespace layers {
+
+class CompositorController
+{
+public:
+ NS_IMETHOD_(MozExternalRefCountType) AddRef() = 0;
+ NS_IMETHOD_(MozExternalRefCountType) Release() = 0;
+
+ virtual void ScheduleRenderOnCompositorThread() = 0;
+ virtual void ScheduleHideAllPluginWindows() = 0;
+ virtual void ScheduleShowAllPluginWindows() = 0;
+
+protected:
+ virtual ~CompositorController() {}
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif // mozilla_layers_CompositorController_h
diff --git a/gfx/layers/apz/public/GeckoContentController.h b/gfx/layers/apz/public/GeckoContentController.h
new file mode 100644
index 000000000..d572a410b
--- /dev/null
+++ b/gfx/layers/apz/public/GeckoContentController.h
@@ -0,0 +1,179 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set sw=4 ts=8 et tw=80 : */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_layers_GeckoContentController_h
+#define mozilla_layers_GeckoContentController_h
+
+#include "FrameMetrics.h" // for FrameMetrics, etc
+#include "InputData.h" // for PinchGestureInput
+#include "Units.h" // for CSSPoint, CSSRect, etc
+#include "mozilla/Assertions.h" // for MOZ_ASSERT_HELPER2
+#include "mozilla/EventForwards.h" // for Modifiers
+#include "nsISupportsImpl.h"
+
+namespace mozilla {
+
+class Runnable;
+
+namespace layers {
+
+class GeckoContentController
+{
+public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(GeckoContentController)
+
+ /**
+ * Requests a paint of the given FrameMetrics |aFrameMetrics| from Gecko.
+ * Implementations per-platform are responsible for actually handling this.
+ *
+ * This method must always be called on the repaint thread, which depends
+ * on the GeckoContentController. For ChromeProcessController it is the
+ * Gecko main thread, while for RemoteContentController it is the compositor
+ * thread where it can send IPDL messages.
+ */
+ virtual void RequestContentRepaint(const FrameMetrics& aFrameMetrics) = 0;
+
+ /**
+ * Different types of tap-related events that can be sent in
+ * the HandleTap function. The names should be relatively self-explanatory.
+ * Note that the eLongTapUp will always be preceded by an eLongTap, but not
+ * all eLongTap notifications will be followed by an eLongTapUp (for instance,
+ * if the user moves their finger after triggering the long-tap but before
+ * lifting it).
+ * The difference between eDoubleTap and eSecondTap is subtle - the eDoubleTap
+ * is for an actual double-tap "gesture" while eSecondTap is for the same user
+ * input but where a double-tap gesture is not allowed. This is used to fire
+ * a click event with detail=2 to web content (similar to what a mouse double-
+ * click would do).
+ */
+ enum class TapType {
+ eSingleTap,
+ eDoubleTap,
+ eSecondTap,
+ eLongTap,
+ eLongTapUp,
+
+ eSentinel,
+ };
+
+ /**
+ * Requests handling of a tap event. |aPoint| is in LD pixels, relative to the
+ * current scroll offset.
+ */
+ virtual void HandleTap(TapType aType,
+ const LayoutDevicePoint& aPoint,
+ Modifiers aModifiers,
+ const ScrollableLayerGuid& aGuid,
+ uint64_t aInputBlockId) = 0;
+
+ /**
+ * When the apz.allow_zooming pref is set to false, the APZ will not
+ * translate pinch gestures to actual zooming. Instead, it will call this
+ * method to notify gecko of the pinch gesture, and allow it to deal with it
+ * however it wishes. Note that this function is not called if the pinch is
+ * prevented by content calling preventDefault() on the touch events, or via
+ * use of the touch-action property.
+ * @param aType One of PINCHGESTURE_START, PINCHGESTURE_SCALE, or
+ * PINCHGESTURE_END, indicating the phase of the pinch.
+ * @param aGuid The guid of the APZ that is detecting the pinch. This is
+ * generally the root APZC for the layers id.
+ * @param aSpanChange For the START or END event, this is always 0.
+ * For a SCALE event, this is the difference in span between the
+ * previous state and the new state.
+ * @param aModifiers The keyboard modifiers depressed during the pinch.
+ */
+ virtual void NotifyPinchGesture(PinchGestureInput::PinchGestureType aType,
+ const ScrollableLayerGuid& aGuid,
+ LayoutDeviceCoord aSpanChange,
+ Modifiers aModifiers) = 0;
+
+ /**
+ * Schedules a runnable to run on the controller/UI thread at some time
+ * in the future.
+ * This method must always be called on the controller thread.
+ */
+ virtual void PostDelayedTask(already_AddRefed<Runnable> aRunnable, int aDelayMs) = 0;
+
+ /**
+ * Returns true if we are currently on the thread that can send repaint requests.
+ */
+ virtual bool IsRepaintThread() = 0;
+
+ /**
+ * Runs the given task on the "repaint" thread.
+ */
+ virtual void DispatchToRepaintThread(already_AddRefed<Runnable> aTask) = 0;
+
+ enum class APZStateChange {
+ /**
+ * APZ started modifying the view (including panning, zooming, and fling).
+ */
+ eTransformBegin,
+ /**
+ * APZ finished modifying the view.
+ */
+ eTransformEnd,
+ /**
+ * APZ started a touch.
+ * |aArg| is 1 if touch can be a pan, 0 otherwise.
+ */
+ eStartTouch,
+ /**
+ * APZ started a pan.
+ */
+ eStartPanning,
+ /**
+ * APZ finished processing a touch.
+ * |aArg| is 1 if touch was a click, 0 otherwise.
+ */
+ eEndTouch,
+
+ // Sentinel value for IPC, this must be the last item in the enum and
+ // should not be used as an actual message value.
+ eSentinel
+ };
+ /**
+ * General notices of APZ state changes for consumers.
+ * |aGuid| identifies the APZC originating the state change.
+ * |aChange| identifies the type of state change
+ * |aArg| is used by some state changes to pass extra information (see
+ * the documentation for each state change above)
+ */
+ virtual void NotifyAPZStateChange(const ScrollableLayerGuid& aGuid,
+ APZStateChange aChange,
+ int aArg = 0) {}
+
+ /**
+ * Notify content of a MozMouseScrollFailed event.
+ */
+ virtual void NotifyMozMouseScrollEvent(const FrameMetrics::ViewID& aScrollId, const nsString& aEvent)
+ {}
+
+ /**
+ * Notify content that the repaint requests have been flushed.
+ */
+ virtual void NotifyFlushComplete() = 0;
+
+ virtual void UpdateOverscrollVelocity(float aX, float aY, bool aIsRootContent) {}
+ virtual void UpdateOverscrollOffset(float aX, float aY, bool aIsRootContent) {}
+ virtual void SetScrollingRootContent(bool isRootContent) {}
+
+ GeckoContentController() {}
+
+ /**
+ * Needs to be called on the main thread.
+ */
+ virtual void Destroy() {}
+
+protected:
+ // Protected destructor, to discourage deletion outside of Release():
+ virtual ~GeckoContentController() {}
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif // mozilla_layers_GeckoContentController_h
diff --git a/gfx/layers/apz/public/IAPZCTreeManager.cpp b/gfx/layers/apz/public/IAPZCTreeManager.cpp
new file mode 100644
index 000000000..372257ae4
--- /dev/null
+++ b/gfx/layers/apz/public/IAPZCTreeManager.cpp
@@ -0,0 +1,164 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=99: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/layers/IAPZCTreeManager.h"
+
+#include "gfxPrefs.h" // for gfxPrefs
+#include "InputData.h" // for InputData, etc
+#include "mozilla/EventStateManager.h" // for WheelPrefs
+#include "mozilla/layers/APZThreadUtils.h" // for AssertOnCompositorThread, etc
+#include "mozilla/MouseEvents.h" // for WidgetMouseEvent
+#include "mozilla/TouchEvents.h" // for WidgetTouchEvent
+
+namespace mozilla {
+namespace layers {
+
+static bool
+WillHandleMouseEvent(const WidgetMouseEventBase& aEvent)
+{
+ return aEvent.mMessage == eMouseMove ||
+ aEvent.mMessage == eMouseDown ||
+ aEvent.mMessage == eMouseUp ||
+ aEvent.mMessage == eDragEnd;
+}
+
+// Returns whether or not a wheel event action will be (or was) performed by
+// APZ. If this returns true, the event must not perform a synchronous
+// scroll.
+//
+// Even if this returns false, all wheel events in APZ-aware widgets must
+// be sent through APZ so they are transformed correctly for TabParent.
+static bool
+WillHandleWheelEvent(WidgetWheelEvent* aEvent)
+{
+ return EventStateManager::WheelEventIsScrollAction(aEvent) &&
+ (aEvent->mDeltaMode == nsIDOMWheelEvent::DOM_DELTA_LINE ||
+ aEvent->mDeltaMode == nsIDOMWheelEvent::DOM_DELTA_PIXEL ||
+ aEvent->mDeltaMode == nsIDOMWheelEvent::DOM_DELTA_PAGE);
+}
+
+nsEventStatus
+IAPZCTreeManager::ReceiveInputEvent(
+ WidgetInputEvent& aEvent,
+ ScrollableLayerGuid* aOutTargetGuid,
+ uint64_t* aOutInputBlockId)
+{
+ APZThreadUtils::AssertOnControllerThread();
+
+ // Initialize aOutInputBlockId to a sane value, and then later we overwrite
+ // it if the input event goes into a block.
+ if (aOutInputBlockId) {
+ *aOutInputBlockId = 0;
+ }
+
+ switch (aEvent.mClass) {
+ case eMouseEventClass:
+ case eDragEventClass: {
+
+ WidgetMouseEvent& mouseEvent = *aEvent.AsMouseEvent();
+
+ // Note, we call this before having transformed the reference point.
+ if (mouseEvent.IsReal()) {
+ UpdateWheelTransaction(mouseEvent.mRefPoint, mouseEvent.mMessage);
+ }
+
+ if (WillHandleMouseEvent(mouseEvent)) {
+
+ MouseInput input(mouseEvent);
+ input.mOrigin = ScreenPoint(mouseEvent.mRefPoint.x, mouseEvent.mRefPoint.y);
+
+ nsEventStatus status = ReceiveInputEvent(input, aOutTargetGuid, aOutInputBlockId);
+
+ mouseEvent.mRefPoint.x = input.mOrigin.x;
+ mouseEvent.mRefPoint.y = input.mOrigin.y;
+ mouseEvent.mFlags.mHandledByAPZ = input.mHandledByAPZ;
+ return status;
+
+ }
+
+ TransformEventRefPoint(&mouseEvent.mRefPoint, aOutTargetGuid);
+ return nsEventStatus_eIgnore;
+ }
+ case eTouchEventClass: {
+
+ WidgetTouchEvent& touchEvent = *aEvent.AsTouchEvent();
+ MultiTouchInput touchInput(touchEvent);
+ nsEventStatus result = ReceiveInputEvent(touchInput, aOutTargetGuid, aOutInputBlockId);
+ // touchInput was modified in-place to possibly remove some
+ // touch points (if we are overscrolled), and the coordinates were
+ // modified using the APZ untransform. We need to copy these changes
+ // back into the WidgetInputEvent.
+ touchEvent.mTouches.Clear();
+ touchEvent.mTouches.SetCapacity(touchInput.mTouches.Length());
+ for (size_t i = 0; i < touchInput.mTouches.Length(); i++) {
+ *touchEvent.mTouches.AppendElement() =
+ touchInput.mTouches[i].ToNewDOMTouch();
+ }
+ touchEvent.mFlags.mHandledByAPZ = touchInput.mHandledByAPZ;
+ return result;
+
+ }
+ case eWheelEventClass: {
+ WidgetWheelEvent& wheelEvent = *aEvent.AsWheelEvent();
+
+ if (WillHandleWheelEvent(&wheelEvent)) {
+
+ ScrollWheelInput::ScrollMode scrollMode = ScrollWheelInput::SCROLLMODE_INSTANT;
+ if (gfxPrefs::SmoothScrollEnabled() &&
+ ((wheelEvent.mDeltaMode == nsIDOMWheelEvent::DOM_DELTA_LINE &&
+ gfxPrefs::WheelSmoothScrollEnabled()) ||
+ (wheelEvent.mDeltaMode == nsIDOMWheelEvent::DOM_DELTA_PAGE &&
+ gfxPrefs::PageSmoothScrollEnabled())))
+ {
+ scrollMode = ScrollWheelInput::SCROLLMODE_SMOOTH;
+ }
+
+ ScreenPoint origin(wheelEvent.mRefPoint.x, wheelEvent.mRefPoint.y);
+ ScrollWheelInput input(wheelEvent.mTime, wheelEvent.mTimeStamp, 0,
+ scrollMode,
+ ScrollWheelInput::DeltaTypeForDeltaMode(
+ wheelEvent.mDeltaMode),
+ origin,
+ wheelEvent.mDeltaX, wheelEvent.mDeltaY,
+ wheelEvent.mAllowToOverrideSystemScrollSpeed);
+
+ // We add the user multiplier as a separate field, rather than premultiplying
+ // it, because if the input is converted back to a WidgetWheelEvent, then
+ // EventStateManager would apply the delta a second time. We could in theory
+ // work around this by asking ESM to customize the event much sooner, and
+ // then save the "mCustomizedByUserPrefs" bit on ScrollWheelInput - but for
+ // now, this seems easier.
+ EventStateManager::GetUserPrefsForWheelEvent(&wheelEvent,
+ &input.mUserDeltaMultiplierX,
+ &input.mUserDeltaMultiplierY);
+
+ nsEventStatus status = ReceiveInputEvent(input, aOutTargetGuid, aOutInputBlockId);
+ wheelEvent.mRefPoint.x = input.mOrigin.x;
+ wheelEvent.mRefPoint.y = input.mOrigin.y;
+ wheelEvent.mFlags.mHandledByAPZ = input.mHandledByAPZ;
+ return status;
+ }
+
+ UpdateWheelTransaction(aEvent.mRefPoint, aEvent.mMessage);
+ TransformEventRefPoint(&aEvent.mRefPoint, aOutTargetGuid);
+ return nsEventStatus_eIgnore;
+
+ }
+ default: {
+
+ UpdateWheelTransaction(aEvent.mRefPoint, aEvent.mMessage);
+ TransformEventRefPoint(&aEvent.mRefPoint, aOutTargetGuid);
+ return nsEventStatus_eIgnore;
+
+ }
+ }
+
+ MOZ_ASSERT_UNREACHABLE("Invalid WidgetInputEvent type.");
+ return nsEventStatus_eConsumeNoDefault;
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/apz/public/IAPZCTreeManager.h b/gfx/layers/apz/public/IAPZCTreeManager.h
new file mode 100644
index 000000000..383181e8f
--- /dev/null
+++ b/gfx/layers/apz/public/IAPZCTreeManager.h
@@ -0,0 +1,223 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=99: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_layers_IAPZCTreeManager_h
+#define mozilla_layers_IAPZCTreeManager_h
+
+#include <stdint.h> // for uint64_t, uint32_t
+
+#include "FrameMetrics.h" // for FrameMetrics, etc
+#include "mozilla/EventForwards.h" // for WidgetInputEvent, nsEventStatus
+#include "mozilla/layers/APZUtils.h" // for HitTestResult
+#include "nsTArrayForwardDeclare.h" // for nsTArray, nsTArray_Impl, etc
+#include "nsISupportsImpl.h" // for MOZ_COUNT_CTOR, etc
+#include "Units.h" // for CSSPoint, CSSRect, etc
+
+namespace mozilla {
+class InputData;
+
+namespace layers {
+
+enum AllowedTouchBehavior {
+ NONE = 0,
+ VERTICAL_PAN = 1 << 0,
+ HORIZONTAL_PAN = 1 << 1,
+ PINCH_ZOOM = 1 << 2,
+ DOUBLE_TAP_ZOOM = 1 << 3,
+ UNKNOWN = 1 << 4
+};
+
+enum ZoomToRectBehavior : uint32_t {
+ DEFAULT_BEHAVIOR = 0,
+ DISABLE_ZOOM_OUT = 1 << 0,
+ PAN_INTO_VIEW_ONLY = 1 << 1,
+ ONLY_ZOOM_TO_DEFAULT_SCALE = 1 << 2
+};
+
+class AsyncDragMetrics;
+
+class IAPZCTreeManager {
+ NS_INLINE_DECL_THREADSAFE_VIRTUAL_REFCOUNTING(IAPZCTreeManager)
+
+public:
+
+ /**
+ * General handler for incoming input events. Manipulates the frame metrics
+ * based on what type of input it is. For example, a PinchGestureEvent will
+ * cause scaling. This should only be called externally to this class, and
+ * must be called on the controller thread.
+ *
+ * This function transforms |aEvent| to have its coordinates in DOM space.
+ * This is so that the event can be passed through the DOM and content can
+ * handle them. The event may need to be converted to a WidgetInputEvent
+ * by the caller if it wants to do this.
+ *
+ * The following values may be returned by this function:
+ * nsEventStatus_eConsumeNoDefault is returned to indicate the
+ * APZ is consuming this event and the caller should discard the event with
+ * extreme prejudice. The exact scenarios under which this is returned is
+ * implementation-dependent and may vary.
+ * nsEventStatus_eIgnore is returned to indicate that the APZ code didn't
+ * use this event. This might be because it was directed at a point on
+ * the screen where there was no APZ, or because the thing the user was
+ * trying to do was not allowed. (For example, attempting to pan a
+ * non-pannable document).
+ * nsEventStatus_eConsumeDoDefault is returned to indicate that the APZ
+ * code may have used this event to do some user-visible thing. Note that
+ * in some cases CONSUMED is returned even if the event was NOT used. This
+ * is because we cannot always know at the time of event delivery whether
+ * the event will be used or not. So we err on the side of sending
+ * CONSUMED when we are uncertain.
+ *
+ * @param aEvent input event object; is modified in-place
+ * @param aOutTargetGuid returns the guid of the apzc this event was
+ * delivered to. May be null.
+ * @param aOutInputBlockId returns the id of the input block that this event
+ * was added to, if that was the case. May be null.
+ */
+ virtual nsEventStatus ReceiveInputEvent(
+ InputData& aEvent,
+ ScrollableLayerGuid* aOutTargetGuid,
+ uint64_t* aOutInputBlockId) = 0;
+
+ /**
+ * WidgetInputEvent handler. Transforms |aEvent| (which is assumed to be an
+ * already-existing instance of an WidgetInputEvent which may be an
+ * WidgetTouchEvent) to have its coordinates in DOM space. This is so that the
+ * event can be passed through the DOM and content can handle them.
+ *
+ * NOTE: Be careful of invoking the WidgetInputEvent variant. This can only be
+ * called on the main thread. See widget/InputData.h for more information on
+ * why we have InputData and WidgetInputEvent separated. If this function is
+ * used, the controller thread must be the main thread, or undefined behaviour
+ * may occur.
+ * NOTE: On unix, mouse events are treated as touch and are forwarded
+ * to the appropriate apz as such.
+ *
+ * See documentation for other ReceiveInputEvent above.
+ */
+ nsEventStatus ReceiveInputEvent(
+ WidgetInputEvent& aEvent,
+ ScrollableLayerGuid* aOutTargetGuid,
+ uint64_t* aOutInputBlockId);
+
+ /**
+ * Kicks an animation to zoom to a rect. This may be either a zoom out or zoom
+ * in. The actual animation is done on the compositor thread after being set
+ * up. |aRect| must be given in CSS pixels, relative to the document.
+ * |aFlags| is a combination of the ZoomToRectBehavior enum values.
+ */
+ virtual void ZoomToRect(
+ const ScrollableLayerGuid& aGuid,
+ const CSSRect& aRect,
+ const uint32_t aFlags = DEFAULT_BEHAVIOR) = 0;
+
+ /**
+ * If we have touch listeners, this should always be called when we know
+ * definitively whether or not content has preventDefaulted any touch events
+ * that have come in. If |aPreventDefault| is true, any touch events in the
+ * queue will be discarded. This function must be called on the controller
+ * thread.
+ */
+ virtual void ContentReceivedInputBlock(
+ uint64_t aInputBlockId,
+ bool aPreventDefault) = 0;
+
+ /**
+ * When the event regions code is enabled, this function should be invoked to
+ * to confirm the target of the input block. This is only needed in cases
+ * where the initial input event of the block hit a dispatch-to-content region
+ * but is safe to call for all input blocks. This function should always be
+ * invoked on the controller thread.
+ * The different elements in the array of targets correspond to the targets
+ * for the different touch points. In the case where the touch point has no
+ * target, or the target is not a scrollable frame, the target's |mScrollId|
+ * should be set to FrameMetrics::NULL_SCROLL_ID.
+ */
+ virtual void SetTargetAPZC(
+ uint64_t aInputBlockId,
+ const nsTArray<ScrollableLayerGuid>& aTargets) = 0;
+
+ /**
+ * Updates any zoom constraints contained in the <meta name="viewport"> tag.
+ * If the |aConstraints| is Nothing() then previously-provided constraints for
+ * the given |aGuid| are cleared.
+ */
+ virtual void UpdateZoomConstraints(
+ const ScrollableLayerGuid& aGuid,
+ const Maybe<ZoomConstraints>& aConstraints) = 0;
+
+ /**
+ * Cancels any currently running animation. Note that all this does is set the
+ * state of the AsyncPanZoomController back to NOTHING, but it is the
+ * animation's responsibility to check this before advancing.
+ */
+ virtual void CancelAnimation(const ScrollableLayerGuid &aGuid) = 0;
+
+ /**
+ * Adjusts the root APZC to compensate for a shift in the surface. See the
+ * documentation on AsyncPanZoomController::AdjustScrollForSurfaceShift for
+ * some more details. This is only currently needed due to surface shifts
+ * caused by the dynamic toolbar on Android.
+ */
+ virtual void AdjustScrollForSurfaceShift(const ScreenPoint& aShift) = 0;
+
+ virtual void SetDPI(float aDpiValue) = 0;
+
+ /**
+ * Sets allowed touch behavior values for current touch-session for specific
+ * input block (determined by aInputBlock).
+ * Should be invoked by the widget. Each value of the aValues arrays
+ * corresponds to the different touch point that is currently active.
+ * Must be called after receiving the TOUCH_START event that starts the
+ * touch-session.
+ * This must be called on the controller thread.
+ */
+ virtual void SetAllowedTouchBehavior(
+ uint64_t aInputBlockId,
+ const nsTArray<TouchBehaviorFlags>& aValues) = 0;
+
+ virtual void StartScrollbarDrag(
+ const ScrollableLayerGuid& aGuid,
+ const AsyncDragMetrics& aDragMetrics) = 0;
+
+ /**
+ * Function used to disable LongTap gestures.
+ *
+ * On slow running tests, drags and touch events can be misinterpreted
+ * as a long tap. This allows tests to disable long tap gesture detection.
+ */
+ virtual void SetLongTapEnabled(bool aTapGestureEnabled) = 0;
+
+ /**
+ * Process touch velocity.
+ * Sometimes the touch move event will have a velocity even though no scrolling
+ * is occurring such as when the toolbar is being hidden/shown in Fennec.
+ * This function can be called to have the y axis' velocity queue updated.
+ */
+ virtual void ProcessTouchVelocity(uint32_t aTimestampMs, float aSpeedY) = 0;
+
+protected:
+
+ // Methods to help process WidgetInputEvents (or manage conversion to/from InputData)
+
+ virtual void TransformEventRefPoint(
+ LayoutDeviceIntPoint* aRefPoint,
+ ScrollableLayerGuid* aOutTargetGuid) = 0;
+
+ virtual void UpdateWheelTransaction(
+ LayoutDeviceIntPoint aRefPoint,
+ EventMessage aEventMessage) = 0;
+
+ // Discourage destruction outside of decref
+
+ virtual ~IAPZCTreeManager() { }
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif // mozilla_layers_IAPZCTreeManager_h
diff --git a/gfx/layers/apz/public/MetricsSharingController.h b/gfx/layers/apz/public/MetricsSharingController.h
new file mode 100644
index 000000000..090878573
--- /dev/null
+++ b/gfx/layers/apz/public/MetricsSharingController.h
@@ -0,0 +1,40 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set sw=4 ts=8 et tw=80 : */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_layers_MetricsSharingController_h
+#define mozilla_layers_MetricsSharingController_h
+
+#include "FrameMetrics.h" // for FrameMetrics
+#include "mozilla/ipc/CrossProcessMutex.h" // for CrossProcessMutexHandle
+#include "mozilla/ipc/SharedMemoryBasic.h" // for SharedMemoryBasic
+#include "mozilla/RefCountType.h" // for MozExternalRefCountType
+#include "nscore.h" // for NS_IMETHOD_
+
+namespace mozilla {
+namespace layers {
+
+class MetricsSharingController
+{
+public:
+ NS_IMETHOD_(MozExternalRefCountType) AddRef() = 0;
+ NS_IMETHOD_(MozExternalRefCountType) Release() = 0;
+
+ virtual base::ProcessId RemotePid() = 0;
+ virtual bool StartSharingMetrics(mozilla::ipc::SharedMemoryBasic::Handle aHandle,
+ CrossProcessMutexHandle aMutexHandle,
+ uint64_t aLayersId,
+ uint32_t aApzcId) = 0;
+ virtual bool StopSharingMetrics(FrameMetrics::ViewID aScrollId,
+ uint32_t aApzcId) = 0;
+
+protected:
+ virtual ~MetricsSharingController() {}
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif // mozilla_layers_MetricsSharingController_h
diff --git a/gfx/layers/apz/src/APZCTreeManager.cpp b/gfx/layers/apz/src/APZCTreeManager.cpp
new file mode 100644
index 000000000..857ae5958
--- /dev/null
+++ b/gfx/layers/apz/src/APZCTreeManager.cpp
@@ -0,0 +1,2099 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include <stack>
+#include "APZCTreeManager.h"
+#include "AsyncPanZoomController.h"
+#include "Compositor.h" // for Compositor
+#include "DragTracker.h" // for DragTracker
+#include "gfxPrefs.h" // for gfxPrefs
+#include "HitTestingTreeNode.h" // for HitTestingTreeNode
+#include "InputBlockState.h" // for InputBlockState
+#include "InputData.h" // for InputData, etc
+#include "Layers.h" // for Layer, etc
+#include "mozilla/dom/Touch.h" // for Touch
+#include "mozilla/gfx/GPUParent.h" // for GPUParent
+#include "mozilla/gfx/Logging.h" // for gfx::TreeLog
+#include "mozilla/gfx/Point.h" // for Point
+#include "mozilla/layers/APZThreadUtils.h" // for AssertOnCompositorThread, etc
+#include "mozilla/layers/AsyncCompositionManager.h" // for ViewTransform
+#include "mozilla/layers/AsyncDragMetrics.h" // for AsyncDragMetrics
+#include "mozilla/layers/CompositorBridgeParent.h" // for CompositorBridgeParent, etc
+#include "mozilla/layers/LayerMetricsWrapper.h"
+#include "mozilla/MouseEvents.h"
+#include "mozilla/mozalloc.h" // for operator new
+#include "mozilla/TouchEvents.h"
+#include "mozilla/Preferences.h" // for Preferences
+#include "mozilla/EventStateManager.h" // for WheelPrefs
+#include "nsDebug.h" // for NS_WARNING
+#include "nsPoint.h" // for nsIntPoint
+#include "nsThreadUtils.h" // for NS_IsMainThread
+#include "OverscrollHandoffState.h" // for OverscrollHandoffState
+#include "TreeTraversal.h" // for ForEachNode, BreadthFirstSearch, etc
+#include "LayersLogging.h" // for Stringify
+#include "Units.h" // for ParentlayerPixel
+#include "GestureEventListener.h" // for GestureEventListener::setLongTapEnabled
+#include "UnitTransforms.h" // for ViewAs
+
+#define ENABLE_APZCTM_LOGGING 0
+// #define ENABLE_APZCTM_LOGGING 1
+
+#if ENABLE_APZCTM_LOGGING
+# define APZCTM_LOG(...) printf_stderr("APZCTM: " __VA_ARGS__)
+#else
+# define APZCTM_LOG(...)
+#endif
+
+namespace mozilla {
+namespace layers {
+
+typedef mozilla::gfx::Point Point;
+typedef mozilla::gfx::Point4D Point4D;
+typedef mozilla::gfx::Matrix4x4 Matrix4x4;
+
+float APZCTreeManager::sDPI = 160.0;
+
+struct APZCTreeManager::TreeBuildingState {
+ TreeBuildingState(const CompositorBridgeParent::LayerTreeState* const aLayerTreeState,
+ bool aIsFirstPaint, uint64_t aOriginatingLayersId,
+ APZTestData* aTestData, uint32_t aPaintSequence)
+ : mLayerTreeState(aLayerTreeState)
+ , mIsFirstPaint(aIsFirstPaint)
+ , mOriginatingLayersId(aOriginatingLayersId)
+ , mPaintLogger(aTestData, aPaintSequence)
+ {
+ }
+
+ // State that doesn't change as we recurse in the tree building
+ const CompositorBridgeParent::LayerTreeState* const mLayerTreeState;
+ const bool mIsFirstPaint;
+ const uint64_t mOriginatingLayersId;
+ const APZPaintLogHelper mPaintLogger;
+
+ // State that is updated as we perform the tree build
+
+ // A list of nodes that need to be destroyed at the end of the tree building.
+ // This is initialized with all nodes in the old tree, and nodes are removed
+ // from it as we reuse them in the new tree.
+ nsTArray<RefPtr<HitTestingTreeNode>> mNodesToDestroy;
+
+ // This map is populated as we place APZCs into the new tree. Its purpose is
+ // to facilitate re-using the same APZC for different layers that scroll
+ // together (and thus have the same ScrollableLayerGuid).
+ std::map<ScrollableLayerGuid, AsyncPanZoomController*> mApzcMap;
+};
+
+class APZCTreeManager::CheckerboardFlushObserver : public nsIObserver {
+public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIOBSERVER
+
+ explicit CheckerboardFlushObserver(APZCTreeManager* aTreeManager)
+ : mTreeManager(aTreeManager)
+ {
+ MOZ_ASSERT(NS_IsMainThread());
+ nsCOMPtr<nsIObserverService> obsSvc = mozilla::services::GetObserverService();
+ MOZ_ASSERT(obsSvc);
+ if (obsSvc) {
+ obsSvc->AddObserver(this, "APZ:FlushActiveCheckerboard", false);
+ }
+ }
+
+ void Unregister()
+ {
+ MOZ_ASSERT(NS_IsMainThread());
+ nsCOMPtr<nsIObserverService> obsSvc = mozilla::services::GetObserverService();
+ if (obsSvc) {
+ obsSvc->RemoveObserver(this, "APZ:FlushActiveCheckerboard");
+ }
+ mTreeManager = nullptr;
+ }
+
+protected:
+ virtual ~CheckerboardFlushObserver() {}
+
+private:
+ RefPtr<APZCTreeManager> mTreeManager;
+};
+
+NS_IMPL_ISUPPORTS(APZCTreeManager::CheckerboardFlushObserver, nsIObserver)
+
+NS_IMETHODIMP
+APZCTreeManager::CheckerboardFlushObserver::Observe(nsISupports* aSubject,
+ const char* aTopic,
+ const char16_t*)
+{
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(mTreeManager.get());
+
+ MutexAutoLock lock(mTreeManager->mTreeLock);
+ if (mTreeManager->mRootNode) {
+ ForEachNode<ReverseIterator>(mTreeManager->mRootNode.get(),
+ [](HitTestingTreeNode* aNode)
+ {
+ if (aNode->IsPrimaryHolder()) {
+ MOZ_ASSERT(aNode->GetApzc());
+ aNode->GetApzc()->FlushActiveCheckerboardReport();
+ }
+ });
+ }
+ if (XRE_IsGPUProcess()) {
+ if (gfx::GPUParent* gpu = gfx::GPUParent::GetSingleton()) {
+ nsCString topic("APZ:FlushActiveCheckerboard:Done");
+ Unused << gpu->SendNotifyUiObservers(topic);
+ }
+ } else {
+ MOZ_ASSERT(XRE_IsParentProcess());
+ nsCOMPtr<nsIObserverService> obsSvc = mozilla::services::GetObserverService();
+ if (obsSvc) {
+ obsSvc->NotifyObservers(nullptr, "APZ:FlushActiveCheckerboard:Done", nullptr);
+ }
+ }
+ return NS_OK;
+}
+
+
+/*static*/ const ScreenMargin
+APZCTreeManager::CalculatePendingDisplayPort(
+ const FrameMetrics& aFrameMetrics,
+ const ParentLayerPoint& aVelocity)
+{
+ return AsyncPanZoomController::CalculatePendingDisplayPort(
+ aFrameMetrics, aVelocity);
+}
+
+APZCTreeManager::APZCTreeManager()
+ : mInputQueue(new InputQueue()),
+ mTreeLock("APZCTreeLock"),
+ mHitResultForInputBlock(HitNothing),
+ mRetainedTouchIdentifier(-1),
+ mApzcTreeLog("apzctree")
+{
+ RefPtr<APZCTreeManager> self(this);
+ NS_DispatchToMainThread(NS_NewRunnableFunction([self] {
+ self->mFlushObserver = new CheckerboardFlushObserver(self);
+ }));
+ AsyncPanZoomController::InitializeGlobalState();
+ mApzcTreeLog.ConditionOnPrefFunction(gfxPrefs::APZPrintTree);
+}
+
+APZCTreeManager::~APZCTreeManager()
+{
+}
+
+/*static*/ void
+APZCTreeManager::InitializeGlobalState()
+{
+ MOZ_ASSERT(NS_IsMainThread());
+ AsyncPanZoomController::InitializeGlobalState();
+}
+
+AsyncPanZoomController*
+APZCTreeManager::NewAPZCInstance(uint64_t aLayersId,
+ GeckoContentController* aController)
+{
+ return new AsyncPanZoomController(aLayersId, this, mInputQueue,
+ aController, AsyncPanZoomController::USE_GESTURE_DETECTOR);
+}
+
+TimeStamp
+APZCTreeManager::GetFrameTime()
+{
+ return TimeStamp::Now();
+}
+
+void
+APZCTreeManager::SetAllowedTouchBehavior(uint64_t aInputBlockId,
+ const nsTArray<TouchBehaviorFlags> &aValues)
+{
+ mInputQueue->SetAllowedTouchBehavior(aInputBlockId, aValues);
+}
+
+void
+APZCTreeManager::UpdateHitTestingTree(uint64_t aRootLayerTreeId,
+ Layer* aRoot,
+ bool aIsFirstPaint,
+ uint64_t aOriginatingLayersId,
+ uint32_t aPaintSequenceNumber)
+{
+ APZThreadUtils::AssertOnCompositorThread();
+
+ MutexAutoLock lock(mTreeLock);
+
+ // For testing purposes, we log some data to the APZTestData associated with
+ // the layers id that originated this update.
+ APZTestData* testData = nullptr;
+ if (gfxPrefs::APZTestLoggingEnabled()) {
+ if (CompositorBridgeParent::LayerTreeState* state = CompositorBridgeParent::GetIndirectShadowTree(aOriginatingLayersId)) {
+ testData = &state->mApzTestData;
+ testData->StartNewPaint(aPaintSequenceNumber);
+ }
+ }
+
+ const CompositorBridgeParent::LayerTreeState* treeState =
+ CompositorBridgeParent::GetIndirectShadowTree(aRootLayerTreeId);
+ MOZ_ASSERT(treeState);
+ TreeBuildingState state(treeState, aIsFirstPaint, aOriginatingLayersId,
+ testData, aPaintSequenceNumber);
+
+ // We do this business with collecting the entire tree into an array because otherwise
+ // it's very hard to determine which APZC instances need to be destroyed. In the worst
+ // case, there are two scenarios: (a) a layer with an APZC is removed from the layer
+ // tree and (b) a layer with an APZC is moved in the layer tree from one place to a
+ // completely different place. In scenario (a) we would want to destroy the APZC while
+ // walking the layer tree and noticing that the layer/APZC is no longer there. But if
+ // we do that then we run into a problem in scenario (b) because we might encounter that
+ // layer later during the walk. To handle both of these we have to 'remember' that the
+ // layer was not found, and then do the destroy only at the end of the tree walk after
+ // we are sure that the layer was removed and not just transplanted elsewhere. Doing that
+ // as part of a recursive tree walk is hard and so maintaining a list and removing
+ // APZCs that are still alive is much simpler.
+ ForEachNode<ReverseIterator>(mRootNode.get(),
+ [&state] (HitTestingTreeNode* aNode)
+ {
+ state.mNodesToDestroy.AppendElement(aNode);
+ });
+ mRootNode = nullptr;
+
+ if (aRoot) {
+ std::stack<gfx::TreeAutoIndent> indents;
+ std::stack<gfx::Matrix4x4> ancestorTransforms;
+ HitTestingTreeNode* parent = nullptr;
+ HitTestingTreeNode* next = nullptr;
+ uint64_t layersId = aRootLayerTreeId;
+ ancestorTransforms.push(Matrix4x4());
+
+ mApzcTreeLog << "[start]\n";
+ LayerMetricsWrapper root(aRoot);
+ mTreeLock.AssertCurrentThreadOwns();
+
+ ForEachNode<ReverseIterator>(root,
+ [&](LayerMetricsWrapper aLayerMetrics)
+ {
+ mApzcTreeLog << aLayerMetrics.Name() << '\t';
+
+ HitTestingTreeNode* node = PrepareNodeForLayer(aLayerMetrics,
+ aLayerMetrics.Metrics(), layersId, ancestorTransforms.top(),
+ parent, next, state);
+ MOZ_ASSERT(node);
+ AsyncPanZoomController* apzc = node->GetApzc();
+ aLayerMetrics.SetApzc(apzc);
+
+ mApzcTreeLog << '\n';
+
+ // Accumulate the CSS transform between layers that have an APZC.
+ // In the terminology of the big comment above APZCTreeManager::GetScreenToApzcTransform, if
+ // we are at layer M, then aAncestorTransform is NC * OC * PC, and we left-multiply MC and
+ // compute ancestorTransform to be MC * NC * OC * PC. This gets passed down as the ancestor
+ // transform to layer L when we recurse into the children below. If we are at a layer
+ // with an APZC, such as P, then we reset the ancestorTransform to just PC, to start
+ // the new accumulation as we go down.
+ // If a transform is a perspective transform, it's ignored for this purpose
+ // (see bug 1168263).
+ Matrix4x4 currentTransform = aLayerMetrics.TransformIsPerspective() ? Matrix4x4() : aLayerMetrics.GetTransform();
+ if (!apzc) {
+ currentTransform = currentTransform * ancestorTransforms.top();
+ }
+ ancestorTransforms.push(currentTransform);
+
+ // Note that |node| at this point will not have any children, otherwise we
+ // we would have to set next to node->GetFirstChild().
+ MOZ_ASSERT(!node->GetFirstChild());
+ parent = node;
+ next = nullptr;
+ layersId = (aLayerMetrics.AsRefLayer() ? aLayerMetrics.AsRefLayer()->GetReferentId() : layersId);
+ indents.push(gfx::TreeAutoIndent(mApzcTreeLog));
+ },
+ [&](LayerMetricsWrapper aLayerMetrics)
+ {
+ next = parent;
+ parent = parent->GetParent();
+ layersId = next->GetLayersId();
+ ancestorTransforms.pop();
+ indents.pop();
+ });
+
+ mApzcTreeLog << "[end]\n";
+ }
+
+ // We do not support tree structures where the root node has siblings.
+ MOZ_ASSERT(!(mRootNode && mRootNode->GetPrevSibling()));
+
+ for (size_t i = 0; i < state.mNodesToDestroy.Length(); i++) {
+ APZCTM_LOG("Destroying node at %p with APZC %p\n",
+ state.mNodesToDestroy[i].get(),
+ state.mNodesToDestroy[i]->GetApzc());
+ state.mNodesToDestroy[i]->Destroy();
+ }
+
+#if ENABLE_APZCTM_LOGGING
+ // Make the hit-test tree line up with the layer dump
+ printf_stderr("APZCTreeManager (%p)\n", this);
+ mRootNode->Dump(" ");
+#endif
+}
+
+// Compute the clip region to be used for a layer with an APZC. This function
+// is only called for layers which actually have scrollable metrics and an APZC.
+static ParentLayerIntRegion
+ComputeClipRegion(GeckoContentController* aController,
+ const LayerMetricsWrapper& aLayer)
+{
+ ParentLayerIntRegion clipRegion;
+ if (aLayer.GetClipRect()) {
+ clipRegion = *aLayer.GetClipRect();
+ } else {
+ // if there is no clip on this layer (which should only happen for the
+ // root scrollable layer in a process, or for some of the LayerMetrics
+ // expansions of a multi-metrics layer), fall back to using the comp
+ // bounds which should be equivalent.
+ clipRegion = RoundedToInt(aLayer.Metrics().GetCompositionBounds());
+ }
+
+ return clipRegion;
+}
+
+void
+APZCTreeManager::PrintAPZCInfo(const LayerMetricsWrapper& aLayer,
+ const AsyncPanZoomController* apzc)
+{
+ const FrameMetrics& metrics = aLayer.Metrics();
+ mApzcTreeLog << "APZC " << apzc->GetGuid()
+ << "\tcb=" << metrics.GetCompositionBounds()
+ << "\tsr=" << metrics.GetScrollableRect()
+ << (aLayer.IsScrollInfoLayer() ? "\tscrollinfo" : "")
+ << (apzc->HasScrollgrab() ? "\tscrollgrab" : "") << "\t"
+ << aLayer.Metadata().GetContentDescription().get();
+}
+
+void
+APZCTreeManager::AttachNodeToTree(HitTestingTreeNode* aNode,
+ HitTestingTreeNode* aParent,
+ HitTestingTreeNode* aNextSibling)
+{
+ if (aNextSibling) {
+ aNextSibling->SetPrevSibling(aNode);
+ } else if (aParent) {
+ aParent->SetLastChild(aNode);
+ } else {
+ MOZ_ASSERT(!mRootNode);
+ mRootNode = aNode;
+ aNode->MakeRoot();
+ }
+}
+
+static EventRegions
+GetEventRegions(const LayerMetricsWrapper& aLayer)
+{
+ if (aLayer.IsScrollInfoLayer()) {
+ ParentLayerIntRect compositionBounds(RoundedToInt(aLayer.Metrics().GetCompositionBounds()));
+ nsIntRegion hitRegion(compositionBounds.ToUnknownRect());
+ EventRegions eventRegions(hitRegion);
+ eventRegions.mDispatchToContentHitRegion = eventRegions.mHitRegion;
+ return eventRegions;
+ }
+ return aLayer.GetEventRegions();
+}
+
+already_AddRefed<HitTestingTreeNode>
+APZCTreeManager::RecycleOrCreateNode(TreeBuildingState& aState,
+ AsyncPanZoomController* aApzc,
+ uint64_t aLayersId)
+{
+ // Find a node without an APZC and return it. Note that unless the layer tree
+ // actually changes, this loop should generally do an early-return on the
+ // first iteration, so it should be cheap in the common case.
+ for (size_t i = 0; i < aState.mNodesToDestroy.Length(); i++) {
+ RefPtr<HitTestingTreeNode> node = aState.mNodesToDestroy[i];
+ if (!node->IsPrimaryHolder()) {
+ aState.mNodesToDestroy.RemoveElement(node);
+ node->RecycleWith(aApzc, aLayersId);
+ return node.forget();
+ }
+ }
+ RefPtr<HitTestingTreeNode> node = new HitTestingTreeNode(aApzc, false, aLayersId);
+ return node.forget();
+}
+
+static EventRegionsOverride
+GetEventRegionsOverride(HitTestingTreeNode* aParent,
+ const LayerMetricsWrapper& aLayer)
+{
+ // Make it so that if the flag is set on the layer tree, it automatically
+ // propagates to all the nodes in the corresponding subtree rooted at that
+ // layer in the hit-test tree. This saves having to walk up the tree every
+ // we want to see if a hit-test node is affected by this flag.
+ EventRegionsOverride result = aLayer.GetEventRegionsOverride();
+ if (aParent) {
+ result |= aParent->GetEventRegionsOverride();
+ }
+ return result;
+}
+
+void
+APZCTreeManager::StartScrollbarDrag(const ScrollableLayerGuid& aGuid,
+ const AsyncDragMetrics& aDragMetrics)
+{
+
+ RefPtr<AsyncPanZoomController> apzc = GetTargetAPZC(aGuid);
+ if (!apzc) {
+ return;
+ }
+
+ uint64_t inputBlockId = aDragMetrics.mDragStartSequenceNumber;
+ mInputQueue->ConfirmDragBlock(inputBlockId, apzc, aDragMetrics);
+}
+
+HitTestingTreeNode*
+APZCTreeManager::PrepareNodeForLayer(const LayerMetricsWrapper& aLayer,
+ const FrameMetrics& aMetrics,
+ uint64_t aLayersId,
+ const gfx::Matrix4x4& aAncestorTransform,
+ HitTestingTreeNode* aParent,
+ HitTestingTreeNode* aNextSibling,
+ TreeBuildingState& aState)
+{
+ mTreeLock.AssertCurrentThreadOwns();
+
+ bool needsApzc = true;
+ if (!aMetrics.IsScrollable()) {
+ needsApzc = false;
+ }
+
+ const CompositorBridgeParent::LayerTreeState* state = CompositorBridgeParent::GetIndirectShadowTree(aLayersId);
+ if (!(state && state->mController.get())) {
+ needsApzc = false;
+ }
+
+ RefPtr<HitTestingTreeNode> node = nullptr;
+ if (!needsApzc) {
+ node = RecycleOrCreateNode(aState, nullptr, aLayersId);
+ AttachNodeToTree(node, aParent, aNextSibling);
+ node->SetHitTestData(
+ GetEventRegions(aLayer),
+ aLayer.GetTransformTyped(),
+ aLayer.GetClipRect() ? Some(ParentLayerIntRegion(*aLayer.GetClipRect())) : Nothing(),
+ GetEventRegionsOverride(aParent, aLayer));
+ node->SetScrollbarData(aLayer.GetScrollbarTargetContainerId(),
+ aLayer.GetScrollbarDirection(),
+ aLayer.GetScrollbarSize(),
+ aLayer.IsScrollbarContainer());
+ node->SetFixedPosData(aLayer.GetFixedPositionScrollContainerId());
+ return node;
+ }
+
+ AsyncPanZoomController* apzc = nullptr;
+ // If we get here, aLayer is a scrollable layer and somebody
+ // has registered a GeckoContentController for it, so we need to ensure
+ // it has an APZC instance to manage its scrolling.
+
+ // aState.mApzcMap allows reusing the exact same APZC instance for different layers
+ // with the same FrameMetrics data. This is needed because in some cases content
+ // that is supposed to scroll together is split into multiple layers because of
+ // e.g. non-scrolling content interleaved in z-index order.
+ ScrollableLayerGuid guid(aLayersId, aMetrics);
+ auto insertResult = aState.mApzcMap.insert(std::make_pair(guid, static_cast<AsyncPanZoomController*>(nullptr)));
+ if (!insertResult.second) {
+ apzc = insertResult.first->second;
+ PrintAPZCInfo(aLayer, apzc);
+ }
+ APZCTM_LOG("Found APZC %p for layer %p with identifiers %" PRId64 " %" PRId64 "\n", apzc, aLayer.GetLayer(), guid.mLayersId, guid.mScrollId);
+
+ // If we haven't encountered a layer already with the same metrics, then we need to
+ // do the full reuse-or-make-an-APZC algorithm, which is contained inside the block
+ // below.
+ if (apzc == nullptr) {
+ apzc = aLayer.GetApzc();
+
+ // If the content represented by the scrollable layer has changed (which may
+ // be possible because of DLBI heuristics) then we don't want to keep using
+ // the same old APZC for the new content. Also, when reparenting a tab into a
+ // new window a layer might get moved to a different layer tree with a
+ // different APZCTreeManager. In these cases we don't want to reuse the same
+ // APZC, so null it out so we run through the code to find another one or
+ // create one.
+ if (apzc && (!apzc->Matches(guid) || !apzc->HasTreeManager(this))) {
+ apzc = nullptr;
+ }
+
+ // See if we can find an APZC from the previous tree that matches the
+ // ScrollableLayerGuid from this layer. If there is one, then we know that
+ // the layout of the page changed causing the layer tree to be rebuilt, but
+ // the underlying content for the APZC is still there somewhere. Therefore,
+ // we want to find the APZC instance and continue using it here.
+ //
+ // We particularly want to find the primary-holder node from the previous
+ // tree that matches, because we don't want that node to get destroyed. If
+ // it does get destroyed, then the APZC will get destroyed along with it by
+ // definition, but we want to keep that APZC around in the new tree.
+ // We leave non-primary-holder nodes in the destroy list because we don't
+ // care about those nodes getting destroyed.
+ for (size_t i = 0; i < aState.mNodesToDestroy.Length(); i++) {
+ RefPtr<HitTestingTreeNode> n = aState.mNodesToDestroy[i];
+ if (n->IsPrimaryHolder() && n->GetApzc() && n->GetApzc()->Matches(guid)) {
+ node = n;
+ if (apzc != nullptr) {
+ // If there is an APZC already then it should match the one from the
+ // old primary-holder node
+ MOZ_ASSERT(apzc == node->GetApzc());
+ }
+ apzc = node->GetApzc();
+ break;
+ }
+ }
+
+ // The APZC we get off the layer may have been destroyed previously if the
+ // layer was inactive or omitted from the layer tree for whatever reason
+ // from a layers update. If it later comes back it will have a reference to
+ // a destroyed APZC and so we need to throw that out and make a new one.
+ bool newApzc = (apzc == nullptr || apzc->IsDestroyed());
+ if (newApzc) {
+ MOZ_ASSERT(aState.mLayerTreeState);
+ apzc = NewAPZCInstance(aLayersId, state->mController);
+ apzc->SetCompositorController(aState.mLayerTreeState->GetCompositorController());
+ if (state->mCrossProcessParent) {
+ apzc->SetMetricsSharingController(state->CrossProcessSharingController());
+ } else {
+ apzc->SetMetricsSharingController(aState.mLayerTreeState->InProcessSharingController());
+ }
+ MOZ_ASSERT(node == nullptr);
+ node = new HitTestingTreeNode(apzc, true, aLayersId);
+ } else {
+ // If we are re-using a node for this layer clear the tree pointers
+ // so that it doesn't continue pointing to nodes that might no longer
+ // be in the tree. These pointers will get reset properly as we continue
+ // building the tree. Also remove it from the set of nodes that are going
+ // to be destroyed, because it's going to remain active.
+ aState.mNodesToDestroy.RemoveElement(node);
+ node->SetPrevSibling(nullptr);
+ node->SetLastChild(nullptr);
+ }
+
+ APZCTM_LOG("Using APZC %p for layer %p with identifiers %" PRId64 " %" PRId64 "\n", apzc, aLayer.GetLayer(), aLayersId, aMetrics.GetScrollId());
+
+ apzc->NotifyLayersUpdated(aLayer.Metadata(), aState.mIsFirstPaint,
+ aLayersId == aState.mOriginatingLayersId);
+
+ // Since this is the first time we are encountering an APZC with this guid,
+ // the node holding it must be the primary holder. It may be newly-created
+ // or not, depending on whether it went through the newApzc branch above.
+ MOZ_ASSERT(node->IsPrimaryHolder() && node->GetApzc() && node->GetApzc()->Matches(guid));
+
+ ParentLayerIntRegion clipRegion = ComputeClipRegion(state->mController, aLayer);
+ node->SetHitTestData(
+ GetEventRegions(aLayer),
+ aLayer.GetTransformTyped(),
+ Some(clipRegion),
+ GetEventRegionsOverride(aParent, aLayer));
+ apzc->SetAncestorTransform(aAncestorTransform);
+
+ PrintAPZCInfo(aLayer, apzc);
+
+ // Bind the APZC instance into the tree of APZCs
+ AttachNodeToTree(node, aParent, aNextSibling);
+
+ // For testing, log the parent scroll id of every APZC that has a
+ // parent. This allows test code to reconstruct the APZC tree.
+ // Note that we currently only do this for APZCs in the layer tree
+ // that originated the update, because the only identifying information
+ // we are logging about APZCs is the scroll id, and otherwise we could
+ // confuse APZCs from different layer trees with the same scroll id.
+ if (aLayersId == aState.mOriginatingLayersId) {
+ if (apzc->HasNoParentWithSameLayersId()) {
+ aState.mPaintLogger.LogTestData(aMetrics.GetScrollId(),
+ "hasNoParentWithSameLayersId", true);
+ } else {
+ MOZ_ASSERT(apzc->GetParent());
+ aState.mPaintLogger.LogTestData(aMetrics.GetScrollId(),
+ "parentScrollId", apzc->GetParent()->GetGuid().mScrollId);
+ }
+ if (aMetrics.IsRootContent()) {
+ aState.mPaintLogger.LogTestData(aMetrics.GetScrollId(),
+ "isRootContent", true);
+ }
+ // Note that the async scroll offset is in ParentLayer pixels
+ aState.mPaintLogger.LogTestData(aMetrics.GetScrollId(), "asyncScrollOffset",
+ apzc->GetCurrentAsyncScrollOffset(AsyncPanZoomController::NORMAL));
+ }
+
+ if (newApzc) {
+ auto it = mZoomConstraints.find(guid);
+ if (it != mZoomConstraints.end()) {
+ // We have a zoomconstraints for this guid, apply it.
+ apzc->UpdateZoomConstraints(it->second);
+ } else if (!apzc->HasNoParentWithSameLayersId()) {
+ // This is a sub-APZC, so inherit the zoom constraints from its parent.
+ // This ensures that if e.g. user-scalable=no was specified, none of the
+ // APZCs for that subtree allow double-tap to zoom.
+ apzc->UpdateZoomConstraints(apzc->GetParent()->GetZoomConstraints());
+ }
+ // Otherwise, this is the root of a layers id, but we didn't have a saved
+ // zoom constraints. Leave it empty for now.
+ }
+
+ // Add a guid -> APZC mapping for the newly created APZC.
+ insertResult.first->second = apzc;
+ } else {
+ // We already built an APZC earlier in this tree walk, but we have another layer
+ // now that will also be using that APZC. The hit-test region on the APZC needs
+ // to be updated to deal with the new layer's hit region.
+
+ node = RecycleOrCreateNode(aState, apzc, aLayersId);
+ AttachNodeToTree(node, aParent, aNextSibling);
+
+ // Even though different layers associated with a given APZC may be at
+ // different levels in the layer tree (e.g. one being an uncle of another),
+ // we require from Layout that the CSS transforms up to their common
+ // ancestor be roughly the same. There are cases in which the transforms
+ // are not exactly the same, for example if the parent is container layer
+ // for an opacity, and this container layer has a resolution-induced scale
+ // as its base transform and a prescale that is supposed to undo that scale.
+ // Due to floating point inaccuracies those transforms can end up not quite
+ // canceling each other. That's why we're using a fuzzy comparison here
+ // instead of an exact one.
+ MOZ_ASSERT(aAncestorTransform.FuzzyEqualsMultiplicative(apzc->GetAncestorTransform()));
+
+ ParentLayerIntRegion clipRegion = ComputeClipRegion(state->mController, aLayer);
+ node->SetHitTestData(
+ GetEventRegions(aLayer),
+ aLayer.GetTransformTyped(),
+ Some(clipRegion),
+ GetEventRegionsOverride(aParent, aLayer));
+ }
+
+ node->SetScrollbarData(aLayer.GetScrollbarTargetContainerId(),
+ aLayer.GetScrollbarDirection(),
+ aLayer.GetScrollbarSize(),
+ aLayer.IsScrollbarContainer());
+ node->SetFixedPosData(aLayer.GetFixedPositionScrollContainerId());
+ return node;
+}
+
+template<typename PanGestureOrScrollWheelInput>
+static bool
+WillHandleInput(const PanGestureOrScrollWheelInput& aPanInput)
+{
+ if (!NS_IsMainThread()) {
+ return true;
+ }
+
+ WidgetWheelEvent wheelEvent = aPanInput.ToWidgetWheelEvent(nullptr);
+ return WillHandleWheelEvent(&wheelEvent);
+}
+
+void
+APZCTreeManager::FlushApzRepaints(uint64_t aLayersId)
+{
+ // Previously, paints were throttled and therefore this method was used to
+ // ensure any pending paints were flushed. Now, paints are flushed
+ // immediately, so it is safe to simply send a notification now.
+ APZCTM_LOG("Flushing repaints for layers id %" PRIu64, aLayersId);
+ const CompositorBridgeParent::LayerTreeState* state =
+ CompositorBridgeParent::GetIndirectShadowTree(aLayersId);
+ MOZ_ASSERT(state && state->mController);
+ state->mController->DispatchToRepaintThread(NewRunnableMethod(
+ state->mController, &GeckoContentController::NotifyFlushComplete));
+}
+
+nsEventStatus
+APZCTreeManager::ReceiveInputEvent(InputData& aEvent,
+ ScrollableLayerGuid* aOutTargetGuid,
+ uint64_t* aOutInputBlockId)
+{
+ APZThreadUtils::AssertOnControllerThread();
+
+ // Initialize aOutInputBlockId to a sane value, and then later we overwrite
+ // it if the input event goes into a block.
+ if (aOutInputBlockId) {
+ *aOutInputBlockId = InputBlockState::NO_BLOCK_ID;
+ }
+ nsEventStatus result = nsEventStatus_eIgnore;
+ HitTestResult hitResult = HitNothing;
+ switch (aEvent.mInputType) {
+ case MULTITOUCH_INPUT: {
+ MultiTouchInput& touchInput = aEvent.AsMultiTouchInput();
+ result = ProcessTouchInput(touchInput, aOutTargetGuid, aOutInputBlockId);
+ break;
+ } case MOUSE_INPUT: {
+ MouseInput& mouseInput = aEvent.AsMouseInput();
+ mouseInput.mHandledByAPZ = true;
+
+ if (DragTracker::StartsDrag(mouseInput)) {
+ // If this is the start of a drag we need to unambiguously know if it's
+ // going to land on a scrollbar or not. We can't apply an untransform
+ // here without knowing that, so we need to ensure the untransform is
+ // a no-op.
+ FlushRepaintsToClearScreenToGeckoTransform();
+ }
+
+ bool hitScrollbar = false;
+ RefPtr<AsyncPanZoomController> apzc = GetTargetAPZC(mouseInput.mOrigin,
+ &hitResult, &hitScrollbar);
+
+ // When the mouse is outside the window we still want to handle dragging
+ // but we won't find an APZC. Fallback to root APZC then.
+ { // scope lock
+ MutexAutoLock lock(mTreeLock);
+ if (!apzc && mRootNode) {
+ apzc = mRootNode->GetApzc();
+ }
+ }
+
+ if (apzc) {
+ bool targetConfirmed = (hitResult != HitNothing && hitResult != HitDispatchToContentRegion);
+ if (gfxPrefs::APZDragEnabled() && hitScrollbar) {
+ // If scrollbar dragging is enabled and we hit a scrollbar, wait
+ // for the main-thread confirmation because it contains drag metrics
+ // that we need.
+ targetConfirmed = false;
+ }
+ result = mInputQueue->ReceiveInputEvent(
+ apzc, targetConfirmed,
+ mouseInput, aOutInputBlockId);
+
+ if (result == nsEventStatus_eConsumeDoDefault) {
+ // This input event is part of a drag block, so whether or not it is
+ // directed at a scrollbar depends on whether the drag block started
+ // on a scrollbar.
+ hitScrollbar = mInputQueue->IsDragOnScrollbar(hitScrollbar);
+ }
+
+ // Update the out-parameters so they are what the caller expects.
+ apzc->GetGuid(aOutTargetGuid);
+
+ if (!hitScrollbar) {
+ // The input was not targeted at a scrollbar, so we untransform it
+ // like we do for other content. Scrollbars are "special" because they
+ // have special handling in AsyncCompositionManager when resolution is
+ // applied. TODO: we should find a better way to deal with this.
+ ScreenToParentLayerMatrix4x4 transformToApzc = GetScreenToApzcTransform(apzc);
+ ParentLayerToScreenMatrix4x4 transformToGecko = GetApzcToGeckoTransform(apzc);
+ ScreenToScreenMatrix4x4 outTransform = transformToApzc * transformToGecko;
+ Maybe<ScreenPoint> untransformedRefPoint = UntransformBy(
+ outTransform, mouseInput.mOrigin);
+ if (untransformedRefPoint) {
+ mouseInput.mOrigin = *untransformedRefPoint;
+ }
+ } else {
+ // Likewise, if the input was targeted at a scrollbar, we don't want to
+ // apply the callback transform in the main thread, so we remove the
+ // scrollid from the guid. We need to keep the layersId intact so
+ // that the response from the child process doesn't get discarded.
+ aOutTargetGuid->mScrollId = FrameMetrics::NULL_SCROLL_ID;
+ }
+ }
+ break;
+ } case SCROLLWHEEL_INPUT: {
+ FlushRepaintsToClearScreenToGeckoTransform();
+
+ ScrollWheelInput& wheelInput = aEvent.AsScrollWheelInput();
+
+ wheelInput.mHandledByAPZ = WillHandleInput(wheelInput);
+ if (!wheelInput.mHandledByAPZ) {
+ return result;
+ }
+
+ RefPtr<AsyncPanZoomController> apzc = GetTargetAPZC(wheelInput.mOrigin,
+ &hitResult);
+ if (apzc) {
+ MOZ_ASSERT(hitResult != HitNothing);
+
+ // For wheel events, the call to ReceiveInputEvent below may result in
+ // scrolling, which changes the async transform. However, the event we
+ // want to pass to gecko should be the pre-scroll event coordinates,
+ // transformed into the gecko space. (pre-scroll because the mouse
+ // cursor is stationary during wheel scrolling, unlike touchmove
+ // events). Since we just flushed the pending repaints the transform to
+ // gecko space should only consist of overscroll-cancelling transforms.
+ ScreenToScreenMatrix4x4 transformToGecko = GetScreenToApzcTransform(apzc)
+ * GetApzcToGeckoTransform(apzc);
+ Maybe<ScreenPoint> untransformedOrigin = UntransformBy(
+ transformToGecko, wheelInput.mOrigin);
+
+ if (!untransformedOrigin) {
+ return result;
+ }
+
+ result = mInputQueue->ReceiveInputEvent(
+ apzc,
+ /* aTargetConfirmed = */ hitResult != HitDispatchToContentRegion,
+ wheelInput, aOutInputBlockId);
+
+ // Update the out-parameters so they are what the caller expects.
+ apzc->GetGuid(aOutTargetGuid);
+ wheelInput.mOrigin = *untransformedOrigin;
+ }
+ break;
+ } case PANGESTURE_INPUT: {
+ FlushRepaintsToClearScreenToGeckoTransform();
+
+ PanGestureInput& panInput = aEvent.AsPanGestureInput();
+ panInput.mHandledByAPZ = WillHandleInput(panInput);
+ if (!panInput.mHandledByAPZ) {
+ return result;
+ }
+
+ // If/when we enable support for pan inputs off-main-thread, we'll need
+ // to duplicate this EventStateManager code or something. See the other
+ // call to GetUserPrefsForWheelEvent in this file for why these fields
+ // are stored separately.
+ MOZ_ASSERT(NS_IsMainThread());
+ WidgetWheelEvent wheelEvent = panInput.ToWidgetWheelEvent(nullptr);
+ EventStateManager::GetUserPrefsForWheelEvent(&wheelEvent,
+ &panInput.mUserDeltaMultiplierX,
+ &panInput.mUserDeltaMultiplierY);
+
+ RefPtr<AsyncPanZoomController> apzc = GetTargetAPZC(panInput.mPanStartPoint,
+ &hitResult);
+ if (apzc) {
+ MOZ_ASSERT(hitResult != HitNothing);
+
+ // For pan gesture events, the call to ReceiveInputEvent below may result in
+ // scrolling, which changes the async transform. However, the event we
+ // want to pass to gecko should be the pre-scroll event coordinates,
+ // transformed into the gecko space. (pre-scroll because the mouse
+ // cursor is stationary during pan gesture scrolling, unlike touchmove
+ // events). Since we just flushed the pending repaints the transform to
+ // gecko space should only consist of overscroll-cancelling transforms.
+ ScreenToScreenMatrix4x4 transformToGecko = GetScreenToApzcTransform(apzc)
+ * GetApzcToGeckoTransform(apzc);
+ Maybe<ScreenPoint> untransformedStartPoint = UntransformBy(
+ transformToGecko, panInput.mPanStartPoint);
+ Maybe<ScreenPoint> untransformedDisplacement = UntransformVector(
+ transformToGecko, panInput.mPanDisplacement, panInput.mPanStartPoint);
+
+ if (!untransformedStartPoint || !untransformedDisplacement) {
+ return result;
+ }
+
+ result = mInputQueue->ReceiveInputEvent(
+ apzc,
+ /* aTargetConfirmed = */ hitResult != HitDispatchToContentRegion,
+ panInput, aOutInputBlockId);
+
+ // Update the out-parameters so they are what the caller expects.
+ apzc->GetGuid(aOutTargetGuid);
+ panInput.mPanStartPoint = *untransformedStartPoint;
+ panInput.mPanDisplacement = *untransformedDisplacement;
+ }
+ break;
+ } case PINCHGESTURE_INPUT: { // note: no one currently sends these
+ PinchGestureInput& pinchInput = aEvent.AsPinchGestureInput();
+ RefPtr<AsyncPanZoomController> apzc = GetTargetAPZC(pinchInput.mFocusPoint,
+ &hitResult);
+ if (apzc) {
+ MOZ_ASSERT(hitResult != HitNothing);
+
+ ScreenToScreenMatrix4x4 outTransform = GetScreenToApzcTransform(apzc)
+ * GetApzcToGeckoTransform(apzc);
+ Maybe<ScreenPoint> untransformedFocusPoint = UntransformBy(
+ outTransform, pinchInput.mFocusPoint);
+
+ if (!untransformedFocusPoint) {
+ return result;
+ }
+
+ result = mInputQueue->ReceiveInputEvent(
+ apzc,
+ /* aTargetConfirmed = */ hitResult != HitDispatchToContentRegion,
+ pinchInput, aOutInputBlockId);
+
+ // Update the out-parameters so they are what the caller expects.
+ apzc->GetGuid(aOutTargetGuid);
+ pinchInput.mFocusPoint = *untransformedFocusPoint;
+ }
+ break;
+ } case TAPGESTURE_INPUT: { // note: no one currently sends these
+ TapGestureInput& tapInput = aEvent.AsTapGestureInput();
+ RefPtr<AsyncPanZoomController> apzc = GetTargetAPZC(tapInput.mPoint,
+ &hitResult);
+ if (apzc) {
+ MOZ_ASSERT(hitResult != HitNothing);
+
+ ScreenToScreenMatrix4x4 outTransform = GetScreenToApzcTransform(apzc)
+ * GetApzcToGeckoTransform(apzc);
+ Maybe<ScreenIntPoint> untransformedPoint =
+ UntransformBy(outTransform, tapInput.mPoint);
+
+ if (!untransformedPoint) {
+ return result;
+ }
+
+ result = mInputQueue->ReceiveInputEvent(
+ apzc,
+ /* aTargetConfirmed = */ hitResult != HitDispatchToContentRegion,
+ tapInput, aOutInputBlockId);
+
+ // Update the out-parameters so they are what the caller expects.
+ apzc->GetGuid(aOutTargetGuid);
+ tapInput.mPoint = *untransformedPoint;
+ }
+ break;
+ } case SENTINEL_INPUT: {
+ MOZ_ASSERT_UNREACHABLE("Invalid InputType.");
+ break;
+ }
+ }
+ return result;
+}
+
+static TouchBehaviorFlags
+ConvertToTouchBehavior(HitTestResult result)
+{
+ switch (result) {
+ case HitNothing:
+ return AllowedTouchBehavior::NONE;
+ case HitLayer:
+ return AllowedTouchBehavior::VERTICAL_PAN
+ | AllowedTouchBehavior::HORIZONTAL_PAN
+ | AllowedTouchBehavior::PINCH_ZOOM
+ | AllowedTouchBehavior::DOUBLE_TAP_ZOOM;
+ case HitLayerTouchActionNone:
+ return AllowedTouchBehavior::NONE;
+ case HitLayerTouchActionPanX:
+ return AllowedTouchBehavior::HORIZONTAL_PAN;
+ case HitLayerTouchActionPanY:
+ return AllowedTouchBehavior::VERTICAL_PAN;
+ case HitLayerTouchActionPanXY:
+ return AllowedTouchBehavior::HORIZONTAL_PAN
+ | AllowedTouchBehavior::VERTICAL_PAN;
+ case HitDispatchToContentRegion:
+ default:
+ return AllowedTouchBehavior::UNKNOWN;
+ }
+}
+
+already_AddRefed<AsyncPanZoomController>
+APZCTreeManager::GetTouchInputBlockAPZC(const MultiTouchInput& aEvent,
+ nsTArray<TouchBehaviorFlags>* aOutTouchBehaviors,
+ HitTestResult* aOutHitResult)
+{
+ RefPtr<AsyncPanZoomController> apzc;
+ if (aEvent.mTouches.Length() == 0) {
+ return apzc.forget();
+ }
+
+ FlushRepaintsToClearScreenToGeckoTransform();
+
+ HitTestResult hitResult;
+ apzc = GetTargetAPZC(aEvent.mTouches[0].mScreenPoint, &hitResult);
+ 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());
+ }
+
+ 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();
+}
+
+nsEventStatus
+APZCTreeManager::ProcessTouchInput(MultiTouchInput& aInput,
+ ScrollableLayerGuid* aOutTargetGuid,
+ uint64_t* aOutInputBlockId)
+{
+ aInput.mHandledByAPZ = true;
+ nsTArray<TouchBehaviorFlags> touchBehaviors;
+ 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.)
+ if (mApzcForInputBlock &&
+ mApzcForInputBlock->IsInPanningState() &&
+ BuildOverscrollHandoffChain(mApzcForInputBlock)->HasOverscrolledApzc()) {
+ if (mRetainedTouchIdentifier == -1) {
+ mRetainedTouchIdentifier = mApzcForInputBlock->GetLastTouchIdentifier();
+ }
+ return nsEventStatus_eConsumeNoDefault;
+ }
+
+ mHitResultForInputBlock = HitNothing;
+ mApzcForInputBlock = GetTouchInputBlockAPZC(aInput, &touchBehaviors, &mHitResultForInputBlock);
+ 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;
+ }
+
+ // 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;
+ }
+ }
+ if (aInput.mTouches.IsEmpty()) {
+ return nsEventStatus_eConsumeNoDefault;
+ }
+ }
+
+ 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);
+ }
+
+ // 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;
+ }
+ 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;
+ }
+
+ return result;
+}
+
+void
+APZCTreeManager::UpdateWheelTransaction(LayoutDeviceIntPoint aRefPoint,
+ EventMessage aEventMessage)
+{
+ WheelBlockState* txn = mInputQueue->GetActiveWheelTransaction();
+ 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 (aEventMessage) {
+ case eMouseMove:
+ case eDragOver: {
+
+ ScreenIntPoint point =
+ ViewAs<ScreenPixel>(aRefPoint,
+ PixelCastJustification::LayoutDeviceIsScreenForUntransformedEvent);
+
+ txn->OnMouseMove(point);
+
+ return;
+ }
+ case eKeyPress:
+ case eKeyUp:
+ case eKeyDown:
+ case eMouseUp:
+ case eMouseDown:
+ case eMouseDoubleClick:
+ case eMouseClick:
+ case eContextMenu:
+ case eDrop:
+ txn->EndTransaction();
+ return;
+ default:
+ break;
+ }
+}
+
+void
+APZCTreeManager::TransformEventRefPoint(LayoutDeviceIntPoint* aRefPoint,
+ ScrollableLayerGuid* aOutTargetGuid)
+{
+ // Transform the aRefPoint.
+ // If the event hits an overscrolled APZC, instruct the caller to ignore it.
+ HitTestResult hitResult = HitNothing;
+ PixelCastJustification LDIsScreen = PixelCastJustification::LayoutDeviceIsScreenForUntransformedEvent;
+ ScreenIntPoint refPointAsScreen =
+ ViewAs<ScreenPixel>(*aRefPoint, LDIsScreen);
+ RefPtr<AsyncPanZoomController> apzc = GetTargetAPZC(refPointAsScreen, &hitResult);
+ if (apzc) {
+ MOZ_ASSERT(hitResult != HitNothing);
+ apzc->GetGuid(aOutTargetGuid);
+ ScreenToParentLayerMatrix4x4 transformToApzc = GetScreenToApzcTransform(apzc);
+ ParentLayerToScreenMatrix4x4 transformToGecko = GetApzcToGeckoTransform(apzc);
+ ScreenToScreenMatrix4x4 outTransform = transformToApzc * transformToGecko;
+ Maybe<ScreenIntPoint> untransformedRefPoint =
+ UntransformBy(outTransform, refPointAsScreen);
+ if (untransformedRefPoint) {
+ *aRefPoint =
+ ViewAs<LayoutDevicePixel>(*untransformedRefPoint, LDIsScreen);
+ }
+ }
+}
+
+void
+APZCTreeManager::ProcessTouchVelocity(uint32_t aTimestampMs, float aSpeedY)
+{
+ if (mApzcForInputBlock) {
+ mApzcForInputBlock->HandleTouchVelocity(aTimestampMs, aSpeedY);
+ }
+}
+
+void
+APZCTreeManager::ZoomToRect(const ScrollableLayerGuid& aGuid,
+ const CSSRect& aRect,
+ const uint32_t aFlags)
+{
+ RefPtr<AsyncPanZoomController> apzc = GetTargetAPZC(aGuid);
+ if (apzc) {
+ apzc->ZoomToRect(aRect, aFlags);
+ }
+}
+
+void
+APZCTreeManager::ContentReceivedInputBlock(uint64_t aInputBlockId, bool aPreventDefault)
+{
+ APZThreadUtils::AssertOnControllerThread();
+
+ mInputQueue->ContentReceivedInputBlock(aInputBlockId, aPreventDefault);
+}
+
+void
+APZCTreeManager::SetTargetAPZC(uint64_t aInputBlockId,
+ const nsTArray<ScrollableLayerGuid>& aTargets)
+{
+ APZThreadUtils::AssertOnControllerThread();
+
+ RefPtr<AsyncPanZoomController> target = nullptr;
+ if (aTargets.Length() > 0) {
+ target = GetTargetAPZC(aTargets[0]);
+ }
+ for (size_t i = 1; i < aTargets.Length(); i++) {
+ RefPtr<AsyncPanZoomController> apzc = GetTargetAPZC(aTargets[i]);
+ target = GetMultitouchTarget(target, apzc);
+ }
+ mInputQueue->SetConfirmedTargetApzc(aInputBlockId, target);
+}
+
+void
+APZCTreeManager::SetTargetAPZC(uint64_t aInputBlockId, const ScrollableLayerGuid& aTarget)
+{
+ APZThreadUtils::AssertOnControllerThread();
+
+ RefPtr<AsyncPanZoomController> apzc = GetTargetAPZC(aTarget);
+ mInputQueue->SetConfirmedTargetApzc(aInputBlockId, apzc);
+}
+
+void
+APZCTreeManager::UpdateZoomConstraints(const ScrollableLayerGuid& aGuid,
+ const Maybe<ZoomConstraints>& aConstraints)
+{
+ MutexAutoLock lock(mTreeLock);
+ RefPtr<HitTestingTreeNode> node = GetTargetNode(aGuid, nullptr);
+ MOZ_ASSERT(!node || node->GetApzc()); // any node returned must have an APZC
+
+ // Propagate the zoom constraints down to the subtree, stopping at APZCs
+ // which have their own zoom constraints or are in a different layers id.
+ if (aConstraints) {
+ APZCTM_LOG("Recording constraints %s for guid %s\n",
+ Stringify(aConstraints.value()).c_str(), Stringify(aGuid).c_str());
+ mZoomConstraints[aGuid] = aConstraints.ref();
+ } else {
+ APZCTM_LOG("Removing constraints for guid %s\n", Stringify(aGuid).c_str());
+ mZoomConstraints.erase(aGuid);
+ }
+ if (node && aConstraints) {
+ ForEachNode<ReverseIterator>(node.get(),
+ [&aConstraints, &node, this](HitTestingTreeNode* aNode)
+ {
+ if (aNode != node) {
+ if (AsyncPanZoomController* childApzc = aNode->GetApzc()) {
+ // We can have subtrees with their own zoom constraints or separate layers
+ // id - leave these alone.
+ if (childApzc->HasNoParentWithSameLayersId() ||
+ this->mZoomConstraints.find(childApzc->GetGuid()) != this->mZoomConstraints.end()) {
+ return TraversalFlag::Skip;
+ }
+ }
+ }
+ if (aNode->IsPrimaryHolder()) {
+ MOZ_ASSERT(aNode->GetApzc());
+ aNode->GetApzc()->UpdateZoomConstraints(aConstraints.ref());
+ }
+ return TraversalFlag::Continue;
+ });
+ }
+}
+
+void
+APZCTreeManager::FlushRepaintsToClearScreenToGeckoTransform()
+{
+ // As the name implies, we flush repaint requests for the entire APZ tree in
+ // order to clear the screen-to-gecko transform (aka the "untransform" applied
+ // to incoming input events before they can be passed on to Gecko).
+ //
+ // The primary reason we do this is to avoid the problem where input events,
+ // after being untransformed, end up hit-testing differently in Gecko. This
+ // might happen in cases where the input event lands on content that is async-
+ // scrolled into view, but Gecko still thinks it is out of view given the
+ // visible area of a scrollframe.
+ //
+ // Another reason we want to clear the untransform is that if our APZ hit-test
+ // hits a dispatch-to-content region then that's an ambiguous result and we
+ // need to ask Gecko what actually got hit. In order to do this we need to
+ // untransform the input event into Gecko space - but to do that we need to
+ // know which APZC got hit! This leads to a circular dependency; the only way
+ // to get out of it is to make sure that the untransform for all the possible
+ // matched APZCs is the same. It is simplest to ensure that by flushing the
+ // pending repaint requests, which makes all of the untransforms empty (and
+ // therefore equal).
+ MutexAutoLock lock(mTreeLock);
+ mTreeLock.AssertCurrentThreadOwns();
+
+ ForEachNode<ReverseIterator>(mRootNode.get(),
+ [](HitTestingTreeNode* aNode)
+ {
+ if (aNode->IsPrimaryHolder()) {
+ MOZ_ASSERT(aNode->GetApzc());
+ aNode->GetApzc()->FlushRepaintForNewInputBlock();
+ }
+ });
+}
+
+void
+APZCTreeManager::CancelAnimation(const ScrollableLayerGuid &aGuid)
+{
+ RefPtr<AsyncPanZoomController> apzc = GetTargetAPZC(aGuid);
+ if (apzc) {
+ apzc->CancelAnimation();
+ }
+}
+
+void
+APZCTreeManager::AdjustScrollForSurfaceShift(const ScreenPoint& aShift)
+{
+ MutexAutoLock lock(mTreeLock);
+ RefPtr<AsyncPanZoomController> apzc = FindRootContentOrRootApzc();
+ if (apzc) {
+ apzc->AdjustScrollForSurfaceShift(aShift);
+ }
+}
+
+void
+APZCTreeManager::ClearTree()
+{
+ // Ensure that no references to APZCs are alive in any lingering input
+ // blocks. This breaks cycles from InputBlockState::mTargetApzc back to
+ // the InputQueue.
+ APZThreadUtils::RunOnControllerThread(NewRunnableMethod(mInputQueue, &InputQueue::Clear));
+
+ MutexAutoLock lock(mTreeLock);
+
+ // Collect the nodes into a list, and then destroy each one.
+ // We can't destroy them as we collect them, because ForEachNode()
+ // does a pre-order traversal of the tree, and Destroy() nulls out
+ // the fields needed to reach the children of the node.
+ nsTArray<RefPtr<HitTestingTreeNode>> nodesToDestroy;
+ ForEachNode<ReverseIterator>(mRootNode.get(),
+ [&nodesToDestroy](HitTestingTreeNode* aNode)
+ {
+ nodesToDestroy.AppendElement(aNode);
+ });
+
+ for (size_t i = 0; i < nodesToDestroy.Length(); i++) {
+ nodesToDestroy[i]->Destroy();
+ }
+ mRootNode = nullptr;
+
+ RefPtr<APZCTreeManager> self(this);
+ NS_DispatchToMainThread(NS_NewRunnableFunction([self] {
+ self->mFlushObserver->Unregister();
+ self->mFlushObserver = nullptr;
+ }));
+}
+
+RefPtr<HitTestingTreeNode>
+APZCTreeManager::GetRootNode() const
+{
+ MutexAutoLock lock(mTreeLock);
+ return mRootNode;
+}
+
+/**
+ * Transform a displacement from the ParentLayer coordinates of a source APZC
+ * to the ParentLayer coordinates of a target APZC.
+ * @param aTreeManager the tree manager for the APZC tree containing |aSource|
+ * and |aTarget|
+ * @param aSource the source APZC
+ * @param aTarget the target APZC
+ * @param aStartPoint the start point of the displacement
+ * @param aEndPoint the end point of the displacement
+ * @return true on success, false if aStartPoint or aEndPoint cannot be transformed into target's coordinate space
+ */
+static bool
+TransformDisplacement(APZCTreeManager* aTreeManager,
+ AsyncPanZoomController* aSource,
+ AsyncPanZoomController* aTarget,
+ ParentLayerPoint& aStartPoint,
+ ParentLayerPoint& aEndPoint) {
+ if (aSource == aTarget) {
+ return true;
+ }
+
+ // Convert start and end points to Screen coordinates.
+ ParentLayerToScreenMatrix4x4 untransformToApzc = aTreeManager->GetScreenToApzcTransform(aSource).Inverse();
+ ScreenPoint screenStart = TransformBy(untransformToApzc, aStartPoint);
+ ScreenPoint screenEnd = TransformBy(untransformToApzc, aEndPoint);
+
+ // Convert start and end points to aTarget's ParentLayer coordinates.
+ ScreenToParentLayerMatrix4x4 transformToApzc = aTreeManager->GetScreenToApzcTransform(aTarget);
+ Maybe<ParentLayerPoint> startPoint = UntransformBy(transformToApzc, screenStart);
+ Maybe<ParentLayerPoint> endPoint = UntransformBy(transformToApzc, screenEnd);
+ if (!startPoint || !endPoint) {
+ return false;
+ }
+ aEndPoint = *endPoint;
+ aStartPoint = *startPoint;
+
+ return true;
+}
+
+void
+APZCTreeManager::DispatchScroll(AsyncPanZoomController* aPrev,
+ ParentLayerPoint& aStartPoint,
+ ParentLayerPoint& aEndPoint,
+ OverscrollHandoffState& aOverscrollHandoffState)
+{
+ const OverscrollHandoffChain& overscrollHandoffChain = aOverscrollHandoffState.mChain;
+ uint32_t overscrollHandoffChainIndex = aOverscrollHandoffState.mChainIndex;
+ RefPtr<AsyncPanZoomController> next;
+ // If we have reached the end of the overscroll handoff chain, there is
+ // nothing more to scroll, so we ignore the rest of the pan gesture.
+ if (overscrollHandoffChainIndex >= overscrollHandoffChain.Length()) {
+ // Nothing more to scroll - ignore the rest of the pan gesture.
+ return;
+ }
+
+ next = overscrollHandoffChain.GetApzcAtIndex(overscrollHandoffChainIndex);
+
+ if (next == nullptr || next->IsDestroyed()) {
+ return;
+ }
+
+ // Convert the start and end points from |aPrev|'s coordinate space to
+ // |next|'s coordinate space.
+ if (!TransformDisplacement(this, aPrev, next, aStartPoint, aEndPoint)) {
+ return;
+ }
+
+ // Scroll |next|. If this causes overscroll, it will call DispatchScroll()
+ // again with an incremented index.
+ if (!next->AttemptScroll(aStartPoint, aEndPoint, aOverscrollHandoffState)) {
+ // Transform |aStartPoint| and |aEndPoint| (which now represent the
+ // portion of the displacement that wasn't consumed by APZCs later
+ // in the handoff chain) back into |aPrev|'s coordinate space. This
+ // allows the caller (which is |aPrev|) to interpret the unconsumed
+ // displacement in its own coordinate space, and make use of it
+ // (e.g. by going into overscroll).
+ if (!TransformDisplacement(this, next, aPrev, aStartPoint, aEndPoint)) {
+ NS_WARNING("Failed to untransform scroll points during dispatch");
+ }
+ }
+}
+
+void
+APZCTreeManager::DispatchFling(AsyncPanZoomController* aPrev,
+ FlingHandoffState& aHandoffState)
+{
+ // If immediate handoff is disallowed, do not allow handoff beyond the
+ // single APZC that's scrolled by the input block that triggered this fling.
+ if (aHandoffState.mIsHandoff &&
+ !gfxPrefs::APZAllowImmediateHandoff() &&
+ aHandoffState.mScrolledApzc == aPrev) {
+ return;
+ }
+
+ const OverscrollHandoffChain* chain = aHandoffState.mChain;
+ RefPtr<AsyncPanZoomController> current;
+ uint32_t overscrollHandoffChainLength = chain->Length();
+ uint32_t startIndex;
+
+ // This will store any velocity left over after the entire handoff.
+ ParentLayerPoint finalResidualVelocity = aHandoffState.mVelocity;
+
+ // The fling's velocity needs to be transformed from the screen coordinates
+ // of |aPrev| to the screen coordinates of |next|. To transform a velocity
+ // correctly, we need to convert it to a displacement. For now, we do this
+ // by anchoring it to a start point of (0, 0).
+ // TODO: For this to be correct in the presence of 3D transforms, we should
+ // use the end point of the touch that started the fling as the start point
+ // rather than (0, 0).
+ ParentLayerPoint startPoint; // (0, 0)
+ ParentLayerPoint endPoint;
+
+ if (aHandoffState.mIsHandoff) {
+ startIndex = chain->IndexOf(aPrev) + 1;
+
+ // IndexOf will return aOverscrollHandoffChain->Length() if
+ // |aPrev| is not found.
+ if (startIndex >= overscrollHandoffChainLength) {
+ return;
+ }
+ } else {
+ startIndex = 0;
+ }
+
+ for (; startIndex < overscrollHandoffChainLength; startIndex++) {
+ current = chain->GetApzcAtIndex(startIndex);
+
+ // Make sure the apcz about to be handled can be handled
+ if (current == nullptr || current->IsDestroyed()) {
+ return;
+ }
+
+ endPoint = startPoint + aHandoffState.mVelocity;
+
+ // Only transform when current apcz can be transformed with previous
+ if (startIndex > 0) {
+ if (!TransformDisplacement(this,
+ chain->GetApzcAtIndex(startIndex - 1),
+ current,
+ startPoint,
+ endPoint)) {
+ return;
+ }
+ }
+
+ ParentLayerPoint transformedVelocity = endPoint - startPoint;
+ aHandoffState.mVelocity = transformedVelocity;
+
+ if (current->AttemptFling(aHandoffState)) {
+ // Coming out of AttemptFling(), the handoff state's velocity is the
+ // residual velocity after attempting to fling |current|.
+ ParentLayerPoint residualVelocity = aHandoffState.mVelocity;
+
+ // If there's no residual velocity, there's nothing more to hand off.
+ if (IsZero(residualVelocity)) {
+ finalResidualVelocity = ParentLayerPoint();
+ break;
+ }
+
+ // If there is residual velocity, subtract the proportion of used
+ // velocity from finalResidualVelocity and continue handoff along the
+ // chain.
+ if (!FuzzyEqualsAdditive(transformedVelocity.x,
+ residualVelocity.x, COORDINATE_EPSILON)) {
+ finalResidualVelocity.x *= (residualVelocity.x / transformedVelocity.x);
+ }
+ if (!FuzzyEqualsAdditive(transformedVelocity.y,
+ residualVelocity.y, COORDINATE_EPSILON)) {
+ finalResidualVelocity.y *= (residualVelocity.y / transformedVelocity.y);
+ }
+ }
+ }
+
+ // Set the handoff state's velocity to any residual velocity left over
+ // after the entire handoff process.
+ aHandoffState.mVelocity = finalResidualVelocity;
+}
+
+bool
+APZCTreeManager::HitTestAPZC(const ScreenIntPoint& aPoint)
+{
+ RefPtr<AsyncPanZoomController> target = GetTargetAPZC(aPoint, nullptr);
+ return target != nullptr;
+}
+
+already_AddRefed<AsyncPanZoomController>
+APZCTreeManager::GetTargetAPZC(const ScrollableLayerGuid& aGuid)
+{
+ MutexAutoLock lock(mTreeLock);
+ RefPtr<HitTestingTreeNode> node = GetTargetNode(aGuid, nullptr);
+ MOZ_ASSERT(!node || node->GetApzc()); // any node returned must have an APZC
+ RefPtr<AsyncPanZoomController> apzc = node ? node->GetApzc() : nullptr;
+ return apzc.forget();
+}
+
+already_AddRefed<HitTestingTreeNode>
+APZCTreeManager::GetTargetNode(const ScrollableLayerGuid& aGuid,
+ GuidComparator aComparator)
+{
+ mTreeLock.AssertCurrentThreadOwns();
+ RefPtr<HitTestingTreeNode> target = DepthFirstSearchPostOrder<ReverseIterator>(mRootNode.get(),
+ [&aGuid, &aComparator](HitTestingTreeNode* node)
+ {
+ bool matches = false;
+ if (node->GetApzc()) {
+ if (aComparator) {
+ matches = aComparator(aGuid, node->GetApzc()->GetGuid());
+ } else {
+ matches = node->GetApzc()->Matches(aGuid);
+ }
+ }
+ return matches;
+ }
+ );
+ return target.forget();
+}
+
+already_AddRefed<AsyncPanZoomController>
+APZCTreeManager::GetTargetAPZC(const ScreenPoint& aPoint,
+ HitTestResult* aOutHitResult,
+ bool* aOutHitScrollbar)
+{
+ MutexAutoLock lock(mTreeLock);
+ HitTestResult hitResult = HitNothing;
+ ParentLayerPoint point = ViewAs<ParentLayerPixel>(aPoint,
+ PixelCastJustification::ScreenIsParentLayerForRoot);
+ RefPtr<AsyncPanZoomController> target = GetAPZCAtPoint(mRootNode, point,
+ &hitResult, aOutHitScrollbar);
+
+ if (aOutHitResult) {
+ *aOutHitResult = hitResult;
+ }
+ return target.forget();
+}
+
+static bool
+GuidComparatorIgnoringPresShell(const ScrollableLayerGuid& aOne, const ScrollableLayerGuid& aTwo)
+{
+ return aOne.mLayersId == aTwo.mLayersId
+ && aOne.mScrollId == aTwo.mScrollId;
+}
+
+RefPtr<const OverscrollHandoffChain>
+APZCTreeManager::BuildOverscrollHandoffChain(const RefPtr<AsyncPanZoomController>& aInitialTarget)
+{
+ // Scroll grabbing is a mechanism that allows content to specify that
+ // the initial target of a pan should be not the innermost scrollable
+ // frame at the touch point (which is what GetTargetAPZC finds), but
+ // something higher up in the tree.
+ // It's not sufficient to just find the initial target, however, as
+ // overscroll can be handed off to another APZC. Without scroll grabbing,
+ // handoff just occurs from child to parent. With scroll grabbing, the
+ // handoff order can be different, so we build a chain of APZCs in the
+ // order in which scroll will be handed off to them.
+
+ // Grab tree lock since we'll be walking the APZC tree.
+ MutexAutoLock lock(mTreeLock);
+
+ // Build the chain. If there is a scroll parent link, we use that. This is
+ // needed to deal with scroll info layers, because they participate in handoff
+ // but do not follow the expected layer tree structure. If there are no
+ // scroll parent links we just walk up the tree to find the scroll parent.
+ OverscrollHandoffChain* result = new OverscrollHandoffChain;
+ AsyncPanZoomController* apzc = aInitialTarget;
+ while (apzc != nullptr) {
+ result->Add(apzc);
+
+ if (apzc->GetScrollHandoffParentId() == FrameMetrics::NULL_SCROLL_ID) {
+ if (!apzc->IsRootForLayersId()) {
+ // This probably indicates a bug or missed case in layout code
+ NS_WARNING("Found a non-root APZ with no handoff parent");
+ }
+ apzc = apzc->GetParent();
+ continue;
+ }
+
+ // Guard against a possible infinite-loop condition. If we hit this, the
+ // layout code that generates the handoff parents did something wrong.
+ MOZ_ASSERT(apzc->GetScrollHandoffParentId() != apzc->GetGuid().mScrollId);
+
+ // Find the AsyncPanZoomController instance with a matching layersId and
+ // the scroll id that matches apzc->GetScrollHandoffParentId().
+ // As an optimization, we start by walking up the APZC tree from 'apzc'
+ // until we reach the top of the layer subtree for this layers id.
+ AsyncPanZoomController* scrollParent = nullptr;
+ AsyncPanZoomController* parent = apzc;
+ while (!parent->HasNoParentWithSameLayersId()) {
+ parent = parent->GetParent();
+ // While walking up to find the root of the subtree, if we encounter the
+ // handoff parent, we don't actually need to do the search so we can
+ // just abort here.
+ if (parent->GetGuid().mScrollId == apzc->GetScrollHandoffParentId()) {
+ scrollParent = parent;
+ break;
+ }
+ }
+ // If that heuristic didn't turn up the scroll parent, do a full tree search.
+ if (!scrollParent) {
+ ScrollableLayerGuid guid(parent->GetGuid().mLayersId, 0, apzc->GetScrollHandoffParentId());
+ RefPtr<HitTestingTreeNode> node = GetTargetNode(guid, &GuidComparatorIgnoringPresShell);
+ MOZ_ASSERT(!node || node->GetApzc()); // any node returned must have an APZC
+ scrollParent = node ? node->GetApzc() : nullptr;
+ }
+ apzc = scrollParent;
+ }
+
+ // Now adjust the chain to account for scroll grabbing. Sorting is a bit
+ // of an overkill here, but scroll grabbing will likely be generalized
+ // to scroll priorities, so we might as well do it this way.
+ result->SortByScrollPriority();
+
+ // Print the overscroll chain for debugging.
+ for (uint32_t i = 0; i < result->Length(); ++i) {
+ APZCTM_LOG("OverscrollHandoffChain[%d] = %p\n", i, result->GetApzcAtIndex(i).get());
+ }
+
+ return result;
+}
+
+void
+APZCTreeManager::SetLongTapEnabled(bool aLongTapEnabled)
+{
+ APZThreadUtils::RunOnControllerThread(
+ NewRunnableFunction(GestureEventListener::SetLongTapEnabled, aLongTapEnabled));
+}
+
+RefPtr<HitTestingTreeNode>
+APZCTreeManager::FindScrollNode(const AsyncDragMetrics& aDragMetrics)
+{
+ MutexAutoLock lock(mTreeLock);
+
+ return DepthFirstSearch<ReverseIterator>(mRootNode.get(),
+ [&aDragMetrics](HitTestingTreeNode* aNode) {
+ return aNode->MatchesScrollDragMetrics(aDragMetrics);
+ });
+}
+
+AsyncPanZoomController*
+APZCTreeManager::GetTargetApzcForNode(HitTestingTreeNode* aNode)
+{
+ for (const HitTestingTreeNode* n = aNode;
+ n && n->GetLayersId() == aNode->GetLayersId();
+ n = n->GetParent()) {
+ if (n->GetApzc()) {
+ APZCTM_LOG("Found target %p using ancestor lookup\n", n->GetApzc());
+ return n->GetApzc();
+ }
+ if (n->GetFixedPosTarget() != FrameMetrics::NULL_SCROLL_ID) {
+ ScrollableLayerGuid guid(n->GetLayersId(), 0, n->GetFixedPosTarget());
+ RefPtr<HitTestingTreeNode> fpNode = GetTargetNode(guid, &GuidComparatorIgnoringPresShell);
+ APZCTM_LOG("Found target node %p using fixed-pos lookup on %" PRIu64 "\n", fpNode.get(), n->GetFixedPosTarget());
+ return fpNode ? fpNode->GetApzc() : nullptr;
+ }
+ }
+ return nullptr;
+}
+
+AsyncPanZoomController*
+APZCTreeManager::GetAPZCAtPoint(HitTestingTreeNode* aNode,
+ const ParentLayerPoint& aHitTestPoint,
+ HitTestResult* aOutHitResult,
+ bool* aOutHitScrollbar)
+{
+ mTreeLock.AssertCurrentThreadOwns();
+
+ // This walks the tree in depth-first, reverse order, so that it encounters
+ // APZCs front-to-back on the screen.
+ HitTestingTreeNode* resultNode;
+ HitTestingTreeNode* root = aNode;
+ std::stack<ParentLayerPoint> hitTestPoints;
+ hitTestPoints.push(aHitTestPoint);
+
+ ForEachNode<ReverseIterator>(root,
+ [&hitTestPoints](HitTestingTreeNode* aNode) {
+ if (aNode->IsOutsideClip(hitTestPoints.top())) {
+ // If the point being tested is outside the clip region for this node
+ // then we don't need to test against this node or any of its children.
+ // Just skip it and move on.
+ APZCTM_LOG("Point %f %f outside clip for node %p\n",
+ hitTestPoints.top().x, hitTestPoints.top().y, aNode);
+ return TraversalFlag::Skip;
+ }
+ // First check the subtree rooted at this node, because deeper nodes
+ // are more "in front".
+ Maybe<LayerPoint> hitTestPointForChildLayers = aNode->Untransform(hitTestPoints.top());
+ APZCTM_LOG("Transformed ParentLayer point %s to layer %s\n",
+ Stringify(hitTestPoints.top()).c_str(),
+ hitTestPointForChildLayers ? Stringify(hitTestPointForChildLayers.ref()).c_str() : "nil");
+ if (!hitTestPointForChildLayers) {
+ return TraversalFlag::Skip;
+ }
+ hitTestPoints.push(ViewAs<ParentLayerPixel>(hitTestPointForChildLayers.ref(),
+ PixelCastJustification::MovingDownToChildren));
+ return TraversalFlag::Continue;
+ },
+ [&resultNode, &hitTestPoints, &aOutHitResult](HitTestingTreeNode* aNode) {
+ hitTestPoints.pop();
+ HitTestResult hitResult = aNode->HitTest(hitTestPoints.top());
+ APZCTM_LOG("Testing ParentLayer point %s against node %p\n",
+ Stringify(hitTestPoints.top()).c_str(), aNode);
+ if (hitResult != HitTestResult::HitNothing) {
+ resultNode = aNode;
+ // If event regions are disabled, *aOutHitResult will be HitLayer
+ *aOutHitResult = hitResult;
+ return TraversalFlag::Abort;
+ }
+ return TraversalFlag::Continue;
+ }
+ );
+
+ if (*aOutHitResult != HitNothing) {
+ MOZ_ASSERT(resultNode);
+ if (aOutHitScrollbar) {
+ for (HitTestingTreeNode* n = resultNode; n; n = n->GetParent()) {
+ if (n->IsScrollbarNode()) {
+ *aOutHitScrollbar = true;
+ }
+ }
+ }
+
+ AsyncPanZoomController* result = GetTargetApzcForNode(resultNode);
+ if (!result) {
+ result = FindRootApzcForLayersId(resultNode->GetLayersId());
+ MOZ_ASSERT(result);
+ APZCTM_LOG("Found target %p using root lookup\n", result);
+ }
+ APZCTM_LOG("Successfully matched APZC %p via node %p (hit result %d)\n",
+ result, resultNode, *aOutHitResult);
+ return result;
+ }
+
+ return nullptr;
+}
+
+AsyncPanZoomController*
+APZCTreeManager::FindRootApzcForLayersId(uint64_t aLayersId) const
+{
+ mTreeLock.AssertCurrentThreadOwns();
+
+ HitTestingTreeNode* resultNode = BreadthFirstSearch<ReverseIterator>(mRootNode.get(),
+ [aLayersId](HitTestingTreeNode* aNode) {
+ AsyncPanZoomController* apzc = aNode->GetApzc();
+ return apzc
+ && apzc->GetLayersId() == aLayersId
+ && apzc->IsRootForLayersId();
+ });
+ return resultNode ? resultNode->GetApzc() : nullptr;
+}
+
+AsyncPanZoomController*
+APZCTreeManager::FindRootContentApzcForLayersId(uint64_t aLayersId) const
+{
+ mTreeLock.AssertCurrentThreadOwns();
+
+ HitTestingTreeNode* resultNode = BreadthFirstSearch<ReverseIterator>(mRootNode.get(),
+ [aLayersId](HitTestingTreeNode* aNode) {
+ AsyncPanZoomController* apzc = aNode->GetApzc();
+ return apzc
+ && apzc->GetLayersId() == aLayersId
+ && apzc->IsRootContent();
+ });
+ return resultNode ? resultNode->GetApzc() : nullptr;
+}
+
+AsyncPanZoomController*
+APZCTreeManager::FindRootContentOrRootApzc() const
+{
+ mTreeLock.AssertCurrentThreadOwns();
+
+ // Note: this is intended to find the same "root" that would be found
+ // by AsyncCompositionManager::ApplyAsyncContentTransformToTree inside
+ // the MOZ_WIDGET_ANDROID block. That is, it should find the RCD node if there
+ // is one, or the root APZC if there is not.
+ // Since BreadthFirstSearch is a pre-order search, we first do a search for
+ // the RCD, and then if we don't find one, we do a search for the root APZC.
+ HitTestingTreeNode* resultNode = BreadthFirstSearch<ReverseIterator>(mRootNode.get(),
+ [](HitTestingTreeNode* aNode) {
+ AsyncPanZoomController* apzc = aNode->GetApzc();
+ return apzc && apzc->IsRootContent();
+ });
+ if (resultNode) {
+ return resultNode->GetApzc();
+ }
+ resultNode = BreadthFirstSearch<ReverseIterator>(mRootNode.get(),
+ [](HitTestingTreeNode* aNode) {
+ AsyncPanZoomController* apzc = aNode->GetApzc();
+ return (apzc != nullptr);
+ });
+ return resultNode ? resultNode->GetApzc() : nullptr;
+}
+
+/* The methods GetScreenToApzcTransform() and GetApzcToGeckoTransform() return
+ some useful transformations that input events may need applied. This is best
+ illustrated with an example. Consider a chain of layers, L, M, N, O, P, Q, R. Layer L
+ is the layer that corresponds to the argument |aApzc|, and layer R is the root
+ of the layer tree. Layer M is the parent of L, N is the parent of M, and so on.
+ When layer L is displayed to the screen by the compositor, the set of transforms that
+ are applied to L are (in order from top to bottom):
+
+ L's CSS transform (hereafter referred to as transform matrix LC)
+ L's nontransient async transform (hereafter referred to as transform matrix LN)
+ L's transient async transform (hereafter referred to as transform matrix LT)
+ M's CSS transform (hereafter referred to as transform matrix MC)
+ M's nontransient async transform (hereafter referred to as transform matrix MN)
+ M's transient async transform (hereafter referred to as transform matrix MT)
+ ...
+ R's CSS transform (hereafter referred to as transform matrix RC)
+ R's nontransient async transform (hereafter referred to as transform matrix RN)
+ R's transient async transform (hereafter referred to as transform matrix RT)
+
+ Also, for any layer, the async transform is the combination of its transient and non-transient
+ parts. That is, for any layer L:
+ LA === LN * LT
+ LA.Inverse() === LT.Inverse() * LN.Inverse()
+
+ If we want user input to modify L's transient async transform, we have to first convert
+ user input from screen space to the coordinate space of L's transient async transform. Doing
+ this involves applying the following transforms (in order from top to bottom):
+ RT.Inverse()
+ RN.Inverse()
+ RC.Inverse()
+ ...
+ MT.Inverse()
+ MN.Inverse()
+ MC.Inverse()
+ This combined transformation is returned by GetScreenToApzcTransform().
+
+ Next, if we want user inputs sent to gecko for event-dispatching, we will need to strip
+ out all of the async transforms that are involved in this chain. This is because async
+ transforms are stored only in the compositor and gecko does not account for them when
+ doing display-list-based hit-testing for event dispatching.
+ Furthermore, because these input events are processed by Gecko in a FIFO queue that
+ includes other things (specifically paint requests), it is possible that by time the
+ input event reaches gecko, it will have painted something else. Therefore, we need to
+ apply another transform to the input events to account for the possible disparity between
+ what we know gecko last painted and the last paint request we sent to gecko. Let this
+ transform be represented by LD, MD, ... RD.
+ Therefore, given a user input in screen space, the following transforms need to be applied
+ (in order from top to bottom):
+ RT.Inverse()
+ RN.Inverse()
+ RC.Inverse()
+ ...
+ MT.Inverse()
+ MN.Inverse()
+ MC.Inverse()
+ LT.Inverse()
+ LN.Inverse()
+ LC.Inverse()
+ LC
+ LD
+ MC
+ MD
+ ...
+ RC
+ RD
+ This sequence can be simplified and refactored to the following:
+ GetScreenToApzcTransform()
+ LA.Inverse()
+ LD
+ MC
+ MD
+ ...
+ RC
+ RD
+ Since GetScreenToApzcTransform() can be obtained by calling that function, GetApzcToGeckoTransform()
+ returns the remaining transforms (LA.Inverse() * LD * ... * RD), so that the caller code can
+ combine it with GetScreenToApzcTransform() to get the final transform required in this case.
+
+ Note that for many of these layers, there will be no AsyncPanZoomController attached, and
+ so the async transform will be the identity transform. So, in the example above, if layers
+ L and P have APZC instances attached, MT, MN, MD, NT, NN, ND, OT, ON, OD, QT, QN, QD, RT,
+ RN and RD will be identity transforms.
+ Additionally, for space-saving purposes, each APZC instance stores its layer's individual
+ CSS transform and the accumulation of CSS transforms to its parent APZC. So the APZC for
+ layer L would store LC and (MC * NC * OC), and the layer P would store PC and (QC * RC).
+ The APZC instances track the last dispatched paint request and so are able to calculate LD and
+ PD using those internally stored values.
+ The APZCs also obviously have LT, LN, PT, and PN, so all of the above transformation combinations
+ required can be generated.
+ */
+
+/*
+ * See the long comment above for a detailed explanation of this function.
+ */
+ScreenToParentLayerMatrix4x4
+APZCTreeManager::GetScreenToApzcTransform(const AsyncPanZoomController *aApzc) const
+{
+ Matrix4x4 result;
+ MutexAutoLock lock(mTreeLock);
+
+ // The comments below assume there is a chain of layers L..R with L and P having APZC instances as
+ // explained in the comment above. This function is called with aApzc at L, and the loop
+ // below performs one iteration, where parent is at P. The comments explain what values are stored
+ // in the variables at these two levels. All the comments use standard matrix notation where the
+ // leftmost matrix in a multiplication is applied first.
+
+ // ancestorUntransform is PC.Inverse() * OC.Inverse() * NC.Inverse() * MC.Inverse()
+ Matrix4x4 ancestorUntransform = aApzc->GetAncestorTransform().Inverse();
+
+ // result is initialized to PC.Inverse() * OC.Inverse() * NC.Inverse() * MC.Inverse()
+ result = ancestorUntransform;
+
+ for (AsyncPanZoomController* parent = aApzc->GetParent(); parent; parent = parent->GetParent()) {
+ // ancestorUntransform is updated to RC.Inverse() * QC.Inverse() when parent == P
+ ancestorUntransform = parent->GetAncestorTransform().Inverse();
+ // asyncUntransform is updated to PA.Inverse() when parent == P
+ Matrix4x4 asyncUntransform = parent->GetCurrentAsyncTransformWithOverscroll(AsyncPanZoomController::NORMAL).Inverse().ToUnknownMatrix();
+ // untransformSinceLastApzc is RC.Inverse() * QC.Inverse() * PA.Inverse()
+ Matrix4x4 untransformSinceLastApzc = ancestorUntransform * asyncUntransform;
+
+ // result is RC.Inverse() * QC.Inverse() * PA.Inverse() * PC.Inverse() * OC.Inverse() * NC.Inverse() * MC.Inverse()
+ result = untransformSinceLastApzc * result;
+
+ // The above value for result when parent == P matches the required output
+ // as explained in the comment above this method. Note that any missing
+ // terms are guaranteed to be identity transforms.
+ }
+
+ return ViewAs<ScreenToParentLayerMatrix4x4>(result);
+}
+
+/*
+ * See the long comment above GetScreenToApzcTransform() for a detailed
+ * explanation of this function.
+ */
+ParentLayerToScreenMatrix4x4
+APZCTreeManager::GetApzcToGeckoTransform(const AsyncPanZoomController *aApzc) const
+{
+ Matrix4x4 result;
+ MutexAutoLock lock(mTreeLock);
+
+ // The comments below assume there is a chain of layers L..R with L and P having APZC instances as
+ // explained in the comment above. This function is called with aApzc at L, and the loop
+ // below performs one iteration, where parent is at P. The comments explain what values are stored
+ // in the variables at these two levels. All the comments use standard matrix notation where the
+ // leftmost matrix in a multiplication is applied first.
+
+ // asyncUntransform is LA.Inverse()
+ Matrix4x4 asyncUntransform = aApzc->GetCurrentAsyncTransformWithOverscroll(AsyncPanZoomController::NORMAL).Inverse().ToUnknownMatrix();
+
+ // aTransformToGeckoOut is initialized to LA.Inverse() * LD * MC * NC * OC * PC
+ result = asyncUntransform * aApzc->GetTransformToLastDispatchedPaint() * aApzc->GetAncestorTransform();
+
+ for (AsyncPanZoomController* parent = aApzc->GetParent(); parent; parent = parent->GetParent()) {
+ // aTransformToGeckoOut is LA.Inverse() * LD * MC * NC * OC * PC * PD * QC * RC
+ result = result * parent->GetTransformToLastDispatchedPaint() * parent->GetAncestorTransform();
+
+ // The above value for result when parent == P matches the required output
+ // as explained in the comment above this method. Note that any missing
+ // terms are guaranteed to be identity transforms.
+ }
+
+ return ViewAs<ParentLayerToScreenMatrix4x4>(result);
+}
+
+already_AddRefed<AsyncPanZoomController>
+APZCTreeManager::GetMultitouchTarget(AsyncPanZoomController* aApzc1, AsyncPanZoomController* aApzc2) const
+{
+ MutexAutoLock lock(mTreeLock);
+ RefPtr<AsyncPanZoomController> apzc;
+ // For now, we only ever want to do pinching on the root-content APZC for
+ // a given layers id.
+ if (aApzc1 && aApzc2 && aApzc1->GetLayersId() == aApzc2->GetLayersId()) {
+ // If the two APZCs have the same layers id, find the root-content APZC
+ // for that layers id. Don't call CommonAncestor() because there may not
+ // be a common ancestor for the layers id (e.g. if one APZCs is inside a
+ // fixed-position element).
+ apzc = FindRootContentApzcForLayersId(aApzc1->GetLayersId());
+ } else {
+ // Otherwise, find the common ancestor (to reach a common layers id), and
+ // get the root-content APZC for that layers id.
+ apzc = CommonAncestor(aApzc1, aApzc2);
+ if (apzc) {
+ apzc = FindRootContentApzcForLayersId(apzc->GetLayersId());
+ }
+ }
+ return apzc.forget();
+}
+
+already_AddRefed<AsyncPanZoomController>
+APZCTreeManager::CommonAncestor(AsyncPanZoomController* aApzc1, AsyncPanZoomController* aApzc2) const
+{
+ mTreeLock.AssertCurrentThreadOwns();
+ RefPtr<AsyncPanZoomController> ancestor;
+
+ // If either aApzc1 or aApzc2 is null, min(depth1, depth2) will be 0 and this function
+ // will return null.
+
+ // Calculate depth of the APZCs in the tree
+ int depth1 = 0, depth2 = 0;
+ for (AsyncPanZoomController* parent = aApzc1; parent; parent = parent->GetParent()) {
+ depth1++;
+ }
+ for (AsyncPanZoomController* parent = aApzc2; parent; parent = parent->GetParent()) {
+ depth2++;
+ }
+
+ // At most one of the following two loops will be executed; the deeper APZC pointer
+ // will get walked up to the depth of the shallower one.
+ int minDepth = depth1 < depth2 ? depth1 : depth2;
+ while (depth1 > minDepth) {
+ depth1--;
+ aApzc1 = aApzc1->GetParent();
+ }
+ while (depth2 > minDepth) {
+ depth2--;
+ aApzc2 = aApzc2->GetParent();
+ }
+
+ // Walk up the ancestor chains of both APZCs, always staying at the same depth for
+ // either APZC, and return the the first common ancestor encountered.
+ while (true) {
+ if (aApzc1 == aApzc2) {
+ ancestor = aApzc1;
+ break;
+ }
+ if (depth1 <= 0) {
+ break;
+ }
+ aApzc1 = aApzc1->GetParent();
+ aApzc2 = aApzc2->GetParent();
+ }
+ return ancestor.forget();
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/apz/src/APZCTreeManager.h b/gfx/layers/apz/src/APZCTreeManager.h
new file mode 100644
index 000000000..c98e292ef
--- /dev/null
+++ b/gfx/layers/apz/src/APZCTreeManager.h
@@ -0,0 +1,531 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_layers_APZCTreeManager_h
+#define mozilla_layers_APZCTreeManager_h
+
+#include <map> // for std::map
+
+#include "gfxPoint.h" // for gfxPoint
+#include "mozilla/Assertions.h" // for MOZ_ASSERT_HELPER2
+#include "mozilla/gfx/Logging.h" // for gfx::TreeLog
+#include "mozilla/gfx/Matrix.h" // for Matrix4x4
+#include "mozilla/layers/TouchCounter.h"// for TouchCounter
+#include "mozilla/layers/IAPZCTreeManager.h" // for IAPZCTreeManager
+#include "mozilla/Mutex.h" // for Mutex
+#include "mozilla/RefPtr.h" // for RefPtr
+#include "mozilla/TimeStamp.h" // for mozilla::TimeStamp
+#include "nsCOMPtr.h" // for already_AddRefed
+
+
+namespace mozilla {
+class MultiTouchInput;
+
+namespace layers {
+
+class Layer;
+class AsyncPanZoomController;
+class APZCTreeManagerParent;
+class CompositorBridgeParent;
+class OverscrollHandoffChain;
+struct OverscrollHandoffState;
+struct FlingHandoffState;
+class LayerMetricsWrapper;
+class InputQueue;
+class GeckoContentController;
+class HitTestingTreeNode;
+
+/**
+ * ****************** NOTE ON LOCK ORDERING IN APZ **************************
+ *
+ * There are two kinds of locks used by APZ: APZCTreeManager::mTreeLock
+ * ("the tree lock") and AsyncPanZoomController::mMonitor ("APZC locks").
+ *
+ * To avoid deadlock, we impose a lock ordering between these locks, which is:
+ *
+ * tree lock -> APZC locks
+ *
+ * The interpretation of the lock ordering is that if lock A precedes lock B
+ * in the ordering sequence, then you must NOT wait on A while holding B.
+ *
+ * **************************************************************************
+ */
+
+/**
+ * This class manages the tree of AsyncPanZoomController instances. There is one
+ * instance of this class owned by each CompositorBridgeParent, and it contains as
+ * many AsyncPanZoomController instances as there are scrollable container layers.
+ * This class generally lives on the compositor thread, although some functions
+ * may be called from other threads as noted; thread safety is ensured internally.
+ *
+ * The bulk of the work of this class happens as part of the UpdateHitTestingTree
+ * function, which is when a layer tree update is received by the compositor.
+ * This function walks through the layer tree and creates a tree of
+ * HitTestingTreeNode instances to match the layer tree and for use in
+ * hit-testing on the controller thread. APZC instances may be preserved across
+ * calls to this function if the corresponding layers are still present in the layer
+ * tree.
+ *
+ * The other functions on this class are used by various pieces of client code to
+ * notify the APZC instances of events relevant to them. This includes, for example,
+ * user input events that drive panning and zooming, changes to the scroll viewport
+ * area, and changes to pan/zoom constraints.
+ *
+ * Note that the ClearTree function MUST be called when this class is no longer needed;
+ * see the method documentation for details.
+ *
+ * Behaviour of APZ is controlled by a number of preferences shown \ref APZCPrefs "here".
+ */
+class APZCTreeManager : public IAPZCTreeManager {
+
+ typedef mozilla::layers::AllowedTouchBehavior AllowedTouchBehavior;
+ typedef mozilla::layers::AsyncDragMetrics AsyncDragMetrics;
+
+ // Helper struct to hold some state while we build the hit-testing tree. The
+ // sole purpose of this struct is to shorten the argument list to
+ // UpdateHitTestingTree. All the state that we don't need to
+ // push on the stack during recursion and pop on unwind is stored here.
+ struct TreeBuildingState;
+
+public:
+ APZCTreeManager();
+
+ /**
+ * Initializes the global state used in AsyncPanZoomController.
+ * This is normally called when it is first needed in the constructor
+ * of APZCTreeManager, but can be called manually to force it to be
+ * initialized earlier.
+ */
+ static void InitializeGlobalState();
+
+ /**
+ * Rebuild the hit-testing tree based on the layer update that just came up.
+ * Preserve nodes and APZC instances where possible, but retire those whose
+ * layers are no longer in the layer tree.
+ *
+ * This must be called on the compositor thread as it walks the layer tree.
+ *
+ * @param aRootLayerTreeId The layer tree ID of the root layer corresponding
+ * to this APZCTreeManager
+ * @param aRoot The root of the (full) layer tree
+ * @param aFirstPaintLayersId The layers id of the subtree to which aIsFirstPaint
+ * applies.
+ * @param aIsFirstPaint True if the layers update that this is called in response
+ * to included a first-paint. If this is true, the part of
+ * the tree that is affected by the first-paint flag is
+ * indicated by the aFirstPaintLayersId parameter.
+ * @param aPaintSequenceNumber The sequence number of the paint that triggered
+ * this layer update. Note that every layer child
+ * process' layer subtree has its own sequence
+ * numbers.
+ */
+ void UpdateHitTestingTree(uint64_t aRootLayerTreeId,
+ Layer* aRoot,
+ bool aIsFirstPaint,
+ uint64_t aOriginatingLayersId,
+ uint32_t aPaintSequenceNumber);
+
+ /**
+ * Walk the tree of APZCs and flushes the repaint requests for all the APZCS
+ * corresponding to the given layers id. Finally, sends a flush complete
+ * notification to the GeckoContentController for the layers id.
+ */
+ void FlushApzRepaints(uint64_t aLayersId);
+
+ /**
+ * General handler for incoming input events. Manipulates the frame metrics
+ * based on what type of input it is. For example, a PinchGestureEvent will
+ * cause scaling. This should only be called externally to this class, and
+ * must be called on the controller thread.
+ *
+ * This function transforms |aEvent| to have its coordinates in DOM space.
+ * This is so that the event can be passed through the DOM and content can
+ * handle them. The event may need to be converted to a WidgetInputEvent
+ * by the caller if it wants to do this.
+ *
+ * The following values may be returned by this function:
+ * nsEventStatus_eConsumeNoDefault is returned to indicate the
+ * APZ is consuming this event and the caller should discard the event with
+ * extreme prejudice. The exact scenarios under which this is returned is
+ * implementation-dependent and may vary.
+ * nsEventStatus_eIgnore is returned to indicate that the APZ code didn't
+ * use this event. This might be because it was directed at a point on
+ * the screen where there was no APZ, or because the thing the user was
+ * trying to do was not allowed. (For example, attempting to pan a
+ * non-pannable document).
+ * nsEventStatus_eConsumeDoDefault is returned to indicate that the APZ
+ * code may have used this event to do some user-visible thing. Note that
+ * in some cases CONSUMED is returned even if the event was NOT used. This
+ * is because we cannot always know at the time of event delivery whether
+ * the event will be used or not. So we err on the side of sending
+ * CONSUMED when we are uncertain.
+ *
+ * @param aEvent input event object; is modified in-place
+ * @param aOutTargetGuid returns the guid of the apzc this event was
+ * delivered to. May be null.
+ * @param aOutInputBlockId returns the id of the input block that this event
+ * was added to, if that was the case. May be null.
+ */
+ nsEventStatus ReceiveInputEvent(
+ InputData& aEvent,
+ ScrollableLayerGuid* aOutTargetGuid,
+ uint64_t* aOutInputBlockId) override;
+
+ /**
+ * Kicks an animation to zoom to a rect. This may be either a zoom out or zoom
+ * in. The actual animation is done on the compositor thread after being set
+ * up. |aRect| must be given in CSS pixels, relative to the document.
+ * |aFlags| is a combination of the ZoomToRectBehavior enum values.
+ */
+ void ZoomToRect(
+ const ScrollableLayerGuid& aGuid,
+ const CSSRect& aRect,
+ const uint32_t aFlags = DEFAULT_BEHAVIOR) override;
+
+ /**
+ * If we have touch listeners, this should always be called when we know
+ * definitively whether or not content has preventDefaulted any touch events
+ * that have come in. If |aPreventDefault| is true, any touch events in the
+ * queue will be discarded. This function must be called on the controller
+ * thread.
+ */
+ void ContentReceivedInputBlock(
+ uint64_t aInputBlockId,
+ bool aPreventDefault) override;
+
+ /**
+ * When the event regions code is enabled, this function should be invoked to
+ * to confirm the target of the input block. This is only needed in cases
+ * where the initial input event of the block hit a dispatch-to-content region
+ * but is safe to call for all input blocks. This function should always be
+ * invoked on the controller thread.
+ * The different elements in the array of targets correspond to the targets
+ * for the different touch points. In the case where the touch point has no
+ * target, or the target is not a scrollable frame, the target's |mScrollId|
+ * should be set to FrameMetrics::NULL_SCROLL_ID.
+ */
+ void SetTargetAPZC(
+ uint64_t aInputBlockId,
+ const nsTArray<ScrollableLayerGuid>& aTargets) override;
+
+ /**
+ * Helper function for SetTargetAPZC when used with single-target events,
+ * such as mouse wheel events.
+ */
+ void SetTargetAPZC(uint64_t aInputBlockId, const ScrollableLayerGuid& aTarget);
+
+ /**
+ * Updates any zoom constraints contained in the <meta name="viewport"> tag.
+ * If the |aConstraints| is Nothing() then previously-provided constraints for
+ * the given |aGuid| are cleared.
+ */
+ void UpdateZoomConstraints(
+ const ScrollableLayerGuid& aGuid,
+ const Maybe<ZoomConstraints>& aConstraints) override;
+
+ /**
+ * Cancels any currently running animation. Note that all this does is set the
+ * state of the AsyncPanZoomController back to NOTHING, but it is the
+ * animation's responsibility to check this before advancing.
+ */
+ void CancelAnimation(const ScrollableLayerGuid &aGuid) override;
+
+ /**
+ * Adjusts the root APZC to compensate for a shift in the surface. See the
+ * documentation on AsyncPanZoomController::AdjustScrollForSurfaceShift for
+ * some more details. This is only currently needed due to surface shifts
+ * caused by the dynamic toolbar on Android.
+ */
+ void AdjustScrollForSurfaceShift(const ScreenPoint& aShift) override;
+
+ /**
+ * Calls Destroy() on all APZC instances attached to the tree, and resets the
+ * tree back to empty. This function must be called exactly once during the
+ * lifetime of this APZCTreeManager, when this APZCTreeManager is no longer
+ * needed. Failing to call this function may prevent objects from being freed
+ * properly.
+ */
+ void ClearTree();
+
+ /**
+ * Tests if a screen point intersect an apz in the tree.
+ */
+ bool HitTestAPZC(const ScreenIntPoint& aPoint);
+
+ /**
+ * See AsyncPanZoomController::CalculatePendingDisplayPort. This
+ * function simply delegates to that one, so that non-layers code
+ * never needs to include AsyncPanZoomController.h
+ */
+ static const ScreenMargin CalculatePendingDisplayPort(
+ const FrameMetrics& aFrameMetrics,
+ const ParentLayerPoint& aVelocity);
+
+ /**
+ * Sets the dpi value used by all AsyncPanZoomControllers.
+ * DPI defaults to 72 if not set using SetDPI() at any point.
+ */
+ void SetDPI(float aDpiValue) override { sDPI = aDpiValue; }
+
+ /**
+ * Returns the current dpi value in use.
+ */
+ static float GetDPI() { return sDPI; }
+
+ /**
+ * Find the hit testing node for the scrollbar thumb that matches these
+ * drag metrics.
+ */
+ RefPtr<HitTestingTreeNode> FindScrollNode(const AsyncDragMetrics& aDragMetrics);
+
+ /**
+ * Sets allowed touch behavior values for current touch-session for specific
+ * input block (determined by aInputBlock).
+ * Should be invoked by the widget. Each value of the aValues arrays
+ * corresponds to the different touch point that is currently active.
+ * Must be called after receiving the TOUCH_START event that starts the
+ * touch-session.
+ * This must be called on the controller thread.
+ */
+ void SetAllowedTouchBehavior(
+ uint64_t aInputBlockId,
+ const nsTArray<TouchBehaviorFlags>& aValues) override;
+
+ /**
+ * This is a callback for AsyncPanZoomController to call when it wants to
+ * scroll in response to a touch-move event, or when it needs to hand off
+ * overscroll to the next APZC. Note that because of scroll grabbing, the
+ * first APZC to scroll may not be the one that is receiving the touch events.
+ *
+ * |aAPZC| is the APZC that received the touch events triggering the scroll
+ * (in the case of an initial scroll), or the last APZC to scroll (in the
+ * case of overscroll)
+ * |aStartPoint| and |aEndPoint| are in |aAPZC|'s transformed screen
+ * coordinates (i.e. the same coordinates in which touch points are given to
+ * APZCs). The amount of (over)scroll is represented by two points rather
+ * than a displacement because with certain 3D transforms, the same
+ * displacement between different points in transformed coordinates can
+ * represent different displacements in untransformed coordinates.
+ * |aOverscrollHandoffChain| is the overscroll handoff chain used for
+ * determining the order in which scroll should be handed off between
+ * APZCs
+ * |aOverscrollHandoffChainIndex| is the next position in the overscroll
+ * handoff chain that should be scrolled.
+ *
+ * aStartPoint and aEndPoint will be modified depending on how much of the
+ * scroll each APZC consumes. This is to allow the sending APZC to go into
+ * an overscrolled state if no APZC further up in the handoff chain accepted
+ * the entire scroll.
+ *
+ * The way this method works is best illustrated with an example.
+ * Consider three nested APZCs, A, B, and C, with C being the innermost one.
+ * Say B is scroll-grabbing.
+ * The touch events go to C because it's the innermost one (so e.g. taps
+ * should go through C), but the overscroll handoff chain is B -> C -> A
+ * because B is scroll-grabbing.
+ * For convenience I'll refer to the three APZC objects as A, B, and C, and
+ * to the tree manager object as TM.
+ * Here's what happens when C receives a touch-move event:
+ * - C.TrackTouch() calls TM.DispatchScroll() with index = 0.
+ * - TM.DispatchScroll() calls B.AttemptScroll() (since B is at index 0 in the chain).
+ * - B.AttemptScroll() scrolls B. If there is overscroll, it calls TM.DispatchScroll() with index = 1.
+ * - TM.DispatchScroll() calls C.AttemptScroll() (since C is at index 1 in the chain)
+ * - C.AttemptScroll() scrolls C. If there is overscroll, it calls TM.DispatchScroll() with index = 2.
+ * - TM.DispatchScroll() calls A.AttemptScroll() (since A is at index 2 in the chain)
+ * - A.AttemptScroll() scrolls A. If there is overscroll, it calls TM.DispatchScroll() with index = 3.
+ * - TM.DispatchScroll() discards the rest of the scroll as there are no more elements in the chain.
+ *
+ * Note: this should be used for panning only. For handing off overscroll for
+ * a fling, use DispatchFling().
+ */
+ void DispatchScroll(AsyncPanZoomController* aApzc,
+ ParentLayerPoint& aStartPoint,
+ ParentLayerPoint& aEndPoint,
+ OverscrollHandoffState& aOverscrollHandoffState);
+
+ /**
+ * This is a callback for AsyncPanZoomController to call when it wants to
+ * start a fling in response to a touch-end event, or when it needs to hand
+ * off a fling to the next APZC. Note that because of scroll grabbing, the
+ * first APZC to fling may not be the one that is receiving the touch events.
+ *
+ * @param aApzc the APZC that wants to start or hand off the fling
+ * @param aHandoffState a collection of state about the operation,
+ * which contains the following:
+ *
+ * mVelocity the current velocity of the fling, in |aApzc|'s screen
+ * pixels per millisecond
+ * mChain the chain of APZCs along which the fling
+ * should be handed off
+ * mIsHandoff is true if |aApzc| is handing off an existing fling (in
+ * this case the fling is given to the next APZC in the
+ * handoff chain after |aApzc|), and false is |aApzc| wants
+ * start a fling (in this case the fling is given to the
+ * first APZC in the chain)
+ *
+ * aHandoffState.mVelocity will be modified depending on how much of that
+ * velocity has been consumed by APZCs in the overscroll hand-off chain.
+ * The caller can use this value to determine whether it should consume
+ * the excess velocity by going into an overscroll fling.
+ */
+ void DispatchFling(AsyncPanZoomController* aApzc,
+ FlingHandoffState& aHandoffState);
+
+ void StartScrollbarDrag(
+ const ScrollableLayerGuid& aGuid,
+ const AsyncDragMetrics& aDragMetrics) override;
+
+ /*
+ * Build the chain of APZCs that will handle overscroll for a pan starting at |aInitialTarget|.
+ */
+ RefPtr<const OverscrollHandoffChain> BuildOverscrollHandoffChain(const RefPtr<AsyncPanZoomController>& aInitialTarget);
+
+ /**
+ * Function used to disable LongTap gestures.
+ *
+ * On slow running tests, drags and touch events can be misinterpreted
+ * as a long tap. This allows tests to disable long tap gesture detection.
+ */
+ void SetLongTapEnabled(bool aTapGestureEnabled) override;
+
+ // Methods to help process WidgetInputEvents (or manage conversion to/from InputData)
+
+ void TransformEventRefPoint(
+ LayoutDeviceIntPoint* aRefPoint,
+ ScrollableLayerGuid* aOutTargetGuid) override;
+
+ void UpdateWheelTransaction(
+ LayoutDeviceIntPoint aRefPoint,
+ EventMessage aEventMessage) override;
+
+protected:
+ // Protected destructor, to discourage deletion outside of Release():
+ virtual ~APZCTreeManager();
+
+ // Protected hooks for gtests subclass
+ virtual AsyncPanZoomController* NewAPZCInstance(uint64_t aLayersId,
+ GeckoContentController* aController);
+public:
+ // Public hooks for gtests subclass
+ virtual TimeStamp GetFrameTime();
+
+public:
+ /* Some helper functions to find an APZC given some identifying input. These functions
+ lock the tree of APZCs while they find the right one, and then return an addref'd
+ pointer to it. This allows caller code to just use the target APZC without worrying
+ about it going away. These are public for testing code and generally should not be
+ used by other production code.
+ */
+ RefPtr<HitTestingTreeNode> GetRootNode() const;
+ already_AddRefed<AsyncPanZoomController> GetTargetAPZC(const ScreenPoint& aPoint,
+ HitTestResult* aOutHitResult,
+ bool* aOutHitScrollbar = nullptr);
+ ScreenToParentLayerMatrix4x4 GetScreenToApzcTransform(const AsyncPanZoomController *aApzc) const;
+ ParentLayerToScreenMatrix4x4 GetApzcToGeckoTransform(const AsyncPanZoomController *aApzc) const;
+
+ /**
+ * Process touch velocity.
+ * Sometimes the touch move event will have a velocity even though no scrolling
+ * is occurring such as when the toolbar is being hidden/shown in Fennec.
+ * This function can be called to have the y axis' velocity queue updated.
+ */
+ void ProcessTouchVelocity(uint32_t aTimestampMs, float aSpeedY) override;
+private:
+ typedef bool (*GuidComparator)(const ScrollableLayerGuid&, const ScrollableLayerGuid&);
+
+ /* Helpers */
+ void AttachNodeToTree(HitTestingTreeNode* aNode,
+ HitTestingTreeNode* aParent,
+ HitTestingTreeNode* aNextSibling);
+ already_AddRefed<AsyncPanZoomController> GetTargetAPZC(const ScrollableLayerGuid& aGuid);
+ already_AddRefed<HitTestingTreeNode> GetTargetNode(const ScrollableLayerGuid& aGuid,
+ GuidComparator aComparator);
+ HitTestingTreeNode* FindTargetNode(HitTestingTreeNode* aNode,
+ const ScrollableLayerGuid& aGuid,
+ GuidComparator aComparator);
+ AsyncPanZoomController* GetTargetApzcForNode(HitTestingTreeNode* aNode);
+ AsyncPanZoomController* GetAPZCAtPoint(HitTestingTreeNode* aNode,
+ const ParentLayerPoint& aHitTestPoint,
+ HitTestResult* aOutHitResult,
+ bool* aOutHitScrollbar);
+ 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;
+ already_AddRefed<AsyncPanZoomController> GetTouchInputBlockAPZC(const MultiTouchInput& aEvent,
+ nsTArray<TouchBehaviorFlags>* aOutTouchBehaviors,
+ HitTestResult* aOutHitResult);
+ nsEventStatus ProcessTouchInput(MultiTouchInput& aInput,
+ ScrollableLayerGuid* aOutTargetGuid,
+ uint64_t* aOutInputBlockId);
+ void FlushRepaintsToClearScreenToGeckoTransform();
+
+ already_AddRefed<HitTestingTreeNode> RecycleOrCreateNode(TreeBuildingState& aState,
+ AsyncPanZoomController* aApzc,
+ uint64_t aLayersId);
+ HitTestingTreeNode* PrepareNodeForLayer(const LayerMetricsWrapper& aLayer,
+ const FrameMetrics& aMetrics,
+ uint64_t aLayersId,
+ const gfx::Matrix4x4& aAncestorTransform,
+ HitTestingTreeNode* aParent,
+ HitTestingTreeNode* aNextSibling,
+ TreeBuildingState& aState);
+
+ void PrintAPZCInfo(const LayerMetricsWrapper& aLayer,
+ const AsyncPanZoomController* apzc);
+
+protected:
+ /* The input queue where input events are held until we know enough to
+ * figure out where they're going. Protected so gtests can access it.
+ */
+ RefPtr<InputQueue> mInputQueue;
+
+private:
+ /* Whenever walking or mutating the tree rooted at mRootNode, mTreeLock must be held.
+ * This lock does not need to be held while manipulating a single APZC instance in
+ * isolation (that is, if its tree pointers are not being accessed or mutated). The
+ * lock also needs to be held when accessing the mRootNode instance variable, as that
+ * is considered part of the APZC tree management state.
+ * Finally, the lock needs to be held when accessing mZoomConstraints.
+ * IMPORTANT: See the note about lock ordering at the top of this file. */
+ mutable mozilla::Mutex mTreeLock;
+ RefPtr<HitTestingTreeNode> mRootNode;
+ /* Holds the zoom constraints for scrollable layers, as determined by the
+ * the main-thread gecko code. */
+ std::map<ScrollableLayerGuid, ZoomConstraints> mZoomConstraints;
+ /* This tracks the APZC that should receive all inputs for the current input event block.
+ * This allows touch points to move outside the thing they started on, but still have the
+ * touch events delivered to the same initial APZC. This will only ever be touched on the
+ * input delivery thread, and so does not require locking.
+ */
+ RefPtr<AsyncPanZoomController> mApzcForInputBlock;
+ /* The hit result for the current input event block; this should always be in
+ * 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;
+ /* Tracks the number of touch points we are tracking that are currently on
+ * the screen. */
+ TouchCounter mTouchCounter;
+ /* For logging the APZC tree for debugging (enabled by the apz.printtree
+ * pref). */
+ gfx::TreeLog mApzcTreeLog;
+
+ class CheckerboardFlushObserver;
+ friend class CheckerboardFlushObserver;
+ RefPtr<CheckerboardFlushObserver> mFlushObserver;
+
+ static float sDPI;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif // mozilla_layers_PanZoomController_h
diff --git a/gfx/layers/apz/src/APZUtils.h b/gfx/layers/apz/src/APZUtils.h
new file mode 100644
index 000000000..222788afa
--- /dev/null
+++ b/gfx/layers/apz/src/APZUtils.h
@@ -0,0 +1,83 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set sw=2 ts=8 et tw=80 : */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#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/FloatingPoint.h"
+
+namespace mozilla {
+namespace layers {
+
+enum HitTestResult {
+ HitNothing,
+ HitLayer,
+ HitLayerTouchActionNone,
+ HitLayerTouchActionPanX,
+ HitLayerTouchActionPanY,
+ HitLayerTouchActionPanXY,
+ HitDispatchToContentRegion,
+};
+
+enum CancelAnimationFlags : uint32_t {
+ Default = 0x0, /* Cancel all animations */
+ ExcludeOverscroll = 0x1, /* Don't clear overscroll */
+ ScrollSnap = 0x2 /* Snap to snap points */
+};
+
+inline CancelAnimationFlags
+operator|(CancelAnimationFlags a, CancelAnimationFlags b)
+{
+ return static_cast<CancelAnimationFlags>(static_cast<int>(a)
+ | static_cast<int>(b));
+}
+
+enum class ScrollSource {
+ // scrollTo() or something similar.
+ DOM,
+
+ // Touch-screen or trackpad with gesture support.
+ Touch,
+
+ // Mouse wheel.
+ Wheel
+};
+
+typedef uint32_t TouchBehaviorFlags;
+
+// Epsilon to be used when comparing 'float' coordinate values
+// with FuzzyEqualsAdditive. The rationale is that 'float' has 7 decimal
+// digits of precision, and coordinate values should be no larger than in the
+// ten thousands. Note also that the smallest legitimate difference in page
+// coordinates is 1 app unit, which is 1/60 of a (CSS pixel), so this epsilon
+// isn't too large.
+const float COORDINATE_EPSILON = 0.01f;
+
+template <typename Units>
+static bool IsZero(const gfx::PointTyped<Units>& aPoint)
+{
+ return FuzzyEqualsAdditive(aPoint.x, 0.0f, COORDINATE_EPSILON)
+ && FuzzyEqualsAdditive(aPoint.y, 0.0f, COORDINATE_EPSILON);
+}
+
+// Deem an AsyncTransformComponentMatrix (obtained by multiplying together
+// one or more AsyncTransformComponentMatrix objects) as constituting a
+// complete async transform.
+inline AsyncTransformMatrix
+CompleteAsyncTransform(const AsyncTransformComponentMatrix& aMatrix)
+{
+ return ViewAs<AsyncTransformMatrix>(aMatrix,
+ PixelCastJustification::MultipleAsyncTransforms);
+}
+
+} // namespace layers
+} // namespace mozilla
+
+#endif // mozilla_layers_APZUtils_h
diff --git a/gfx/layers/apz/src/AndroidAPZ.cpp b/gfx/layers/apz/src/AndroidAPZ.cpp
new file mode 100644
index 000000000..70042a870
--- /dev/null
+++ b/gfx/layers/apz/src/AndroidAPZ.cpp
@@ -0,0 +1,274 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set sw=2 ts=8 et tw=80 : */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "AndroidAPZ.h"
+
+#include "AsyncPanZoomController.h"
+#include "GeneratedJNIWrappers.h"
+#include "gfxPrefs.h"
+#include "OverscrollHandoffState.h"
+#include "ViewConfiguration.h"
+
+#define ANDROID_APZ_LOG(...)
+// #define ANDROID_APZ_LOG(...) printf_stderr("ANDROID_APZ: " __VA_ARGS__)
+
+static float sMaxFlingSpeed = 0.0f;
+
+namespace mozilla {
+namespace layers {
+
+AndroidSpecificState::AndroidSpecificState() {
+ using namespace mozilla::java;
+
+ sdk::ViewConfiguration::LocalRef config;
+ if (sdk::ViewConfiguration::Get(GeckoAppShell::GetApplicationContext(), &config) == NS_OK) {
+ int32_t speed = 0;
+ if (config->GetScaledMaximumFlingVelocity(&speed) == NS_OK) {
+ sMaxFlingSpeed = (float)speed * 0.001f;
+ } else {
+ ANDROID_APZ_LOG("%p Failed to query ViewConfiguration for scaled maximum fling velocity\n", this);
+ }
+ } else {
+ ANDROID_APZ_LOG("%p Failed to get ViewConfiguration\n", this);
+ }
+
+ StackScroller::LocalRef scroller;
+ if (StackScroller::New(GeckoAppShell::GetApplicationContext(), &scroller) != NS_OK) {
+ ANDROID_APZ_LOG("%p Failed to create Android StackScroller\n", this);
+ return;
+ }
+ mOverScroller = scroller;
+}
+
+const float BOUNDS_EPSILON = 1.0f;
+
+// This function is used to convert the scroll offset from a float to an integer
+// suitable for using with the Android OverScroller Class.
+// The Android OverScroller class (unfortunately) operates in integers instead of floats.
+// When casting a float value such as 1.5 to an integer, the value is converted to 1.
+// If this value represents the max scroll offset, the OverScroller class will never scroll
+// to the end of the page as it will always be 0.5 pixels short. To work around this issue,
+// the min and max scroll extents are floor/ceil to convert them to the nearest integer
+// just outside of the actual scroll extents. This means, the starting
+// scroll offset must be converted the same way so that if the frame has already been
+// scrolled 1.5 pixels, it won't be snapped back when converted to an integer. This integer
+// rounding error was one of several causes of Bug 1276463.
+static int32_t
+ClampStart(float aOrigin, float aMin, float aMax)
+{
+ if (aOrigin <= aMin) {
+ return (int32_t)floor(aMin);
+ } else if (aOrigin >= aMax) {
+ return (int32_t)ceil(aMax);
+ }
+ return (int32_t)aOrigin;
+}
+
+AndroidFlingAnimation::AndroidFlingAnimation(AsyncPanZoomController& aApzc,
+ PlatformSpecificStateBase* aPlatformSpecificState,
+ const RefPtr<const OverscrollHandoffChain>& aOverscrollHandoffChain,
+ bool aFlingIsHandoff,
+ const RefPtr<const AsyncPanZoomController>& aScrolledApzc)
+ : mApzc(aApzc)
+ , mOverscrollHandoffChain(aOverscrollHandoffChain)
+ , mScrolledApzc(aScrolledApzc)
+ , mSentBounceX(false)
+ , mSentBounceY(false)
+ , mFlingDuration(0)
+{
+ MOZ_ASSERT(mOverscrollHandoffChain);
+ AndroidSpecificState* state = aPlatformSpecificState->AsAndroidSpecificState();
+ MOZ_ASSERT(state);
+ mOverScroller = state->mOverScroller;
+ MOZ_ASSERT(mOverScroller);
+
+ // Drop any velocity on axes where we don't have room to scroll anyways
+ // (in this APZC, or an APZC further in the handoff chain).
+ // This ensures that we don't take the 'overscroll' path in Sample()
+ // on account of one axis which can't scroll having a velocity.
+ if (!mOverscrollHandoffChain->CanScrollInDirection(&mApzc, Layer::HORIZONTAL)) {
+ ReentrantMonitorAutoEnter lock(mApzc.mMonitor);
+ mApzc.mX.SetVelocity(0);
+ }
+ if (!mOverscrollHandoffChain->CanScrollInDirection(&mApzc, Layer::VERTICAL)) {
+ ReentrantMonitorAutoEnter lock(mApzc.mMonitor);
+ mApzc.mY.SetVelocity(0);
+ }
+
+ ParentLayerPoint velocity = mApzc.GetVelocityVector();
+
+ float scrollRangeStartX = mApzc.mX.GetPageStart().value;
+ float scrollRangeEndX = mApzc.mX.GetScrollRangeEnd().value;
+ float scrollRangeStartY = mApzc.mY.GetPageStart().value;
+ float scrollRangeEndY = mApzc.mY.GetScrollRangeEnd().value;
+ mStartOffset.x = mPreviousOffset.x = mApzc.mX.GetOrigin().value;
+ mStartOffset.y = mPreviousOffset.y = mApzc.mY.GetOrigin().value;
+ float length = velocity.Length();
+ if (length > 0.0f) {
+ mFlingDirection = velocity / length;
+
+ if ((sMaxFlingSpeed > 0.0f) && (length > sMaxFlingSpeed)) {
+ velocity = mFlingDirection * sMaxFlingSpeed;
+ }
+ }
+
+ mPreviousVelocity = velocity;
+
+ int32_t originX = ClampStart(mStartOffset.x, scrollRangeStartX, scrollRangeEndX);
+ int32_t originY = ClampStart(mStartOffset.y, scrollRangeStartY, scrollRangeEndY);
+ if (!state->mLastFling.IsNull()) {
+ // If it's been too long since the previous fling, or if the new fling's
+ // velocity is too low, don't allow flywheel to kick in. If we do allow
+ // flywheel to kick in, then we need to update the timestamp on the
+ // StackScroller because otherwise it might use a stale velocity.
+ TimeDuration flingDuration = TimeStamp::Now() - state->mLastFling;
+ if (flingDuration.ToMilliseconds() < gfxPrefs::APZFlingAccelInterval()
+ && velocity.Length() >= gfxPrefs::APZFlingAccelMinVelocity()) {
+ bool unused = false;
+ mOverScroller->ComputeScrollOffset(flingDuration.ToMilliseconds(), &unused);
+ } else {
+ mOverScroller->ForceFinished(true);
+ }
+ }
+ mOverScroller->Fling(originX, originY,
+ // Android needs the velocity in pixels per second and it is in pixels per ms.
+ (int32_t)(velocity.x * 1000.0f), (int32_t)(velocity.y * 1000.0f),
+ (int32_t)floor(scrollRangeStartX), (int32_t)ceil(scrollRangeEndX),
+ (int32_t)floor(scrollRangeStartY), (int32_t)ceil(scrollRangeEndY),
+ 0, 0, 0);
+ state->mLastFling = TimeStamp::Now();
+}
+
+/**
+ * Advances a fling by an interpolated amount based on the Android OverScroller.
+ * This should be called whenever sampling the content transform for this
+ * frame. Returns true if the fling animation should be advanced by one frame,
+ * or false if there is no fling or the fling has ended.
+ */
+bool
+AndroidFlingAnimation::DoSample(FrameMetrics& aFrameMetrics,
+ const TimeDuration& aDelta)
+{
+ bool shouldContinueFling = true;
+
+ mFlingDuration += aDelta.ToMilliseconds();
+ mOverScroller->ComputeScrollOffset(mFlingDuration, &shouldContinueFling);
+
+ int32_t currentX = 0;
+ int32_t currentY = 0;
+ mOverScroller->GetCurrX(&currentX);
+ mOverScroller->GetCurrY(&currentY);
+ ParentLayerPoint offset((float)currentX, (float)currentY);
+ ParentLayerPoint preCheckedOffset(offset);
+
+ bool hitBoundX = CheckBounds(mApzc.mX, offset.x, mFlingDirection.x, &(offset.x));
+ bool hitBoundY = CheckBounds(mApzc.mY, offset.y, mFlingDirection.y, &(offset.y));
+
+ ParentLayerPoint velocity = mPreviousVelocity;
+
+ // Sometimes the OverScroller fails to update the offset for a frame.
+ // If the frame can still scroll we just use the velocity from the previous
+ // frame. However, if the frame can no longer scroll in the direction
+ // of the fling, then end the animation.
+ if (offset != mPreviousOffset) {
+ if (aDelta.ToMilliseconds() > 0) {
+ mOverScroller->GetCurrSpeedX(&velocity.x);
+ mOverScroller->GetCurrSpeedY(&velocity.y);
+
+ velocity.x /= 1000;
+ velocity.y /= 1000;
+
+ mPreviousVelocity = velocity;
+ }
+ } else if ((fabsf(offset.x - preCheckedOffset.x) > BOUNDS_EPSILON) || (fabsf(offset.y - preCheckedOffset.y) > BOUNDS_EPSILON)) {
+ // The page is no longer scrolling but the fling animation is still animating beyond the page bounds. If it goes
+ // beyond the BOUNDS_EPSILON then it has overflowed and will never stop. In that case, stop the fling animation.
+ shouldContinueFling = false;
+ } else if (hitBoundX && hitBoundY) {
+ // We can't scroll any farther along either axis.
+ shouldContinueFling = false;
+ }
+
+ float speed = velocity.Length();
+
+ // gfxPrefs::APZFlingStoppedThreshold is only used in tests.
+ if (!shouldContinueFling || (speed < gfxPrefs::APZFlingStoppedThreshold())) {
+ if (shouldContinueFling) {
+ // The OverScroller thinks it should continue but the speed is below
+ // the stopping threshold so abort the animation.
+ mOverScroller->AbortAnimation();
+ }
+ // This animation is going to end. If DeferHandleFlingOverscroll
+ // has not been called and there is still some velocity left,
+ // call it so that fling hand off may occur if applicable.
+ if (!mSentBounceX && !mSentBounceY && (speed > 0.0f)) {
+ DeferHandleFlingOverscroll(velocity);
+ }
+ return false;
+ }
+
+ mPreviousOffset = offset;
+
+ mApzc.SetVelocityVector(velocity);
+ aFrameMetrics.SetScrollOffset(offset / aFrameMetrics.GetZoom());
+
+ // If we hit a bounds while flinging, send the velocity so that the bounce
+ // animation can play.
+ if (hitBoundX || hitBoundY) {
+ ParentLayerPoint bounceVelocity = velocity;
+
+ if (!mSentBounceX && hitBoundX && fabsf(offset.x - mStartOffset.x) > BOUNDS_EPSILON) {
+ mSentBounceX = true;
+ } else {
+ bounceVelocity.x = 0.0f;
+ }
+
+ if (!mSentBounceY && hitBoundY && fabsf(offset.y - mStartOffset.y) > BOUNDS_EPSILON) {
+ mSentBounceY = true;
+ } else {
+ bounceVelocity.y = 0.0f;
+ }
+ if (!IsZero(bounceVelocity)) {
+ DeferHandleFlingOverscroll(bounceVelocity);
+ }
+ }
+
+ return true;
+}
+
+void
+AndroidFlingAnimation::DeferHandleFlingOverscroll(ParentLayerPoint& aVelocity)
+{
+ mDeferredTasks.AppendElement(
+ NewRunnableMethod<ParentLayerPoint,
+ RefPtr<const OverscrollHandoffChain>,
+ RefPtr<const AsyncPanZoomController>>(&mApzc,
+ &AsyncPanZoomController::HandleFlingOverscroll,
+ aVelocity,
+ mOverscrollHandoffChain,
+ mScrolledApzc));
+
+}
+
+bool
+AndroidFlingAnimation::CheckBounds(Axis& aAxis, float aValue, float aDirection, float* aClamped)
+{
+ if ((aDirection < 0.0f) && (aValue <= aAxis.GetPageStart().value)) {
+ if (aClamped) {
+ *aClamped = aAxis.GetPageStart().value;
+ }
+ return true;
+ } else if ((aDirection > 0.0f) && (aValue >= aAxis.GetScrollRangeEnd().value)) {
+ if (aClamped) {
+ *aClamped = aAxis.GetScrollRangeEnd().value;
+ }
+ return true;
+ }
+ return false;
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/apz/src/AndroidAPZ.h b/gfx/layers/apz/src/AndroidAPZ.h
new file mode 100644
index 000000000..404892da5
--- /dev/null
+++ b/gfx/layers/apz/src/AndroidAPZ.h
@@ -0,0 +1,61 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set sw=2 ts=8 et tw=80 : */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_layers_AndroidAPZ_h_
+#define mozilla_layers_AndroidAPZ_h_
+
+#include "AsyncPanZoomAnimation.h"
+#include "AsyncPanZoomController.h"
+#include "GeneratedJNIWrappers.h"
+
+namespace mozilla {
+namespace layers {
+
+class AndroidSpecificState : public PlatformSpecificStateBase {
+public:
+ AndroidSpecificState();
+
+ virtual AndroidSpecificState* AsAndroidSpecificState() override {
+ return this;
+ }
+
+ java::StackScroller::GlobalRef mOverScroller;
+ TimeStamp mLastFling;
+};
+
+class AndroidFlingAnimation: public AsyncPanZoomAnimation {
+public:
+ AndroidFlingAnimation(AsyncPanZoomController& aApzc,
+ PlatformSpecificStateBase* aPlatformSpecificState,
+ const RefPtr<const OverscrollHandoffChain>& aOverscrollHandoffChain,
+ bool aFlingIsHandoff /* ignored */,
+ const RefPtr<const AsyncPanZoomController>& aScrolledApzc);
+ virtual bool DoSample(FrameMetrics& aFrameMetrics,
+ const TimeDuration& aDelta) override;
+private:
+ void DeferHandleFlingOverscroll(ParentLayerPoint& aVelocity);
+ // Returns true if value is on or outside of axis bounds.
+ bool CheckBounds(Axis& aAxis, float aValue, float aDirection, float* aClamped);
+
+ AsyncPanZoomController& mApzc;
+ java::StackScroller::GlobalRef mOverScroller;
+ RefPtr<const OverscrollHandoffChain> mOverscrollHandoffChain;
+ RefPtr<const AsyncPanZoomController> mScrolledApzc;
+ bool mSentBounceX;
+ bool mSentBounceY;
+ long mFlingDuration;
+ ParentLayerPoint mStartOffset;
+ ParentLayerPoint mPreviousOffset;
+ // Unit vector in the direction of the fling.
+ ParentLayerPoint mFlingDirection;
+ ParentLayerPoint mPreviousVelocity;
+};
+
+
+} // namespace layers
+} // namespace mozilla
+
+#endif // mozilla_layers_AndroidAPZ_h_
diff --git a/gfx/layers/apz/src/AsyncDragMetrics.h b/gfx/layers/apz/src/AsyncDragMetrics.h
new file mode 100644
index 000000000..54b60f823
--- /dev/null
+++ b/gfx/layers/apz/src/AsyncDragMetrics.h
@@ -0,0 +1,65 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set sw=2 ts=8 et tw=80 : */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_layers_DragMetrics_h
+#define mozilla_layers_DragMetrics_h
+
+#include "FrameMetrics.h"
+
+namespace IPC {
+template <typename T> struct ParamTraits;
+} // namespace IPC
+
+namespace mozilla {
+
+namespace layers {
+
+class AsyncDragMetrics {
+ friend struct IPC::ParamTraits<mozilla::layers::AsyncDragMetrics>;
+
+public:
+ enum DragDirection {
+ NONE,
+ VERTICAL,
+ HORIZONTAL,
+ SENTINEL,
+ };
+
+ // IPC constructor
+ AsyncDragMetrics()
+ : mViewId(0)
+ , mPresShellId(0)
+ , mDragStartSequenceNumber(0)
+ , mScrollbarDragOffset(0)
+ , mDirection(NONE)
+ {}
+
+ AsyncDragMetrics(const FrameMetrics::ViewID& aViewId,
+ uint32_t aPresShellId,
+ uint64_t aDragStartSequenceNumber,
+ CSSIntCoord aScrollbarDragOffset,
+ const CSSIntRect& aScrollTrack,
+ DragDirection aDirection)
+ : mViewId(aViewId)
+ , mPresShellId(aPresShellId)
+ , mDragStartSequenceNumber(aDragStartSequenceNumber)
+ , mScrollbarDragOffset(aScrollbarDragOffset)
+ , mScrollTrack(aScrollTrack)
+ , mDirection(aDirection)
+ {}
+
+ FrameMetrics::ViewID mViewId;
+ uint32_t mPresShellId;
+ uint64_t mDragStartSequenceNumber;
+ CSSIntCoord mScrollbarDragOffset;
+ CSSIntRect mScrollTrack;
+ DragDirection mDirection;
+};
+
+}
+}
+
+#endif
diff --git a/gfx/layers/apz/src/AsyncPanZoomAnimation.h b/gfx/layers/apz/src/AsyncPanZoomAnimation.h
new file mode 100644
index 000000000..039c0684e
--- /dev/null
+++ b/gfx/layers/apz/src/AsyncPanZoomAnimation.h
@@ -0,0 +1,80 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set sw=2 ts=8 et tw=80 : */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_layers_AsyncPanZoomAnimation_h_
+#define mozilla_layers_AsyncPanZoomAnimation_h_
+
+#include "base/message_loop.h"
+#include "mozilla/RefPtr.h"
+#include "mozilla/TimeStamp.h"
+#include "FrameMetrics.h"
+#include "nsISupportsImpl.h"
+#include "nsTArray.h"
+
+namespace mozilla {
+namespace layers {
+
+class WheelScrollAnimation;
+class SmoothScrollAnimation;
+
+class AsyncPanZoomAnimation {
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(AsyncPanZoomAnimation)
+
+public:
+ explicit AsyncPanZoomAnimation()
+ { }
+
+ virtual bool DoSample(FrameMetrics& aFrameMetrics,
+ const TimeDuration& aDelta) = 0;
+
+ bool Sample(FrameMetrics& aFrameMetrics,
+ const TimeDuration& aDelta) {
+ // In some situations, particularly when handoff is involved, it's possible
+ // for |aDelta| to be negative on the first call to sample. Ignore such a
+ // sample here, to avoid each derived class having to deal with this case.
+ if (aDelta.ToMilliseconds() <= 0) {
+ return true;
+ }
+
+ return DoSample(aFrameMetrics, aDelta);
+ }
+
+ /**
+ * Get the deferred tasks in |mDeferredTasks| and place them in |aTasks|. See
+ * |mDeferredTasks| for more information. Clears |mDeferredTasks|.
+ */
+ nsTArray<RefPtr<Runnable>> TakeDeferredTasks() {
+ return Move(mDeferredTasks);
+ }
+
+ virtual WheelScrollAnimation* AsWheelScrollAnimation() {
+ return nullptr;
+ }
+ virtual SmoothScrollAnimation* AsSmoothScrollAnimation() {
+ return nullptr;
+ }
+
+ virtual bool WantsRepaints() {
+ return true;
+ }
+
+protected:
+ // Protected destructor, to discourage deletion outside of Release():
+ virtual ~AsyncPanZoomAnimation()
+ { }
+
+ /**
+ * Tasks scheduled for execution after the APZC's mMonitor is released.
+ * Derived classes can add tasks here in Sample(), and the APZC can call
+ * ExecuteDeferredTasks() to execute them.
+ */
+ nsTArray<RefPtr<Runnable>> mDeferredTasks;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif // mozilla_layers_AsyncPanZoomAnimation_h_
diff --git a/gfx/layers/apz/src/AsyncPanZoomController.cpp b/gfx/layers/apz/src/AsyncPanZoomController.cpp
new file mode 100644
index 000000000..102f282f3
--- /dev/null
+++ b/gfx/layers/apz/src/AsyncPanZoomController.cpp
@@ -0,0 +1,4030 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set sw=2 ts=8 et tw=80 : */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include <math.h> // for fabsf, fabs, atan2
+#include <stdint.h> // for uint32_t, uint64_t
+#include <sys/types.h> // for int32_t
+#include <algorithm> // for max, min
+#include "AsyncPanZoomController.h" // for AsyncPanZoomController, etc
+#include "Axis.h" // for AxisX, AxisY, Axis, etc
+#include "CheckerboardEvent.h" // for CheckerboardEvent
+#include "Compositor.h" // for Compositor
+#include "FrameMetrics.h" // for FrameMetrics, etc
+#include "GenericFlingAnimation.h" // for GenericFlingAnimation
+#include "GestureEventListener.h" // for GestureEventListener
+#include "HitTestingTreeNode.h" // for HitTestingTreeNode
+#include "InputData.h" // for MultiTouchInput, etc
+#include "InputBlockState.h" // for InputBlockState, TouchBlockState
+#include "InputQueue.h" // for InputQueue
+#include "Overscroll.h" // for OverscrollAnimation
+#include "OverscrollHandoffState.h" // for OverscrollHandoffState
+#include "Units.h" // for CSSRect, CSSPoint, etc
+#include "UnitTransforms.h" // for TransformTo
+#include "base/message_loop.h" // for MessageLoop
+#include "base/task.h" // for NewRunnableMethod, etc
+#include "gfxPrefs.h" // for gfxPrefs
+#include "gfxTypes.h" // for gfxFloat
+#include "LayersLogging.h" // for print_stderr
+#include "mozilla/Assertions.h" // for MOZ_ASSERT, etc
+#include "mozilla/BasicEvents.h" // for Modifiers, MODIFIER_*
+#include "mozilla/ClearOnShutdown.h" // for ClearOnShutdown
+#include "mozilla/ComputedTimingFunction.h" // for ComputedTimingFunction
+#include "mozilla/EventForwards.h" // for nsEventStatus_*
+#include "mozilla/MouseEvents.h" // for WidgetWheelEvent
+#include "mozilla/Preferences.h" // for Preferences
+#include "mozilla/ReentrantMonitor.h" // for ReentrantMonitorAutoEnter, etc
+#include "mozilla/RefPtr.h" // for RefPtr
+#include "mozilla/StaticPtr.h" // for StaticAutoPtr
+#include "mozilla/Telemetry.h" // for Telemetry
+#include "mozilla/TimeStamp.h" // for TimeDuration, TimeStamp
+#include "mozilla/dom/CheckerboardReportService.h" // for CheckerboardEventStorage
+ // note: CheckerboardReportService.h actually lives in gfx/layers/apz/util/
+#include "mozilla/dom/Touch.h" // for Touch
+#include "mozilla/gfx/BasePoint.h" // for BasePoint
+#include "mozilla/gfx/BaseRect.h" // for BaseRect
+#include "mozilla/gfx/Point.h" // for Point, RoundedToInt, etc
+#include "mozilla/gfx/Rect.h" // for RoundedIn
+#include "mozilla/gfx/ScaleFactor.h" // for ScaleFactor
+#include "mozilla/layers/APZCTreeManager.h" // for ScrollableLayerGuid
+#include "mozilla/layers/APZThreadUtils.h" // for AssertOnControllerThread, etc
+#include "mozilla/layers/AsyncCompositionManager.h" // for ViewTransform
+#include "mozilla/layers/AxisPhysicsModel.h" // for AxisPhysicsModel
+#include "mozilla/layers/AxisPhysicsMSDModel.h" // for AxisPhysicsMSDModel
+#include "mozilla/layers/CompositorController.h" // for CompositorController
+#include "mozilla/layers/LayerTransactionParent.h" // for LayerTransactionParent
+#include "mozilla/layers/MetricsSharingController.h" // for MetricsSharingController
+#include "mozilla/layers/ScrollInputMethods.h" // for ScrollInputMethod
+#include "mozilla/mozalloc.h" // for operator new, etc
+#include "mozilla/Unused.h" // for unused
+#include "mozilla/FloatingPoint.h" // for FuzzyEquals*
+#include "nsAlgorithm.h" // for clamped
+#include "nsCOMPtr.h" // for already_AddRefed
+#include "nsDebug.h" // for NS_WARNING
+#include "nsIDOMWindowUtils.h" // for nsIDOMWindowUtils
+#include "nsMathUtils.h" // for NS_hypot
+#include "nsPoint.h" // for nsIntPoint
+#include "nsStyleConsts.h"
+#include "nsStyleStruct.h" // for nsTimingFunction
+#include "nsTArray.h" // for nsTArray, nsTArray_Impl, etc
+#include "nsThreadUtils.h" // for NS_IsMainThread
+#include "nsViewportInfo.h" // for kViewportMinScale, kViewportMaxScale
+#include "prsystem.h" // for PR_GetPhysicalMemorySize
+#include "SharedMemoryBasic.h" // for SharedMemoryBasic
+#include "ScrollSnap.h" // for ScrollSnapUtils
+#include "WheelScrollAnimation.h"
+#if defined(MOZ_WIDGET_ANDROID)
+#include "AndroidAPZ.h"
+#endif // defined(MOZ_WIDGET_ANDROID)
+
+#define ENABLE_APZC_LOGGING 0
+// #define ENABLE_APZC_LOGGING 1
+
+#if ENABLE_APZC_LOGGING
+# define APZC_LOG(...) printf_stderr("APZC: " __VA_ARGS__)
+# define APZC_LOG_FM(fm, prefix, ...) \
+ { std::stringstream ss; \
+ ss << nsPrintfCString(prefix, __VA_ARGS__).get(); \
+ AppendToString(ss, fm, ":", "", true); \
+ APZC_LOG("%s\n", ss.str().c_str()); \
+ }
+#else
+# define APZC_LOG(...)
+# define APZC_LOG_FM(fm, prefix, ...)
+#endif
+
+namespace mozilla {
+namespace layers {
+
+typedef mozilla::layers::AllowedTouchBehavior AllowedTouchBehavior;
+typedef GeckoContentController::APZStateChange APZStateChange;
+typedef GeckoContentController::TapType TapType;
+typedef mozilla::gfx::Point Point;
+typedef mozilla::gfx::Matrix4x4 Matrix4x4;
+using mozilla::gfx::PointTyped;
+
+// Choose between platform-specific implementations.
+#ifdef MOZ_WIDGET_ANDROID
+typedef WidgetOverscrollEffect OverscrollEffect;
+typedef AndroidSpecificState PlatformSpecificState;
+typedef AndroidFlingAnimation FlingAnimation;
+#else
+typedef GenericOverscrollEffect OverscrollEffect;
+typedef PlatformSpecificStateBase PlatformSpecificState; // no extra state, just use the base class
+typedef GenericFlingAnimation FlingAnimation;
+#endif
+
+/**
+ * \page APZCPrefs APZ preferences
+ *
+ * The following prefs are used to control the behaviour of the APZC.
+ * The default values are provided in gfxPrefs.h.
+ *
+ * \li\b apz.allow_checkerboarding
+ * Pref that allows or disallows checkerboarding
+ *
+ * \li\b apz.allow_immediate_handoff
+ * If set to true, scroll can be handed off from one APZC to another within
+ * a single input block. If set to false, a single input block can only
+ * scroll one APZC.
+ *
+ * \li\b apz.axis_lock.mode
+ * The preferred axis locking style. See AxisLockMode for possible values.
+ *
+ * \li\b apz.axis_lock.lock_angle
+ * Angle from axis within which we stay axis-locked.\n
+ * Units: radians
+ *
+ * \li\b apz.axis_lock.breakout_threshold
+ * Distance in inches the user must pan before axis lock can be broken.\n
+ * Units: (real-world, i.e. screen) inches
+ *
+ * \li\b apz.axis_lock.breakout_angle
+ * Angle at which axis lock can be broken.\n
+ * Units: radians
+ *
+ * \li\b apz.axis_lock.direct_pan_angle
+ * If the angle from an axis to the line drawn by a pan move is less than
+ * this value, we can assume that panning can be done in the allowed direction
+ * (horizontal or vertical).\n
+ * Currently used only for touch-action css property stuff and was addded to
+ * keep behaviour consistent with IE.\n
+ * Units: radians
+ *
+ * \li\b apz.content_response_timeout
+ * Amount of time before we timeout response from content. For example, if
+ * content is being unruly/slow and we don't get a response back within this
+ * time, we will just pretend that content did not preventDefault any touch
+ * events we dispatched to it.\n
+ * Units: milliseconds
+ *
+ * \li\b apz.danger_zone_x
+ * \li\b apz.danger_zone_y
+ * When drawing high-res tiles, we drop down to drawing low-res tiles
+ * when we know we can't keep up with the scrolling. The way we determine
+ * this is by checking if we are entering the "danger zone", which is the
+ * boundary of the painted content. For example, if the painted content
+ * goes from y=0...1000 and the visible portion is y=250...750 then
+ * we're far from checkerboarding. If we get to y=490...990 though then we're
+ * only 10 pixels away from showing checkerboarding so we are probably in
+ * a state where we can't keep up with scrolling. The danger zone prefs specify
+ * how wide this margin is; in the above example a y-axis danger zone of 10
+ * pixels would make us drop to low-res at y=490...990.\n
+ * This value is in layer pixels.
+ *
+ * \li\b apz.disable_for_scroll_linked_effects
+ * Setting this pref to true will disable APZ scrolling on documents where
+ * scroll-linked effects are detected. A scroll linked effect is detected if
+ * positioning or transform properties are updated inside a scroll event
+ * dispatch; we assume that such an update is in response to the scroll event
+ * and is therefore a scroll-linked effect which will be laggy with APZ
+ * scrolling.
+ *
+ * \li\b apz.displayport_expiry_ms
+ * While a scrollable frame is scrolling async, we set a displayport on it
+ * to make sure it is layerized. However this takes up memory, so once the
+ * scrolling stops we want to remove the displayport. This pref controls how
+ * long after scrolling stops the displayport is removed. A value of 0 will
+ * disable the expiry behavior entirely.
+ * Units: milliseconds
+ *
+ * \li\b apz.enlarge_displayport_when_clipped
+ * Pref that enables enlarging of the displayport along one axis when the
+ * generated displayport's size is beyond that of the scrollable rect on the
+ * opposite axis.
+ *
+ * \li\b apz.fling_accel_interval_ms
+ * The time that determines whether a second fling will be treated as
+ * accelerated. If two flings are started within this interval, the second one
+ * will be accelerated. Setting an interval of 0 means that acceleration will
+ * be disabled.\n
+ * Units: milliseconds
+ *
+ * \li\b apz.fling_accel_min_velocity
+ * The minimum velocity of the second fling for it to be considered for fling
+ * acceleration.
+ * Units: screen pixels per milliseconds
+ *
+ * \li\b apz.fling_accel_base_mult
+ * \li\b apz.fling_accel_supplemental_mult
+ * When applying an acceleration on a fling, the new computed velocity is
+ * (new_fling_velocity * base_mult) + (old_velocity * supplemental_mult).
+ * The base_mult and supplemental_mult multiplier values are controlled by
+ * these prefs. Note that "old_velocity" here is the initial velocity of the
+ * previous fling _after_ acceleration was applied to it (if applicable).
+ *
+ * \li\b apz.fling_curve_function_x1
+ * \li\b apz.fling_curve_function_y1
+ * \li\b apz.fling_curve_function_x2
+ * \li\b apz.fling_curve_function_y2
+ * \li\b apz.fling_curve_threshold_inches_per_ms
+ * These five parameters define a Bezier curve function and threshold used to
+ * increase the actual velocity relative to the user's finger velocity. When the
+ * finger velocity is below the threshold (or if the threshold is not positive),
+ * the velocity is used as-is. If the finger velocity exceeds the threshold
+ * velocity, then the function defined by the curve is applied on the part of
+ * the velocity that exceeds the threshold. Note that the upper bound of the
+ * velocity is still specified by the \b apz.max_velocity_inches_per_ms pref, and
+ * the function will smoothly curve the velocity from the threshold to the
+ * max. In general the function parameters chosen should define an ease-out
+ * curve in order to increase the velocity in this range, or an ease-in curve to
+ * decrease the velocity. A straight-line curve is equivalent to disabling the
+ * curve entirely by setting the threshold to -1. The max velocity pref must
+ * also be set in order for the curving to take effect, as it defines the upper
+ * bound of the velocity curve.\n
+ * The points (x1, y1) and (x2, y2) used as the two intermediate control points
+ * in the cubic bezier curve; the first and last points are (0,0) and (1,1).\n
+ * Some example values for these prefs can be found at\n
+ * https://dxr.mozilla.org/mozilla-central/rev/70e05c6832e831374604ac3ce7433971368dffe0/layout/style/nsStyleStruct.cpp#2729
+ *
+ * \li\b apz.fling_friction
+ * Amount of friction applied during flings. This is used in the following
+ * formula: v(t1) = v(t0) * (1 - f)^(t1 - t0), where v(t1) is the velocity
+ * for a new sample, v(t0) is the velocity at the previous sample, f is the
+ * value of this pref, and (t1 - t0) is the amount of time, in milliseconds,
+ * that has elapsed between the two samples.\n
+ * NOTE: Not currently used in Android fling calculations.
+ *
+ * \li\b apz.fling_min_velocity_threshold
+ * Minimum velocity for a fling to actually kick off. If the user pans and lifts
+ * their finger such that the velocity is smaller than this amount, no fling
+ * is initiated.\n
+ * Units: screen pixels per millisecond
+ *
+ * \li\b apz.fling_stop_on_tap_threshold
+ * When flinging, if the velocity is above this number, then a tap on the
+ * screen will stop the fling without dispatching a tap to content. If the
+ * velocity is below this threshold a tap will also be dispatched.
+ * Note: when modifying this pref be sure to run the APZC gtests as some of
+ * them depend on the value of this pref.\n
+ * Units: screen pixels per millisecond
+ *
+ * \li\b apz.fling_stopped_threshold
+ * When flinging, if the velocity goes below this number, we just stop the
+ * animation completely. This is to prevent asymptotically approaching 0
+ * velocity and rerendering unnecessarily.\n
+ * Units: screen pixels per millisecond.\n
+ * NOTE: Should not be set to anything
+ * other than 0.0 for Android except for tests to disable flings.
+ *
+ * \li\b apz.max_velocity_inches_per_ms
+ * Maximum velocity. Velocity will be capped at this value if a faster fling
+ * occurs. Negative values indicate unlimited velocity.\n
+ * Units: (real-world, i.e. screen) inches per millisecond
+ *
+ * \li\b apz.max_velocity_queue_size
+ * Maximum size of velocity queue. The queue contains last N velocity records.
+ * On touch end we calculate the average velocity in order to compensate
+ * touch/mouse drivers misbehaviour.
+ *
+ * \li\b apz.min_skate_speed
+ * Minimum amount of speed along an axis before we switch to "skate" multipliers
+ * rather than using the "stationary" multipliers.\n
+ * Units: CSS pixels per millisecond
+ *
+ * \li\b apz.overscroll.enabled
+ * Pref that enables overscrolling. If this is disabled, excess scroll that
+ * cannot be handed off is discarded.
+ *
+ * \li\b apz.overscroll.min_pan_distance_ratio
+ * The minimum ratio of the pan distance along one axis to the pan distance
+ * along the other axis needed to initiate overscroll along the first axis
+ * during panning.
+ *
+ * \li\b apz.overscroll.stretch_factor
+ * How much overscrolling can stretch content along an axis.
+ * The maximum stretch along an axis is a factor of (1 + kStretchFactor).
+ * (So if kStretchFactor is 0, you can't stretch at all; if kStretchFactor
+ * is 1, you can stretch at most by a factor of 2).
+ *
+ * \li\b apz.overscroll.spring_stiffness
+ * The stiffness of the spring used in the physics model for the overscroll
+ * animation.
+ *
+ * \li\b apz.overscroll.spring_friction
+ * The friction of the spring used in the physics model for the overscroll
+ * animation.
+ * Even though a realistic physics model would dictate that this be the same
+ * as \b apz.fling_friction, we allow it to be set to be something different,
+ * because in practice we want flings to skate smoothly (low friction), while
+ * we want the overscroll bounce-back to oscillate few times (high friction).
+ *
+ * \li\b apz.overscroll.stop_distance_threshold
+ * \li\b apz.overscroll.stop_velocity_threshold
+ * Thresholds for stopping the overscroll animation. When both the distance
+ * and the velocity fall below their thresholds, we stop oscillating.\n
+ * Units: screen pixels (for distance)
+ * screen pixels per millisecond (for velocity)
+ *
+ * \li\b apz.paint_skipping.enabled
+ * When APZ is scrolling and sending repaint requests to the main thread, often
+ * 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.record_checkerboarding
+ * Whether or not to record detailed info on checkerboarding events.
+ *
+ * \li\b apz.test.logging_enabled
+ * Enable logging of APZ test data (see bug 961289).
+ *
+ * \li\b apz.touch_move_tolerance
+ * See the description for apz.touch_start_tolerance below. This is a similar
+ * threshold, except it is used to suppress touchmove events from being delivered
+ * to content for NON-scrollable frames (or more precisely, for APZCs where
+ * ArePointerEventsConsumable returns false).\n
+ * Units: (real-world, i.e. screen) inches
+ *
+ * \li\b apz.touch_start_tolerance
+ * Constant describing the tolerance in distance we use, multiplied by the
+ * device DPI, before we start panning the screen. This is to prevent us from
+ * accidentally processing taps as touch moves, and from very short/accidental
+ * touches moving the screen. touchmove events are also not delivered to content
+ * within this distance on scrollable frames.\n
+ * Units: (real-world, i.e. screen) inches
+ *
+ * \li\b apz.velocity_bias
+ * How much to adjust the displayport in the direction of scrolling. This value
+ * is multiplied by the velocity and added to the displayport offset.
+ *
+ * \li\b apz.velocity_relevance_time_ms
+ * When computing a fling velocity from the most recently stored velocity
+ * information, only velocities within the most X milliseconds are used.
+ * This pref controls the value of X.\n
+ * Units: ms
+ *
+ * \li\b apz.x_skate_size_multiplier
+ * \li\b apz.y_skate_size_multiplier
+ * The multiplier we apply to the displayport size if it is skating (current
+ * velocity is above \b apz.min_skate_speed). We prefer to increase the size of
+ * the Y axis because it is more natural in the case that a user is reading a
+ * page page that scrolls up/down. Note that one, both or neither of these may be
+ * used at any instant.\n
+ * In general we want \b apz.[xy]_skate_size_multiplier to be smaller than the corresponding
+ * stationary size multiplier because when panning fast we would like to paint
+ * less and get faster, more predictable paint times. When panning slowly we
+ * can afford to paint more even though it's slower.
+ *
+ * \li\b apz.x_stationary_size_multiplier
+ * \li\b apz.y_stationary_size_multiplier
+ * The multiplier we apply to the displayport size if it is not skating (see
+ * documentation for the skate size multipliers above).
+ *
+ * \li\b apz.x_skate_highmem_adjust
+ * \li\b apz.y_skate_highmem_adjust
+ * On high memory systems, we adjust the displayport during skating
+ * to be larger so we can reduce checkerboarding.
+ *
+ * \li\b apz.zoom_animation_duration_ms
+ * This controls how long the zoom-to-rect animation takes.\n
+ * Units: ms
+ *
+ * \li\b apz.scale_repaint_delay_ms
+ * How long to delay between repaint requests during a scale.
+ * A negative number prevents repaint requests during a scale.\n
+ * Units: ms
+ *
+ */
+
+/**
+ * Computed time function used for sampling frames of a zoom to animation.
+ */
+StaticAutoPtr<ComputedTimingFunction> gZoomAnimationFunction;
+
+/**
+ * Computed time function used for curving up velocity when it gets high.
+ */
+StaticAutoPtr<ComputedTimingFunction> gVelocityCurveFunction;
+
+/**
+ * The estimated duration of a paint for the purposes of calculating a new
+ * displayport, in milliseconds.
+ */
+static const double kDefaultEstimatedPaintDurationMs = 50;
+
+/**
+ * Returns true if this is a high memory system and we can use
+ * extra memory for a larger displayport to reduce checkerboarding.
+ */
+static bool gIsHighMemSystem = false;
+static bool IsHighMemSystem()
+{
+ return gIsHighMemSystem;
+}
+
+/**
+ * Is aAngle within the given threshold of the horizontal axis?
+ * @param aAngle an angle in radians in the range [0, pi]
+ * @param aThreshold an angle in radians in the range [0, pi/2]
+ */
+static bool IsCloseToHorizontal(float aAngle, float aThreshold)
+{
+ return (aAngle < aThreshold || aAngle > (M_PI - aThreshold));
+}
+
+// As above, but for the vertical axis.
+static bool IsCloseToVertical(float aAngle, float aThreshold)
+{
+ return (fabs(aAngle - (M_PI / 2)) < aThreshold);
+}
+
+// Counter used to give each APZC a unique id
+static uint32_t sAsyncPanZoomControllerCount = 0;
+
+TimeStamp
+AsyncPanZoomController::GetFrameTime() const
+{
+ APZCTreeManager* treeManagerLocal = GetApzcTreeManager();
+ return treeManagerLocal ? treeManagerLocal->GetFrameTime() : TimeStamp::Now();
+}
+
+class MOZ_STACK_CLASS StateChangeNotificationBlocker {
+public:
+ explicit StateChangeNotificationBlocker(AsyncPanZoomController* aApzc)
+ : mApzc(aApzc)
+ {
+ ReentrantMonitorAutoEnter lock(mApzc->mMonitor);
+ mInitialState = mApzc->mState;
+ mApzc->mNotificationBlockers++;
+ }
+
+ ~StateChangeNotificationBlocker()
+ {
+ AsyncPanZoomController::PanZoomState newState;
+ {
+ ReentrantMonitorAutoEnter lock(mApzc->mMonitor);
+ mApzc->mNotificationBlockers--;
+ newState = mApzc->mState;
+ }
+ mApzc->DispatchStateChangeNotification(mInitialState, newState);
+ }
+
+private:
+ AsyncPanZoomController* mApzc;
+ AsyncPanZoomController::PanZoomState mInitialState;
+};
+
+class ZoomAnimation: public AsyncPanZoomAnimation {
+public:
+ ZoomAnimation(CSSPoint aStartOffset, CSSToParentLayerScale2D aStartZoom,
+ CSSPoint aEndOffset, CSSToParentLayerScale2D aEndZoom)
+ : mTotalDuration(TimeDuration::FromMilliseconds(gfxPrefs::APZZoomAnimationDuration()))
+ , mStartOffset(aStartOffset)
+ , mStartZoom(aStartZoom)
+ , mEndOffset(aEndOffset)
+ , mEndZoom(aEndZoom)
+ {}
+
+ virtual bool DoSample(FrameMetrics& aFrameMetrics,
+ const TimeDuration& aDelta) override
+ {
+ mDuration += aDelta;
+ double animPosition = mDuration / mTotalDuration;
+
+ if (animPosition >= 1.0) {
+ aFrameMetrics.SetZoom(mEndZoom);
+ aFrameMetrics.SetScrollOffset(mEndOffset);
+ return false;
+ }
+
+ // Sample the zoom at the current time point. The sampled zoom
+ // will affect the final computed resolution.
+ float sampledPosition =
+ gZoomAnimationFunction->GetValue(animPosition,
+ ComputedTimingFunction::BeforeFlag::Unset);
+
+ // We scale the scrollOffset linearly with sampledPosition, so the zoom
+ // needs to scale inversely to match.
+ aFrameMetrics.SetZoom(CSSToParentLayerScale2D(
+ 1 / (sampledPosition / mEndZoom.xScale + (1 - sampledPosition) / mStartZoom.xScale),
+ 1 / (sampledPosition / mEndZoom.yScale + (1 - sampledPosition) / mStartZoom.yScale)));
+
+ aFrameMetrics.SetScrollOffset(CSSPoint::FromUnknownPoint(gfx::Point(
+ mEndOffset.x * sampledPosition + mStartOffset.x * (1 - sampledPosition),
+ mEndOffset.y * sampledPosition + mStartOffset.y * (1 - sampledPosition)
+ )));
+
+ return true;
+ }
+
+ virtual bool WantsRepaints() override
+ {
+ return false;
+ }
+
+private:
+ TimeDuration mDuration;
+ const TimeDuration mTotalDuration;
+
+ // Old metrics from before we started a zoom animation. This is only valid
+ // when we are in the "ANIMATED_ZOOM" state. This is used so that we can
+ // interpolate between the start and end frames. We only use the
+ // |mViewportScrollOffset| and |mResolution| fields on this.
+ CSSPoint mStartOffset;
+ CSSToParentLayerScale2D mStartZoom;
+
+ // Target metrics for a zoom to animation. This is only valid when we are in
+ // the "ANIMATED_ZOOM" state. We only use the |mViewportScrollOffset| and
+ // |mResolution| fields on this.
+ CSSPoint mEndOffset;
+ CSSToParentLayerScale2D mEndZoom;
+};
+
+
+class SmoothScrollAnimation : public AsyncPanZoomAnimation {
+public:
+ SmoothScrollAnimation(AsyncPanZoomController& aApzc,
+ const nsPoint &aInitialPosition,
+ const nsPoint &aInitialVelocity,
+ const nsPoint& aDestination, double aSpringConstant,
+ double aDampingRatio)
+ : mApzc(aApzc)
+ , mXAxisModel(aInitialPosition.x, aDestination.x, aInitialVelocity.x,
+ aSpringConstant, aDampingRatio)
+ , mYAxisModel(aInitialPosition.y, aDestination.y, aInitialVelocity.y,
+ aSpringConstant, aDampingRatio)
+ {
+ }
+
+ /**
+ * Advances a smooth scroll simulation based on the time passed in |aDelta|.
+ * This should be called whenever sampling the content transform for this
+ * frame. Returns true if the smooth scroll should be advanced by one frame,
+ * or false if the smooth scroll has ended.
+ */
+ bool DoSample(FrameMetrics& aFrameMetrics, const TimeDuration& aDelta) override {
+ nsPoint oneParentLayerPixel =
+ CSSPoint::ToAppUnits(ParentLayerPoint(1, 1) / aFrameMetrics.GetZoom());
+ if (mXAxisModel.IsFinished(oneParentLayerPixel.x) &&
+ mYAxisModel.IsFinished(oneParentLayerPixel.y)) {
+ // Set the scroll offset to the exact destination. If we allow the scroll
+ // offset to end up being a bit off from the destination, we can get
+ // artefacts like "scroll to the next snap point in this direction"
+ // scrolling to the snap point we're already supposed to be at.
+ aFrameMetrics.SetScrollOffset(
+ aFrameMetrics.CalculateScrollRange().ClampPoint(
+ CSSPoint::FromAppUnits(nsPoint(mXAxisModel.GetDestination(),
+ mYAxisModel.GetDestination()))));
+ return false;
+ }
+
+ mXAxisModel.Simulate(aDelta);
+ mYAxisModel.Simulate(aDelta);
+
+ CSSPoint position = CSSPoint::FromAppUnits(nsPoint(mXAxisModel.GetPosition(),
+ mYAxisModel.GetPosition()));
+ CSSPoint css_velocity = CSSPoint::FromAppUnits(nsPoint(mXAxisModel.GetVelocity(),
+ mYAxisModel.GetVelocity()));
+
+ // Convert from points/second to points/ms
+ ParentLayerPoint velocity = ParentLayerPoint(css_velocity.x, css_velocity.y) / 1000.0f;
+
+ // Keep the velocity updated for the Axis class so that any animations
+ // chained off of the smooth scroll will inherit it.
+ if (mXAxisModel.IsFinished(oneParentLayerPixel.x)) {
+ mApzc.mX.SetVelocity(0);
+ } else {
+ mApzc.mX.SetVelocity(velocity.x);
+ }
+ if (mYAxisModel.IsFinished(oneParentLayerPixel.y)) {
+ mApzc.mY.SetVelocity(0);
+ } else {
+ mApzc.mY.SetVelocity(velocity.y);
+ }
+ // If we overscroll, hand off to a fling animation that will complete the
+ // spring back.
+ CSSToParentLayerScale2D zoom = aFrameMetrics.GetZoom();
+ ParentLayerPoint displacement = (position - aFrameMetrics.GetScrollOffset()) * zoom;
+
+ ParentLayerPoint overscroll;
+ ParentLayerPoint adjustedOffset;
+ mApzc.mX.AdjustDisplacement(displacement.x, adjustedOffset.x, overscroll.x);
+ mApzc.mY.AdjustDisplacement(displacement.y, adjustedOffset.y, overscroll.y);
+
+ aFrameMetrics.ScrollBy(adjustedOffset / zoom);
+
+ // The smooth scroll may have caused us to reach the end of our scroll range.
+ // This can happen if either the layout.css.scroll-behavior.damping-ratio
+ // preference is set to less than 1 (underdamped) or if a smooth scroll
+ // inherits velocity from a fling gesture.
+ if (!IsZero(overscroll)) {
+ // Hand off a fling with the remaining momentum to the next APZC in the
+ // overscroll handoff chain.
+
+ // We may have reached the end of the scroll range along one axis but
+ // not the other. In such a case we only want to hand off the relevant
+ // component of the fling.
+ if (FuzzyEqualsAdditive(overscroll.x, 0.0f, COORDINATE_EPSILON)) {
+ velocity.x = 0;
+ } else if (FuzzyEqualsAdditive(overscroll.y, 0.0f, COORDINATE_EPSILON)) {
+ velocity.y = 0;
+ }
+
+ // To hand off the fling, we attempt to find a target APZC and start a new
+ // fling with the same velocity on that APZC. For simplicity, the actual
+ // overscroll of the current sample is discarded rather than being handed
+ // off. The compositor should sample animations sufficiently frequently
+ // that this is not noticeable. The target APZC is chosen by seeing if
+ // there is an APZC further in the handoff chain which is pannable; if
+ // there isn't, we take the new fling ourselves, entering an overscrolled
+ // state.
+ // Note: APZC is holding mMonitor, so directly calling
+ // HandleSmoothScrollOverscroll() (which acquires the tree lock) would violate
+ // the lock ordering. Instead we schedule HandleSmoothScrollOverscroll() to be
+ // called after mMonitor is released.
+ mDeferredTasks.AppendElement(
+ NewRunnableMethod<ParentLayerPoint>(&mApzc,
+ &AsyncPanZoomController::HandleSmoothScrollOverscroll,
+ velocity));
+ return false;
+ }
+
+ return true;
+ }
+
+ void SetDestination(const nsPoint& aNewDestination) {
+ mXAxisModel.SetDestination(static_cast<int32_t>(aNewDestination.x));
+ mYAxisModel.SetDestination(static_cast<int32_t>(aNewDestination.y));
+ }
+
+ CSSPoint GetDestination() const {
+ return CSSPoint::FromAppUnits(
+ nsPoint(mXAxisModel.GetDestination(), mYAxisModel.GetDestination()));
+ }
+
+ SmoothScrollAnimation* AsSmoothScrollAnimation() override {
+ return this;
+ }
+
+private:
+ AsyncPanZoomController& mApzc;
+ AxisPhysicsMSDModel mXAxisModel, mYAxisModel;
+};
+
+/*static*/ void
+AsyncPanZoomController::InitializeGlobalState()
+{
+ static bool sInitialized = false;
+ if (sInitialized)
+ return;
+ sInitialized = true;
+
+ MOZ_ASSERT(NS_IsMainThread());
+
+ gZoomAnimationFunction = new ComputedTimingFunction();
+ gZoomAnimationFunction->Init(
+ nsTimingFunction(NS_STYLE_TRANSITION_TIMING_FUNCTION_EASE));
+ ClearOnShutdown(&gZoomAnimationFunction);
+ gVelocityCurveFunction = new ComputedTimingFunction();
+ gVelocityCurveFunction->Init(
+ nsTimingFunction(gfxPrefs::APZCurveFunctionX1(),
+ gfxPrefs::APZCurveFunctionY2(),
+ gfxPrefs::APZCurveFunctionX2(),
+ gfxPrefs::APZCurveFunctionY2()));
+ ClearOnShutdown(&gVelocityCurveFunction);
+
+ uint64_t sysmem = PR_GetPhysicalMemorySize();
+ uint64_t threshold = 1LL << 32; // 4 GB in bytes
+ gIsHighMemSystem = sysmem >= threshold;
+}
+
+AsyncPanZoomController::AsyncPanZoomController(uint64_t aLayersId,
+ APZCTreeManager* aTreeManager,
+ const RefPtr<InputQueue>& aInputQueue,
+ GeckoContentController* aGeckoContentController,
+ GestureBehavior aGestures)
+ : mLayersId(aLayersId),
+ mGeckoContentController(aGeckoContentController),
+ mRefPtrMonitor("RefPtrMonitor"),
+ // mTreeManager must be initialized before GetFrameTime() is called
+ mTreeManager(aTreeManager),
+ mFrameMetrics(mScrollMetadata.GetMetrics()),
+ mMonitor("AsyncPanZoomController"),
+ mLastContentPaintMetrics(mLastContentPaintMetadata.GetMetrics()),
+ mX(this),
+ mY(this),
+ mPanDirRestricted(false),
+ mZoomConstraints(false, false,
+ mFrameMetrics.GetDevPixelsPerCSSPixel() * kViewportMinScale / ParentLayerToScreenScale(1),
+ mFrameMetrics.GetDevPixelsPerCSSPixel() * kViewportMaxScale / ParentLayerToScreenScale(1)),
+ mLastSampleTime(GetFrameTime()),
+ mLastCheckerboardReport(GetFrameTime()),
+ mOverscrollEffect(MakeUnique<OverscrollEffect>(*this)),
+ mState(NOTHING),
+ mNotificationBlockers(0),
+ mInputQueue(aInputQueue),
+ mPinchPaintTimerSet(false),
+ mAPZCId(sAsyncPanZoomControllerCount++),
+ mSharedLock(nullptr),
+ mAsyncTransformAppliedToContent(false),
+ mCheckerboardEventLock("APZCBELock")
+{
+ if (aGestures == USE_GESTURE_DETECTOR) {
+ mGestureEventListener = new GestureEventListener(this);
+ }
+}
+
+AsyncPanZoomController::~AsyncPanZoomController()
+{
+ MOZ_ASSERT(IsDestroyed());
+}
+
+PlatformSpecificStateBase*
+AsyncPanZoomController::GetPlatformSpecificState()
+{
+ if (!mPlatformSpecificState) {
+ mPlatformSpecificState = MakeUnique<PlatformSpecificState>();
+ }
+ return mPlatformSpecificState.get();
+}
+
+already_AddRefed<GeckoContentController>
+AsyncPanZoomController::GetGeckoContentController() const {
+ MonitorAutoLock lock(mRefPtrMonitor);
+ RefPtr<GeckoContentController> controller = mGeckoContentController;
+ return controller.forget();
+}
+
+already_AddRefed<GestureEventListener>
+AsyncPanZoomController::GetGestureEventListener() const {
+ MonitorAutoLock lock(mRefPtrMonitor);
+ RefPtr<GestureEventListener> listener = mGestureEventListener;
+ return listener.forget();
+}
+
+const RefPtr<InputQueue>&
+AsyncPanZoomController::GetInputQueue() const {
+ return mInputQueue;
+}
+
+void
+AsyncPanZoomController::Destroy()
+{
+ APZThreadUtils::AssertOnCompositorThread();
+
+ CancelAnimation(CancelAnimationFlags::ScrollSnap);
+
+ { // scope the lock
+ MonitorAutoLock lock(mRefPtrMonitor);
+ mGeckoContentController = nullptr;
+ mGestureEventListener = nullptr;
+ }
+ mParent = nullptr;
+ mTreeManager = nullptr;
+
+ // Only send the release message if the SharedFrameMetrics has been created.
+ if (mMetricsSharingController && mSharedFrameMetricsBuffer) {
+ Unused << mMetricsSharingController->StopSharingMetrics(mFrameMetrics.GetScrollId(), mAPZCId);
+ }
+
+ { // scope the lock
+ ReentrantMonitorAutoEnter lock(mMonitor);
+ mSharedFrameMetricsBuffer = nullptr;
+ delete mSharedLock;
+ mSharedLock = nullptr;
+ }
+}
+
+bool
+AsyncPanZoomController::IsDestroyed() const
+{
+ return mTreeManager == nullptr;
+}
+
+/* static */ScreenCoord
+AsyncPanZoomController::GetTouchStartTolerance()
+{
+ return (gfxPrefs::APZTouchStartTolerance() * APZCTreeManager::GetDPI());
+}
+
+/* static */AsyncPanZoomController::AxisLockMode AsyncPanZoomController::GetAxisLockMode()
+{
+ return static_cast<AxisLockMode>(gfxPrefs::APZAxisLockMode());
+}
+
+bool
+AsyncPanZoomController::ArePointerEventsConsumable(TouchBlockState* aBlock, uint32_t aTouchPoints) {
+ if (aTouchPoints == 0) {
+ // Cant' do anything with zero touch points
+ return false;
+ }
+
+ // This logic is simplified, erring on the side of returning true
+ // if we're not sure. It's safer to pretend that we can consume the
+ // event and then not be able to than vice-versa.
+ // We could probably enhance this logic to determine things like "we're
+ // not pannable, so we can only zoom in, and the zoom is already maxed
+ // out, so we're not zoomable either" but no need for that at this point.
+
+ bool pannable = aBlock->GetOverscrollHandoffChain()->CanBePanned(this);
+ bool zoomable = mZoomConstraints.mAllowZoom;
+
+ pannable &= (aBlock->TouchActionAllowsPanningX() || aBlock->TouchActionAllowsPanningY());
+ zoomable &= (aBlock->TouchActionAllowsPinchZoom());
+
+ // XXX once we fix bug 1031443, consumable should be assigned
+ // pannable || zoomable if aTouchPoints > 1.
+ bool consumable = (aTouchPoints == 1 ? pannable : zoomable);
+ if (!consumable) {
+ return false;
+ }
+
+ return true;
+}
+
+template <typename Units>
+static CoordTyped<Units> GetAxisStart(AsyncDragMetrics::DragDirection aDir, const PointTyped<Units>& aValue) {
+ if (aDir == AsyncDragMetrics::HORIZONTAL) {
+ return aValue.x;
+ } else {
+ return aValue.y;
+ }
+}
+
+template <typename Units>
+static CoordTyped<Units> GetAxisStart(AsyncDragMetrics::DragDirection aDir, const RectTyped<Units>& aValue) {
+ if (aDir == AsyncDragMetrics::HORIZONTAL) {
+ return aValue.x;
+ } else {
+ return aValue.y;
+ }
+}
+
+template <typename Units>
+static IntCoordTyped<Units> GetAxisStart(AsyncDragMetrics::DragDirection aDir, const IntRectTyped<Units>& aValue) {
+ if (aDir == AsyncDragMetrics::HORIZONTAL) {
+ return aValue.x;
+ } else {
+ return aValue.y;
+ }
+}
+
+template <typename Units>
+static IntCoordTyped<Units> GetAxisEnd(AsyncDragMetrics::DragDirection aDir, const IntRectTyped<Units>& aValue) {
+ if (aDir == AsyncDragMetrics::HORIZONTAL) {
+ return aValue.x + aValue.width;
+ } else {
+ return aValue.y + aValue.height;
+ }
+}
+
+template <typename Units>
+static CoordTyped<Units> GetAxisSize(AsyncDragMetrics::DragDirection aDir, const RectTyped<Units>& aValue) {
+ if (aDir == AsyncDragMetrics::HORIZONTAL) {
+ return aValue.width;
+ } else {
+ return aValue.height;
+ }
+}
+
+template <typename FromUnits, typename ToUnits>
+static float GetAxisScale(AsyncDragMetrics::DragDirection aDir, const ScaleFactors2D<FromUnits, ToUnits>& aValue) {
+ if (aDir == AsyncDragMetrics::HORIZONTAL) {
+ return aValue.xScale;
+ } else {
+ return aValue.yScale;
+ }
+}
+
+nsEventStatus AsyncPanZoomController::HandleDragEvent(const MouseInput& aEvent,
+ const AsyncDragMetrics& aDragMetrics)
+{
+ if (!gfxPrefs::APZDragEnabled()) {
+ return nsEventStatus_eIgnore;
+ }
+
+ if (!GetApzcTreeManager()) {
+ return nsEventStatus_eConsumeNoDefault;
+ }
+
+ RefPtr<HitTestingTreeNode> node =
+ GetApzcTreeManager()->FindScrollNode(aDragMetrics);
+ if (!node) {
+ return nsEventStatus_eConsumeNoDefault;
+ }
+
+ mozilla::Telemetry::Accumulate(mozilla::Telemetry::SCROLL_INPUT_METHODS,
+ (uint32_t) ScrollInputMethod::ApzScrollbarDrag);
+
+ ReentrantMonitorAutoEnter lock(mMonitor);
+ CSSPoint scrollFramePoint = aEvent.mLocalOrigin / GetFrameMetrics().GetZoom();
+ // The scrollbar can be transformed with the frame but the pres shell
+ // resolution is only applied to the scroll frame.
+ CSSPoint scrollbarPoint = scrollFramePoint * mFrameMetrics.GetPresShellResolution();
+ CSSRect cssCompositionBound = mFrameMetrics.CalculateCompositedRectInCssPixels();
+
+ CSSCoord mousePosition = GetAxisStart(aDragMetrics.mDirection, scrollbarPoint) -
+ CSSCoord(aDragMetrics.mScrollbarDragOffset) -
+ GetAxisStart(aDragMetrics.mDirection, cssCompositionBound) -
+ CSSCoord(GetAxisStart(aDragMetrics.mDirection, aDragMetrics.mScrollTrack));
+
+ CSSCoord scrollMax = CSSCoord(GetAxisEnd(aDragMetrics.mDirection, aDragMetrics.mScrollTrack));
+ scrollMax -= node->GetScrollSize() /
+ GetAxisScale(aDragMetrics.mDirection, mFrameMetrics.GetZoom()) *
+ mFrameMetrics.GetPresShellResolution();
+
+ float scrollPercent = mousePosition / scrollMax;
+
+ CSSCoord minScrollPosition =
+ GetAxisStart(aDragMetrics.mDirection, mFrameMetrics.GetScrollableRect().TopLeft());
+ CSSCoord maxScrollPosition =
+ GetAxisSize(aDragMetrics.mDirection, mFrameMetrics.GetScrollableRect()) -
+ GetAxisSize(aDragMetrics.mDirection, cssCompositionBound);
+ CSSCoord scrollPosition = scrollPercent * maxScrollPosition;
+
+ scrollPosition = std::max(scrollPosition, minScrollPosition);
+ scrollPosition = std::min(scrollPosition, maxScrollPosition);
+
+ CSSPoint scrollOffset = mFrameMetrics.GetScrollOffset();
+ if (aDragMetrics.mDirection == AsyncDragMetrics::HORIZONTAL) {
+ scrollOffset.x = scrollPosition;
+ } else {
+ scrollOffset.y = scrollPosition;
+ }
+ mFrameMetrics.SetScrollOffset(scrollOffset);
+ ScheduleCompositeAndMaybeRepaint();
+ UpdateSharedCompositorFrameMetrics();
+
+ return nsEventStatus_eConsumeNoDefault;
+}
+
+nsEventStatus AsyncPanZoomController::HandleInputEvent(const InputData& aEvent,
+ const ScreenToParentLayerMatrix4x4& aTransformToApzc) {
+ APZThreadUtils::AssertOnControllerThread();
+
+ nsEventStatus rv = nsEventStatus_eIgnore;
+
+ switch (aEvent.mInputType) {
+ case MULTITOUCH_INPUT: {
+ MultiTouchInput multiTouchInput = aEvent.AsMultiTouchInput();
+ if (!multiTouchInput.TransformToLocal(aTransformToApzc)) {
+ return rv;
+ }
+
+ RefPtr<GestureEventListener> listener = GetGestureEventListener();
+ if (listener) {
+ rv = listener->HandleInputEvent(multiTouchInput);
+ if (rv == nsEventStatus_eConsumeNoDefault) {
+ return rv;
+ }
+ }
+
+ switch (multiTouchInput.mType) {
+ case MultiTouchInput::MULTITOUCH_START: rv = OnTouchStart(multiTouchInput); break;
+ case MultiTouchInput::MULTITOUCH_MOVE: rv = OnTouchMove(multiTouchInput); break;
+ case MultiTouchInput::MULTITOUCH_END: rv = OnTouchEnd(multiTouchInput); break;
+ case MultiTouchInput::MULTITOUCH_CANCEL: rv = OnTouchCancel(multiTouchInput); break;
+ default: NS_WARNING("Unhandled multitouch"); break;
+ }
+ break;
+ }
+ case PANGESTURE_INPUT: {
+ PanGestureInput panGestureInput = aEvent.AsPanGestureInput();
+ if (!panGestureInput.TransformToLocal(aTransformToApzc)) {
+ return rv;
+ }
+
+ switch (panGestureInput.mType) {
+ case PanGestureInput::PANGESTURE_MAYSTART: rv = OnPanMayBegin(panGestureInput); break;
+ case PanGestureInput::PANGESTURE_CANCELLED: rv = OnPanCancelled(panGestureInput); break;
+ case PanGestureInput::PANGESTURE_START: rv = OnPanBegin(panGestureInput); break;
+ case PanGestureInput::PANGESTURE_PAN: rv = OnPan(panGestureInput, true); break;
+ case PanGestureInput::PANGESTURE_END: rv = OnPanEnd(panGestureInput); break;
+ case PanGestureInput::PANGESTURE_MOMENTUMSTART: rv = OnPanMomentumStart(panGestureInput); break;
+ case PanGestureInput::PANGESTURE_MOMENTUMPAN: rv = OnPan(panGestureInput, false); break;
+ case PanGestureInput::PANGESTURE_MOMENTUMEND: rv = OnPanMomentumEnd(panGestureInput); break;
+ default: NS_WARNING("Unhandled pan gesture"); break;
+ }
+ break;
+ }
+ case MOUSE_INPUT: {
+ MouseInput mouseInput = aEvent.AsMouseInput();
+ if (!mouseInput.TransformToLocal(aTransformToApzc)) {
+ return rv;
+ }
+
+ // TODO Need to implement blocks to properly handle this.
+ //rv = HandleDragEvent(mouseInput, dragMetrics);
+ break;
+ }
+ case SCROLLWHEEL_INPUT: {
+ ScrollWheelInput scrollInput = aEvent.AsScrollWheelInput();
+ if (!scrollInput.TransformToLocal(aTransformToApzc)) {
+ return rv;
+ }
+
+ rv = OnScrollWheel(scrollInput);
+ break;
+ }
+ case PINCHGESTURE_INPUT: {
+ PinchGestureInput pinchInput = aEvent.AsPinchGestureInput();
+ if (!pinchInput.TransformToLocal(aTransformToApzc)) {
+ return rv;
+ }
+
+ rv = HandleGestureEvent(pinchInput);
+ break;
+ }
+ case TAPGESTURE_INPUT: {
+ TapGestureInput tapInput = aEvent.AsTapGestureInput();
+ if (!tapInput.TransformToLocal(aTransformToApzc)) {
+ return rv;
+ }
+
+ rv = HandleGestureEvent(tapInput);
+ break;
+ }
+ default: NS_WARNING("Unhandled input event type"); break;
+ }
+
+ return rv;
+}
+
+nsEventStatus AsyncPanZoomController::HandleGestureEvent(const InputData& aEvent)
+{
+ APZThreadUtils::AssertOnControllerThread();
+
+ nsEventStatus rv = nsEventStatus_eIgnore;
+
+ switch (aEvent.mInputType) {
+ case PINCHGESTURE_INPUT: {
+ const PinchGestureInput& pinchGestureInput = aEvent.AsPinchGestureInput();
+ switch (pinchGestureInput.mType) {
+ case PinchGestureInput::PINCHGESTURE_START: rv = OnScaleBegin(pinchGestureInput); break;
+ case PinchGestureInput::PINCHGESTURE_SCALE: rv = OnScale(pinchGestureInput); break;
+ case PinchGestureInput::PINCHGESTURE_END: rv = OnScaleEnd(pinchGestureInput); break;
+ default: NS_WARNING("Unhandled pinch gesture"); break;
+ }
+ break;
+ }
+ case TAPGESTURE_INPUT: {
+ const TapGestureInput& tapGestureInput = aEvent.AsTapGestureInput();
+ switch (tapGestureInput.mType) {
+ case TapGestureInput::TAPGESTURE_LONG: rv = OnLongPress(tapGestureInput); break;
+ case TapGestureInput::TAPGESTURE_LONG_UP: rv = OnLongPressUp(tapGestureInput); break;
+ case TapGestureInput::TAPGESTURE_UP: rv = OnSingleTapUp(tapGestureInput); break;
+ case TapGestureInput::TAPGESTURE_CONFIRMED: rv = OnSingleTapConfirmed(tapGestureInput); break;
+ case TapGestureInput::TAPGESTURE_DOUBLE: rv = OnDoubleTap(tapGestureInput); break;
+ case TapGestureInput::TAPGESTURE_SECOND: rv = OnSecondTap(tapGestureInput); break;
+ case TapGestureInput::TAPGESTURE_CANCEL: rv = OnCancelTap(tapGestureInput); break;
+ default: NS_WARNING("Unhandled tap gesture"); break;
+ }
+ break;
+ }
+ default: NS_WARNING("Unhandled input event"); break;
+ }
+
+ return rv;
+}
+
+void AsyncPanZoomController::HandleTouchVelocity(uint32_t aTimesampMs, float aSpeedY)
+{
+ mY.HandleTouchVelocity(aTimesampMs, aSpeedY);
+}
+
+nsEventStatus AsyncPanZoomController::OnTouchStart(const MultiTouchInput& aEvent) {
+ APZC_LOG("%p got a touch-start in state %d\n", this, mState);
+ mPanDirRestricted = false;
+ ParentLayerPoint point = GetFirstTouchPoint(aEvent);
+
+ switch (mState) {
+ case FLING:
+ case ANIMATING_ZOOM:
+ case SMOOTH_SCROLL:
+ case OVERSCROLL_ANIMATION:
+ case WHEEL_SCROLL:
+ case PAN_MOMENTUM:
+ MOZ_ASSERT(GetCurrentTouchBlock());
+ GetCurrentTouchBlock()->GetOverscrollHandoffChain()->CancelAnimations(ExcludeOverscroll);
+ MOZ_FALLTHROUGH;
+ case NOTHING: {
+ mX.StartTouch(point.x, aEvent.mTime);
+ mY.StartTouch(point.y, aEvent.mTime);
+ if (RefPtr<GeckoContentController> controller = GetGeckoContentController()) {
+ MOZ_ASSERT(GetCurrentTouchBlock());
+ controller->NotifyAPZStateChange(
+ GetGuid(), APZStateChange::eStartTouch,
+ GetCurrentTouchBlock()->GetOverscrollHandoffChain()->CanBePanned(this));
+ }
+ SetState(TOUCHING);
+ break;
+ }
+ case TOUCHING:
+ case PANNING:
+ case PANNING_LOCKED_X:
+ case PANNING_LOCKED_Y:
+ case PINCHING:
+ NS_WARNING("Received impossible touch in OnTouchStart");
+ break;
+ default:
+ NS_WARNING("Unhandled case in OnTouchStart");
+ break;
+ }
+
+ return nsEventStatus_eConsumeNoDefault;
+}
+
+nsEventStatus AsyncPanZoomController::OnTouchMove(const MultiTouchInput& aEvent) {
+ APZC_LOG("%p got a touch-move in state %d\n", this, mState);
+ switch (mState) {
+ case FLING:
+ case SMOOTH_SCROLL:
+ case NOTHING:
+ case ANIMATING_ZOOM:
+ // May happen if the user double-taps and drags without lifting after the
+ // second tap. Ignore the move if this happens.
+ return nsEventStatus_eIgnore;
+
+ case TOUCHING: {
+ ScreenCoord panThreshold = GetTouchStartTolerance();
+ UpdateWithTouchAtDevicePoint(aEvent);
+
+ if (PanDistance() < panThreshold) {
+ return nsEventStatus_eIgnore;
+ }
+
+ MOZ_ASSERT(GetCurrentTouchBlock());
+ if (gfxPrefs::TouchActionEnabled() && GetCurrentTouchBlock()->TouchActionAllowsPanningXY()) {
+ // User tries to trigger a touch behavior. If allowed touch behavior is vertical pan
+ // + horizontal pan (touch-action value is equal to AUTO) we can return ConsumeNoDefault
+ // status immediately to trigger cancel event further. It should happen independent of
+ // the parent type (whether it is scrolling or not).
+ StartPanning(aEvent);
+ return nsEventStatus_eConsumeNoDefault;
+ }
+
+ return StartPanning(aEvent);
+ }
+
+ case PANNING:
+ case PANNING_LOCKED_X:
+ case PANNING_LOCKED_Y:
+ case PAN_MOMENTUM:
+ TrackTouch(aEvent);
+ return nsEventStatus_eConsumeNoDefault;
+
+ case PINCHING:
+ // The scale gesture listener should have handled this.
+ NS_WARNING("Gesture listener should have handled pinching in OnTouchMove.");
+ return nsEventStatus_eIgnore;
+
+ case WHEEL_SCROLL:
+ case OVERSCROLL_ANIMATION:
+ // Should not receive a touch-move in the OVERSCROLL_ANIMATION state
+ // as touch blocks that begin in an overscrolled state cancel the
+ // animation. The same is true for wheel scroll animations.
+ NS_WARNING("Received impossible touch in OnTouchMove");
+ break;
+ }
+
+ return nsEventStatus_eConsumeNoDefault;
+}
+
+nsEventStatus AsyncPanZoomController::OnTouchEnd(const MultiTouchInput& aEvent) {
+ APZC_LOG("%p got a touch-end in state %d\n", this, mState);
+
+ RefPtr<GeckoContentController> controller = GetGeckoContentController();
+ if (controller) {
+ controller->SetScrollingRootContent(false);
+ }
+
+ OnTouchEndOrCancel();
+
+ // In case no touch behavior triggered previously we can avoid sending
+ // scroll events or requesting content repaint. This condition is added
+ // to make tests consistent - in case touch-action is NONE (and therefore
+ // no pans/zooms can be performed) we expected neither scroll or repaint
+ // events.
+ if (mState != NOTHING) {
+ ReentrantMonitorAutoEnter lock(mMonitor);
+ }
+
+ switch (mState) {
+ case FLING:
+ // Should never happen.
+ NS_WARNING("Received impossible touch end in OnTouchEnd.");
+ MOZ_FALLTHROUGH;
+ case ANIMATING_ZOOM:
+ case SMOOTH_SCROLL:
+ case NOTHING:
+ // May happen if the user double-taps and drags without lifting after the
+ // second tap. Ignore if this happens.
+ return nsEventStatus_eIgnore;
+
+ case TOUCHING:
+ // We may have some velocity stored on the axis from move events
+ // that were not big enough to trigger scrolling. Clear that out.
+ mX.SetVelocity(0);
+ mY.SetVelocity(0);
+ MOZ_ASSERT(GetCurrentTouchBlock());
+ APZC_LOG("%p still has %u touch points active\n", this,
+ GetCurrentTouchBlock()->GetActiveTouchCount());
+ // In cases where the user is panning, then taps the second finger without
+ // entering a pinch, we will arrive here when the second finger is lifted.
+ // However the first finger is still down so we want to remain in state
+ // TOUCHING.
+ if (GetCurrentTouchBlock()->GetActiveTouchCount() == 0) {
+ // It's possible we may be overscrolled if the user tapped during a
+ // previous overscroll pan. Make sure to snap back in this situation.
+ // An ancestor APZC could be overscrolled instead of this APZC, so
+ // walk the handoff chain as well.
+ GetCurrentTouchBlock()->GetOverscrollHandoffChain()->SnapBackOverscrolledApzc(this);
+ // SnapBackOverscrolledApzc() will put any APZC it causes to snap back
+ // into the OVERSCROLL_ANIMATION state. If that's not us, since we're
+ // done TOUCHING enter the NOTHING state.
+ if (mState != OVERSCROLL_ANIMATION) {
+ SetState(NOTHING);
+ }
+ }
+ return nsEventStatus_eIgnore;
+
+ case PANNING:
+ case PANNING_LOCKED_X:
+ case PANNING_LOCKED_Y:
+ case PAN_MOMENTUM:
+ {
+ MOZ_ASSERT(GetCurrentTouchBlock());
+ GetCurrentTouchBlock()->GetOverscrollHandoffChain()->FlushRepaints();
+ mX.EndTouch(aEvent.mTime);
+ mY.EndTouch(aEvent.mTime);
+ ParentLayerPoint flingVelocity = GetVelocityVector();
+ // Clear our velocities; if DispatchFling() gives the fling to us,
+ // the fling velocity gets *added* to our existing velocity in
+ // AcceptFling().
+ mX.SetVelocity(0);
+ mY.SetVelocity(0);
+ // Clear our state so that we don't stay in the PANNING state
+ // if DispatchFling() gives the fling to somone else. However,
+ // don't send the state change notification until we've determined
+ // what our final state is to avoid notification churn.
+ StateChangeNotificationBlocker blocker(this);
+ SetState(NOTHING);
+
+ APZC_LOG("%p starting a fling animation if %f >= %f\n", this,
+ flingVelocity.Length().value, gfxPrefs::APZFlingMinVelocityThreshold());
+
+ if (flingVelocity.Length() < gfxPrefs::APZFlingMinVelocityThreshold()) {
+ return nsEventStatus_eConsumeNoDefault;
+ }
+
+ // Make a local copy of the tree manager pointer and check that it's not
+ // null before calling DispatchFling(). This is necessary because Destroy(),
+ // which nulls out mTreeManager, could be called concurrently.
+ if (APZCTreeManager* treeManagerLocal = GetApzcTreeManager()) {
+ FlingHandoffState handoffState{flingVelocity,
+ GetCurrentTouchBlock()->GetOverscrollHandoffChain(),
+ false /* not handoff */,
+ GetCurrentTouchBlock()->GetScrolledApzc()};
+ treeManagerLocal->DispatchFling(this, handoffState);
+ }
+ return nsEventStatus_eConsumeNoDefault;
+ }
+ case PINCHING:
+ SetState(NOTHING);
+ // Scale gesture listener should have handled this.
+ NS_WARNING("Gesture listener should have handled pinching in OnTouchEnd.");
+ return nsEventStatus_eIgnore;
+
+ case WHEEL_SCROLL:
+ case OVERSCROLL_ANIMATION:
+ // Should not receive a touch-end in the OVERSCROLL_ANIMATION state
+ // as touch blocks that begin in an overscrolled state cancel the
+ // animation. The same is true for WHEEL_SCROLL.
+ NS_WARNING("Received impossible touch in OnTouchEnd");
+ break;
+ }
+
+ return nsEventStatus_eConsumeNoDefault;
+}
+
+nsEventStatus AsyncPanZoomController::OnTouchCancel(const MultiTouchInput& aEvent) {
+ APZC_LOG("%p got a touch-cancel in state %d\n", this, mState);
+ OnTouchEndOrCancel();
+ CancelAnimationAndGestureState();
+ return nsEventStatus_eConsumeNoDefault;
+}
+
+nsEventStatus AsyncPanZoomController::OnScaleBegin(const PinchGestureInput& aEvent) {
+ APZC_LOG("%p got a scale-begin in state %d\n", this, mState);
+
+ 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.
+ if (HasReadyTouchBlock() && !GetCurrentTouchBlock()->TouchActionAllowsPinchZoom()) {
+ return nsEventStatus_eIgnore;
+ }
+
+ // For platforms that don't support APZ zooming, dispatch a message to the
+ // content controller, it may want to do something else with this gesture.
+ if (!gfxPrefs::APZAllowZooming()) {
+ if (RefPtr<GeckoContentController> controller = GetGeckoContentController()) {
+ controller->NotifyPinchGesture(aEvent.mType, GetGuid(), 0, aEvent.modifiers);
+ }
+ }
+
+ SetState(PINCHING);
+ mX.SetVelocity(0);
+ mY.SetVelocity(0);
+ mLastZoomFocus = aEvent.mLocalFocusPoint - mFrameMetrics.GetCompositionBounds().TopLeft();
+
+ return nsEventStatus_eConsumeNoDefault;
+}
+
+nsEventStatus AsyncPanZoomController::OnScale(const PinchGestureInput& aEvent) {
+ APZC_LOG("%p got a scale in state %d\n", this, mState);
+
+ if (HasReadyTouchBlock() && !GetCurrentTouchBlock()->TouchActionAllowsPinchZoom()) {
+ return nsEventStatus_eIgnore;
+ }
+
+ if (mState != PINCHING) {
+ return nsEventStatus_eConsumeNoDefault;
+ }
+
+ if (!gfxPrefs::APZAllowZooming()) {
+ if (RefPtr<GeckoContentController> controller = GetGeckoContentController()) {
+ controller->NotifyPinchGesture(aEvent.mType, GetGuid(),
+ ViewAs<LayoutDevicePixel>(aEvent.mCurrentSpan - aEvent.mPreviousSpan,
+ PixelCastJustification::LayoutDeviceIsParentLayerForRCDRSF),
+ aEvent.modifiers);
+ }
+ }
+
+ // Only the root APZC is zoomable, and the root APZC is not allowed to have
+ // different x and y scales. If it did, the calculations in this function
+ // would have to be adjusted (as e.g. it would no longer be valid to take
+ // the minimum or maximum of the ratios of the widths and heights of the
+ // page rect and the composition bounds).
+ MOZ_ASSERT(mFrameMetrics.IsRootContent());
+ MOZ_ASSERT(mFrameMetrics.GetZoom().AreScalesSame());
+
+ {
+ ReentrantMonitorAutoEnter lock(mMonitor);
+
+ 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);
+ focusChange.y -= mY.DisplacementWillOverscrollAmount(focusChange.y);
+ ScrollBy(focusChange / userZoom);
+
+ // If the span is zero or close to it, we don't want to process this zoom
+ // change because we're going to get wonky numbers for the spanRatio. So
+ // let's bail out here. Note that we do this after the focus-change-scroll
+ // above, so that if we have a pinch with zero span but changing focus,
+ // such as generated by some Synaptics touchpads on Windows, we still
+ // scroll properly.
+ float prevSpan = aEvent.mPreviousSpan;
+ if (fabsf(prevSpan) <= EPSILON || fabsf(aEvent.mCurrentSpan) <= EPSILON) {
+ // We might have done a nonzero ScrollBy above, so update metrics and
+ // repaint/recomposite
+ ScheduleCompositeAndMaybeRepaint();
+ UpdateSharedCompositorFrameMetrics();
+ return nsEventStatus_eConsumeNoDefault;
+ }
+ float spanRatio = aEvent.mCurrentSpan / aEvent.mPreviousSpan;
+
+ // When we zoom in with focus, we can zoom too much towards the boundaries
+ // that we actually go over them. These are the needed displacements along
+ // either axis such that we don't overscroll the boundaries when zooming.
+ CSSPoint neededDisplacement;
+
+ CSSToParentLayerScale realMinZoom = mZoomConstraints.mMinZoom;
+ CSSToParentLayerScale realMaxZoom = mZoomConstraints.mMaxZoom;
+ realMinZoom.scale = std::max(realMinZoom.scale,
+ mFrameMetrics.GetCompositionBounds().width / mFrameMetrics.GetScrollableRect().width);
+ realMinZoom.scale = std::max(realMinZoom.scale,
+ mFrameMetrics.GetCompositionBounds().height / mFrameMetrics.GetScrollableRect().height);
+ if (realMaxZoom < realMinZoom) {
+ realMaxZoom = realMinZoom;
+ }
+
+ bool doScale = (spanRatio > 1.0 && userZoom < realMaxZoom) ||
+ (spanRatio < 1.0 && userZoom > realMinZoom);
+
+ if (!mZoomConstraints.mAllowZoom) {
+ doScale = false;
+ }
+
+ if (doScale) {
+ spanRatio = clamped(spanRatio,
+ realMinZoom.scale / userZoom.scale,
+ realMaxZoom.scale / userZoom.scale);
+
+ // Note that the spanRatio here should never put us into OVERSCROLL_BOTH because
+ // up above we clamped it.
+ neededDisplacement.x = -mX.ScaleWillOverscrollAmount(spanRatio, cssFocusPoint.x);
+ neededDisplacement.y = -mY.ScaleWillOverscrollAmount(spanRatio, cssFocusPoint.y);
+
+ ScaleWithFocus(spanRatio, cssFocusPoint);
+
+ if (neededDisplacement != CSSPoint()) {
+ ScrollBy(neededDisplacement);
+ }
+
+ // We don't want to redraw on every scale, so throttle it.
+ if (!mPinchPaintTimerSet) {
+ const int delay = gfxPrefs::APZScaleRepaintDelay();
+ if (delay >= 0) {
+ if (RefPtr<GeckoContentController> controller = GetGeckoContentController()) {
+ mPinchPaintTimerSet = true;
+ controller->PostDelayedTask(
+ NewRunnableMethod(this,
+ &AsyncPanZoomController::DoDelayedRequestContentRepaint),
+ delay);
+ }
+ }
+ }
+
+ UpdateSharedCompositorFrameMetrics();
+ }
+
+ // We did a ScrollBy call above even if we didn't do a scale, so we
+ // should composite for that.
+ ScheduleComposite();
+ }
+
+ return nsEventStatus_eConsumeNoDefault;
+}
+
+nsEventStatus AsyncPanZoomController::OnScaleEnd(const PinchGestureInput& aEvent) {
+ APZC_LOG("%p got a scale-end in state %d\n", this, mState);
+
+ mPinchPaintTimerSet = false;
+
+ if (HasReadyTouchBlock() && !GetCurrentTouchBlock()->TouchActionAllowsPinchZoom()) {
+ return nsEventStatus_eIgnore;
+ }
+
+ if (!gfxPrefs::APZAllowZooming()) {
+ if (RefPtr<GeckoContentController> controller = GetGeckoContentController()) {
+ controller->NotifyPinchGesture(aEvent.mType, GetGuid(), 0, aEvent.modifiers);
+ }
+ }
+
+ SetState(NOTHING);
+
+ {
+ ReentrantMonitorAutoEnter lock(mMonitor);
+ ScheduleComposite();
+ RequestContentRepaint();
+ UpdateSharedCompositorFrameMetrics();
+ }
+
+ // Non-negative focus point would indicate that one finger is still down
+ if (aEvent.mLocalFocusPoint.x != -1 && aEvent.mLocalFocusPoint.y != -1) {
+ mPanDirRestricted = false;
+ mX.StartTouch(aEvent.mLocalFocusPoint.x, aEvent.mTime);
+ mY.StartTouch(aEvent.mLocalFocusPoint.y, aEvent.mTime);
+ SetState(TOUCHING);
+ } else {
+ // Otherwise, handle the fingers being lifted.
+ ReentrantMonitorAutoEnter lock(mMonitor);
+
+ // We can get into a situation where we are overscrolled at the end of a
+ // pinch if we go into overscroll with a two-finger pan, and then turn
+ // that into a pinch by increasing the span sufficiently. In such a case,
+ // there is no snap-back animation to get us out of overscroll, so we need
+ // to get out of it somehow.
+ // Moreover, in cases of scroll handoff, the overscroll can be on an APZC
+ // further up in the handoff chain rather than on the current APZC, so
+ // we need to clear overscroll along the entire handoff chain.
+ if (HasReadyTouchBlock()) {
+ GetCurrentTouchBlock()->GetOverscrollHandoffChain()->ClearOverscroll();
+ } else {
+ ClearOverscroll();
+ }
+ // Along with clearing the overscroll, we also want to snap to the nearest
+ // snap point as appropriate.
+ ScrollSnap();
+ }
+
+ return nsEventStatus_eConsumeNoDefault;
+}
+
+bool
+AsyncPanZoomController::ConvertToGecko(const ScreenIntPoint& aPoint, LayoutDevicePoint* aOut)
+{
+ if (APZCTreeManager* treeManagerLocal = GetApzcTreeManager()) {
+ ScreenToScreenMatrix4x4 transformScreenToGecko =
+ treeManagerLocal->GetScreenToApzcTransform(this)
+ * treeManagerLocal->GetApzcToGeckoTransform(this);
+
+ Maybe<ScreenIntPoint> layoutPoint = UntransformBy(
+ transformScreenToGecko, aPoint);
+ if (!layoutPoint) {
+ return false;
+ }
+
+ *aOut = LayoutDevicePoint(ViewAs<LayoutDevicePixel>(*layoutPoint,
+ PixelCastJustification::LayoutDeviceIsScreenForUntransformedEvent));
+ return true;
+ }
+ return false;
+}
+
+static bool
+AllowsScrollingMoreThanOnePage(double aMultiplier)
+{
+ const int32_t kMinAllowPageScroll =
+ EventStateManager::MIN_MULTIPLIER_VALUE_ALLOWING_OVER_ONE_PAGE_SCROLL;
+ return Abs(aMultiplier) >= kMinAllowPageScroll;
+}
+
+ParentLayerPoint
+AsyncPanZoomController::GetScrollWheelDelta(const ScrollWheelInput& aEvent) const
+{
+ ParentLayerSize scrollAmount;
+ ParentLayerSize pageScrollSize;
+
+ {
+ // Grab the lock to access the frame metrics.
+ ReentrantMonitorAutoEnter lock(mMonitor);
+ LayoutDeviceIntSize scrollAmountLD = mScrollMetadata.GetLineScrollAmount();
+ LayoutDeviceIntSize pageScrollSizeLD = mScrollMetadata.GetPageScrollAmount();
+ scrollAmount = scrollAmountLD /
+ mFrameMetrics.GetDevPixelsPerCSSPixel() * mFrameMetrics.GetZoom();
+ pageScrollSize = pageScrollSizeLD /
+ mFrameMetrics.GetDevPixelsPerCSSPixel() * mFrameMetrics.GetZoom();
+ }
+
+ ParentLayerPoint delta;
+ switch (aEvent.mDeltaType) {
+ case ScrollWheelInput::SCROLLDELTA_LINE: {
+ delta.x = aEvent.mDeltaX * scrollAmount.width;
+ delta.y = aEvent.mDeltaY * scrollAmount.height;
+ break;
+ }
+ case ScrollWheelInput::SCROLLDELTA_PAGE: {
+ delta.x = aEvent.mDeltaX * pageScrollSize.width;
+ delta.y = aEvent.mDeltaY * pageScrollSize.height;
+ break;
+ }
+ case ScrollWheelInput::SCROLLDELTA_PIXEL: {
+ delta = ToParentLayerCoordinates(ScreenPoint(aEvent.mDeltaX, aEvent.mDeltaY), aEvent.mOrigin);
+ break;
+ }
+ default:
+ MOZ_ASSERT_UNREACHABLE("unexpected scroll delta type");
+ }
+
+ // Apply user-set multipliers.
+ delta.x *= aEvent.mUserDeltaMultiplierX;
+ delta.y *= aEvent.mUserDeltaMultiplierY;
+
+ // For the conditions under which we allow system scroll overrides, see
+ // EventStateManager::DeltaAccumulator::ComputeScrollAmountForDefaultAction
+ // and WheelTransaction::OverrideSystemScrollSpeed. Note that we do *not*
+ // restrict this to the root content, see bug 1217715 for discussion on this.
+ if (gfxPrefs::MouseWheelHasRootScrollDeltaOverride() &&
+ !aEvent.IsCustomizedByUserPrefs() &&
+ aEvent.mDeltaType == ScrollWheelInput::SCROLLDELTA_LINE &&
+ aEvent.mAllowToOverrideSystemScrollSpeed) {
+ delta.x = WidgetWheelEvent::ComputeOverriddenDelta(delta.x, false);
+ delta.y = WidgetWheelEvent::ComputeOverriddenDelta(delta.y, true);
+ }
+
+ // If this is a line scroll, and this event was part of a scroll series, then
+ // it might need extra acceleration. See WheelHandlingHelper.cpp.
+ if (aEvent.mDeltaType == ScrollWheelInput::SCROLLDELTA_LINE &&
+ aEvent.mScrollSeriesNumber > 0)
+ {
+ int32_t start = gfxPrefs::MouseWheelAccelerationStart();
+ if (start >= 0 && aEvent.mScrollSeriesNumber >= uint32_t(start)) {
+ int32_t factor = gfxPrefs::MouseWheelAccelerationFactor();
+ if (factor > 0) {
+ delta.x = ComputeAcceleratedWheelDelta(delta.x, aEvent.mScrollSeriesNumber, factor);
+ delta.y = ComputeAcceleratedWheelDelta(delta.y, aEvent.mScrollSeriesNumber, factor);
+ }
+ }
+ }
+
+ // We shouldn't scroll more than one page at once except when the
+ // user preference is large.
+ if (!AllowsScrollingMoreThanOnePage(aEvent.mUserDeltaMultiplierX) &&
+ Abs(delta.x) > pageScrollSize.width) {
+ delta.x = (delta.x >= 0)
+ ? pageScrollSize.width
+ : -pageScrollSize.width;
+ }
+ if (!AllowsScrollingMoreThanOnePage(aEvent.mUserDeltaMultiplierY) &&
+ Abs(delta.y) > pageScrollSize.height) {
+ delta.y = (delta.y >= 0)
+ ? pageScrollSize.height
+ : -pageScrollSize.height;
+ }
+
+ return delta;
+}
+
+// Return whether or not the underlying layer can be scrolled on either axis.
+bool
+AsyncPanZoomController::CanScroll(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);
+ }
+ if (!delta.x && !delta.y) {
+ return false;
+ }
+
+ return CanScrollWithWheel(delta);
+}
+
+bool
+AsyncPanZoomController::CanScrollWithWheel(const ParentLayerPoint& aDelta) const
+{
+ ReentrantMonitorAutoEnter lock(mMonitor);
+ if (mX.CanScroll(aDelta.x)) {
+ return true;
+ }
+ if (mY.CanScroll(aDelta.y) && mScrollMetadata.AllowVerticalScrollWithWheel()) {
+ return true;
+ }
+ return false;
+}
+
+bool
+AsyncPanZoomController::CanScroll(Layer::ScrollDirection aDirection) const
+{
+ ReentrantMonitorAutoEnter lock(mMonitor);
+ switch (aDirection) {
+ case Layer::HORIZONTAL: return mX.CanScroll();
+ case Layer::VERTICAL: return mY.CanScroll();
+ default: MOZ_ASSERT(false); return false;
+ }
+}
+
+bool
+AsyncPanZoomController::AllowScrollHandoffInCurrentBlock() const
+{
+ bool result = mInputQueue->AllowScrollHandoff();
+ if (!gfxPrefs::APZAllowImmediateHandoff()) {
+ if (InputBlockState* currentBlock = GetCurrentInputBlock()) {
+ // Do not allow handoff beyond the first APZC to scroll.
+ if (currentBlock->GetScrolledApzc() == this) {
+ result = false;
+ }
+ }
+ }
+ return result;
+}
+
+void AsyncPanZoomController::DoDelayedRequestContentRepaint()
+{
+ if (!IsDestroyed() && mPinchPaintTimerSet) {
+ ReentrantMonitorAutoEnter lock(mMonitor);
+ RequestContentRepaint();
+ }
+ mPinchPaintTimerSet = false;
+}
+
+static ScrollInputMethod
+ScrollInputMethodForWheelDeltaType(ScrollWheelInput::ScrollDeltaType aDeltaType)
+{
+ switch (aDeltaType) {
+ case ScrollWheelInput::SCROLLDELTA_LINE: {
+ return ScrollInputMethod::ApzWheelLine;
+ }
+ case ScrollWheelInput::SCROLLDELTA_PAGE: {
+ return ScrollInputMethod::ApzWheelPage;
+ }
+ case ScrollWheelInput::SCROLLDELTA_PIXEL: {
+ return ScrollInputMethod::ApzWheelPixel;
+ }
+ default:
+ MOZ_ASSERT_UNREACHABLE("unexpected scroll delta type");
+ return ScrollInputMethod::ApzWheelLine;
+ }
+}
+
+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() &&
+ gfxPrefs::MouseScrollTestingEnabled()) {
+ if (RefPtr<GeckoContentController> controller = GetGeckoContentController()) {
+ controller->NotifyMozMouseScrollEvent(
+ mFrameMetrics.GetScrollId(),
+ NS_LITERAL_STRING("MozMouseScrollFailed"));
+ }
+ }
+ return nsEventStatus_eConsumeNoDefault;
+ }
+
+ 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));
+
+
+ switch (aEvent.mScrollMode) {
+ case ScrollWheelInput::SCROLLMODE_INSTANT: {
+
+ // Wheel events from "clicky" mouse wheels trigger scroll snapping to the
+ // next snap point. Check for this, and adjust the delta to take into
+ // account the snap point.
+ 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);
+
+ SetState(NOTHING);
+
+ // The calls above handle their own locking; moreover,
+ // ToScreenCoordinates() and CallDispatchScroll() can grab the tree lock.
+ ReentrantMonitorAutoEnter lock(mMonitor);
+ RequestContentRepaint();
+
+ break;
+ }
+
+ case ScrollWheelInput::SCROLLMODE_SMOOTH: {
+ // The lock must be held across the entire update operation, so the
+ // compositor doesn't end the animation before we get a chance to
+ // update it.
+ ReentrantMonitorAutoEnter lock(mMonitor);
+
+ // Perform scroll snapping if appropriate.
+ CSSPoint startPosition = mFrameMetrics.GetScrollOffset();
+ // If we're already in a wheel scroll or smooth scroll animation,
+ // the delta is applied to its destination, not to the current
+ // scroll position. Take this into account when finding a snap point.
+ if (mState == WHEEL_SCROLL) {
+ startPosition = mAnimation->AsWheelScrollAnimation()->GetDestination();
+ } else if (mState == SMOOTH_SCROLL) {
+ startPosition = mAnimation->AsSmoothScrollAnimation()->GetDestination();
+ }
+ if (MaybeAdjustDeltaForScrollSnapping(aEvent, delta, startPosition)) {
+ // If we're scroll snapping, use a smooth scroll animation to get
+ // the desired physics. Note that SmoothScrollTo() will re-use an
+ // existing smooth scroll animation if there is one.
+ APZC_LOG("%p wheel scrolling to snap point %s\n", this, Stringify(startPosition).c_str());
+ SmoothScrollTo(startPosition);
+ break;
+ }
+
+ // Otherwise, use a wheel scroll animation, also reusing one if possible.
+ if (mState != WHEEL_SCROLL) {
+ CancelAnimation();
+ SetState(WHEEL_SCROLL);
+
+ nsPoint initialPosition = CSSPoint::ToAppUnits(mFrameMetrics.GetScrollOffset());
+ StartAnimation(new WheelScrollAnimation(
+ *this, initialPosition, aEvent.mDeltaType));
+ }
+
+ nsPoint deltaInAppUnits =
+ CSSPoint::ToAppUnits(delta / mFrameMetrics.GetZoom());
+ // Cast velocity from ParentLayerPoints/ms to CSSPoints/ms then convert to
+ // appunits/second
+ nsPoint velocity =
+ CSSPoint::ToAppUnits(CSSPoint(mX.GetVelocity(), mY.GetVelocity())) * 1000.0f;
+
+ WheelScrollAnimation* animation = mAnimation->AsWheelScrollAnimation();
+ animation->Update(aEvent.mTimeStamp, deltaInAppUnits, nsSize(velocity.x, velocity.y));
+ break;
+ }
+
+ case ScrollWheelInput::SCROLLMODE_SENTINEL: {
+ MOZ_ASSERT_UNREACHABLE("Invalid ScrollMode.");
+ break;
+ }
+ }
+
+ return nsEventStatus_eConsumeNoDefault;
+}
+
+void
+AsyncPanZoomController::NotifyMozMouseScrollEvent(const nsString& aString) const
+{
+ RefPtr<GeckoContentController> controller = GetGeckoContentController();
+ if (!controller) {
+ return;
+ }
+
+ controller->NotifyMozMouseScrollEvent(mFrameMetrics.GetScrollId(), aString);
+}
+
+nsEventStatus AsyncPanZoomController::OnPanMayBegin(const PanGestureInput& aEvent) {
+ APZC_LOG("%p got a pan-maybegin in state %d\n", this, mState);
+
+ mX.StartTouch(aEvent.mLocalPanStartPoint.x, aEvent.mTime);
+ mY.StartTouch(aEvent.mLocalPanStartPoint.y, aEvent.mTime);
+ MOZ_ASSERT(GetCurrentPanGestureBlock());
+ GetCurrentPanGestureBlock()->GetOverscrollHandoffChain()->CancelAnimations();
+
+ return nsEventStatus_eConsumeNoDefault;
+}
+
+nsEventStatus AsyncPanZoomController::OnPanCancelled(const PanGestureInput& aEvent) {
+ APZC_LOG("%p got a pan-cancelled in state %d\n", this, mState);
+
+ mX.CancelGesture();
+ mY.CancelGesture();
+
+ return nsEventStatus_eConsumeNoDefault;
+}
+
+
+nsEventStatus AsyncPanZoomController::OnPanBegin(const PanGestureInput& aEvent) {
+ APZC_LOG("%p got a pan-begin in state %d\n", this, mState);
+
+ if (mState == SMOOTH_SCROLL) {
+ // SMOOTH_SCROLL scrolls are cancelled by pan gestures.
+ CancelAnimation();
+ }
+
+ mX.StartTouch(aEvent.mLocalPanStartPoint.x, aEvent.mTime);
+ mY.StartTouch(aEvent.mLocalPanStartPoint.y, aEvent.mTime);
+
+ if (GetAxisLockMode() == FREE) {
+ SetState(PANNING);
+ return nsEventStatus_eConsumeNoDefault;
+ }
+
+ float dx = aEvent.mPanDisplacement.x, dy = aEvent.mPanDisplacement.y;
+
+ if (dx || dy) {
+ double angle = atan2(dy, dx); // range [-pi, pi]
+ angle = fabs(angle); // range [0, pi]
+ HandlePanning(angle);
+ } else {
+ SetState(PANNING);
+ }
+
+ // Call into OnPan in order to process any delta included in this event.
+ OnPan(aEvent, true);
+
+ return nsEventStatus_eConsumeNoDefault;
+}
+
+nsEventStatus AsyncPanZoomController::OnPan(const PanGestureInput& aEvent, bool aFingersOnTouchpad) {
+ APZC_LOG("%p got a pan-pan in state %d\n", this, mState);
+
+ if (mState == SMOOTH_SCROLL) {
+ if (!aFingersOnTouchpad) {
+ // When a SMOOTH_SCROLL scroll is being processed on a frame, mouse
+ // wheel and trackpad momentum scroll position updates will not cancel the
+ // SMOOTH_SCROLL scroll animations, enabling scripts that depend on
+ // them to be responsive without forcing the user to wait for the momentum
+ // scrolling to completely stop.
+ return nsEventStatus_eConsumeNoDefault;
+ }
+
+ // SMOOTH_SCROLL scrolls are cancelled by pan gestures.
+ CancelAnimation();
+ }
+
+ if (mState == NOTHING) {
+ // This event block was interrupted by something else. If the user's fingers
+ // are still on on the touchpad we want to resume scrolling, otherwise we
+ // ignore the rest of the scroll gesture.
+ if (!aFingersOnTouchpad) {
+ return nsEventStatus_eConsumeNoDefault;
+ }
+ // Resume / restart the pan.
+ // PanBegin will call back into this function with mState == PANNING.
+ return OnPanBegin(aEvent);
+ }
+
+ // 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();
+
+ // 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.
+ // However, an increase in scroll position means that the scrolled contents
+ // are moved to the left / upwards. Since our simulated "touches" determine
+ // the motion of the scrolled contents, not of the scroll position, they need
+ // to move in the opposite direction of the pan displacement.
+ ParentLayerPoint startPoint = aEvent.mLocalPanStartPoint;
+ ParentLayerPoint endPoint = aEvent.mLocalPanStartPoint - logicalPanDisplacement;
+ CallDispatchScroll(startPoint, endPoint, handoffState);
+
+ return nsEventStatus_eConsumeNoDefault;
+}
+
+nsEventStatus AsyncPanZoomController::OnPanEnd(const PanGestureInput& aEvent) {
+ APZC_LOG("%p got a pan-end in state %d\n", this, mState);
+
+ // Call into OnPan in order to process any delta included in this event.
+ OnPan(aEvent, true);
+
+ mX.EndTouch(aEvent.mTime);
+ mY.EndTouch(aEvent.mTime);
+
+ // Drop any velocity on axes where we don't have room to scroll anyways
+ // (in this APZC, or an APZC further in the handoff chain).
+ // This ensures that we don't enlarge the display port unnecessarily.
+ MOZ_ASSERT(GetCurrentPanGestureBlock());
+ RefPtr<const OverscrollHandoffChain> overscrollHandoffChain =
+ GetCurrentPanGestureBlock()->GetOverscrollHandoffChain();
+ if (!overscrollHandoffChain->CanScrollInDirection(this, Layer::HORIZONTAL)) {
+ mX.SetVelocity(0);
+ }
+ if (!overscrollHandoffChain->CanScrollInDirection(this, Layer::VERTICAL)) {
+ mY.SetVelocity(0);
+ }
+
+ SetState(NOTHING);
+ RequestContentRepaint();
+
+ if (!aEvent.mFollowedByMomentum) {
+ ScrollSnap();
+ }
+
+ return nsEventStatus_eConsumeNoDefault;
+}
+
+nsEventStatus AsyncPanZoomController::OnPanMomentumStart(const PanGestureInput& aEvent) {
+ APZC_LOG("%p got a pan-momentumstart in state %d\n", this, mState);
+
+ if (mState == SMOOTH_SCROLL) {
+ // SMOOTH_SCROLL scrolls are cancelled by pan gestures.
+ CancelAnimation();
+ }
+
+ SetState(PAN_MOMENTUM);
+ ScrollSnapToDestination();
+
+ // Call into OnPan in order to process any delta included in this event.
+ OnPan(aEvent, false);
+
+ return nsEventStatus_eConsumeNoDefault;
+}
+
+nsEventStatus AsyncPanZoomController::OnPanMomentumEnd(const PanGestureInput& aEvent) {
+ APZC_LOG("%p got a pan-momentumend in state %d\n", this, mState);
+
+ // Call into OnPan in order to process any delta included in this event.
+ OnPan(aEvent, false);
+
+ // We need to reset the velocity to zero. We don't really have a "touch"
+ // here because the touch has already ended long before the momentum
+ // animation started, but I guess it doesn't really matter for now.
+ mX.CancelGesture();
+ mY.CancelGesture();
+ SetState(NOTHING);
+
+ RequestContentRepaint();
+
+ return nsEventStatus_eConsumeNoDefault;
+}
+
+nsEventStatus AsyncPanZoomController::OnLongPress(const TapGestureInput& aEvent) {
+ APZC_LOG("%p got a long-press in state %d\n", this, mState);
+ RefPtr<GeckoContentController> controller = GetGeckoContentController();
+ if (controller) {
+ LayoutDevicePoint geckoScreenPoint;
+ if (ConvertToGecko(aEvent.mPoint, &geckoScreenPoint)) {
+ TouchBlockState* touch = GetCurrentTouchBlock();
+ if (!touch) {
+ APZC_LOG("%p dropping long-press because some non-touch block interrupted it\n", this);
+ return nsEventStatus_eIgnore;
+ }
+ if (touch->IsDuringFastFling()) {
+ APZC_LOG("%p dropping long-press because of fast fling\n", this);
+ return nsEventStatus_eIgnore;
+ }
+ uint64_t blockId = GetInputQueue()->InjectNewTouchBlock(this);
+ controller->HandleTap(TapType::eLongTap, geckoScreenPoint, aEvent.modifiers, GetGuid(), blockId);
+ return nsEventStatus_eConsumeNoDefault;
+ }
+ }
+ return nsEventStatus_eIgnore;
+}
+
+nsEventStatus AsyncPanZoomController::OnLongPressUp(const TapGestureInput& aEvent) {
+ APZC_LOG("%p got a long-tap-up in state %d\n", this, mState);
+ return GenerateSingleTap(TapType::eLongTapUp, aEvent.mPoint, aEvent.modifiers);
+}
+
+nsEventStatus AsyncPanZoomController::GenerateSingleTap(TapType aType,
+ const ScreenIntPoint& aPoint, mozilla::Modifiers aModifiers) {
+ RefPtr<GeckoContentController> controller = GetGeckoContentController();
+ if (controller) {
+ LayoutDevicePoint geckoScreenPoint;
+ if (ConvertToGecko(aPoint, &geckoScreenPoint)) {
+ TouchBlockState* touch = GetCurrentTouchBlock();
+ // |touch| may be null in the case where this function is
+ // invoked by GestureEventListener on a timeout. In that case we already
+ // verified that the single tap is allowed so we let it through.
+ // XXX there is a bug here that in such a case the touch block that
+ // generated this tap will not get its mSingleTapOccurred flag set.
+ // See https://bugzilla.mozilla.org/show_bug.cgi?id=1256344#c6
+ if (touch) {
+ if (touch->IsDuringFastFling()) {
+ APZC_LOG("%p dropping single-tap because it was during a fast-fling\n", this);
+ return nsEventStatus_eIgnore;
+ }
+ touch->SetSingleTapOccurred();
+ }
+ // Because this may be being running as part of APZCTreeManager::ReceiveInputEvent,
+ // calling controller->HandleTap directly might mean that content receives
+ // the single tap message before the corresponding touch-up. To avoid that we
+ // schedule the singletap message to run on the next spin of the event loop.
+ // See bug 965381 for the issue this was causing.
+ RefPtr<Runnable> runnable =
+ NewRunnableMethod<TapType, LayoutDevicePoint, mozilla::Modifiers,
+ ScrollableLayerGuid, uint64_t>(controller,
+ &GeckoContentController::HandleTap,
+ aType, geckoScreenPoint,
+ aModifiers, GetGuid(),
+ touch ? touch->GetBlockId() : 0);
+
+ controller->PostDelayedTask(runnable.forget(), 0);
+ return nsEventStatus_eConsumeNoDefault;
+ }
+ }
+ return nsEventStatus_eIgnore;
+}
+
+void AsyncPanZoomController::OnTouchEndOrCancel() {
+ if (RefPtr<GeckoContentController> controller = GetGeckoContentController()) {
+ MOZ_ASSERT(GetCurrentTouchBlock());
+ controller->NotifyAPZStateChange(
+ GetGuid(), APZStateChange::eEndTouch, GetCurrentTouchBlock()->SingleTapOccurred());
+ }
+}
+
+nsEventStatus AsyncPanZoomController::OnSingleTapUp(const TapGestureInput& aEvent) {
+ APZC_LOG("%p got a single-tap-up in state %d\n", this, mState);
+ // If mZoomConstraints.mAllowDoubleTapZoom is true we wait for a call to OnSingleTapConfirmed before
+ // sending event to content
+ MOZ_ASSERT(GetCurrentTouchBlock());
+ if (!(mZoomConstraints.mAllowDoubleTapZoom && GetCurrentTouchBlock()->TouchActionAllowsDoubleTapZoom())) {
+ return GenerateSingleTap(TapType::eSingleTap, aEvent.mPoint, aEvent.modifiers);
+ }
+ return nsEventStatus_eIgnore;
+}
+
+nsEventStatus AsyncPanZoomController::OnSingleTapConfirmed(const TapGestureInput& aEvent) {
+ APZC_LOG("%p got a single-tap-confirmed in state %d\n", this, mState);
+ return GenerateSingleTap(TapType::eSingleTap, aEvent.mPoint, aEvent.modifiers);
+}
+
+nsEventStatus AsyncPanZoomController::OnDoubleTap(const TapGestureInput& aEvent) {
+ APZC_LOG("%p got a double-tap in state %d\n", this, mState);
+ RefPtr<GeckoContentController> controller = GetGeckoContentController();
+ if (controller) {
+ MOZ_ASSERT(GetCurrentTouchBlock());
+ if (mZoomConstraints.mAllowDoubleTapZoom && GetCurrentTouchBlock()->TouchActionAllowsDoubleTapZoom()) {
+ LayoutDevicePoint geckoScreenPoint;
+ if (ConvertToGecko(aEvent.mPoint, &geckoScreenPoint)) {
+ controller->HandleTap(TapType::eDoubleTap, geckoScreenPoint,
+ aEvent.modifiers, GetGuid(), GetCurrentTouchBlock()->GetBlockId());
+ }
+ }
+ return nsEventStatus_eConsumeNoDefault;
+ }
+ return nsEventStatus_eIgnore;
+}
+
+nsEventStatus AsyncPanZoomController::OnSecondTap(const TapGestureInput& aEvent)
+{
+ APZC_LOG("%p got a second-tap in state %d\n", this, mState);
+ return GenerateSingleTap(TapType::eSecondTap, aEvent.mPoint, aEvent.modifiers);
+}
+
+nsEventStatus AsyncPanZoomController::OnCancelTap(const TapGestureInput& aEvent) {
+ APZC_LOG("%p got a cancel-tap in state %d\n", this, mState);
+ // XXX: Implement this.
+ return nsEventStatus_eIgnore;
+}
+
+
+ScreenToParentLayerMatrix4x4 AsyncPanZoomController::GetTransformToThis() const {
+ if (APZCTreeManager* treeManagerLocal = GetApzcTreeManager()) {
+ return treeManagerLocal->GetScreenToApzcTransform(this);
+ }
+ return ScreenToParentLayerMatrix4x4();
+}
+
+ScreenPoint AsyncPanZoomController::ToScreenCoordinates(const ParentLayerPoint& aVector,
+ const ParentLayerPoint& aAnchor) const {
+ return TransformVector(GetTransformToThis().Inverse(), aVector, aAnchor);
+}
+
+// TODO: figure out a good way to check the w-coordinate is positive and return the result
+ParentLayerPoint AsyncPanZoomController::ToParentLayerCoordinates(const ScreenPoint& aVector,
+ const ScreenPoint& aAnchor) const {
+ return TransformVector(GetTransformToThis(), aVector, aAnchor);
+}
+
+bool AsyncPanZoomController::Contains(const ScreenIntPoint& aPoint) const
+{
+ ScreenToParentLayerMatrix4x4 transformToThis = GetTransformToThis();
+ Maybe<ParentLayerIntPoint> point = UntransformBy(transformToThis, aPoint);
+ if (!point) {
+ return false;
+ }
+
+ ParentLayerIntRect cb;
+ {
+ ReentrantMonitorAutoEnter lock(mMonitor);
+ GetFrameMetrics().GetCompositionBounds().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();
+ }
+ return ToScreenCoordinates(panVector, panStart).Length();
+}
+
+ParentLayerPoint AsyncPanZoomController::PanStart() const {
+ return ParentLayerPoint(mX.PanStart(), mY.PanStart());
+}
+
+const ParentLayerPoint AsyncPanZoomController::GetVelocityVector() const {
+ return ParentLayerPoint(mX.GetVelocity(), mY.GetVelocity());
+}
+
+void AsyncPanZoomController::SetVelocityVector(const ParentLayerPoint& aVelocityVector) {
+ mX.SetVelocity(aVelocityVector.x);
+ mY.SetVelocity(aVelocityVector.y);
+}
+
+void AsyncPanZoomController::HandlePanningWithTouchAction(double aAngle) {
+ // Handling of cross sliding will need to be added in this method after touch-action released
+ // enabled by default.
+ MOZ_ASSERT(GetCurrentTouchBlock());
+ if (GetCurrentTouchBlock()->TouchActionAllowsPanningXY()) {
+ if (mX.CanScrollNow() && mY.CanScrollNow()) {
+ if (IsCloseToHorizontal(aAngle, gfxPrefs::APZAxisLockAngle())) {
+ mY.SetAxisLocked(true);
+ SetState(PANNING_LOCKED_X);
+ } else if (IsCloseToVertical(aAngle, gfxPrefs::APZAxisLockAngle())) {
+ mX.SetAxisLocked(true);
+ SetState(PANNING_LOCKED_Y);
+ } else {
+ SetState(PANNING);
+ }
+ } else if (mX.CanScrollNow() || mY.CanScrollNow()) {
+ SetState(PANNING);
+ } else {
+ SetState(NOTHING);
+ }
+ } else if (GetCurrentTouchBlock()->TouchActionAllowsPanningX()) {
+ // Using bigger angle for panning to keep behavior consistent
+ // with IE.
+ if (IsCloseToHorizontal(aAngle, gfxPrefs::APZAllowedDirectPanAngle())) {
+ mY.SetAxisLocked(true);
+ SetState(PANNING_LOCKED_X);
+ mPanDirRestricted = true;
+ } else {
+ // Don't treat these touches as pan/zoom movements since 'touch-action' value
+ // requires it.
+ SetState(NOTHING);
+ }
+ } else if (GetCurrentTouchBlock()->TouchActionAllowsPanningY()) {
+ if (IsCloseToVertical(aAngle, gfxPrefs::APZAllowedDirectPanAngle())) {
+ mX.SetAxisLocked(true);
+ SetState(PANNING_LOCKED_Y);
+ mPanDirRestricted = true;
+ } else {
+ SetState(NOTHING);
+ }
+ } else {
+ SetState(NOTHING);
+ }
+ if (!IsInPanningState()) {
+ // If we didn't enter a panning state because touch-action disallowed it,
+ // make sure to clear any leftover velocity from the pre-threshold
+ // touchmoves.
+ mX.SetVelocity(0);
+ mY.SetVelocity(0);
+ }
+}
+
+void AsyncPanZoomController::HandlePanning(double aAngle) {
+ ReentrantMonitorAutoEnter lock(mMonitor);
+ MOZ_ASSERT(GetCurrentInputBlock());
+ RefPtr<const OverscrollHandoffChain> overscrollHandoffChain =
+ GetCurrentInputBlock()->GetOverscrollHandoffChain();
+ bool canScrollHorizontal = !mX.IsAxisLocked() &&
+ overscrollHandoffChain->CanScrollInDirection(this, Layer::HORIZONTAL);
+ bool canScrollVertical = !mY.IsAxisLocked() &&
+ overscrollHandoffChain->CanScrollInDirection(this, Layer::VERTICAL);
+
+ if (!canScrollHorizontal || !canScrollVertical) {
+ SetState(PANNING);
+ } else if (IsCloseToHorizontal(aAngle, gfxPrefs::APZAxisLockAngle())) {
+ mY.SetAxisLocked(true);
+ if (canScrollHorizontal) {
+ SetState(PANNING_LOCKED_X);
+ }
+ } else if (IsCloseToVertical(aAngle, gfxPrefs::APZAxisLockAngle())) {
+ mX.SetAxisLocked(true);
+ if (canScrollVertical) {
+ SetState(PANNING_LOCKED_Y);
+ }
+ } else {
+ SetState(PANNING);
+ }
+}
+
+void AsyncPanZoomController::HandlePanningUpdate(const ScreenPoint& aPanDistance) {
+ // If we're axis-locked, check if the user is trying to break the lock
+ if (GetAxisLockMode() == STICKY && !mPanDirRestricted) {
+
+ double angle = atan2(aPanDistance.y, aPanDistance.x); // range [-pi, pi]
+ angle = fabs(angle); // range [0, pi]
+
+ float breakThreshold = gfxPrefs::APZAxisBreakoutThreshold() * APZCTreeManager::GetDPI();
+
+ if (fabs(aPanDistance.x) > breakThreshold || fabs(aPanDistance.y) > breakThreshold) {
+ if (mState == PANNING_LOCKED_X) {
+ if (!IsCloseToHorizontal(angle, gfxPrefs::APZAxisBreakoutAngle())) {
+ mY.SetAxisLocked(false);
+ SetState(PANNING);
+ }
+ } else if (mState == PANNING_LOCKED_Y) {
+ if (!IsCloseToVertical(angle, gfxPrefs::APZAxisBreakoutAngle())) {
+ mX.SetAxisLocked(false);
+ SetState(PANNING);
+ }
+ }
+ }
+ }
+}
+
+nsEventStatus AsyncPanZoomController::StartPanning(const MultiTouchInput& aEvent) {
+ ReentrantMonitorAutoEnter lock(mMonitor);
+
+ ParentLayerPoint point = GetFirstTouchPoint(aEvent);
+ float dx = mX.PanDistance(point.x);
+ float dy = mY.PanDistance(point.y);
+
+ double angle = atan2(dy, dx); // range [-pi, pi]
+ angle = fabs(angle); // range [0, pi]
+
+ if (gfxPrefs::TouchActionEnabled()) {
+ HandlePanningWithTouchAction(angle);
+ } else {
+ if (GetAxisLockMode() == FREE) {
+ SetState(PANNING);
+ } else {
+ HandlePanning(angle);
+ }
+ }
+
+ if (IsInPanningState()) {
+ if (RefPtr<GeckoContentController> controller = GetGeckoContentController()) {
+ controller->NotifyAPZStateChange(GetGuid(), APZStateChange::eStartPanning);
+ }
+ return nsEventStatus_eConsumeNoDefault;
+ }
+ // Don't consume an event that didn't trigger a panning.
+ return nsEventStatus_eIgnore;
+}
+
+void AsyncPanZoomController::UpdateWithTouchAtDevicePoint(const MultiTouchInput& aEvent) {
+ ParentLayerPoint point = GetFirstTouchPoint(aEvent);
+ mX.UpdateWithTouchAtDevicePoint(point.x, 0, aEvent.mTime);
+ mY.UpdateWithTouchAtDevicePoint(point.y, 0, aEvent.mTime);
+}
+
+bool AsyncPanZoomController::AttemptScroll(ParentLayerPoint& aStartPoint,
+ ParentLayerPoint& aEndPoint,
+ OverscrollHandoffState& aOverscrollHandoffState) {
+
+ // "start - end" rather than "end - start" because e.g. moving your finger
+ // down (*positive* direction along y axis) causes the vertical scroll offset
+ // to *decrease* as the page follows your finger.
+ ParentLayerPoint displacement = aStartPoint - aEndPoint;
+
+ ParentLayerPoint overscroll; // will be used outside monitor block
+
+ // If the direction of panning is reversed within the same input block,
+ // a later event in the block could potentially scroll an APZC earlier
+ // in the handoff chain, than an earlier event in the block (because
+ // the earlier APZC was scrolled to its extent in the original direction).
+ // We want to disallow this.
+ bool scrollThisApzc = false;
+ if (InputBlockState* block = GetCurrentInputBlock()) {
+ scrollThisApzc = !block->GetScrolledApzc() || block->IsDownchainOfScrolledApzc(this);
+ }
+
+ if (scrollThisApzc) {
+ ReentrantMonitorAutoEnter lock(mMonitor);
+
+ ParentLayerPoint adjustedDisplacement;
+ bool forceVerticalOverscroll =
+ (aOverscrollHandoffState.mScrollSource == ScrollSource::Wheel &&
+ !mScrollMetadata.AllowVerticalScrollWithWheel());
+ bool yChanged = mY.AdjustDisplacement(displacement.y, adjustedDisplacement.y, overscroll.y,
+ forceVerticalOverscroll);
+ bool xChanged = mX.AdjustDisplacement(displacement.x, adjustedDisplacement.x, overscroll.x);
+
+ if (xChanged || yChanged) {
+ ScheduleComposite();
+ }
+
+ if (!IsZero(adjustedDisplacement)) {
+ ScrollBy(adjustedDisplacement / mFrameMetrics.GetZoom());
+ if (CancelableBlockState* block = GetCurrentInputBlock()) {
+ if (block->AsTouchBlock() && (block->GetScrolledApzc() != this)) {
+ RefPtr<GeckoContentController> controller = GetGeckoContentController();
+ if (controller) {
+ controller->SetScrollingRootContent(IsRootContent());
+ }
+ }
+ block->SetScrolledApzc(this);
+ }
+ ScheduleCompositeAndMaybeRepaint();
+ UpdateSharedCompositorFrameMetrics();
+ }
+
+ // Adjust the start point to reflect the consumed portion of the scroll.
+ aStartPoint = aEndPoint + overscroll;
+ } else {
+ overscroll = displacement;
+ }
+
+ // If we consumed the entire displacement as a normal scroll, great.
+ if (IsZero(overscroll)) {
+ return true;
+ }
+
+ if (AllowScrollHandoffInCurrentBlock()) {
+ // If there is overscroll, first try to hand it off to an APZC later
+ // in the handoff chain to consume (either as a normal scroll or as
+ // overscroll).
+ // Note: "+ overscroll" rather than "- overscroll" because "overscroll"
+ // is what's left of "displacement", and "displacement" is "start - end".
+ ++aOverscrollHandoffState.mChainIndex;
+ CallDispatchScroll(aStartPoint, aEndPoint, aOverscrollHandoffState);
+
+ overscroll = aStartPoint - aEndPoint;
+ if (IsZero(overscroll)) {
+ return true;
+ }
+ }
+
+ // If there is no APZC later in the handoff chain that accepted the
+ // overscroll, try to accept it ourselves. We only accept it if we
+ // are pannable.
+ APZC_LOG("%p taking overscroll during panning\n", this);
+ OverscrollForPanning(overscroll, aOverscrollHandoffState.mPanDistance);
+ aStartPoint = aEndPoint + overscroll;
+
+ return IsZero(overscroll);
+}
+
+void AsyncPanZoomController::OverscrollForPanning(ParentLayerPoint& aOverscroll,
+ const ScreenPoint& aPanDistance) {
+ // Only allow entering overscroll along an axis if the pan distance along
+ // that axis is greater than the pan distance along the other axis by a
+ // configurable factor. If we are already overscrolled, don't check this.
+ if (!IsOverscrolled()) {
+ if (aPanDistance.x < gfxPrefs::APZMinPanDistanceRatio() * aPanDistance.y) {
+ aOverscroll.x = 0;
+ }
+ if (aPanDistance.y < gfxPrefs::APZMinPanDistanceRatio() * aPanDistance.x) {
+ aOverscroll.y = 0;
+ }
+ }
+
+ OverscrollBy(aOverscroll);
+}
+
+void AsyncPanZoomController::OverscrollBy(ParentLayerPoint& aOverscroll) {
+ if (!gfxPrefs::APZOverscrollEnabled()) {
+ return;
+ }
+
+ ReentrantMonitorAutoEnter lock(mMonitor);
+ // Do not go into overscroll in a direction in which we have no room to
+ // scroll to begin with.
+ bool xCanScroll = mX.CanScroll();
+ bool yCanScroll = mY.CanScroll();
+ bool xConsumed = FuzzyEqualsAdditive(aOverscroll.x, 0.0f, COORDINATE_EPSILON);
+ bool yConsumed = FuzzyEqualsAdditive(aOverscroll.y, 0.0f, COORDINATE_EPSILON);
+
+ bool shouldOverscrollX = xCanScroll && !xConsumed;
+ bool shouldOverscrollY = yCanScroll && !yConsumed;
+
+ mOverscrollEffect->ConsumeOverscroll(aOverscroll, shouldOverscrollX, shouldOverscrollY);
+}
+
+RefPtr<const OverscrollHandoffChain> AsyncPanZoomController::BuildOverscrollHandoffChain() {
+ if (APZCTreeManager* treeManagerLocal = GetApzcTreeManager()) {
+ return treeManagerLocal->BuildOverscrollHandoffChain(this);
+ }
+
+ // This APZC IsDestroyed(). To avoid callers having to special-case this
+ // scenario, just build a 1-element chain containing ourselves.
+ OverscrollHandoffChain* result = new OverscrollHandoffChain;
+ result->Add(this);
+ return result;
+}
+
+void AsyncPanZoomController::AcceptFling(FlingHandoffState& aHandoffState) {
+ ReentrantMonitorAutoEnter lock(mMonitor);
+
+ // We may have a pre-existing velocity for whatever reason (for example,
+ // a previously handed off fling). We don't want to clobber that.
+ APZC_LOG("%p accepting fling with velocity %s\n", this,
+ Stringify(aHandoffState.mVelocity).c_str());
+ if (mX.CanScroll()) {
+ mX.SetVelocity(mX.GetVelocity() + aHandoffState.mVelocity.x);
+ aHandoffState.mVelocity.x = 0;
+ }
+ if (mY.CanScroll()) {
+ mY.SetVelocity(mY.GetVelocity() + aHandoffState.mVelocity.y);
+ aHandoffState.mVelocity.y = 0;
+ }
+
+ // If there's a scroll snap point near the predicted fling destination,
+ // scroll there using a smooth scroll animation. Otherwise, start a
+ // fling animation.
+ ScrollSnapToDestination();
+ if (mState != SMOOTH_SCROLL) {
+ SetState(FLING);
+ FlingAnimation *fling = new FlingAnimation(*this,
+ GetPlatformSpecificState(),
+ aHandoffState.mChain,
+ aHandoffState.mIsHandoff,
+ aHandoffState.mScrolledApzc);
+ StartAnimation(fling);
+ }
+}
+
+bool AsyncPanZoomController::AttemptFling(FlingHandoffState& aHandoffState) {
+ // If we are pannable, take over the fling ourselves.
+ if (IsPannable()) {
+ AcceptFling(aHandoffState);
+ return true;
+ }
+
+ return false;
+}
+
+void AsyncPanZoomController::HandleFlingOverscroll(const ParentLayerPoint& aVelocity,
+ const RefPtr<const OverscrollHandoffChain>& aOverscrollHandoffChain,
+ const RefPtr<const AsyncPanZoomController>& aScrolledApzc) {
+ APZCTreeManager* treeManagerLocal = GetApzcTreeManager();
+ if (treeManagerLocal) {
+ FlingHandoffState handoffState{aVelocity,
+ aOverscrollHandoffChain,
+ true /* handoff */,
+ aScrolledApzc};
+ treeManagerLocal->DispatchFling(this, handoffState);
+ if (!IsZero(handoffState.mVelocity) && IsPannable() && gfxPrefs::APZOverscrollEnabled()) {
+ mOverscrollEffect->HandleFlingOverscroll(handoffState.mVelocity);
+ }
+ }
+}
+
+void AsyncPanZoomController::HandleSmoothScrollOverscroll(const ParentLayerPoint& aVelocity) {
+ // We must call BuildOverscrollHandoffChain from this deferred callback
+ // function in order to avoid a deadlock when acquiring the tree lock.
+ HandleFlingOverscroll(aVelocity, BuildOverscrollHandoffChain(), nullptr);
+}
+
+void AsyncPanZoomController::SmoothScrollTo(const CSSPoint& aDestination) {
+ if (mState == SMOOTH_SCROLL && mAnimation) {
+ APZC_LOG("%p updating destination on existing animation\n", this);
+ RefPtr<SmoothScrollAnimation> animation(
+ static_cast<SmoothScrollAnimation*>(mAnimation.get()));
+ animation->SetDestination(CSSPoint::ToAppUnits(aDestination));
+ } else {
+ CancelAnimation();
+ SetState(SMOOTH_SCROLL);
+ nsPoint initialPosition = CSSPoint::ToAppUnits(mFrameMetrics.GetScrollOffset());
+ // Cast velocity from ParentLayerPoints/ms to CSSPoints/ms then convert to
+ // appunits/second
+ nsPoint initialVelocity = CSSPoint::ToAppUnits(CSSPoint(mX.GetVelocity(),
+ mY.GetVelocity())) * 1000.0f;
+ nsPoint destination = CSSPoint::ToAppUnits(aDestination);
+
+ StartAnimation(new SmoothScrollAnimation(*this,
+ initialPosition, initialVelocity,
+ destination,
+ gfxPrefs::ScrollBehaviorSpringConstant(),
+ gfxPrefs::ScrollBehaviorDampingRatio()));
+ }
+}
+
+void AsyncPanZoomController::StartOverscrollAnimation(const ParentLayerPoint& aVelocity) {
+ SetState(OVERSCROLL_ANIMATION);
+ StartAnimation(new OverscrollAnimation(*this, aVelocity));
+}
+
+void AsyncPanZoomController::CallDispatchScroll(ParentLayerPoint& aStartPoint,
+ ParentLayerPoint& aEndPoint,
+ OverscrollHandoffState& aOverscrollHandoffState) {
+ // Make a local copy of the tree manager pointer and check if it's not
+ // null before calling DispatchScroll(). This is necessary because
+ // Destroy(), which nulls out mTreeManager, could be called concurrently.
+ APZCTreeManager* treeManagerLocal = GetApzcTreeManager();
+ if (!treeManagerLocal) {
+ return;
+ }
+ treeManagerLocal->DispatchScroll(this,
+ aStartPoint, aEndPoint,
+ aOverscrollHandoffState);
+}
+
+void AsyncPanZoomController::TrackTouch(const MultiTouchInput& aEvent) {
+ ParentLayerPoint prevTouchPoint(mX.GetPos(), mY.GetPos());
+ ParentLayerPoint touchPoint = GetFirstTouchPoint(aEvent);
+
+ ScreenPoint panDistance = ToScreenCoordinates(
+ ParentLayerPoint(mX.PanDistance(touchPoint.x),
+ mY.PanDistance(touchPoint.y)),
+ PanStart());
+ HandlePanningUpdate(panDistance);
+
+ UpdateWithTouchAtDevicePoint(aEvent);
+
+ if (prevTouchPoint != touchPoint) {
+ mozilla::Telemetry::Accumulate(mozilla::Telemetry::SCROLL_INPUT_METHODS,
+ (uint32_t) ScrollInputMethod::ApzTouch);
+ MOZ_ASSERT(GetCurrentTouchBlock());
+ OverscrollHandoffState handoffState(
+ *GetCurrentTouchBlock()->GetOverscrollHandoffChain(),
+ panDistance,
+ ScrollSource::Touch);
+ CallDispatchScroll(prevTouchPoint, touchPoint, handoffState);
+ }
+}
+
+ParentLayerPoint AsyncPanZoomController::GetFirstTouchPoint(const MultiTouchInput& aEvent) {
+ return ((SingleTouchData&)aEvent.mTouches[0]).mLocalScreenPoint;
+}
+
+void AsyncPanZoomController::StartAnimation(AsyncPanZoomAnimation* aAnimation)
+{
+ ReentrantMonitorAutoEnter lock(mMonitor);
+ mAnimation = aAnimation;
+ mLastSampleTime = GetFrameTime();
+ ScheduleComposite();
+}
+
+void AsyncPanZoomController::CancelAnimation(CancelAnimationFlags aFlags) {
+ ReentrantMonitorAutoEnter lock(mMonitor);
+ APZC_LOG("%p running CancelAnimation in state %d\n", this, mState);
+ SetState(NOTHING);
+ mAnimation = nullptr;
+ // Since there is no animation in progress now the axes should
+ // have no velocity either. If we are dropping the velocity from a non-zero
+ // value we should trigger a repaint as the displayport margins are dependent
+ // on the velocity and the last repaint request might not have good margins
+ // any more.
+ bool repaint = !IsZero(GetVelocityVector());
+ mX.SetVelocity(0);
+ mY.SetVelocity(0);
+ mX.SetAxisLocked(false);
+ mY.SetAxisLocked(false);
+ // Setting the state to nothing and cancelling the animation can
+ // preempt normal mechanisms for relieving overscroll, so we need to clear
+ // overscroll here.
+ if (!(aFlags & ExcludeOverscroll) && IsOverscrolled()) {
+ ClearOverscroll();
+ repaint = true;
+ }
+ // Similar to relieving overscroll, we also need to snap to any snap points
+ // if appropriate.
+ if (aFlags & CancelAnimationFlags::ScrollSnap) {
+ ScrollSnap();
+ }
+ if (repaint) {
+ RequestContentRepaint();
+ ScheduleComposite();
+ UpdateSharedCompositorFrameMetrics();
+ }
+}
+
+void AsyncPanZoomController::ClearOverscroll() {
+ ReentrantMonitorAutoEnter lock(mMonitor);
+ mX.ClearOverscroll();
+ mY.ClearOverscroll();
+}
+
+void AsyncPanZoomController::SetCompositorController(CompositorController* aCompositorController)
+{
+ mCompositorController = aCompositorController;
+}
+
+void AsyncPanZoomController::SetMetricsSharingController(MetricsSharingController* aMetricsSharingController)
+{
+ mMetricsSharingController = aMetricsSharingController;
+}
+
+void AsyncPanZoomController::AdjustScrollForSurfaceShift(const ScreenPoint& aShift)
+{
+ ReentrantMonitorAutoEnter lock(mMonitor);
+ CSSPoint adjustment =
+ ViewAs<ParentLayerPixel>(aShift, PixelCastJustification::ScreenIsParentLayerForRoot)
+ / mFrameMetrics.GetZoom();
+ APZC_LOG("%p adjusting scroll position by %s for surface shift\n",
+ this, Stringify(adjustment).c_str());
+ CSSPoint scrollOffset = mFrameMetrics.GetScrollOffset();
+ scrollOffset.y = mY.ClampOriginToScrollableRect(scrollOffset.y + adjustment.y);
+ scrollOffset.x = mX.ClampOriginToScrollableRect(scrollOffset.x + adjustment.x);
+ mFrameMetrics.SetScrollOffset(scrollOffset);
+ ScheduleCompositeAndMaybeRepaint();
+ UpdateSharedCompositorFrameMetrics();
+}
+
+void AsyncPanZoomController::ScrollBy(const CSSPoint& aOffset) {
+ mFrameMetrics.ScrollBy(aOffset);
+}
+
+void AsyncPanZoomController::ScaleWithFocus(float aScale,
+ const CSSPoint& aFocus) {
+ mFrameMetrics.ZoomBy(aScale);
+ // We want to adjust the scroll offset such that the CSS point represented by aFocus remains
+ // at the same position on the screen before and after the change in zoom. The below code
+ // accomplishes this; see https://bugzilla.mozilla.org/show_bug.cgi?id=923431#c6 for an
+ // in-depth explanation of how.
+ mFrameMetrics.SetScrollOffset((mFrameMetrics.GetScrollOffset() + aFocus) - (aFocus / aScale));
+}
+
+/**
+ * Enlarges the displayport along both axes based on the velocity.
+ */
+static CSSSize
+CalculateDisplayPortSize(const CSSSize& aCompositionSize,
+ const CSSPoint& aVelocity)
+{
+ bool xIsStationarySpeed = fabsf(aVelocity.x) < gfxPrefs::APZMinSkateSpeed();
+ bool yIsStationarySpeed = fabsf(aVelocity.y) < gfxPrefs::APZMinSkateSpeed();
+ float xMultiplier = xIsStationarySpeed
+ ? gfxPrefs::APZXStationarySizeMultiplier()
+ : gfxPrefs::APZXSkateSizeMultiplier();
+ float yMultiplier = yIsStationarySpeed
+ ? gfxPrefs::APZYStationarySizeMultiplier()
+ : gfxPrefs::APZYSkateSizeMultiplier();
+
+ if (IsHighMemSystem() && !xIsStationarySpeed) {
+ xMultiplier += gfxPrefs::APZXSkateHighMemAdjust();
+ }
+
+ if (IsHighMemSystem() && !yIsStationarySpeed) {
+ yMultiplier += gfxPrefs::APZYSkateHighMemAdjust();
+ }
+
+ return aCompositionSize * CSSSize(xMultiplier, yMultiplier);
+}
+
+/**
+ * Ensures that the displayport is at least as large as the visible area
+ * inflated by the danger zone. If this is not the case then the
+ * "AboutToCheckerboard" function in TiledContentClient.cpp will return true
+ * even in the stable state.
+ */
+static CSSSize
+ExpandDisplayPortToDangerZone(const CSSSize& aDisplayPortSize,
+ const FrameMetrics& aFrameMetrics)
+{
+ CSSSize dangerZone(0.0f, 0.0f);
+ if (aFrameMetrics.LayersPixelsPerCSSPixel().xScale != 0 &&
+ aFrameMetrics.LayersPixelsPerCSSPixel().yScale != 0) {
+ dangerZone = LayerSize(
+ gfxPrefs::APZDangerZoneX(),
+ gfxPrefs::APZDangerZoneY()) / aFrameMetrics.LayersPixelsPerCSSPixel();
+ }
+ const CSSSize compositionSize = aFrameMetrics.CalculateBoundedCompositedSizeInCssPixels();
+
+ const float xSize = std::max(aDisplayPortSize.width,
+ compositionSize.width + (2 * dangerZone.width));
+
+ const float ySize = std::max(aDisplayPortSize.height,
+ compositionSize.height + (2 * dangerZone.height));
+
+ return CSSSize(xSize, ySize);
+}
+
+/**
+ * Attempts to redistribute any area in the displayport that would get clipped
+ * by the scrollable rect, or be inaccessible due to disabled scrolling, to the
+ * other axis, while maintaining total displayport area.
+ */
+static void
+RedistributeDisplayPortExcess(CSSSize& aDisplayPortSize,
+ const CSSRect& aScrollableRect)
+{
+ // As aDisplayPortSize.height * aDisplayPortSize.width does not change,
+ // we are just scaling by the ratio and its inverse.
+ if (aDisplayPortSize.height > aScrollableRect.height) {
+ aDisplayPortSize.width *= (aDisplayPortSize.height / aScrollableRect.height);
+ aDisplayPortSize.height = aScrollableRect.height;
+ } else if (aDisplayPortSize.width > aScrollableRect.width) {
+ aDisplayPortSize.height *= (aDisplayPortSize.width / aScrollableRect.width);
+ aDisplayPortSize.width = aScrollableRect.width;
+ }
+}
+
+/* static */
+const ScreenMargin AsyncPanZoomController::CalculatePendingDisplayPort(
+ const FrameMetrics& aFrameMetrics,
+ const ParentLayerPoint& aVelocity)
+{
+ if (aFrameMetrics.IsScrollInfoLayer()) {
+ // Don't compute margins. Since we can't asynchronously scroll this frame,
+ // we don't want to paint anything more than the composition bounds.
+ return ScreenMargin();
+ }
+
+ CSSSize compositionSize = aFrameMetrics.CalculateBoundedCompositedSizeInCssPixels();
+ CSSPoint velocity;
+ if (aFrameMetrics.GetZoom() != CSSToParentLayerScale2D(0, 0)) {
+ velocity = aVelocity / aFrameMetrics.GetZoom(); // avoid division by zero
+ }
+ CSSRect scrollableRect = aFrameMetrics.GetExpandedScrollableRect();
+
+ // Calculate the displayport size based on how fast we're moving along each axis.
+ CSSSize displayPortSize = CalculateDisplayPortSize(compositionSize, velocity);
+
+ displayPortSize = ExpandDisplayPortToDangerZone(displayPortSize, aFrameMetrics);
+
+ if (gfxPrefs::APZEnlargeDisplayPortWhenClipped()) {
+ RedistributeDisplayPortExcess(displayPortSize, scrollableRect);
+ }
+
+ // We calculate a "displayport" here which is relative to the scroll offset.
+ // Note that the scroll offset we have here in the APZ code may not be the
+ // same as the base rect that gets used on the layout side when the displayport
+ // margins are actually applied, so it is important to only consider the
+ // displayport as margins relative to a scroll offset rather than relative to
+ // something more unchanging like the scrollable rect origin.
+
+ // Center the displayport based on its expansion over the composition size.
+ CSSRect displayPort((compositionSize.width - displayPortSize.width) / 2.0f,
+ (compositionSize.height - displayPortSize.height) / 2.0f,
+ displayPortSize.width, displayPortSize.height);
+
+ // Offset the displayport, depending on how fast we're moving and the
+ // estimated time it takes to paint, to try to minimise checkerboarding.
+ float paintFactor = kDefaultEstimatedPaintDurationMs;
+ displayPort.MoveBy(velocity * paintFactor * gfxPrefs::APZVelocityBias());
+
+ APZC_LOG_FM(aFrameMetrics,
+ "Calculated displayport as (%f %f %f %f) from velocity %s paint time %f metrics",
+ displayPort.x, displayPort.y, displayPort.width, displayPort.height,
+ ToString(aVelocity).c_str(), paintFactor);
+
+ CSSMargin cssMargins;
+ cssMargins.left = -displayPort.x;
+ cssMargins.top = -displayPort.y;
+ cssMargins.right = displayPort.width - compositionSize.width - cssMargins.left;
+ cssMargins.bottom = displayPort.height - compositionSize.height - cssMargins.top;
+
+ return cssMargins * aFrameMetrics.DisplayportPixelsPerCSSPixel();
+}
+
+void AsyncPanZoomController::ScheduleComposite() {
+ if (mCompositorController) {
+ mCompositorController->ScheduleRenderOnCompositorThread();
+ }
+}
+
+void AsyncPanZoomController::ScheduleCompositeAndMaybeRepaint() {
+ ScheduleComposite();
+ RequestContentRepaint();
+}
+
+void AsyncPanZoomController::FlushRepaintForOverscrollHandoff() {
+ ReentrantMonitorAutoEnter lock(mMonitor);
+ RequestContentRepaint();
+ UpdateSharedCompositorFrameMetrics();
+}
+
+void AsyncPanZoomController::FlushRepaintForNewInputBlock() {
+ APZC_LOG("%p flushing repaint for new input block\n", this);
+
+ ReentrantMonitorAutoEnter lock(mMonitor);
+ RequestContentRepaint();
+ UpdateSharedCompositorFrameMetrics();
+}
+
+bool AsyncPanZoomController::SnapBackIfOverscrolled() {
+ ReentrantMonitorAutoEnter lock(mMonitor);
+ // It's possible that we're already in the middle of an overscroll
+ // animation - if so, don't start a new one.
+ if (IsOverscrolled() && mState != OVERSCROLL_ANIMATION) {
+ APZC_LOG("%p is overscrolled, starting snap-back\n", this);
+ StartOverscrollAnimation(ParentLayerPoint(0, 0));
+ return true;
+ }
+ // If we don't kick off an overscroll animation, we still need to ask the
+ // main thread to snap to any nearby snap points, assuming we haven't already
+ // done so when we started this fling
+ if (mState != FLING) {
+ ScrollSnap();
+ }
+ return false;
+}
+
+bool AsyncPanZoomController::IsFlingingFast() const {
+ ReentrantMonitorAutoEnter lock(mMonitor);
+ if (mState == FLING &&
+ GetVelocityVector().Length() > gfxPrefs::APZFlingStopOnTapThreshold()) {
+ APZC_LOG("%p is moving fast\n", this);
+ return true;
+ }
+ return false;
+}
+
+bool AsyncPanZoomController::IsPannable() const {
+ ReentrantMonitorAutoEnter lock(mMonitor);
+ return mX.CanScroll() || mY.CanScroll();
+}
+
+int32_t AsyncPanZoomController::GetLastTouchIdentifier() const {
+ RefPtr<GestureEventListener> listener = GetGestureEventListener();
+ return listener ? listener->GetLastTouchIdentifier() : -1;
+}
+
+void AsyncPanZoomController::RequestContentRepaint(bool aUserAction) {
+ // Reinvoke this method on the repaint thread if it's not there already. It's
+ // important to do this before the call to CalculatePendingDisplayPort, so
+ // that CalculatePendingDisplayPort uses the most recent available version of
+ // mFrameMetrics, just before the paint request is dispatched to content.
+ RefPtr<GeckoContentController> controller = GetGeckoContentController();
+ if (!controller) {
+ return;
+ }
+ if (!controller->IsRepaintThread()) {
+ // use the local variable to resolve the function overload.
+ auto func = static_cast<void (AsyncPanZoomController::*)(bool)>
+ (&AsyncPanZoomController::RequestContentRepaint);
+ controller->DispatchToRepaintThread(NewRunnableMethod<bool>(this, func, aUserAction));
+ return;
+ }
+
+ MOZ_ASSERT(controller->IsRepaintThread());
+
+ ReentrantMonitorAutoEnter lock(mMonitor);
+ ParentLayerPoint velocity = GetVelocityVector();
+ mFrameMetrics.SetDisplayPortMargins(CalculatePendingDisplayPort(mFrameMetrics, velocity));
+ mFrameMetrics.SetUseDisplayPortMargins(true);
+ mFrameMetrics.SetPaintRequestTime(TimeStamp::Now());
+ mFrameMetrics.SetRepaintDrivenByUserAction(aUserAction);
+ RequestContentRepaint(mFrameMetrics, velocity);
+}
+
+/*static*/ CSSRect
+GetDisplayPortRect(const FrameMetrics& aFrameMetrics)
+{
+ // This computation is based on what happens in CalculatePendingDisplayPort. If that
+ // changes then this might need to change too
+ CSSRect baseRect(aFrameMetrics.GetScrollOffset(),
+ aFrameMetrics.CalculateBoundedCompositedSizeInCssPixels());
+ baseRect.Inflate(aFrameMetrics.GetDisplayPortMargins() / aFrameMetrics.DisplayportPixelsPerCSSPixel());
+ return baseRect;
+}
+
+void
+AsyncPanZoomController::RequestContentRepaint(const FrameMetrics& aFrameMetrics,
+ const ParentLayerPoint& aVelocity)
+{
+ RefPtr<GeckoContentController> controller = GetGeckoContentController();
+ if (!controller) {
+ return;
+ }
+ MOZ_ASSERT(controller->IsRepaintThread());
+
+ // If we're trying to paint what we already think is painted, discard this
+ // request since it's a pointless paint.
+ ScreenMargin marginDelta = (mLastPaintRequestMetrics.GetDisplayPortMargins()
+ - aFrameMetrics.GetDisplayPortMargins());
+ if (fabsf(marginDelta.left) < EPSILON &&
+ fabsf(marginDelta.top) < EPSILON &&
+ fabsf(marginDelta.right) < EPSILON &&
+ fabsf(marginDelta.bottom) < EPSILON &&
+ fabsf(mLastPaintRequestMetrics.GetScrollOffset().x -
+ aFrameMetrics.GetScrollOffset().x) < EPSILON &&
+ fabsf(mLastPaintRequestMetrics.GetScrollOffset().y -
+ aFrameMetrics.GetScrollOffset().y) < EPSILON &&
+ aFrameMetrics.GetPresShellResolution() == mLastPaintRequestMetrics.GetPresShellResolution() &&
+ aFrameMetrics.GetZoom() == mLastPaintRequestMetrics.GetZoom() &&
+ fabsf(aFrameMetrics.GetViewport().width -
+ mLastPaintRequestMetrics.GetViewport().width) < EPSILON &&
+ fabsf(aFrameMetrics.GetViewport().height -
+ mLastPaintRequestMetrics.GetViewport().height) < EPSILON &&
+ aFrameMetrics.GetScrollGeneration() ==
+ mLastPaintRequestMetrics.GetScrollGeneration() &&
+ aFrameMetrics.GetScrollUpdateType() ==
+ mLastPaintRequestMetrics.GetScrollUpdateType()) {
+ return;
+ }
+
+ APZC_LOG_FM(aFrameMetrics, "%p requesting content repaint", this);
+ { // scope lock
+ MutexAutoLock lock(mCheckerboardEventLock);
+ if (mCheckerboardEvent && mCheckerboardEvent->IsRecordingTrace()) {
+ std::stringstream info;
+ info << " velocity " << aVelocity;
+ std::string str = info.str();
+ mCheckerboardEvent->UpdateRendertraceProperty(
+ CheckerboardEvent::RequestedDisplayPort, GetDisplayPortRect(aFrameMetrics),
+ str);
+ }
+ }
+
+ MOZ_ASSERT(aFrameMetrics.GetScrollUpdateType() == FrameMetrics::eNone ||
+ aFrameMetrics.GetScrollUpdateType() == FrameMetrics::eUserAction);
+ controller->RequestContentRepaint(aFrameMetrics);
+ mExpectedGeckoMetrics = aFrameMetrics;
+ mLastPaintRequestMetrics = aFrameMetrics;
+}
+
+bool AsyncPanZoomController::UpdateAnimation(const TimeStamp& aSampleTime,
+ nsTArray<RefPtr<Runnable>>* aOutDeferredTasks)
+{
+ APZThreadUtils::AssertOnCompositorThread();
+
+ // This function may get called multiple with the same sample time, because
+ // there may be multiple layers with this APZC, and each layer invokes this
+ // function during composition. However we only want to do one animation step
+ // per composition so we need to deduplicate these calls first.
+ if (mLastSampleTime == aSampleTime) {
+ return false;
+ }
+ TimeDuration sampleTimeDelta = aSampleTime - mLastSampleTime;
+ mLastSampleTime = aSampleTime;
+
+ if (mAnimation) {
+ bool continueAnimation = mAnimation->Sample(mFrameMetrics, sampleTimeDelta);
+ bool wantsRepaints = mAnimation->WantsRepaints();
+ *aOutDeferredTasks = mAnimation->TakeDeferredTasks();
+ if (!continueAnimation) {
+ mAnimation = nullptr;
+ SetState(NOTHING);
+ }
+ // Request a repaint at the end of the animation in case something such as a
+ // call to NotifyLayersUpdated was invoked during the animation and Gecko's
+ // current state is some intermediate point of the animation.
+ if (!continueAnimation || wantsRepaints) {
+ RequestContentRepaint();
+ }
+ UpdateSharedCompositorFrameMetrics();
+ return true;
+ }
+ return false;
+}
+
+AsyncTransformComponentMatrix
+AsyncPanZoomController::GetOverscrollTransform(AsyncMode aMode) const
+{
+ ReentrantMonitorAutoEnter lock(mMonitor);
+
+ if (aMode == RESPECT_FORCE_DISABLE && mScrollMetadata.IsApzForceDisabled()) {
+ return AsyncTransformComponentMatrix();
+ }
+
+ if (!IsOverscrolled()) {
+ return AsyncTransformComponentMatrix();
+ }
+
+ // The overscroll effect is a uniform stretch along the overscrolled axis,
+ // with the edge of the content where we have reached the end of the
+ // scrollable area pinned into place.
+
+ // The kStretchFactor parameter determines how much overscroll can stretch the
+ // content.
+ const float kStretchFactor = gfxPrefs::APZOverscrollStretchFactor();
+
+ // Compute the amount of the stretch along each axis. The stretch is
+ // proportional to the amount by which we are overscrolled along that axis.
+ ParentLayerSize compositionSize(mX.GetCompositionLength(), mY.GetCompositionLength());
+ float scaleX = 1 + kStretchFactor * fabsf(mX.GetOverscroll()) / mX.GetCompositionLength();
+ float scaleY = 1 + kStretchFactor * fabsf(mY.GetOverscroll()) / mY.GetCompositionLength();
+
+ // The scale is applied relative to the origin of the composition bounds, i.e.
+ // it keeps the top-left corner of the content in place. This is fine if we
+ // are overscrolling at the top or on the left, but if we are overscrolling
+ // at the bottom or on the right, we want the bottom or right edge of the
+ // content to stay in place instead, so we add a translation to compensate.
+ ParentLayerPoint translation;
+ bool overscrolledOnRight = mX.GetOverscroll() > 0;
+ if (overscrolledOnRight) {
+ ParentLayerCoord overscrolledCompositionWidth = scaleX * compositionSize.width;
+ ParentLayerCoord extraCompositionWidth = overscrolledCompositionWidth - compositionSize.width;
+ translation.x = -extraCompositionWidth;
+ }
+ bool overscrolledAtBottom = mY.GetOverscroll() > 0;
+ if (overscrolledAtBottom) {
+ ParentLayerCoord overscrolledCompositionHeight = scaleY * compositionSize.height;
+ ParentLayerCoord extraCompositionHeight = overscrolledCompositionHeight - compositionSize.height;
+ translation.y = -extraCompositionHeight;
+ }
+
+ // Combine the transformations into a matrix.
+ return AsyncTransformComponentMatrix::Scaling(scaleX, scaleY, 1)
+ .PostTranslate(translation.x, translation.y, 0);
+}
+
+bool AsyncPanZoomController::AdvanceAnimations(const TimeStamp& aSampleTime)
+{
+ APZThreadUtils::AssertOnCompositorThread();
+
+ // Don't send any state-change notifications until the end of the function,
+ // because we may go through some intermediate states while we finish
+ // animations and start new ones.
+ StateChangeNotificationBlocker blocker(this);
+
+ // The eventual return value of this function. The compositor needs to know
+ // whether or not to advance by a frame as soon as it can. For example, if a
+ // fling is happening, it has to keep compositing so that the animation is
+ // smooth. If an animation frame is requested, it is the compositor's
+ // responsibility to schedule a composite.
+ mAsyncTransformAppliedToContent = false;
+ bool requestAnimationFrame = false;
+ nsTArray<RefPtr<Runnable>> deferredTasks;
+
+ {
+ ReentrantMonitorAutoEnter lock(mMonitor);
+
+ requestAnimationFrame = UpdateAnimation(aSampleTime, &deferredTasks);
+
+ { // scope lock
+ MutexAutoLock lock(mCheckerboardEventLock);
+ if (mCheckerboardEvent) {
+ mCheckerboardEvent->UpdateRendertraceProperty(
+ CheckerboardEvent::UserVisible,
+ CSSRect(mFrameMetrics.GetScrollOffset(),
+ mFrameMetrics.CalculateCompositedSizeInCssPixels()));
+ }
+ }
+ }
+
+ // Execute any deferred tasks queued up by mAnimation's Sample() (called by
+ // UpdateAnimation()). This needs to be done after the monitor is released
+ // since the tasks are allowed to call APZCTreeManager methods which can grab
+ // the tree lock.
+ for (uint32_t i = 0; i < deferredTasks.Length(); ++i) {
+ deferredTasks[i]->Run();
+ deferredTasks[i] = nullptr;
+ }
+
+ // One of the deferred tasks may have started a new animation. In this case,
+ // we want to ask the compositor to schedule a new composite.
+ requestAnimationFrame |= (mAnimation != nullptr);
+
+ return requestAnimationFrame;
+}
+
+ParentLayerPoint
+AsyncPanZoomController::GetCurrentAsyncScrollOffset(AsyncMode aMode) const
+{
+ ReentrantMonitorAutoEnter lock(mMonitor);
+
+ if (aMode == RESPECT_FORCE_DISABLE && mScrollMetadata.IsApzForceDisabled()) {
+ return mLastContentPaintMetrics.GetScrollOffset() * mLastContentPaintMetrics.GetZoom();
+ }
+
+ return (mFrameMetrics.GetScrollOffset() + mTestAsyncScrollOffset)
+ * mFrameMetrics.GetZoom() * mTestAsyncZoom.scale;
+}
+
+AsyncTransform
+AsyncPanZoomController::GetCurrentAsyncTransform(AsyncMode aMode) const
+{
+ ReentrantMonitorAutoEnter lock(mMonitor);
+
+ if (aMode == RESPECT_FORCE_DISABLE && mScrollMetadata.IsApzForceDisabled()) {
+ return AsyncTransform();
+ }
+
+ CSSPoint lastPaintScrollOffset;
+ if (mLastContentPaintMetrics.IsScrollable()) {
+ lastPaintScrollOffset = mLastContentPaintMetrics.GetScrollOffset();
+ }
+
+ CSSPoint currentScrollOffset = mFrameMetrics.GetScrollOffset() +
+ mTestAsyncScrollOffset;
+
+ // If checkerboarding has been disallowed, clamp the scroll position to stay
+ // within rendered content.
+ if (!gfxPrefs::APZAllowCheckerboarding() &&
+ !mLastContentPaintMetrics.GetDisplayPort().IsEmpty()) {
+ CSSSize compositedSize = mLastContentPaintMetrics.CalculateCompositedSizeInCssPixels();
+ CSSPoint maxScrollOffset = lastPaintScrollOffset +
+ CSSPoint(mLastContentPaintMetrics.GetDisplayPort().XMost() - compositedSize.width,
+ mLastContentPaintMetrics.GetDisplayPort().YMost() - compositedSize.height);
+ CSSPoint minScrollOffset = lastPaintScrollOffset + mLastContentPaintMetrics.GetDisplayPort().TopLeft();
+
+ if (minScrollOffset.x < maxScrollOffset.x) {
+ currentScrollOffset.x = clamped(currentScrollOffset.x, minScrollOffset.x, maxScrollOffset.x);
+ }
+ if (minScrollOffset.y < maxScrollOffset.y) {
+ currentScrollOffset.y = clamped(currentScrollOffset.y, minScrollOffset.y, maxScrollOffset.y);
+ }
+ }
+
+ ParentLayerPoint translation = (currentScrollOffset - lastPaintScrollOffset)
+ * mFrameMetrics.GetZoom() * mTestAsyncZoom.scale;
+
+ return AsyncTransform(
+ LayerToParentLayerScale(mFrameMetrics.GetAsyncZoom().scale * mTestAsyncZoom.scale),
+ -translation);
+}
+
+AsyncTransformComponentMatrix
+AsyncPanZoomController::GetCurrentAsyncTransformWithOverscroll(AsyncMode aMode) const
+{
+ return AsyncTransformComponentMatrix(GetCurrentAsyncTransform(aMode))
+ * GetOverscrollTransform(aMode);
+}
+
+Matrix4x4 AsyncPanZoomController::GetTransformToLastDispatchedPaint() const {
+ ReentrantMonitorAutoEnter lock(mMonitor);
+
+ LayerPoint scrollChange =
+ (mLastContentPaintMetrics.GetScrollOffset() - mExpectedGeckoMetrics.GetScrollOffset())
+ * mLastContentPaintMetrics.GetDevPixelsPerCSSPixel()
+ * mLastContentPaintMetrics.GetCumulativeResolution();
+
+ // We're interested in the async zoom change. Factor out the content scale
+ // that may change when dragging the window to a monitor with a different
+ // content scale.
+ LayoutDeviceToParentLayerScale2D lastContentZoom =
+ mLastContentPaintMetrics.GetZoom() / mLastContentPaintMetrics.GetDevPixelsPerCSSPixel();
+ LayoutDeviceToParentLayerScale2D lastDispatchedZoom =
+ mExpectedGeckoMetrics.GetZoom() / mExpectedGeckoMetrics.GetDevPixelsPerCSSPixel();
+ gfxSize zoomChange = lastContentZoom / lastDispatchedZoom;
+
+ return Matrix4x4::Translation(scrollChange.x, scrollChange.y, 0).
+ PostScale(zoomChange.width, zoomChange.height, 1);
+}
+
+uint32_t
+AsyncPanZoomController::GetCheckerboardMagnitude() const
+{
+ ReentrantMonitorAutoEnter lock(mMonitor);
+
+ CSSPoint currentScrollOffset = mFrameMetrics.GetScrollOffset() + mTestAsyncScrollOffset;
+ CSSRect painted = mLastContentPaintMetrics.GetDisplayPort() + mLastContentPaintMetrics.GetScrollOffset();
+ CSSRect visible = CSSRect(currentScrollOffset, mFrameMetrics.CalculateCompositedSizeInCssPixels());
+
+ CSSIntRegion checkerboard;
+ // Round so as to minimize checkerboarding; if we're only showing fractional
+ // pixels of checkerboarding it's not really worth counting
+ checkerboard.Sub(RoundedIn(visible), RoundedOut(painted));
+ return checkerboard.Area();
+}
+
+void
+AsyncPanZoomController::ReportCheckerboard(const TimeStamp& aSampleTime)
+{
+ if (mLastCheckerboardReport == aSampleTime) {
+ // This function will get called multiple times for each APZC on a single
+ // composite (once for each layer it is attached to). Only report the
+ // checkerboard once per composite though.
+ return;
+ }
+ mLastCheckerboardReport = aSampleTime;
+
+ bool recordTrace = gfxPrefs::APZRecordCheckerboarding();
+ bool forTelemetry = Telemetry::CanRecordExtended();
+ uint32_t magnitude = GetCheckerboardMagnitude();
+
+ MutexAutoLock lock(mCheckerboardEventLock);
+ if (!mCheckerboardEvent && (recordTrace || forTelemetry)) {
+ mCheckerboardEvent = MakeUnique<CheckerboardEvent>(recordTrace);
+ }
+ mPotentialCheckerboardTracker.InTransform(IsTransformingState(mState));
+ if (magnitude) {
+ mPotentialCheckerboardTracker.CheckerboardSeen();
+ }
+ UpdateCheckerboardEvent(lock, magnitude);
+}
+
+void
+AsyncPanZoomController::UpdateCheckerboardEvent(const MutexAutoLock& aProofOfLock,
+ uint32_t aMagnitude)
+{
+ if (mCheckerboardEvent && mCheckerboardEvent->RecordFrameInfo(aMagnitude)) {
+ // This checkerboard event is done. Report some metrics to telemetry.
+ mozilla::Telemetry::Accumulate(mozilla::Telemetry::CHECKERBOARD_SEVERITY,
+ mCheckerboardEvent->GetSeverity());
+ mozilla::Telemetry::Accumulate(mozilla::Telemetry::CHECKERBOARD_PEAK,
+ mCheckerboardEvent->GetPeak());
+ mozilla::Telemetry::Accumulate(mozilla::Telemetry::CHECKERBOARD_DURATION,
+ (uint32_t)mCheckerboardEvent->GetDuration().ToMilliseconds());
+
+ mPotentialCheckerboardTracker.CheckerboardDone();
+
+ if (gfxPrefs::APZRecordCheckerboarding()) {
+ // if the pref is enabled, also send it to the storage class. it may be
+ // chosen for public display on about:checkerboard, the hall of fame for
+ // checkerboard events.
+ uint32_t severity = mCheckerboardEvent->GetSeverity();
+ std::string log = mCheckerboardEvent->GetLog();
+ CheckerboardEventStorage::Report(severity, log);
+ }
+ mCheckerboardEvent = nullptr;
+ }
+}
+
+void
+AsyncPanZoomController::FlushActiveCheckerboardReport()
+{
+ MutexAutoLock lock(mCheckerboardEventLock);
+ // Pretend like we got a frame with 0 pixels checkerboarded. This will
+ // terminate the checkerboard event and flush it out
+ UpdateCheckerboardEvent(lock, 0);
+}
+
+bool AsyncPanZoomController::IsCurrentlyCheckerboarding() const {
+ ReentrantMonitorAutoEnter lock(mMonitor);
+
+ if (!gfxPrefs::APZAllowCheckerboarding() || mScrollMetadata.IsApzForceDisabled()) {
+ return false;
+ }
+
+ CSSPoint currentScrollOffset = mFrameMetrics.GetScrollOffset() + mTestAsyncScrollOffset;
+ CSSRect painted = mLastContentPaintMetrics.GetDisplayPort() + mLastContentPaintMetrics.GetScrollOffset();
+ painted.Inflate(CSSMargin::FromAppUnits(nsMargin(1, 1, 1, 1))); // fuzz for rounding error
+ CSSRect visible = CSSRect(currentScrollOffset, mFrameMetrics.CalculateCompositedSizeInCssPixels());
+ if (painted.Contains(visible)) {
+ return false;
+ }
+ APZC_LOG_FM(mFrameMetrics, "%p is currently checkerboarding (painted %s visble %s)",
+ this, Stringify(painted).c_str(), Stringify(visible).c_str());
+ return true;
+}
+
+void AsyncPanZoomController::NotifyLayersUpdated(const ScrollMetadata& aScrollMetadata,
+ bool aIsFirstPaint,
+ bool aThisLayerTreeUpdated)
+{
+ APZThreadUtils::AssertOnCompositorThread();
+
+ ReentrantMonitorAutoEnter lock(mMonitor);
+ bool isDefault = mScrollMetadata.IsDefault();
+
+ const FrameMetrics& aLayerMetrics = aScrollMetadata.GetMetrics();
+
+ if ((aScrollMetadata == mLastContentPaintMetadata) && !isDefault) {
+ // No new information here, skip it.
+ APZC_LOG("%p NotifyLayersUpdated short-circuit\n", this);
+ return;
+ }
+
+ // If the mFrameMetrics scroll offset is different from the last scroll offset
+ // that the main-thread sent us, then we know that the user has been doing
+ // something that triggers a scroll. This check is the APZ equivalent of the
+ // check on the main-thread at
+ // https://hg.mozilla.org/mozilla-central/file/97a52326b06a/layout/generic/nsGfxScrollFrame.cpp#l4050
+ // There is code below (the use site of userScrolled) that prevents a restored-
+ // scroll-position update from overwriting a user scroll, again equivalent to
+ // how the main thread code does the same thing.
+ CSSPoint lastScrollOffset = mLastContentPaintMetadata.GetMetrics().GetScrollOffset();
+ bool userScrolled =
+ !FuzzyEqualsAdditive(mFrameMetrics.GetScrollOffset().x, lastScrollOffset.x) ||
+ !FuzzyEqualsAdditive(mFrameMetrics.GetScrollOffset().y, lastScrollOffset.y);
+
+ if (aLayerMetrics.GetScrollUpdateType() != FrameMetrics::ScrollOffsetUpdateType::ePending) {
+ mLastContentPaintMetadata = aScrollMetadata;
+ }
+
+ mScrollMetadata.SetScrollParentId(aScrollMetadata.GetScrollParentId());
+ APZC_LOG_FM(aLayerMetrics, "%p got a NotifyLayersUpdated with aIsFirstPaint=%d, aThisLayerTreeUpdated=%d",
+ this, aIsFirstPaint, aThisLayerTreeUpdated);
+
+ { // scope lock
+ MutexAutoLock lock(mCheckerboardEventLock);
+ if (mCheckerboardEvent && mCheckerboardEvent->IsRecordingTrace()) {
+ std::string str;
+ if (aThisLayerTreeUpdated) {
+ if (!aLayerMetrics.GetPaintRequestTime().IsNull()) {
+ // Note that we might get the paint request time as non-null, but with
+ // aThisLayerTreeUpdated false. That can happen if we get a layer transaction
+ // from a different process right after we get the layer transaction with
+ // aThisLayerTreeUpdated == true. In this case we want to ignore the
+ // paint request time because it was already dumped in the previous layer
+ // transaction.
+ TimeDuration paintTime = TimeStamp::Now() - aLayerMetrics.GetPaintRequestTime();
+ std::stringstream info;
+ info << " painttime " << paintTime.ToMilliseconds();
+ str = info.str();
+ } else {
+ // This might be indicative of a wasted paint particularly if it happens
+ // during a checkerboard event.
+ str = " (this layertree updated)";
+ }
+ }
+ mCheckerboardEvent->UpdateRendertraceProperty(
+ CheckerboardEvent::Page, aLayerMetrics.GetScrollableRect());
+ mCheckerboardEvent->UpdateRendertraceProperty(
+ CheckerboardEvent::PaintedDisplayPort,
+ aLayerMetrics.GetDisplayPort() + aLayerMetrics.GetScrollOffset(),
+ str);
+ if (!aLayerMetrics.GetCriticalDisplayPort().IsEmpty()) {
+ mCheckerboardEvent->UpdateRendertraceProperty(
+ CheckerboardEvent::PaintedCriticalDisplayPort,
+ aLayerMetrics.GetCriticalDisplayPort() + aLayerMetrics.GetScrollOffset());
+ }
+ }
+ }
+
+ bool needContentRepaint = false;
+ bool viewportUpdated = false;
+ if (FuzzyEqualsAdditive(aLayerMetrics.GetCompositionBounds().width, mFrameMetrics.GetCompositionBounds().width) &&
+ FuzzyEqualsAdditive(aLayerMetrics.GetCompositionBounds().height, mFrameMetrics.GetCompositionBounds().height)) {
+ // Remote content has sync'd up to the composition geometry
+ // change, so we can accept the viewport it's calculated.
+ if (mFrameMetrics.GetViewport().width != aLayerMetrics.GetViewport().width ||
+ mFrameMetrics.GetViewport().height != aLayerMetrics.GetViewport().height) {
+ needContentRepaint = true;
+ viewportUpdated = true;
+ }
+ mFrameMetrics.SetViewport(aLayerMetrics.GetViewport());
+ }
+
+ // If the layers update was not triggered by our own repaint request, then
+ // we want to take the new scroll offset. Check the scroll generation as well
+ // to filter duplicate calls to NotifyLayersUpdated with the same scroll offset
+ // update message.
+ bool scrollOffsetUpdated = aLayerMetrics.GetScrollOffsetUpdated()
+ && (aLayerMetrics.GetScrollGeneration() != mFrameMetrics.GetScrollGeneration());
+
+ if (scrollOffsetUpdated && userScrolled &&
+ aLayerMetrics.GetScrollUpdateType() == FrameMetrics::ScrollOffsetUpdateType::eRestore) {
+ APZC_LOG("%p dropping scroll update of type eRestore because of user scroll\n", this);
+ scrollOffsetUpdated = false;
+ }
+
+ bool smoothScrollRequested = aLayerMetrics.GetDoSmoothScroll()
+ && (aLayerMetrics.GetScrollGeneration() != mFrameMetrics.GetScrollGeneration());
+
+ // TODO if we're in a drag and scrollOffsetUpdated is set then we want to
+ // ignore it
+
+ if ((aIsFirstPaint && aThisLayerTreeUpdated) || isDefault) {
+ // Initialize our internal state to something sane when the content
+ // that was just painted is something we knew nothing about previously
+ CancelAnimation();
+
+ mScrollMetadata = aScrollMetadata;
+ mExpectedGeckoMetrics = aLayerMetrics;
+ ShareCompositorFrameMetrics();
+
+ if (mFrameMetrics.GetDisplayPortMargins() != ScreenMargin()) {
+ // A non-zero display port margin here indicates a displayport has
+ // been set by a previous APZC for the content at this guid. The
+ // scrollable rect may have changed since then, making the margins
+ // wrong, so we need to calculate a new display port.
+ APZC_LOG("%p detected non-empty margins which probably need updating\n", this);
+ needContentRepaint = true;
+ }
+ } else {
+ // If we're not taking the aLayerMetrics wholesale we still need to pull
+ // in some things into our local mFrameMetrics because these things are
+ // determined by Gecko and our copy in mFrameMetrics may be stale.
+
+ if (FuzzyEqualsAdditive(mFrameMetrics.GetCompositionBounds().width, aLayerMetrics.GetCompositionBounds().width) &&
+ mFrameMetrics.GetDevPixelsPerCSSPixel() == aLayerMetrics.GetDevPixelsPerCSSPixel() &&
+ !viewportUpdated) {
+ // Any change to the pres shell resolution was requested by APZ and is
+ // already included in our zoom; however, other components of the
+ // cumulative resolution (a parent document's pres-shell resolution, or
+ // the css-driven resolution) may have changed, and we need to update
+ // our zoom to reflect that. Note that we can't just take
+ // aLayerMetrics.mZoom because the APZ may have additional async zoom
+ // since the repaint request.
+ gfxSize totalResolutionChange = aLayerMetrics.GetCumulativeResolution()
+ / mFrameMetrics.GetCumulativeResolution();
+ float presShellResolutionChange = aLayerMetrics.GetPresShellResolution()
+ / mFrameMetrics.GetPresShellResolution();
+ if (presShellResolutionChange != 1.0f) {
+ needContentRepaint = true;
+ }
+ mFrameMetrics.ZoomBy(totalResolutionChange / presShellResolutionChange);
+ } else {
+ // Take the new zoom as either device scale or composition width or
+ // viewport size got changed (e.g. due to orientation change, or content
+ // changing the meta-viewport tag).
+ mFrameMetrics.SetZoom(aLayerMetrics.GetZoom());
+ mFrameMetrics.SetDevPixelsPerCSSPixel(aLayerMetrics.GetDevPixelsPerCSSPixel());
+ }
+ if (!mFrameMetrics.GetScrollableRect().IsEqualEdges(aLayerMetrics.GetScrollableRect())) {
+ mFrameMetrics.SetScrollableRect(aLayerMetrics.GetScrollableRect());
+ needContentRepaint = true;
+ }
+ mFrameMetrics.SetCompositionBounds(aLayerMetrics.GetCompositionBounds());
+ mFrameMetrics.SetRootCompositionSize(aLayerMetrics.GetRootCompositionSize());
+ mFrameMetrics.SetPresShellResolution(aLayerMetrics.GetPresShellResolution());
+ mFrameMetrics.SetCumulativeResolution(aLayerMetrics.GetCumulativeResolution());
+ mScrollMetadata.SetHasScrollgrab(aScrollMetadata.GetHasScrollgrab());
+ mScrollMetadata.SetLineScrollAmount(aScrollMetadata.GetLineScrollAmount());
+ mScrollMetadata.SetPageScrollAmount(aScrollMetadata.GetPageScrollAmount());
+ mScrollMetadata.SetSnapInfo(ScrollSnapInfo(aScrollMetadata.GetSnapInfo()));
+ // The scroll clip can differ between layers associated a given scroll frame,
+ // so APZC (which keeps a single copy of ScrollMetadata per scroll frame)
+ // has no business using it.
+ mScrollMetadata.SetScrollClip(Nothing());
+ mScrollMetadata.SetIsLayersIdRoot(aScrollMetadata.IsLayersIdRoot());
+ mScrollMetadata.SetUsesContainerScrolling(aScrollMetadata.UsesContainerScrolling());
+ mFrameMetrics.SetIsScrollInfoLayer(aLayerMetrics.IsScrollInfoLayer());
+ mScrollMetadata.SetForceDisableApz(aScrollMetadata.IsApzForceDisabled());
+
+ if (scrollOffsetUpdated) {
+ APZC_LOG("%p updating scroll offset from %s to %s\n", this,
+ ToString(mFrameMetrics.GetScrollOffset()).c_str(),
+ ToString(aLayerMetrics.GetScrollOffset()).c_str());
+
+ // Send an acknowledgement with the new scroll generation so that any
+ // repaint requests later in this function go through.
+ // Because of the scroll generation update, any inflight paint requests are
+ // going to be ignored by layout, and so mExpectedGeckoMetrics
+ // becomes incorrect for the purposes of calculating the LD transform. To
+ // correct this we need to update mExpectedGeckoMetrics to be the
+ // last thing we know was painted by Gecko.
+ mFrameMetrics.CopyScrollInfoFrom(aLayerMetrics);
+ mExpectedGeckoMetrics = aLayerMetrics;
+
+ // Cancel the animation (which might also trigger a repaint request)
+ // after we update the scroll offset above. Otherwise we can be left
+ // in a state where things are out of sync.
+ CancelAnimation();
+
+ // Since the scroll offset has changed, we need to recompute the
+ // displayport margins and send them to layout. Otherwise there might be
+ // scenarios where for example we scroll from the top of a page (where the
+ // top displayport margin is zero) to the bottom of a page, which will
+ // result in a displayport that doesn't extend upwards at all.
+ // Note that even if the CancelAnimation call above requested a repaint
+ // this is fine because we already have repaint request deduplication.
+ needContentRepaint = true;
+ }
+ }
+
+ if (smoothScrollRequested) {
+ // A smooth scroll has been requested for animation on the compositor
+ // thread. This flag will be reset by the main thread when it receives
+ // the scroll update acknowledgement.
+
+ APZC_LOG("%p smooth scrolling from %s to %s in state %d\n", this,
+ Stringify(mFrameMetrics.GetScrollOffset()).c_str(),
+ Stringify(aLayerMetrics.GetSmoothScrollOffset()).c_str(),
+ mState);
+
+ // See comment on the similar code in the |if (scrollOffsetUpdated)| block
+ // above.
+ mFrameMetrics.CopySmoothScrollInfoFrom(aLayerMetrics);
+ needContentRepaint = true;
+ mExpectedGeckoMetrics = aLayerMetrics;
+
+ SmoothScrollTo(mFrameMetrics.GetSmoothScrollOffset());
+ }
+
+ if (needContentRepaint) {
+ // This repaint request is not driven by a user action on the APZ side
+ RequestContentRepaint(false);
+ }
+ UpdateSharedCompositorFrameMetrics();
+}
+
+const FrameMetrics& AsyncPanZoomController::GetFrameMetrics() const {
+ mMonitor.AssertCurrentThreadIn();
+ return mFrameMetrics;
+}
+
+APZCTreeManager* AsyncPanZoomController::GetApzcTreeManager() const {
+ mMonitor.AssertNotCurrentThreadIn();
+ return mTreeManager;
+}
+
+void AsyncPanZoomController::ZoomToRect(CSSRect aRect, const uint32_t aFlags) {
+ if (!aRect.IsFinite()) {
+ NS_WARNING("ZoomToRect got called with a non-finite rect; ignoring...");
+ return;
+ } else if (aRect.IsEmpty() && (aFlags & DISABLE_ZOOM_OUT)) {
+ // Double-tap-to-zooming uses an empty rect to mean "zoom out".
+ // If zooming out is disabled, an empty rect is nonsensical
+ // and will produce undesirable scrolling.
+ NS_WARNING("ZoomToRect got called with an empty rect and zoom out disabled; ignoring...");
+ return;
+ }
+
+ // Only the root APZC is zoomable, and the root APZC is not allowed to have
+ // different x and y scales. If it did, the calculations in this function
+ // would have to be adjusted (as e.g. it would no longer be valid to take
+ // the minimum or maximum of the ratios of the widths and heights of the
+ // page rect and the composition bounds).
+ MOZ_ASSERT(mFrameMetrics.IsRootContent());
+ MOZ_ASSERT(mFrameMetrics.GetZoom().AreScalesSame());
+
+ SetState(ANIMATING_ZOOM);
+
+ {
+ ReentrantMonitorAutoEnter lock(mMonitor);
+
+ ParentLayerRect compositionBounds = mFrameMetrics.GetCompositionBounds();
+ CSSRect cssPageRect = mFrameMetrics.GetScrollableRect();
+ CSSPoint scrollOffset = mFrameMetrics.GetScrollOffset();
+ CSSToParentLayerScale currentZoom = mFrameMetrics.GetZoom().ToScaleFactor();
+ CSSToParentLayerScale targetZoom;
+
+ // The minimum zoom to prevent over-zoom-out.
+ // If the zoom factor is lower than this (i.e. we are zoomed more into the page),
+ // then the CSS content rect, in layers pixels, will be smaller than the
+ // composition bounds. If this happens, we can't fill the target composited
+ // area with this frame.
+ CSSToParentLayerScale localMinZoom(std::max(mZoomConstraints.mMinZoom.scale,
+ std::max(compositionBounds.width / cssPageRect.width,
+ compositionBounds.height / cssPageRect.height)));
+ CSSToParentLayerScale localMaxZoom = mZoomConstraints.mMaxZoom;
+
+ if (!aRect.IsEmpty()) {
+ // Intersect the zoom-to-rect to the CSS rect to make sure it fits.
+ aRect = aRect.Intersect(cssPageRect);
+ targetZoom = CSSToParentLayerScale(std::min(compositionBounds.width / aRect.width,
+ compositionBounds.height / aRect.height));
+ }
+
+ // 1. If the rect is empty, the content-side logic for handling a double-tap
+ // requested that we zoom out.
+ // 2. currentZoom is equal to mZoomConstraints.mMaxZoom and user still double-tapping it
+ // 3. currentZoom is equal to localMinZoom and user still double-tapping it
+ // Treat these three cases as a request to zoom out as much as possible.
+ bool zoomOut;
+ if (aFlags & DISABLE_ZOOM_OUT) {
+ zoomOut = false;
+ } else {
+ zoomOut = aRect.IsEmpty() ||
+ (currentZoom == localMaxZoom && targetZoom >= localMaxZoom) ||
+ (currentZoom == localMinZoom && targetZoom <= localMinZoom);
+ }
+
+ if (zoomOut) {
+ CSSSize compositedSize = mFrameMetrics.CalculateCompositedSizeInCssPixels();
+ float y = scrollOffset.y;
+ float newHeight =
+ cssPageRect.width * (compositedSize.height / compositedSize.width);
+ float dh = compositedSize.height - newHeight;
+
+ aRect = CSSRect(0.0f,
+ y + dh/2,
+ cssPageRect.width,
+ newHeight);
+ aRect = aRect.Intersect(cssPageRect);
+ targetZoom = CSSToParentLayerScale(std::min(compositionBounds.width / aRect.width,
+ compositionBounds.height / aRect.height));
+ }
+
+ targetZoom.scale = clamped(targetZoom.scale, localMinZoom.scale, localMaxZoom.scale);
+ FrameMetrics endZoomToMetrics = mFrameMetrics;
+ if (aFlags & PAN_INTO_VIEW_ONLY) {
+ targetZoom = currentZoom;
+ } else if(aFlags & ONLY_ZOOM_TO_DEFAULT_SCALE) {
+ CSSToParentLayerScale zoomAtDefaultScale =
+ mFrameMetrics.GetDevPixelsPerCSSPixel() * LayoutDeviceToParentLayerScale(1.0);
+ if (targetZoom.scale > zoomAtDefaultScale.scale) {
+ // Only change the zoom if we are less than the default zoom
+ if (currentZoom.scale < zoomAtDefaultScale.scale) {
+ targetZoom = zoomAtDefaultScale;
+ } else {
+ targetZoom = currentZoom;
+ }
+ }
+ }
+ endZoomToMetrics.SetZoom(CSSToParentLayerScale2D(targetZoom));
+
+ // Adjust the zoomToRect to a sensible position to prevent overscrolling.
+ CSSSize sizeAfterZoom = endZoomToMetrics.CalculateCompositedSizeInCssPixels();
+
+ // Vertically center the zoomed element in the screen.
+ if (!zoomOut && (sizeAfterZoom.height > aRect.height)) {
+ aRect.y -= (sizeAfterZoom.height - aRect.height) * 0.5f;
+ if (aRect.y < 0.0f) {
+ aRect.y = 0.0f;
+ }
+ }
+
+ // If either of these conditions are met, the page will be
+ // overscrolled after zoomed
+ if (aRect.y + sizeAfterZoom.height > cssPageRect.height) {
+ aRect.y = cssPageRect.height - sizeAfterZoom.height;
+ aRect.y = aRect.y > 0 ? aRect.y : 0;
+ }
+ if (aRect.x + sizeAfterZoom.width > cssPageRect.width) {
+ aRect.x = cssPageRect.width - sizeAfterZoom.width;
+ aRect.x = aRect.x > 0 ? aRect.x : 0;
+ }
+
+ endZoomToMetrics.SetScrollOffset(aRect.TopLeft());
+
+ StartAnimation(new ZoomAnimation(
+ mFrameMetrics.GetScrollOffset(),
+ mFrameMetrics.GetZoom(),
+ endZoomToMetrics.GetScrollOffset(),
+ endZoomToMetrics.GetZoom()));
+
+ // Schedule a repaint now, so the new displayport will be painted before the
+ // animation finishes.
+ ParentLayerPoint velocity(0, 0);
+ endZoomToMetrics.SetDisplayPortMargins(
+ CalculatePendingDisplayPort(endZoomToMetrics, velocity));
+ endZoomToMetrics.SetUseDisplayPortMargins(true);
+ endZoomToMetrics.SetPaintRequestTime(TimeStamp::Now());
+ endZoomToMetrics.SetRepaintDrivenByUserAction(true);
+
+ RefPtr<GeckoContentController> controller = GetGeckoContentController();
+ if (!controller) {
+ return;
+ }
+ if (controller->IsRepaintThread()) {
+ RequestContentRepaint(endZoomToMetrics, velocity);
+ } else {
+ // use a local var to resolve the function overload
+ auto func = static_cast<void (AsyncPanZoomController::*)(const FrameMetrics&, const ParentLayerPoint&)>
+ (&AsyncPanZoomController::RequestContentRepaint);
+ controller->DispatchToRepaintThread(
+ NewRunnableMethod<FrameMetrics, ParentLayerPoint>(
+ this, func, endZoomToMetrics, velocity));
+ }
+ }
+}
+
+CancelableBlockState*
+AsyncPanZoomController::GetCurrentInputBlock() const
+{
+ return GetInputQueue()->GetCurrentBlock();
+}
+
+TouchBlockState*
+AsyncPanZoomController::GetCurrentTouchBlock() const
+{
+ return GetInputQueue()->GetCurrentTouchBlock();
+}
+
+PanGestureBlockState*
+AsyncPanZoomController::GetCurrentPanGestureBlock() const
+{
+ return GetInputQueue()->GetCurrentPanGestureBlock();
+}
+
+void
+AsyncPanZoomController::ResetTouchInputState()
+{
+ MultiTouchInput cancel(MultiTouchInput::MULTITOUCH_CANCEL, 0, TimeStamp::Now(), 0);
+ RefPtr<GestureEventListener> listener = GetGestureEventListener();
+ if (listener) {
+ listener->HandleInputEvent(cancel);
+ }
+ CancelAnimationAndGestureState();
+ // Clear overscroll along the entire handoff chain, in case an APZC
+ // later in the chain is overscrolled.
+ if (TouchBlockState* block = GetCurrentTouchBlock()) {
+ block->GetOverscrollHandoffChain()->ClearOverscroll();
+ }
+}
+
+void
+AsyncPanZoomController::CancelAnimationAndGestureState()
+{
+ mX.CancelGesture();
+ mY.CancelGesture();
+ CancelAnimation(CancelAnimationFlags::ScrollSnap);
+}
+
+bool
+AsyncPanZoomController::HasReadyTouchBlock() const
+{
+ return GetInputQueue()->HasReadyTouchBlock();
+}
+
+void AsyncPanZoomController::SetState(PanZoomState aNewState)
+{
+ PanZoomState oldState;
+
+ // Intentional scoping for mutex
+ {
+ ReentrantMonitorAutoEnter lock(mMonitor);
+ APZC_LOG("%p changing from state %d to %d\n", this, mState, aNewState);
+ oldState = mState;
+ mState = aNewState;
+ }
+
+ DispatchStateChangeNotification(oldState, aNewState);
+}
+
+void AsyncPanZoomController::DispatchStateChangeNotification(PanZoomState aOldState,
+ PanZoomState aNewState)
+{
+ { // scope the lock
+ ReentrantMonitorAutoEnter lock(mMonitor);
+ if (mNotificationBlockers > 0) {
+ return;
+ }
+ }
+
+ if (RefPtr<GeckoContentController> controller = GetGeckoContentController()) {
+ if (!IsTransformingState(aOldState) && IsTransformingState(aNewState)) {
+ controller->NotifyAPZStateChange(
+ GetGuid(), APZStateChange::eTransformBegin);
+#if defined(XP_WIN) || defined(MOZ_WIDGET_GTK)
+ // Let the compositor know about scroll state changes so it can manage
+ // windowed plugins.
+ if (gfxPrefs::HidePluginsForScroll() && mCompositorController) {
+ mCompositorController->ScheduleHideAllPluginWindows();
+ }
+#endif
+ } else if (IsTransformingState(aOldState) && !IsTransformingState(aNewState)) {
+ controller->NotifyAPZStateChange(
+ GetGuid(), APZStateChange::eTransformEnd);
+#if defined(XP_WIN) || defined(MOZ_WIDGET_GTK)
+ if (gfxPrefs::HidePluginsForScroll() && mCompositorController) {
+ mCompositorController->ScheduleShowAllPluginWindows();
+ }
+#endif
+ }
+ }
+}
+
+bool AsyncPanZoomController::IsTransformingState(PanZoomState aState) {
+ return !(aState == NOTHING || aState == TOUCHING);
+}
+
+bool AsyncPanZoomController::IsInPanningState() const {
+ return (mState == PANNING || mState == PANNING_LOCKED_X || mState == PANNING_LOCKED_Y);
+}
+
+void AsyncPanZoomController::UpdateZoomConstraints(const ZoomConstraints& aConstraints) {
+ APZC_LOG("%p updating zoom constraints to %d %d %f %f\n", this, aConstraints.mAllowZoom,
+ aConstraints.mAllowDoubleTapZoom, aConstraints.mMinZoom.scale, aConstraints.mMaxZoom.scale);
+ if (IsNaN(aConstraints.mMinZoom.scale) || IsNaN(aConstraints.mMaxZoom.scale)) {
+ NS_WARNING("APZC received zoom constraints with NaN values; dropping...");
+ return;
+ }
+
+ CSSToParentLayerScale min = mFrameMetrics.GetDevPixelsPerCSSPixel()
+ * kViewportMinScale / ParentLayerToScreenScale(1);
+ CSSToParentLayerScale max = mFrameMetrics.GetDevPixelsPerCSSPixel()
+ * kViewportMaxScale / ParentLayerToScreenScale(1);
+
+ // inf float values and other bad cases should be sanitized by the code below.
+ mZoomConstraints.mAllowZoom = aConstraints.mAllowZoom;
+ mZoomConstraints.mAllowDoubleTapZoom = aConstraints.mAllowDoubleTapZoom;
+ mZoomConstraints.mMinZoom = (min > aConstraints.mMinZoom ? min : aConstraints.mMinZoom);
+ mZoomConstraints.mMaxZoom = (max > aConstraints.mMaxZoom ? aConstraints.mMaxZoom : max);
+ if (mZoomConstraints.mMaxZoom < mZoomConstraints.mMinZoom) {
+ mZoomConstraints.mMaxZoom = mZoomConstraints.mMinZoom;
+ }
+}
+
+ZoomConstraints
+AsyncPanZoomController::GetZoomConstraints() const
+{
+ return mZoomConstraints;
+}
+
+
+void AsyncPanZoomController::PostDelayedTask(already_AddRefed<Runnable> aTask, int aDelayMs) {
+ APZThreadUtils::AssertOnControllerThread();
+ RefPtr<Runnable> task = aTask;
+ RefPtr<GeckoContentController> controller = GetGeckoContentController();
+ if (controller) {
+ controller->PostDelayedTask(task.forget(), aDelayMs);
+ }
+ // If there is no controller, that means this APZC has been destroyed, and
+ // we probably don't need to run the task. It will get destroyed when the
+ // RefPtr goes out of scope.
+}
+
+bool AsyncPanZoomController::Matches(const ScrollableLayerGuid& aGuid)
+{
+ return aGuid == GetGuid();
+}
+
+bool AsyncPanZoomController::HasTreeManager(const APZCTreeManager* aTreeManager) const
+{
+ return GetApzcTreeManager() == aTreeManager;
+}
+
+void AsyncPanZoomController::GetGuid(ScrollableLayerGuid* aGuidOut) const
+{
+ if (aGuidOut) {
+ *aGuidOut = GetGuid();
+ }
+}
+
+ScrollableLayerGuid AsyncPanZoomController::GetGuid() const
+{
+ return ScrollableLayerGuid(mLayersId, mFrameMetrics);
+}
+
+void AsyncPanZoomController::UpdateSharedCompositorFrameMetrics()
+{
+ mMonitor.AssertCurrentThreadIn();
+
+ FrameMetrics* frame = mSharedFrameMetricsBuffer ?
+ static_cast<FrameMetrics*>(mSharedFrameMetricsBuffer->memory()) : nullptr;
+
+ if (frame && mSharedLock && gfxPrefs::ProgressivePaint()) {
+ mSharedLock->Lock();
+ *frame = mFrameMetrics;
+ mSharedLock->Unlock();
+ }
+}
+
+void AsyncPanZoomController::ShareCompositorFrameMetrics()
+{
+ APZThreadUtils::AssertOnCompositorThread();
+
+ // Only create the shared memory buffer if it hasn't already been created,
+ // we are using progressive tile painting, and we have a
+ // controller to pass the shared memory back to the content process/thread.
+ if (!mSharedFrameMetricsBuffer && mMetricsSharingController && gfxPrefs::ProgressivePaint()) {
+
+ // Create shared memory and initialize it with the current FrameMetrics value
+ mSharedFrameMetricsBuffer = new ipc::SharedMemoryBasic;
+ FrameMetrics* frame = nullptr;
+ mSharedFrameMetricsBuffer->Create(sizeof(FrameMetrics));
+ mSharedFrameMetricsBuffer->Map(sizeof(FrameMetrics));
+ frame = static_cast<FrameMetrics*>(mSharedFrameMetricsBuffer->memory());
+
+ if (frame) {
+
+ { // scope the monitor, only needed to copy the FrameMetrics.
+ ReentrantMonitorAutoEnter lock(mMonitor);
+ *frame = mFrameMetrics;
+ }
+
+ // Get the process id of the content process
+ base::ProcessId otherPid = mMetricsSharingController->RemotePid();
+ ipc::SharedMemoryBasic::Handle mem = ipc::SharedMemoryBasic::NULLHandle();
+
+ // Get the shared memory handle to share with the content process
+ mSharedFrameMetricsBuffer->ShareToProcess(otherPid, &mem);
+
+ // Get the cross process mutex handle to share with the content process
+ mSharedLock = new CrossProcessMutex("AsyncPanZoomControlLock");
+ CrossProcessMutexHandle handle = mSharedLock->ShareToProcess(otherPid);
+
+ // Send the shared memory handle and cross process handle to the content
+ // process by an asynchronous ipc call. Include the APZC unique ID
+ // so the content process know which APZC sent this shared FrameMetrics.
+ if (!mMetricsSharingController->StartSharingMetrics(mem, handle, mLayersId, mAPZCId)) {
+ APZC_LOG("%p failed to share FrameMetrics with content process.", this);
+ }
+ }
+ }
+}
+
+Maybe<CSSPoint> AsyncPanZoomController::FindSnapPointNear(
+ const CSSPoint& aDestination, nsIScrollableFrame::ScrollUnit aUnit) {
+ mMonitor.AssertCurrentThreadIn();
+ APZC_LOG("%p scroll snapping near %s\n", this, Stringify(aDestination).c_str());
+ CSSRect scrollRange = mFrameMetrics.CalculateScrollRange();
+ if (Maybe<nsPoint> snapPoint = ScrollSnapUtils::GetSnapPointForDestination(
+ mScrollMetadata.GetSnapInfo(),
+ aUnit,
+ CSSSize::ToAppUnits(mFrameMetrics.CalculateCompositedSizeInCssPixels()),
+ CSSRect::ToAppUnits(scrollRange),
+ CSSPoint::ToAppUnits(mFrameMetrics.GetScrollOffset()),
+ CSSPoint::ToAppUnits(aDestination))) {
+ CSSPoint cssSnapPoint = CSSPoint::FromAppUnits(snapPoint.ref());
+ // GetSnapPointForDestination() can produce a destination that's outside
+ // of the scroll frame's scroll range. Clamp it here (this matches the
+ // behaviour of the main-thread code path, which clamps it in
+ // nsGfxScrollFrame::ScrollTo()).
+ return Some(scrollRange.ClampPoint(cssSnapPoint));
+ }
+ return Nothing();
+}
+
+void AsyncPanZoomController::ScrollSnapNear(const CSSPoint& aDestination) {
+ if (Maybe<CSSPoint> snapPoint =
+ FindSnapPointNear(aDestination, nsIScrollableFrame::DEVICE_PIXELS)) {
+ if (*snapPoint != mFrameMetrics.GetScrollOffset()) {
+ APZC_LOG("%p smooth scrolling to snap point %s\n", this, Stringify(*snapPoint).c_str());
+ SmoothScrollTo(*snapPoint);
+ }
+ }
+}
+
+void AsyncPanZoomController::ScrollSnap() {
+ ReentrantMonitorAutoEnter lock(mMonitor);
+ ScrollSnapNear(mFrameMetrics.GetScrollOffset());
+}
+
+void AsyncPanZoomController::ScrollSnapToDestination() {
+ ReentrantMonitorAutoEnter lock(mMonitor);
+
+ float friction = gfxPrefs::APZFlingFriction();
+ ParentLayerPoint velocity(mX.GetVelocity(), mY.GetVelocity());
+ ParentLayerPoint predictedDelta;
+ // "-velocity / log(1.0 - friction)" is the integral of the deceleration
+ // curve modeled for flings in the "Axis" class.
+ if (velocity.x != 0.0f) {
+ predictedDelta.x = -velocity.x / log(1.0 - friction);
+ }
+ if (velocity.y != 0.0f) {
+ predictedDelta.y = -velocity.y / log(1.0 - friction);
+ }
+ CSSPoint predictedDestination = mFrameMetrics.GetScrollOffset() + predictedDelta / mFrameMetrics.GetZoom();
+
+ // If the fling will overscroll, don't scroll snap, because then the user
+ // user would not see any overscroll animation.
+ bool flingWillOverscroll = IsOverscrolled() && ((velocity.x * mX.GetOverscroll() >= 0) ||
+ (velocity.y * mY.GetOverscroll() >= 0));
+ if (!flingWillOverscroll) {
+ APZC_LOG("%p fling snapping. friction: %f velocity: %f, %f "
+ "predictedDelta: %f, %f position: %f, %f "
+ "predictedDestination: %f, %f\n",
+ this, friction, velocity.x, velocity.y, (float)predictedDelta.x,
+ (float)predictedDelta.y, (float)mFrameMetrics.GetScrollOffset().x,
+ (float)mFrameMetrics.GetScrollOffset().y,
+ (float)predictedDestination.x, (float)predictedDestination.y);
+
+ ScrollSnapNear(predictedDestination);
+ }
+}
+
+bool AsyncPanZoomController::MaybeAdjustDeltaForScrollSnapping(
+ const ScrollWheelInput& aEvent,
+ ParentLayerPoint& aDelta,
+ CSSPoint& aStartPosition)
+{
+ // Don't scroll snap for pixel scrolls. This matches the main thread
+ // behaviour in EventStateManager::DoScrollText().
+ if (aEvent.mDeltaType == ScrollWheelInput::SCROLLDELTA_PIXEL) {
+ return false;
+ }
+
+ ReentrantMonitorAutoEnter lock(mMonitor);
+ CSSToParentLayerScale2D zoom = mFrameMetrics.GetZoom();
+ CSSPoint destination = mFrameMetrics.CalculateScrollRange().ClampPoint(
+ aStartPosition + (aDelta / zoom));
+ nsIScrollableFrame::ScrollUnit unit =
+ ScrollWheelInput::ScrollUnitForDeltaType(aEvent.mDeltaType);
+
+ if (Maybe<CSSPoint> snapPoint = FindSnapPointNear(destination, unit)) {
+ aDelta = (*snapPoint - aStartPosition) * zoom;
+ aStartPosition = *snapPoint;
+ return true;
+ }
+ return false;
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/apz/src/AsyncPanZoomController.h b/gfx/layers/apz/src/AsyncPanZoomController.h
new file mode 100644
index 000000000..36f2f308c
--- /dev/null
+++ b/gfx/layers/apz/src/AsyncPanZoomController.h
@@ -0,0 +1,1224 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set sw=2 ts=8 et tw=80 : */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_layers_AsyncPanZoomController_h
+#define mozilla_layers_AsyncPanZoomController_h
+
+#include "CrossProcessMutex.h"
+#include "mozilla/layers/GeckoContentController.h"
+#include "mozilla/layers/APZCTreeManager.h"
+#include "mozilla/layers/AsyncPanZoomAnimation.h"
+#include "mozilla/Attributes.h"
+#include "mozilla/EventForwards.h"
+#include "mozilla/Monitor.h"
+#include "mozilla/ReentrantMonitor.h"
+#include "mozilla/RefPtr.h"
+#include "mozilla/UniquePtr.h"
+#include "mozilla/Atomics.h"
+#include "InputData.h"
+#include "Axis.h"
+#include "InputQueue.h"
+#include "APZUtils.h"
+#include "Layers.h" // for Layer::ScrollDirection
+#include "LayersTypes.h"
+#include "mozilla/gfx/Matrix.h"
+#include "nsIScrollableFrame.h"
+#include "nsRegion.h"
+#include "nsTArray.h"
+#include "PotentialCheckerboardDurationTracker.h"
+
+#include "base/message_loop.h"
+
+namespace mozilla {
+
+namespace ipc {
+
+class SharedMemoryBasic;
+
+} // namespace ipc
+
+namespace layers {
+
+class AsyncDragMetrics;
+struct ScrollableLayerGuid;
+class CompositorController;
+class MetricsSharingController;
+class GestureEventListener;
+struct AsyncTransform;
+class AsyncPanZoomAnimation;
+class AndroidFlingAnimation;
+class GenericFlingAnimation;
+class InputBlockState;
+class TouchBlockState;
+class PanGestureBlockState;
+class OverscrollHandoffChain;
+class StateChangeNotificationBlocker;
+class CheckerboardEvent;
+class OverscrollEffectBase;
+class WidgetOverscrollEffect;
+class GenericOverscrollEffect;
+class AndroidSpecificState;
+
+// Base class for grouping platform-specific APZC state variables.
+class PlatformSpecificStateBase {
+public:
+ virtual ~PlatformSpecificStateBase() {}
+ virtual AndroidSpecificState* AsAndroidSpecificState() { return nullptr; }
+};
+
+/**
+ * Controller for all panning and zooming logic. Any time a user input is
+ * detected and it must be processed in some way to affect what the user sees,
+ * it goes through here. Listens for any input event from InputData and can
+ * optionally handle WidgetGUIEvent-derived touch events, but this must be done
+ * on the main thread. Note that this class completely cross-platform.
+ *
+ * Input events originate on the UI thread of the platform that this runs on,
+ * and are then sent to this class. This class processes the event in some way;
+ * for example, a touch move will usually lead to a panning of content (though
+ * of course there are exceptions, such as if content preventDefaults the event,
+ * or if the target frame is not scrollable). The compositor interacts with this
+ * class by locking it and querying it for the current transform matrix based on
+ * the panning and zooming logic that was invoked on the UI thread.
+ *
+ * Currently, each outer DOM window (i.e. a website in a tab, but not any
+ * subframes) has its own AsyncPanZoomController. In the future, to support
+ * asynchronously scrolled subframes, we want to have one AsyncPanZoomController
+ * per frame.
+ */
+class AsyncPanZoomController {
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(AsyncPanZoomController)
+
+ typedef mozilla::MonitorAutoLock MonitorAutoLock;
+ typedef mozilla::gfx::Matrix4x4 Matrix4x4;
+
+public:
+ enum GestureBehavior {
+ // The platform code is responsible for forwarding gesture events here. We
+ // will not attempt to generate gesture events from MultiTouchInputs.
+ DEFAULT_GESTURES,
+ // An instance of GestureEventListener is used to detect gestures. This is
+ // handled completely internally within this class.
+ USE_GESTURE_DETECTOR
+ };
+
+ /**
+ * Constant describing the tolerance in distance we use, multiplied by the
+ * device DPI, before we start panning the screen. This is to prevent us from
+ * accidentally processing taps as touch moves, and from very short/accidental
+ * touches moving the screen.
+ * Note: It's an abuse of the 'Coord' class to use it to represent a 2D
+ * distance, but it's the closest thing we currently have.
+ */
+ static ScreenCoord GetTouchStartTolerance();
+
+ AsyncPanZoomController(uint64_t aLayersId,
+ APZCTreeManager* aTreeManager,
+ const RefPtr<InputQueue>& aInputQueue,
+ GeckoContentController* aController,
+ GestureBehavior aGestures = DEFAULT_GESTURES);
+
+ // --------------------------------------------------------------------------
+ // These methods must only be called on the gecko thread.
+ //
+
+ /**
+ * Read the various prefs and do any global initialization for all APZC instances.
+ * This must be run on the gecko thread before any APZC instances are actually
+ * used for anything meaningful.
+ */
+ static void InitializeGlobalState();
+
+ // --------------------------------------------------------------------------
+ // These methods must only be called on the controller/UI thread.
+ //
+
+ /**
+ * Kicks an animation to zoom to a rect. This may be either a zoom out or zoom
+ * in. The actual animation is done on the compositor thread after being set
+ * up.
+ */
+ void ZoomToRect(CSSRect aRect, const uint32_t aFlags);
+
+ /**
+ * Updates any zoom constraints contained in the <meta name="viewport"> tag.
+ */
+ void UpdateZoomConstraints(const ZoomConstraints& aConstraints);
+
+ /**
+ * Return the zoom constraints last set for this APZC (in the constructor
+ * or in UpdateZoomConstraints()).
+ */
+ ZoomConstraints GetZoomConstraints() const;
+
+ /**
+ * Schedules a runnable to run on the controller/UI thread at some time
+ * in the future.
+ */
+ void PostDelayedTask(already_AddRefed<Runnable> aTask, int aDelayMs);
+
+ // --------------------------------------------------------------------------
+ // These methods must only be called on the compositor thread.
+ //
+
+ /**
+ * Advances any animations currently running to the given timestamp.
+ * This may be called multiple times with the same timestamp.
+ *
+ * The return value indicates whether or not any currently running animation
+ * should continue. If true, the compositor should schedule another composite.
+ */
+ bool AdvanceAnimations(const TimeStamp& aSampleTime);
+
+ bool UpdateAnimation(const TimeStamp& aSampleTime,
+ nsTArray<RefPtr<Runnable>>* aOutDeferredTasks);
+
+ /**
+ * A shadow layer update has arrived. |aScrollMetdata| is the new ScrollMetadata
+ * for the container layer corresponding to this APZC.
+ * |aIsFirstPaint| is a flag passed from the shadow
+ * layers code indicating that the scroll metadata being sent with this call are
+ * the initial metadata and the initial paint of the frame has just happened.
+ */
+ void NotifyLayersUpdated(const ScrollMetadata& aScrollMetadata, bool aIsFirstPaint,
+ bool aThisLayerTreeUpdated);
+
+ /**
+ * The platform implementation must set the compositor controller so that we can
+ * request composites.
+ */
+ void SetCompositorController(CompositorController* aCompositorController);
+
+ /**
+ * If we need to share the frame metrics with some other thread, this controller
+ * needs to be set and provides relevant information/APIs.
+ */
+ void SetMetricsSharingController(MetricsSharingController* aMetricsSharingController);
+
+ // --------------------------------------------------------------------------
+ // These methods can be called from any thread.
+ //
+
+ /**
+ * Shut down the controller/UI thread state and prepare to be
+ * deleted (which may happen from any thread).
+ */
+ void Destroy();
+
+ /**
+ * Returns true if Destroy() has already been called on this APZC instance.
+ */
+ bool IsDestroyed() const;
+
+ /**
+ * Returns the transform to take something from the coordinate space of the
+ * last thing we know gecko painted, to the coordinate space of the last thing
+ * we asked gecko to paint. In cases where that last request has not yet been
+ * processed, this is needed to transform input events properly into a space
+ * gecko will understand.
+ */
+ Matrix4x4 GetTransformToLastDispatchedPaint() const;
+
+ /**
+ * Returns the number of CSS pixels of checkerboard according to the metrics
+ * in this APZC.
+ */
+ uint32_t GetCheckerboardMagnitude() const;
+
+ /**
+ * Report the number of CSSPixel-milliseconds of checkerboard to telemetry.
+ */
+ void ReportCheckerboard(const TimeStamp& aSampleTime);
+
+ /**
+ * Flush any active checkerboard report that's in progress. This basically
+ * pretends like any in-progress checkerboard event has terminated, and pushes
+ * out the report to the checkerboard reporting service and telemetry. If the
+ * checkerboard event has not really finished, it will start a new event
+ * on the next composite.
+ */
+ void FlushActiveCheckerboardReport();
+
+ /**
+ * Returns whether or not the APZC is currently in a state of checkerboarding.
+ * This is a simple computation based on the last-painted content and whether
+ * the async transform has pushed it so far that it doesn't fully contain the
+ * composition bounds.
+ */
+ bool IsCurrentlyCheckerboarding() const;
+
+ /**
+ * Recalculates the displayport. Ideally, this should paint an area bigger
+ * than the composite-to dimensions so that when you scroll down, you don't
+ * checkerboard immediately. This includes a bunch of logic, including
+ * algorithms to bias painting in the direction of the velocity.
+ */
+ static const ScreenMargin CalculatePendingDisplayPort(
+ const FrameMetrics& aFrameMetrics,
+ const ParentLayerPoint& aVelocity);
+
+ nsEventStatus HandleDragEvent(const MouseInput& aEvent,
+ const AsyncDragMetrics& aDragMetrics);
+
+ /**
+ * Handler for events which should not be intercepted by the touch listener.
+ */
+ nsEventStatus HandleInputEvent(const InputData& aEvent,
+ const ScreenToParentLayerMatrix4x4& aTransformToApzc);
+
+ /**
+ * Handler for gesture events.
+ * Currently some gestures are detected in GestureEventListener that calls
+ * APZC back through this handler in order to avoid recursive calls to
+ * APZC::HandleInputEvent() which is supposed to do the work for
+ * ReceiveInputEvent().
+ */
+ nsEventStatus HandleGestureEvent(const InputData& aEvent);
+
+ /**
+ * Handler for touch velocity.
+ * Sometimes the touch move event will have a velocity even though no scrolling
+ * is occurring such as when the toolbar is being hidden/shown in Fennec.
+ * This function can be called to have the y axis' velocity queue updated.
+ */
+ void HandleTouchVelocity(uint32_t aTimesampMs, float aSpeedY);
+
+ /**
+ * Populates the provided object (if non-null) with the scrollable guid of this apzc.
+ */
+ void GetGuid(ScrollableLayerGuid* aGuidOut) const;
+
+ /**
+ * Returns the scrollable guid of this apzc.
+ */
+ ScrollableLayerGuid GetGuid() const;
+
+ /**
+ * Returns true if this APZC instance is for the layer identified by the guid.
+ */
+ bool Matches(const ScrollableLayerGuid& aGuid);
+
+ /**
+ * Returns true if the tree manager of this APZC is the same as the one
+ * passed in.
+ */
+ bool HasTreeManager(const APZCTreeManager* aTreeManager) const;
+
+ void StartAnimation(AsyncPanZoomAnimation* aAnimation);
+
+ /**
+ * Cancels any currently running animation.
+ * aFlags is a bit-field to provide specifics of how to cancel the animation.
+ * See CancelAnimationFlags.
+ */
+ void CancelAnimation(CancelAnimationFlags aFlags = Default);
+
+ /**
+ * Adjusts the scroll position to compensate for a shift in the surface, such
+ * that the content appears to remain visually in the same position. i.e. if
+ * the surface moves up by 10 screenpixels, the scroll position should also
+ * move up by 10 pixels so that what used to be at the top of the surface is
+ * now 10 pixels down the surface.
+ */
+ void AdjustScrollForSurfaceShift(const ScreenPoint& aShift);
+
+ /**
+ * Clear any overscroll on this APZC.
+ */
+ void ClearOverscroll();
+
+ /**
+ * Returns whether this APZC is for an element marked with the 'scrollgrab'
+ * attribute.
+ */
+ bool HasScrollgrab() const { return mScrollMetadata.GetHasScrollgrab(); }
+
+ /**
+ * Returns whether this APZC has room to be panned (in any direction).
+ */
+ bool IsPannable() const;
+
+ /**
+ * Returns true if the APZC has been flung with a velocity greater than the
+ * stop-on-tap fling velocity threshold (which is pref-controlled).
+ */
+ bool IsFlingingFast() const;
+
+ /**
+ * Returns the identifier of the touch in the last touch event processed by
+ * this APZC. This should only be called when the last touch event contained
+ * only one touch.
+ */
+ int32_t GetLastTouchIdentifier() const;
+
+ /**
+ * Returns the matrix that transforms points from global screen space into
+ * this APZC's ParentLayer space.
+ * To respect the lock ordering, mMonitor must NOT be held when calling
+ * this function (since this function acquires the tree lock).
+ */
+ ScreenToParentLayerMatrix4x4 GetTransformToThis() const;
+
+ /**
+ * Convert the vector |aVector|, rooted at the point |aAnchor|, from
+ * this APZC's ParentLayer coordinates into screen coordinates.
+ * The anchor is necessary because with 3D tranforms, the location of the
+ * vector can affect the result of the transform.
+ * To respect the lock ordering, mMonitor must NOT be held when calling
+ * this function (since this function acquires the tree lock).
+ */
+ ScreenPoint ToScreenCoordinates(const ParentLayerPoint& aVector,
+ const ParentLayerPoint& aAnchor) const;
+
+ /**
+ * Convert the vector |aVector|, rooted at the point |aAnchor|, from
+ * screen coordinates into this APZC's ParentLayer coordinates.
+ * The anchor is necessary because with 3D tranforms, the location of the
+ * vector can affect the result of the transform.
+ * To respect the lock ordering, mMonitor must NOT be held when calling
+ * this function (since this function acquires the tree lock).
+ */
+ 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 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(Layer::ScrollDirection aDirection) const;
+
+ void NotifyMozMouseScrollEvent(const nsString& aString) const;
+
+protected:
+ // Protected destructor, to discourage deletion outside of Release():
+ virtual ~AsyncPanZoomController();
+
+ // Returns the cached current frame time.
+ TimeStamp GetFrameTime() const;
+
+ /**
+ * Helper method for touches beginning. Sets everything up for panning and any
+ * multitouch gestures.
+ */
+ nsEventStatus OnTouchStart(const MultiTouchInput& aEvent);
+
+ /**
+ * Helper method for touches moving. Does any transforms needed when panning.
+ */
+ nsEventStatus OnTouchMove(const MultiTouchInput& aEvent);
+
+ /**
+ * Helper method for touches ending. Redraws the screen if necessary and does
+ * any cleanup after a touch has ended.
+ */
+ nsEventStatus OnTouchEnd(const MultiTouchInput& aEvent);
+
+ /**
+ * Helper method for touches being cancelled. Treated roughly the same as a
+ * touch ending (OnTouchEnd()).
+ */
+ nsEventStatus OnTouchCancel(const MultiTouchInput& aEvent);
+
+ /**
+ * Helper method for scales beginning. Distinct from the OnTouch* handlers in
+ * that this implies some outside implementation has determined that the user
+ * is pinching.
+ */
+ nsEventStatus OnScaleBegin(const PinchGestureInput& aEvent);
+
+ /**
+ * Helper method for scaling. As the user moves their fingers when pinching,
+ * this changes the scale of the page.
+ */
+ nsEventStatus OnScale(const PinchGestureInput& aEvent);
+
+ /**
+ * Helper method for scales ending. Redraws the screen if necessary and does
+ * any cleanup after a scale has ended.
+ */
+ nsEventStatus OnScaleEnd(const PinchGestureInput& aEvent);
+
+ /**
+ * Helper methods for handling pan events.
+ */
+ nsEventStatus OnPanMayBegin(const PanGestureInput& aEvent);
+ nsEventStatus OnPanCancelled(const PanGestureInput& aEvent);
+ nsEventStatus OnPanBegin(const PanGestureInput& aEvent);
+ nsEventStatus OnPan(const PanGestureInput& aEvent, bool aFingersOnTouchpad);
+ nsEventStatus OnPanEnd(const PanGestureInput& aEvent);
+ nsEventStatus OnPanMomentumStart(const PanGestureInput& aEvent);
+ nsEventStatus OnPanMomentumEnd(const PanGestureInput& aEvent);
+
+ /**
+ * Helper methods for handling scroll wheel events.
+ */
+ nsEventStatus OnScrollWheel(const ScrollWheelInput& aEvent);
+
+ ParentLayerPoint GetScrollWheelDelta(const ScrollWheelInput& aEvent) const;
+
+ /**
+ * Helper methods for long press gestures.
+ */
+ nsEventStatus OnLongPress(const TapGestureInput& aEvent);
+ nsEventStatus OnLongPressUp(const TapGestureInput& aEvent);
+
+ /**
+ * Helper method for single tap gestures.
+ */
+ nsEventStatus OnSingleTapUp(const TapGestureInput& aEvent);
+
+ /**
+ * Helper method for a single tap confirmed.
+ */
+ nsEventStatus OnSingleTapConfirmed(const TapGestureInput& aEvent);
+
+ /**
+ * Helper method for double taps.
+ */
+ nsEventStatus OnDoubleTap(const TapGestureInput& aEvent);
+
+ /**
+ * Helper method for double taps where the double-tap gesture is disabled.
+ */
+ nsEventStatus OnSecondTap(const TapGestureInput& aEvent);
+
+ /**
+ * Helper method to cancel any gesture currently going to Gecko. Used
+ * primarily when a user taps the screen over some clickable content but then
+ * pans down instead of letting go (i.e. to cancel a previous touch so that a
+ * new one can properly take effect.
+ */
+ nsEventStatus OnCancelTap(const TapGestureInput& aEvent);
+
+ /**
+ * Scrolls the viewport by an X,Y offset.
+ */
+ void ScrollBy(const CSSPoint& aOffset);
+
+ /**
+ * Scales the viewport by an amount (note that it multiplies this scale in to
+ * the current scale, it doesn't set it to |aScale|). Also considers a focus
+ * point so that the page zooms inward/outward from that point.
+ */
+ void ScaleWithFocus(float aScale,
+ const CSSPoint& aFocus);
+
+ /**
+ * Schedules a composite on the compositor thread.
+ */
+ void ScheduleComposite();
+
+ /**
+ * Schedules a composite, and if enough time has elapsed since the last
+ * paint, a paint.
+ */
+ void ScheduleCompositeAndMaybeRepaint();
+
+ /**
+ * Gets the displacement of the current touch since it began. That is, it is
+ * the distance between the current position and the initial position of the
+ * current touch (this only makes sense if a touch is currently happening and
+ * OnTouchMove() or the equivalent for pan gestures is being invoked).
+ * Note: It's an abuse of the 'Coord' class to use it to represent a 2D
+ * distance, but it's the closest thing we currently have.
+ */
+ ScreenCoord PanDistance() const;
+
+ /**
+ * Gets the start point of the current touch.
+ * Like PanDistance(), this only makes sense if a touch is currently
+ * happening and OnTouchMove() or the equivalent for pan gestures is
+ * being invoked.
+ */
+ ParentLayerPoint PanStart() const;
+
+ /**
+ * Gets a vector of the velocities of each axis.
+ */
+ const ParentLayerPoint GetVelocityVector() const;
+
+ /**
+ * Sets the velocities of each axis.
+ */
+ void SetVelocityVector(const ParentLayerPoint& aVelocityVector);
+
+ /**
+ * Gets the first touch point from a MultiTouchInput. This gets only
+ * the first one and assumes the rest are either missing or not relevant.
+ */
+ ParentLayerPoint GetFirstTouchPoint(const MultiTouchInput& aEvent);
+
+ /**
+ * Sets the panning state basing on the pan direction angle and current touch-action value.
+ */
+ void HandlePanningWithTouchAction(double angle);
+
+ /**
+ * Sets the panning state ignoring the touch action value.
+ */
+ void HandlePanning(double angle);
+
+ /**
+ * Update the panning state and axis locks.
+ */
+ void HandlePanningUpdate(const ScreenPoint& aDelta);
+
+ /**
+ * Sets up anything needed for panning. This takes us out of the "TOUCHING"
+ * state and starts actually panning us.
+ */
+ nsEventStatus StartPanning(const MultiTouchInput& aStartPoint);
+
+ /**
+ * Wrapper for Axis::UpdateWithTouchAtDevicePoint(). Calls this function for
+ * both axes and factors in the time delta from the last update.
+ */
+ void UpdateWithTouchAtDevicePoint(const MultiTouchInput& aEvent);
+
+ /**
+ * Does any panning required due to a new touch event.
+ */
+ void TrackTouch(const MultiTouchInput& aEvent);
+
+ /**
+ * Utility function to send updated FrameMetrics to Gecko so that it can paint
+ * the displayport area. Calls into GeckoContentController to do the actual
+ * work. This call will use the current metrics. If this function is called
+ * from a non-main thread, it will redispatch itself to the main thread, and
+ * use the latest metrics during the redispatch.
+ */
+ void RequestContentRepaint(bool aUserAction = true);
+
+ /**
+ * Send the provided metrics to Gecko to trigger a repaint. This function
+ * may filter duplicate calls with the same metrics. This function must be
+ * called on the main thread.
+ */
+ void RequestContentRepaint(const FrameMetrics& aFrameMetrics,
+ const ParentLayerPoint& aVelocity);
+
+ /**
+ * Gets the current frame metrics. This is *not* the Gecko copy stored in the
+ * layers code.
+ */
+ const FrameMetrics& GetFrameMetrics() const;
+
+ /**
+ * Gets the pointer to the apzc tree manager. All the access to tree manager
+ * should be made via this method and not via private variable since this method
+ * ensures that no lock is set.
+ */
+ APZCTreeManager* GetApzcTreeManager() const;
+
+ /**
+ * Convert ScreenPoint relative to the screen to LayoutDevicePoint relative
+ * to the parent document. This excludes the transient compositor transform.
+ * NOTE: This must be converted to LayoutDevicePoint relative to the child
+ * document before sending over IPC to a child process.
+ */
+ bool ConvertToGecko(const ScreenIntPoint& aPoint, LayoutDevicePoint* aOut);
+
+ enum AxisLockMode {
+ FREE, /* No locking at all */
+ STANDARD, /* Default axis locking mode that remains locked until pan ends*/
+ STICKY, /* Allow lock to be broken, with hysteresis */
+ };
+
+ static AxisLockMode GetAxisLockMode();
+
+ // Helper function for OnSingleTapUp(), OnSingleTapConfirmed(), and
+ // OnLongPressUp().
+ nsEventStatus GenerateSingleTap(GeckoContentController::TapType aType,
+ const ScreenIntPoint& aPoint,
+ mozilla::Modifiers aModifiers);
+
+ // Common processing at the end of a touch block.
+ void OnTouchEndOrCancel();
+
+ uint64_t mLayersId;
+ RefPtr<CompositorController> mCompositorController;
+ RefPtr<MetricsSharingController> mMetricsSharingController;
+
+ /* Access to the following two fields is protected by the mRefPtrMonitor,
+ since they are accessed on the UI thread but can be cleared on the
+ compositor thread. */
+ RefPtr<GeckoContentController> mGeckoContentController;
+ RefPtr<GestureEventListener> mGestureEventListener;
+ mutable Monitor mRefPtrMonitor;
+
+ // This is a raw pointer to avoid introducing a reference cycle between
+ // AsyncPanZoomController and APZCTreeManager. Since these objects don't
+ // live on the main thread, we can't use the cycle collector with them.
+ // The APZCTreeManager owns the lifetime of the APZCs, so nulling this
+ // pointer out in Destroy() will prevent accessing deleted memory.
+ Atomic<APZCTreeManager*> mTreeManager;
+
+ /* Utility functions that return a addrefed pointer to the corresponding fields. */
+ already_AddRefed<GeckoContentController> GetGeckoContentController() const;
+ already_AddRefed<GestureEventListener> GetGestureEventListener() const;
+
+ PlatformSpecificStateBase* GetPlatformSpecificState();
+
+protected:
+ // Both |mFrameMetrics| and |mLastContentPaintMetrics| are protected by the
+ // monitor. Do not read from or modify either of them without locking.
+ ScrollMetadata mScrollMetadata;
+ FrameMetrics& mFrameMetrics; // for convenience, refers to mScrollMetadata.mMetrics
+
+ // Protects |mFrameMetrics|, |mLastContentPaintMetrics|, and |mState|.
+ // Before manipulating |mFrameMetrics| or |mLastContentPaintMetrics|, the
+ // monitor should be held. When setting |mState|, either the SetState()
+ // function can be used, or the monitor can be held and then |mState| updated.
+ // IMPORTANT: See the note about lock ordering at the top of APZCTreeManager.h.
+ // This is mutable to allow entering it from 'const' methods; doing otherwise
+ // would significantly limit what methods could be 'const'.
+ mutable ReentrantMonitor mMonitor;
+
+private:
+ // Metadata of the container layer corresponding to this APZC. This is
+ // stored here so that it is accessible from the UI/controller thread.
+ // These are the metrics at last content paint, the most recent
+ // values we were notified of in NotifyLayersUpdate(). Since it represents
+ // the Gecko state, it should be used as a basis for untransformation when
+ // sending messages back to Gecko.
+ ScrollMetadata mLastContentPaintMetadata;
+ FrameMetrics& mLastContentPaintMetrics; // for convenience, refers to mLastContentPaintMetadata.mMetrics
+ // The last metrics used for a content repaint request.
+ FrameMetrics mLastPaintRequestMetrics;
+ // The metrics that we expect content to have. This is updated when we
+ // request a content repaint, and when we receive a shadow layers update.
+ // This allows us to transform events into Gecko's coordinate space.
+ FrameMetrics mExpectedGeckoMetrics;
+
+ AxisX mX;
+ AxisY mY;
+
+ // This flag is set to true when we are in a axis-locked pan as a result of
+ // the touch-action CSS property.
+ bool mPanDirRestricted;
+
+ // 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.
+ ZoomConstraints mZoomConstraints;
+
+ // The last time the compositor has sampled the content transform for this
+ // frame.
+ TimeStamp mLastSampleTime;
+
+ // The last sample time at which we submitted a checkerboarding report.
+ TimeStamp mLastCheckerboardReport;
+
+ // Stores the previous focus point if there is a pinch gesture happening. Used
+ // to allow panning by moving multiple fingers (thus moving the focus point).
+ ParentLayerPoint mLastZoomFocus;
+
+ RefPtr<AsyncPanZoomAnimation> mAnimation;
+
+ UniquePtr<OverscrollEffectBase> mOverscrollEffect;
+
+ // Groups state variables that are specific to a platform.
+ // Initialized on first use.
+ UniquePtr<PlatformSpecificStateBase> mPlatformSpecificState;
+
+ friend class Axis;
+
+
+ /* ===================================================================
+ * The functions and members in this section are used to expose
+ * the current async transform state to callers.
+ */
+public:
+ /**
+ * Allows callers to specify which type of async transform they want:
+ * NORMAL provides the actual async transforms of the APZC, whereas
+ * RESPECT_FORCE_DISABLE will provide empty async transforms if and only if
+ * the metrics has the mForceDisableApz flag set. In general the latter should
+ * only be used by call sites that are applying the transform to update
+ * a layer's position.
+ */
+ enum AsyncMode {
+ NORMAL,
+ RESPECT_FORCE_DISABLE,
+ };
+
+ /**
+ * Query the transforms that should be applied to the layer corresponding
+ * to this APZC due to asynchronous panning and zooming.
+ * This function returns the async transform via the |aOutTransform|
+ * out parameter.
+ */
+ ParentLayerPoint GetCurrentAsyncScrollOffset(AsyncMode aMode) const;
+
+ /**
+ * Return a visual effect that reflects this apzc's
+ * overscrolled state, if any.
+ */
+ AsyncTransformComponentMatrix GetOverscrollTransform(AsyncMode aMode) const;
+
+ /**
+ * Returns the incremental transformation corresponding to the async pan/zoom
+ * in progress. That is, when this transform is multiplied with the layer's
+ * existing transform, it will make the layer appear with the desired pan/zoom
+ * amount.
+ */
+ AsyncTransform GetCurrentAsyncTransform(AsyncMode aMode) const;
+
+ /**
+ * Returns the same transform as GetCurrentAsyncTransform(), but includes
+ * any transform due to axis over-scroll.
+ */
+ AsyncTransformComponentMatrix GetCurrentAsyncTransformWithOverscroll(AsyncMode aMode) const;
+
+
+ /* ===================================================================
+ * The functions and members in this section are used to manage
+ * the state that tracks what this APZC is doing with the input events.
+ */
+protected:
+ enum PanZoomState {
+ NOTHING, /* no touch-start events received */
+ FLING, /* all touches removed, but we're still scrolling page */
+ TOUCHING, /* one touch-start event received */
+
+ PANNING, /* panning the frame */
+ PANNING_LOCKED_X, /* touch-start followed by move (i.e. panning with axis lock) X axis */
+ PANNING_LOCKED_Y, /* as above for Y axis */
+
+ PAN_MOMENTUM, /* like PANNING, but controlled by momentum PanGestureInput events */
+
+ PINCHING, /* nth touch-start, where n > 1. this mode allows pan and zoom */
+ ANIMATING_ZOOM, /* animated zoom to a new rect */
+ OVERSCROLL_ANIMATION, /* Spring-based animation used to relieve overscroll once
+ the finger is lifted. */
+ SMOOTH_SCROLL, /* Smooth scrolling to destination. Used by
+ CSSOM-View smooth scroll-behavior */
+ WHEEL_SCROLL /* Smooth scrolling to a destination for a wheel event. */
+ };
+
+ // This is in theory protected by |mMonitor|; that is, it should be held whenever
+ // this is updated. In practice though... see bug 897017.
+ PanZoomState mState;
+
+private:
+ friend class StateChangeNotificationBlocker;
+ /**
+ * A counter of how many StateChangeNotificationBlockers are active.
+ * A non-zero count will prevent state change notifications from
+ * being dispatched. Only code that holds mMonitor should touch this.
+ */
+ int mNotificationBlockers;
+
+ /**
+ * Helper to set the current state. Holds the monitor before actually setting
+ * it and fires content controller events based on state changes. Always set
+ * the state using this call, do not set it directly.
+ */
+ void SetState(PanZoomState aState);
+ /**
+ * Fire content controller notifications about state changes, assuming no
+ * StateChangeNotificationBlocker has been activated.
+ */
+ void DispatchStateChangeNotification(PanZoomState aOldState, PanZoomState aNewState);
+ /**
+ * Internal helpers for checking general state of this apzc.
+ */
+ static bool IsTransformingState(PanZoomState aState);
+
+ /* ===================================================================
+ * The functions and members in this section are used to manage
+ * blocks of touch events and the state needed to deal with content
+ * listeners.
+ */
+public:
+ /**
+ * Flush a repaint request if one is needed, without throttling it with the
+ * paint throttler.
+ */
+ void FlushRepaintForNewInputBlock();
+
+ /**
+ * Given the number of touch points in an input event and touch block they
+ * belong to, check if the event can result in a panning/zooming behavior.
+ * This is primarily used to figure out when to dispatch the pointercancel
+ * event for the pointer events spec.
+ */
+ bool ArePointerEventsConsumable(TouchBlockState* aBlock, uint32_t aTouchPoints);
+
+ /**
+ * Clear internal state relating to touch input handling.
+ */
+ void ResetTouchInputState();
+
+ /**
+ * Gets a ref to the input queue that is shared across the entire tree manager.
+ */
+ const RefPtr<InputQueue>& GetInputQueue() const;
+
+private:
+ void CancelAnimationAndGestureState();
+
+ RefPtr<InputQueue> mInputQueue;
+ CancelableBlockState* GetCurrentInputBlock() const;
+ TouchBlockState* GetCurrentTouchBlock() const;
+ bool HasReadyTouchBlock() const;
+
+ PanGestureBlockState* GetCurrentPanGestureBlock() const;
+
+private:
+ /* ===================================================================
+ * The functions and members in this section are used to manage
+ * fling animations, smooth scroll animations, and overscroll
+ * during a fling or smooth scroll.
+ */
+public:
+ /**
+ * Attempt a fling with the velocity specified in |aHandoffState|.
+ * If we are not pannable, the fling is handed off to the next APZC in
+ * the handoff chain via mTreeManager->DispatchFling().
+ * Returns true iff. the entire velocity of the fling was consumed by
+ * this APZC. |aHandoffState.mVelocity| is modified to contain any
+ * unused, residual velocity.
+ * |aHandoffState.mIsHandoff| should be true iff. the fling was handed off
+ * from a previous APZC, and determines whether acceleration is applied
+ * to the fling.
+ */
+ bool AttemptFling(FlingHandoffState& aHandoffState);
+
+private:
+ friend class AndroidFlingAnimation;
+ friend class GenericFlingAnimation;
+ friend class OverscrollAnimation;
+ friend class SmoothScrollAnimation;
+ friend class WheelScrollAnimation;
+
+ friend class GenericOverscrollEffect;
+ friend class WidgetOverscrollEffect;
+
+ // The initial velocity of the most recent fling.
+ ParentLayerPoint mLastFlingVelocity;
+ // The time at which the most recent fling started.
+ TimeStamp mLastFlingTime;
+ // Indicates if the repaint-during-pinch timer is currently set
+ bool mPinchPaintTimerSet;
+
+ // Deal with overscroll resulting from a fling animation. This is only ever
+ // called on APZC instances that were actually performing a fling.
+ // The overscroll is handled by trying to hand the fling off to an APZC
+ // later in the handoff chain, or if there are no takers, continuing the
+ // fling and entering an overscrolled state.
+ void HandleFlingOverscroll(const ParentLayerPoint& aVelocity,
+ const RefPtr<const OverscrollHandoffChain>& aOverscrollHandoffChain,
+ const RefPtr<const AsyncPanZoomController>& aScrolledApzc);
+
+ void HandleSmoothScrollOverscroll(const ParentLayerPoint& aVelocity);
+
+ // Helper function used by AttemptFling().
+ void AcceptFling(FlingHandoffState& aHandoffState);
+
+ // Start an overscroll animation with the given initial velocity.
+ void StartOverscrollAnimation(const ParentLayerPoint& aVelocity);
+
+ void SmoothScrollTo(const CSSPoint& aDestination);
+
+ // Returns whether overscroll is allowed during an event.
+ bool AllowScrollHandoffInCurrentBlock() const;
+
+ // Invoked by the pinch repaint timer.
+ void DoDelayedRequestContentRepaint();
+
+ /* ===================================================================
+ * The functions and members in this section are used to make ancestor chains
+ * out of APZC instances. These chains can only be walked or manipulated
+ * while holding the lock in the associated APZCTreeManager instance.
+ */
+public:
+ void SetParent(AsyncPanZoomController* aParent) {
+ mParent = aParent;
+ }
+
+ AsyncPanZoomController* GetParent() const {
+ return mParent;
+ }
+
+ /* Returns true if there is no APZC higher in the tree with the same
+ * layers id.
+ */
+ bool HasNoParentWithSameLayersId() const {
+ return !mParent || (mParent->mLayersId != mLayersId);
+ }
+
+ bool IsRootForLayersId() const {
+ ReentrantMonitorAutoEnter lock(mMonitor);
+ return mScrollMetadata.IsLayersIdRoot();
+ }
+
+ bool IsRootContent() const {
+ ReentrantMonitorAutoEnter lock(mMonitor);
+ return mFrameMetrics.IsRootContent();
+ }
+
+private:
+ // |mTreeManager| belongs in this section but it's declaration is a bit
+ // further above due to initialization-order constraints.
+
+ RefPtr<AsyncPanZoomController> mParent;
+
+
+ /* ===================================================================
+ * The functions and members in this section are used for scrolling,
+ * including handing off scroll to another APZC, and overscrolling.
+ */
+public:
+ FrameMetrics::ViewID GetScrollHandoffParentId() const {
+ return mScrollMetadata.GetScrollParentId();
+ }
+
+ /**
+ * Attempt to scroll in response to a touch-move from |aStartPoint| to
+ * |aEndPoint|, which are in our (transformed) screen coordinates.
+ * Due to overscroll handling, there may not actually have been a touch-move
+ * at these points, but this function will scroll as if there had been.
+ * If this attempt causes overscroll (i.e. the layer cannot be scrolled
+ * by the entire amount requested), the overscroll is passed back to the
+ * tree manager via APZCTreeManager::DispatchScroll(). If the tree manager
+ * does not find an APZC further in the handoff chain to accept the
+ * overscroll, and this APZC is pannable, this APZC enters an overscrolled
+ * state.
+ * |aOverscrollHandoffChain| and |aOverscrollHandoffChainIndex| are used by
+ * the tree manager to keep track of which APZC to hand off the overscroll
+ * to; this function increments the chain and the index and passes it on to
+ * APZCTreeManager::DispatchScroll() in the event of overscroll.
+ * Returns true iff. this APZC, or an APZC further down the
+ * handoff chain, accepted the scroll (possibly entering an overscrolled
+ * state). If this returns false, the caller APZC knows that it should enter
+ * an overscrolled state itself if it can.
+ * aStartPoint and aEndPoint are modified depending on how much of the
+ * scroll gesture was consumed by APZCs in the handoff chain.
+ */
+ bool AttemptScroll(ParentLayerPoint& aStartPoint,
+ ParentLayerPoint& aEndPoint,
+ OverscrollHandoffState& aOverscrollHandoffState);
+
+ void FlushRepaintForOverscrollHandoff();
+
+ /**
+ * If overscrolled, start a snap-back animation and return true.
+ * Otherwise return false.
+ */
+ bool SnapBackIfOverscrolled();
+
+ /**
+ * Build the chain of APZCs along which scroll will be handed off when
+ * this APZC receives input events.
+ *
+ * Notes on lifetime and const-correctness:
+ * - The returned handoff chain is |const|, to indicate that it cannot be
+ * changed after being built.
+ * - When passing the chain to a function that uses it without storing it,
+ * pass it by reference-to-const (as in |const OverscrollHandoffChain&|).
+ * - When storing the chain, store it by RefPtr-to-const (as in
+ * |RefPtr<const OverscrollHandoffChain>|). This ensures the chain is
+ * kept alive. Note that queueing a task that uses the chain as an
+ * argument constitutes storing, as the task may outlive its queuer.
+ * - When passing the chain to a function that will store it, pass it as
+ * |const RefPtr<const OverscrollHandoffChain>&|. This allows the
+ * function to copy it into the |RefPtr<const OverscrollHandoffChain>|
+ * that will store it, while avoiding an unnecessary copy (and thus
+ * AddRef() and Release()) when passing it.
+ */
+ RefPtr<const OverscrollHandoffChain> BuildOverscrollHandoffChain();
+
+private:
+ /**
+ * A helper function for calling APZCTreeManager::DispatchScroll().
+ * Guards against the case where the APZC is being concurrently destroyed
+ * (and thus mTreeManager is being nulled out).
+ */
+ void CallDispatchScroll(ParentLayerPoint& aStartPoint,
+ ParentLayerPoint& aEndPoint,
+ OverscrollHandoffState& aOverscrollHandoffState);
+
+ /**
+ * A helper function for overscrolling during panning. This is a wrapper
+ * around OverscrollBy() that also implements restrictions on entering
+ * overscroll based on the pan angle.
+ */
+ void OverscrollForPanning(ParentLayerPoint& aOverscroll,
+ const ScreenPoint& aPanDistance);
+
+ /**
+ * 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);
+
+
+ /* ===================================================================
+ * 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) {
+ 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();
+ }
+
+ bool IsInPanningState() const;
+
+private:
+ /* This is the cumulative CSS transform for all the layers from (and including)
+ * the parent APZC down to (but excluding) this one, and excluding any
+ * perspective transforms. */
+ Matrix4x4 mAncestorTransform;
+
+
+ /* ===================================================================
+ * The functions and members in this section are used for sharing the
+ * FrameMetrics across processes for the progressive tiling code.
+ */
+private:
+ /* Unique id assigned to each APZC. Used with ViewID to uniquely identify
+ * shared FrameMeterics used in progressive tile painting. */
+ const uint32_t mAPZCId;
+
+ RefPtr<ipc::SharedMemoryBasic> mSharedFrameMetricsBuffer;
+ CrossProcessMutex* mSharedLock;
+ /**
+ * Called when ever mFrameMetrics is updated so that if it is being
+ * shared with the content process the shared FrameMetrics may be updated.
+ */
+ void UpdateSharedCompositorFrameMetrics();
+ /**
+ * Create a shared memory buffer for containing the FrameMetrics and
+ * a CrossProcessMutex that may be shared with the content process
+ * for use in progressive tiled update calculations.
+ */
+ void ShareCompositorFrameMetrics();
+
+
+ /* ===================================================================
+ * The functions and members in this section are used for testing
+ * and assertion purposes only.
+ */
+public:
+ /**
+ * Set an extra offset for testing async scrolling.
+ */
+ void SetTestAsyncScrollOffset(const CSSPoint& aPoint)
+ {
+ mTestAsyncScrollOffset = aPoint;
+ }
+ /**
+ * Set an extra offset for testing async scrolling.
+ */
+ void SetTestAsyncZoom(const LayerToParentLayerScale& aZoom)
+ {
+ mTestAsyncZoom = aZoom;
+ }
+
+ void MarkAsyncTransformAppliedToContent()
+ {
+ mAsyncTransformAppliedToContent = true;
+ }
+
+ bool GetAsyncTransformAppliedToContent() const
+ {
+ return mAsyncTransformAppliedToContent;
+ }
+
+ uint64_t GetLayersId() const
+ {
+ return mLayersId;
+ }
+
+private:
+ // Extra offset to add to the async scroll position for testing
+ CSSPoint mTestAsyncScrollOffset;
+ // Extra zoom to include in the aync zoom for testing
+ LayerToParentLayerScale mTestAsyncZoom;
+ // Flag to track whether or not the APZ transform is not used. This
+ // flag is recomputed for every composition frame.
+ bool mAsyncTransformAppliedToContent;
+
+
+ /* ===================================================================
+ * The functions and members in this section are used for checkerboard
+ * recording.
+ */
+private:
+ // Helper function to update the in-progress checkerboard event, if any.
+ void UpdateCheckerboardEvent(const MutexAutoLock& aProofOfLock,
+ uint32_t aMagnitude);
+
+ // Mutex protecting mCheckerboardEvent
+ Mutex mCheckerboardEventLock;
+ // This is created when this APZC instance is first included as part of a
+ // composite. If a checkerboard event takes place, this is destroyed at the
+ // end of the event, and a new one is created on the next composite.
+ UniquePtr<CheckerboardEvent> mCheckerboardEvent;
+ // This is used to track the total amount of time that we could reasonably
+ // be checkerboarding. Combined with other info, this allows us to meaningfully
+ // say how frequently users actually encounter checkerboarding.
+ PotentialCheckerboardDurationTracker mPotentialCheckerboardTracker;
+
+
+ /* ===================================================================
+ * The functions in this section are used for CSS scroll snapping.
+ */
+
+ // If |aEvent| should trigger scroll snapping, adjust |aDelta| to reflect
+ // the snapping (that is, make it a delta that will take us to the desired
+ // snap point). The delta is interpreted as being relative to
+ // |aStartPosition|, and if a target snap point is found, |aStartPosition|
+ // is also updated, to the value of the snap point.
+ // Returns true iff. a target snap point was found.
+ bool MaybeAdjustDeltaForScrollSnapping(const ScrollWheelInput& aEvent,
+ ParentLayerPoint& aDelta,
+ CSSPoint& aStartPosition);
+
+ // Snap to a snap position nearby the current scroll position, if appropriate.
+ void ScrollSnap();
+
+ // Snap to a snap position nearby the destination predicted based on the
+ // current velocity, if appropriate.
+ void ScrollSnapToDestination();
+
+ // Snap to a snap position nearby the provided destination, if appropriate.
+ void ScrollSnapNear(const CSSPoint& aDestination);
+
+ // Find a snap point near |aDestination| that we should snap to.
+ // Returns the snap point if one was found, or an empty Maybe otherwise.
+ // |aUnit| affects the snapping behaviour (see ScrollSnapUtils::
+ // GetSnapPointForDestination). It should generally be determined by the
+ // type of event that's triggering the scroll.
+ Maybe<CSSPoint> FindSnapPointNear(const CSSPoint& aDestination,
+ nsIScrollableFrame::ScrollUnit aUnit);
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif // mozilla_layers_PanZoomController_h
diff --git a/gfx/layers/apz/src/Axis.cpp b/gfx/layers/apz/src/Axis.cpp
new file mode 100644
index 000000000..ddd660e0b
--- /dev/null
+++ b/gfx/layers/apz/src/Axis.cpp
@@ -0,0 +1,681 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set sw=2 ts=8 et tw=80 : */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "Axis.h"
+#include <math.h> // for fabsf, pow, powf
+#include <algorithm> // for max
+#include "AsyncPanZoomController.h" // for AsyncPanZoomController
+#include "mozilla/layers/APZCTreeManager.h" // for APZCTreeManager
+#include "mozilla/layers/APZThreadUtils.h" // for AssertOnControllerThread
+#include "FrameMetrics.h" // for FrameMetrics
+#include "mozilla/Attributes.h" // for final
+#include "mozilla/ComputedTimingFunction.h" // for ComputedTimingFunction
+#include "mozilla/Preferences.h" // for Preferences
+#include "mozilla/gfx/Rect.h" // for RoundedIn
+#include "mozilla/mozalloc.h" // for operator new
+#include "mozilla/FloatingPoint.h" // for FuzzyEqualsAdditive
+#include "mozilla/StaticPtr.h" // for StaticAutoPtr
+#include "nsMathUtils.h" // for NS_lround
+#include "nsPrintfCString.h" // for nsPrintfCString
+#include "nsThreadUtils.h" // for NS_DispatchToMainThread, etc
+#include "nscore.h" // for NS_IMETHOD
+#include "gfxPrefs.h" // for the preferences
+
+#define AXIS_LOG(...)
+// #define AXIS_LOG(...) printf_stderr("AXIS: " __VA_ARGS__)
+
+namespace mozilla {
+namespace layers {
+
+// When we compute the velocity we do so by taking two input events and
+// dividing the distance delta over the time delta. In some cases the time
+// delta can be really small, which can make the velocity computation very
+// volatile. To avoid this we impose a minimum time delta below which we do
+// not recompute the velocity.
+const uint32_t MIN_VELOCITY_SAMPLE_TIME_MS = 5;
+
+bool FuzzyEqualsCoordinate(float aValue1, float aValue2)
+{
+ return FuzzyEqualsAdditive(aValue1, aValue2, COORDINATE_EPSILON)
+ || FuzzyEqualsMultiplicative(aValue1, aValue2);
+}
+
+extern StaticAutoPtr<ComputedTimingFunction> gVelocityCurveFunction;
+
+Axis::Axis(AsyncPanZoomController* aAsyncPanZoomController)
+ : mPos(0),
+ mVelocitySampleTimeMs(0),
+ mVelocitySamplePos(0),
+ mVelocity(0.0f),
+ mAxisLocked(false),
+ mAsyncPanZoomController(aAsyncPanZoomController),
+ mOverscroll(0),
+ mFirstOverscrollAnimationSample(0),
+ mLastOverscrollPeak(0),
+ mOverscrollScale(1.0f)
+{
+}
+
+float Axis::ToLocalVelocity(float aVelocityInchesPerMs) const {
+ ScreenPoint velocity = MakePoint(aVelocityInchesPerMs * APZCTreeManager::GetDPI());
+ // Use ToScreenCoordinates() to convert a point rather than a vector by
+ // treating the point as a vector, and using (0, 0) as the anchor.
+ ScreenPoint panStart = mAsyncPanZoomController->ToScreenCoordinates(
+ mAsyncPanZoomController->PanStart(),
+ ParentLayerPoint());
+ ParentLayerPoint localVelocity =
+ mAsyncPanZoomController->ToParentLayerCoordinates(velocity, panStart);
+ return localVelocity.Length();
+}
+
+void Axis::UpdateWithTouchAtDevicePoint(ParentLayerCoord aPos, ParentLayerCoord aAdditionalDelta, uint32_t aTimestampMs) {
+ // mVelocityQueue is controller-thread only
+ APZThreadUtils::AssertOnControllerThread();
+
+ if (aTimestampMs <= mVelocitySampleTimeMs + MIN_VELOCITY_SAMPLE_TIME_MS) {
+ // See also the comment on MIN_VELOCITY_SAMPLE_TIME_MS.
+ // We still update mPos so that the positioning is correct (and we don't run
+ // into problems like bug 1042734) but the velocity will remain where it was.
+ // In particular we don't update either mVelocitySampleTimeMs or
+ // mVelocitySamplePos so that eventually when we do get an event with the
+ // required time delta we use the corresponding distance delta as well.
+ AXIS_LOG("%p|%s skipping velocity computation for small time delta %dms\n",
+ mAsyncPanZoomController, Name(), (aTimestampMs - mVelocitySampleTimeMs));
+ mPos = aPos;
+ return;
+ }
+
+ float newVelocity = mAxisLocked ? 0.0f : (float)(mVelocitySamplePos - aPos + aAdditionalDelta) / (float)(aTimestampMs - mVelocitySampleTimeMs);
+
+ newVelocity = ApplyFlingCurveToVelocity(newVelocity);
+
+ AXIS_LOG("%p|%s updating velocity to %f with touch\n",
+ mAsyncPanZoomController, Name(), newVelocity);
+ mVelocity = newVelocity;
+ mPos = aPos;
+ mVelocitySampleTimeMs = aTimestampMs;
+ mVelocitySamplePos = aPos;
+
+ AddVelocityToQueue(aTimestampMs, mVelocity);
+}
+
+float Axis::ApplyFlingCurveToVelocity(float aVelocity) const {
+ float newVelocity = aVelocity;
+ if (gfxPrefs::APZMaxVelocity() > 0.0f) {
+ bool velocityIsNegative = (newVelocity < 0);
+ newVelocity = fabs(newVelocity);
+
+ float maxVelocity = ToLocalVelocity(gfxPrefs::APZMaxVelocity());
+ newVelocity = std::min(newVelocity, maxVelocity);
+
+ if (gfxPrefs::APZCurveThreshold() > 0.0f && gfxPrefs::APZCurveThreshold() < gfxPrefs::APZMaxVelocity()) {
+ float curveThreshold = ToLocalVelocity(gfxPrefs::APZCurveThreshold());
+ if (newVelocity > curveThreshold) {
+ // here, 0 < curveThreshold < newVelocity <= maxVelocity, so we apply the curve
+ float scale = maxVelocity - curveThreshold;
+ float funcInput = (newVelocity - curveThreshold) / scale;
+ float funcOutput =
+ gVelocityCurveFunction->GetValue(funcInput,
+ ComputedTimingFunction::BeforeFlag::Unset);
+ float curvedVelocity = (funcOutput * scale) + curveThreshold;
+ AXIS_LOG("%p|%s curving up velocity from %f to %f\n",
+ mAsyncPanZoomController, Name(), newVelocity, curvedVelocity);
+ newVelocity = curvedVelocity;
+ }
+ }
+
+ if (velocityIsNegative) {
+ newVelocity = -newVelocity;
+ }
+ }
+
+ return newVelocity;
+}
+
+void Axis::AddVelocityToQueue(uint32_t aTimestampMs, float aVelocity) {
+ mVelocityQueue.AppendElement(std::make_pair(aTimestampMs, aVelocity));
+ if (mVelocityQueue.Length() > gfxPrefs::APZMaxVelocityQueueSize()) {
+ mVelocityQueue.RemoveElementAt(0);
+ }
+}
+
+void Axis::HandleTouchVelocity(uint32_t aTimestampMs, float aSpeed) {
+ // mVelocityQueue is controller-thread only
+ APZThreadUtils::AssertOnControllerThread();
+
+ mVelocity = ApplyFlingCurveToVelocity(aSpeed);
+ mVelocitySampleTimeMs = aTimestampMs;
+
+ AddVelocityToQueue(aTimestampMs, mVelocity);
+}
+
+void Axis::StartTouch(ParentLayerCoord aPos, uint32_t aTimestampMs) {
+ mStartPos = aPos;
+ mPos = aPos;
+ mVelocitySampleTimeMs = aTimestampMs;
+ mVelocitySamplePos = aPos;
+ mAxisLocked = false;
+}
+
+bool Axis::AdjustDisplacement(ParentLayerCoord aDisplacement,
+ /* ParentLayerCoord */ float& aDisplacementOut,
+ /* ParentLayerCoord */ float& aOverscrollAmountOut,
+ bool aForceOverscroll /* = false */)
+{
+ if (mAxisLocked) {
+ aOverscrollAmountOut = 0;
+ aDisplacementOut = 0;
+ return false;
+ }
+ if (aForceOverscroll) {
+ aOverscrollAmountOut = aDisplacement;
+ aDisplacementOut = 0;
+ return false;
+ }
+
+ EndOverscrollAnimation();
+
+ ParentLayerCoord displacement = aDisplacement;
+
+ // First consume any overscroll in the opposite direction along this axis.
+ ParentLayerCoord consumedOverscroll = 0;
+ if (mOverscroll > 0 && aDisplacement < 0) {
+ consumedOverscroll = std::min(mOverscroll, -aDisplacement);
+ } else if (mOverscroll < 0 && aDisplacement > 0) {
+ consumedOverscroll = 0.f - std::min(-mOverscroll, aDisplacement);
+ }
+ mOverscroll -= consumedOverscroll;
+ displacement += consumedOverscroll;
+
+ // Split the requested displacement into an allowed displacement that does
+ // not overscroll, and an overscroll amount.
+ aOverscrollAmountOut = DisplacementWillOverscrollAmount(displacement);
+ if (aOverscrollAmountOut != 0.0f) {
+ // No need to have a velocity along this axis anymore; it won't take us
+ // anywhere, so we're just spinning needlessly.
+ AXIS_LOG("%p|%s has overscrolled, clearing velocity\n",
+ mAsyncPanZoomController, Name());
+ mVelocity = 0.0f;
+ displacement -= aOverscrollAmountOut;
+ }
+ aDisplacementOut = displacement;
+ return fabsf(consumedOverscroll) > EPSILON;
+}
+
+ParentLayerCoord Axis::ApplyResistance(ParentLayerCoord aRequestedOverscroll) const {
+ // 'resistanceFactor' is a value between 0 and 1, which:
+ // - tends to 1 as the existing overscroll tends to 0
+ // - tends to 0 as the existing overscroll tends to the composition length
+ // The actual overscroll is the requested overscroll multiplied by this
+ // factor; this should prevent overscrolling by more than the composition
+ // length.
+ float resistanceFactor = 1 - fabsf(GetOverscroll()) / GetCompositionLength();
+ return resistanceFactor < 0 ? ParentLayerCoord(0) : aRequestedOverscroll * resistanceFactor;
+}
+
+void Axis::OverscrollBy(ParentLayerCoord aOverscroll) {
+ MOZ_ASSERT(CanScroll());
+ // We can get some spurious calls to OverscrollBy() with near-zero values
+ // due to rounding error. Ignore those (they might trip the asserts below.)
+ if (FuzzyEqualsAdditive(aOverscroll.value, 0.0f, COORDINATE_EPSILON)) {
+ return;
+ }
+ EndOverscrollAnimation();
+ aOverscroll = ApplyResistance(aOverscroll);
+ if (aOverscroll > 0) {
+#ifdef DEBUG
+ if (!FuzzyEqualsCoordinate(GetCompositionEnd().value, GetPageEnd().value)) {
+ nsPrintfCString message("composition end (%f) is not equal (within error) to page end (%f)\n",
+ GetCompositionEnd().value, GetPageEnd().value);
+ NS_ASSERTION(false, message.get());
+ MOZ_CRASH("GFX: Overscroll issue > 0");
+ }
+#endif
+ MOZ_ASSERT(mOverscroll >= 0);
+ } else if (aOverscroll < 0) {
+#ifdef DEBUG
+ if (!FuzzyEqualsCoordinate(GetOrigin().value, GetPageStart().value)) {
+ nsPrintfCString message("composition origin (%f) is not equal (within error) to page origin (%f)\n",
+ GetOrigin().value, GetPageStart().value);
+ NS_ASSERTION(false, message.get());
+ MOZ_CRASH("GFX: Overscroll issue < 0");
+ }
+#endif
+ MOZ_ASSERT(mOverscroll <= 0);
+ }
+ mOverscroll += aOverscroll;
+}
+
+ParentLayerCoord Axis::GetOverscroll() const {
+ ParentLayerCoord result = (mOverscroll - mLastOverscrollPeak) / mOverscrollScale;
+
+ // Assert that we return overscroll in the correct direction
+#ifdef DEBUG
+ if ((result.value * mFirstOverscrollAnimationSample.value) < 0.0f) {
+ nsPrintfCString message("GetOverscroll() (%f) and first overscroll animation sample (%f) have different signs\n",
+ result.value, mFirstOverscrollAnimationSample.value);
+ NS_ASSERTION(false, message.get());
+ MOZ_CRASH("GFX: Overscroll issue");
+ }
+#endif
+
+ return result;
+}
+
+void Axis::StartOverscrollAnimation(float aVelocity) {
+ // Make sure any state from a previous animation has been cleared.
+ MOZ_ASSERT(mFirstOverscrollAnimationSample == 0 &&
+ mLastOverscrollPeak == 0 &&
+ mOverscrollScale == 1);
+
+ SetVelocity(aVelocity);
+}
+
+void Axis::EndOverscrollAnimation() {
+ ParentLayerCoord overscroll = GetOverscroll();
+ mFirstOverscrollAnimationSample = 0;
+ mLastOverscrollPeak = 0;
+ mOverscrollScale = 1.0f;
+ mOverscroll = overscroll;
+}
+
+void Axis::StepOverscrollAnimation(double aStepDurationMilliseconds) {
+ // Apply spring physics to the overscroll as time goes on.
+ // Note: this method of sampling isn't perfectly smooth, as it assumes
+ // a constant velocity over 'aDelta', instead of an accelerating velocity.
+ // (The way we applying friction to flings has the same issue.)
+ // Hooke's law with damping:
+ // F = -kx - bv
+ // where
+ // k is a constant related to the stiffness of the spring
+ // The larger the constant, the stiffer the spring.
+ // x is the displacement of the end of the spring from its equilibrium
+ // In our scenario, it's the amount of overscroll on the axis.
+ // b is a constant that provides damping (friction)
+ // v is the velocity of the point at the end of the spring
+ // See http://gafferongames.com/game-physics/spring-physics/
+ const float kSpringStiffness = gfxPrefs::APZOverscrollSpringStiffness();
+ const float kSpringFriction = gfxPrefs::APZOverscrollSpringFriction();
+
+ // Apply spring force.
+ float springForce = -1 * kSpringStiffness * mOverscroll;
+ // Assume unit mass, so force = acceleration.
+ float oldVelocity = mVelocity;
+ mVelocity += springForce * aStepDurationMilliseconds;
+
+ // Apply dampening.
+ mVelocity *= pow(double(1 - kSpringFriction), aStepDurationMilliseconds);
+ AXIS_LOG("%p|%s sampled overscroll animation, leaving velocity at %f\n",
+ mAsyncPanZoomController, Name(), mVelocity);
+
+ // At the peak of each oscillation, record new offset and scaling factors for
+ // overscroll, to ensure that GetOverscroll always returns a value of the
+ // same sign, and that this value is correctly adjusted as the spring is
+ // dampened.
+ // To handle the case where one of the velocity samples is exaclty zero,
+ // consider a sign change to have occurred when the outgoing velocity is zero.
+ bool velocitySignChange = (oldVelocity * mVelocity) < 0 || mVelocity == 0;
+ if (mFirstOverscrollAnimationSample == 0.0f) {
+ mFirstOverscrollAnimationSample = mOverscroll;
+
+ // It's possible to start sampling overscroll with velocity == 0, or
+ // velocity in the opposite direction of overscroll, so make sure we
+ // correctly record the peak in this case.
+ if (mOverscroll != 0 && ((mOverscroll > 0 ? oldVelocity : -oldVelocity) <= 0.0f)) {
+ velocitySignChange = true;
+ }
+ }
+ if (velocitySignChange) {
+ bool oddOscillation = (mOverscroll.value * mFirstOverscrollAnimationSample.value) < 0.0f;
+ mLastOverscrollPeak = oddOscillation ? mOverscroll : -mOverscroll;
+ mOverscrollScale = 2.0f;
+ }
+
+ // Adjust the amount of overscroll based on the velocity.
+ // Note that we allow for oscillations.
+ mOverscroll += (mVelocity * aStepDurationMilliseconds);
+
+ // Our mechanism for translating a set of mOverscroll values that oscillate
+ // around zero to a set of GetOverscroll() values that have the same sign
+ // (so content is always stretched, never compressed) assumes that
+ // mOverscroll does not exceed mLastOverscrollPeak in magnitude. If our
+ // calculations were exact, this would be the case, as a dampened spring
+ // should never attain a displacement greater in magnitude than a previous
+ // peak. In our approximation calculations, however, this may not hold
+ // exactly. To ensure the assumption is not violated, we clamp the magnitude
+ // of mOverscroll.
+ if (mLastOverscrollPeak != 0 && fabs(mOverscroll) > fabs(mLastOverscrollPeak)) {
+ mOverscroll = (mOverscroll >= 0) ? fabs(mLastOverscrollPeak) : -fabs(mLastOverscrollPeak);
+ }
+}
+
+bool Axis::SampleOverscrollAnimation(const TimeDuration& aDelta) {
+ // Short-circuit early rather than running through all the sampling code.
+ if (mVelocity == 0.0f && mOverscroll == 0.0f) {
+ return false;
+ }
+
+ // We approximate the curve traced out by the velocity of the spring
+ // over time by breaking up the curve into small segments over which we
+ // consider the velocity to be constant. If the animation is sampled
+ // sufficiently often, then treating |aDelta| as a single segment of this
+ // sort would be fine, but the frequency at which the animation is sampled
+ // can be affected by external factors, and as the segment size grows larger,
+ // the approximation gets worse and the approximated curve can even diverge
+ // (i.e. oscillate forever, with displacements of increasing absolute value)!
+ // To avoid this, we break up |aDelta| into smaller segments of length 1 ms
+ // each, and a segment of any remaining fractional milliseconds.
+ double milliseconds = aDelta.ToMilliseconds();
+ int wholeMilliseconds = (int) aDelta.ToMilliseconds();
+ double fractionalMilliseconds = milliseconds - wholeMilliseconds;
+ for (int i = 0; i < wholeMilliseconds; ++i) {
+ StepOverscrollAnimation(1);
+ }
+ StepOverscrollAnimation(fractionalMilliseconds);
+
+ // If both the velocity and the displacement fall below a threshold, stop
+ // the animation so we don't continue doing tiny oscillations that aren't
+ // noticeable.
+ if (fabs(mOverscroll) < gfxPrefs::APZOverscrollStopDistanceThreshold() &&
+ fabs(mVelocity) < gfxPrefs::APZOverscrollStopVelocityThreshold()) {
+ // "Jump" to the at-rest state. The jump shouldn't be noticeable as the
+ // velocity and overscroll are already low.
+ AXIS_LOG("%p|%s oscillation dropped below threshold, going to rest\n",
+ mAsyncPanZoomController, Name());
+ ClearOverscroll();
+ mVelocity = 0;
+ return false;
+ }
+
+ // Otherwise, continue the animation.
+ return true;
+}
+
+bool Axis::IsOverscrolled() const {
+ return mOverscroll != 0.f;
+}
+
+void Axis::ClearOverscroll() {
+ EndOverscrollAnimation();
+ mOverscroll = 0;
+}
+
+ParentLayerCoord Axis::PanStart() const {
+ return mStartPos;
+}
+
+ParentLayerCoord Axis::PanDistance() const {
+ return fabs(mPos - mStartPos);
+}
+
+ParentLayerCoord Axis::PanDistance(ParentLayerCoord aPos) const {
+ return fabs(aPos - mStartPos);
+}
+
+void Axis::EndTouch(uint32_t aTimestampMs) {
+ // mVelocityQueue is controller-thread only
+ APZThreadUtils::AssertOnControllerThread();
+
+ mAxisLocked = false;
+ mVelocity = 0;
+ int count = 0;
+ while (!mVelocityQueue.IsEmpty()) {
+ uint32_t timeDelta = (aTimestampMs - mVelocityQueue[0].first);
+ if (timeDelta < gfxPrefs::APZVelocityRelevanceTime()) {
+ count++;
+ mVelocity += mVelocityQueue[0].second;
+ }
+ mVelocityQueue.RemoveElementAt(0);
+ }
+ if (count > 1) {
+ mVelocity /= count;
+ }
+ AXIS_LOG("%p|%s ending touch, computed velocity %f\n",
+ mAsyncPanZoomController, Name(), mVelocity);
+}
+
+void Axis::CancelGesture() {
+ // mVelocityQueue is controller-thread only
+ APZThreadUtils::AssertOnControllerThread();
+
+ AXIS_LOG("%p|%s cancelling touch, clearing velocity queue\n",
+ mAsyncPanZoomController, Name());
+ mVelocity = 0.0f;
+ while (!mVelocityQueue.IsEmpty()) {
+ mVelocityQueue.RemoveElementAt(0);
+ }
+}
+
+bool Axis::CanScroll() const {
+ return GetPageLength() - GetCompositionLength() > COORDINATE_EPSILON;
+}
+
+bool Axis::CanScroll(ParentLayerCoord aDelta) const
+{
+ if (!CanScroll() || mAxisLocked) {
+ return false;
+ }
+
+ return fabs(DisplacementWillOverscrollAmount(aDelta) - aDelta) > COORDINATE_EPSILON;
+}
+
+CSSCoord Axis::ClampOriginToScrollableRect(CSSCoord aOrigin) const
+{
+ CSSToParentLayerScale zoom = GetScaleForAxis(GetFrameMetrics().GetZoom());
+ ParentLayerCoord origin = aOrigin * zoom;
+
+ ParentLayerCoord result;
+ if (origin < GetPageStart()) {
+ result = GetPageStart();
+ } else if (origin + GetCompositionLength() > GetPageEnd()) {
+ result = GetPageEnd() - GetCompositionLength();
+ } else {
+ return aOrigin;
+ }
+
+ return result / zoom;
+}
+
+bool Axis::CanScrollNow() const {
+ return !mAxisLocked && CanScroll();
+}
+
+bool Axis::FlingApplyFrictionOrCancel(const TimeDuration& aDelta,
+ float aFriction,
+ float aThreshold) {
+ if (fabsf(mVelocity) <= aThreshold) {
+ // If the velocity is very low, just set it to 0 and stop the fling,
+ // otherwise we'll just asymptotically approach 0 and the user won't
+ // actually see any changes.
+ mVelocity = 0.0f;
+ return false;
+ } else {
+ mVelocity *= pow(1.0f - aFriction, float(aDelta.ToMilliseconds()));
+ }
+ AXIS_LOG("%p|%s reduced velocity to %f due to friction\n",
+ mAsyncPanZoomController, Name(), mVelocity);
+ return true;
+}
+
+ParentLayerCoord Axis::DisplacementWillOverscrollAmount(ParentLayerCoord aDisplacement) const {
+ ParentLayerCoord newOrigin = GetOrigin() + aDisplacement;
+ ParentLayerCoord newCompositionEnd = GetCompositionEnd() + aDisplacement;
+ // If the current pan plus a displacement takes the window to the left of or
+ // above the current page rect.
+ bool minus = newOrigin < GetPageStart();
+ // If the current pan plus a displacement takes the window to the right of or
+ // below the current page rect.
+ bool plus = newCompositionEnd > GetPageEnd();
+ if (minus && plus) {
+ // Don't handle overscrolled in both directions; a displacement can't cause
+ // this, it must have already been zoomed out too far.
+ return 0;
+ }
+ if (minus) {
+ return newOrigin - GetPageStart();
+ }
+ if (plus) {
+ return newCompositionEnd - GetPageEnd();
+ }
+ return 0;
+}
+
+CSSCoord Axis::ScaleWillOverscrollAmount(float aScale, CSSCoord aFocus) const {
+ // Internally, do computations in ParentLayer coordinates *before* the scale
+ // is applied.
+ CSSToParentLayerScale zoom = GetFrameMetrics().GetZoom().ToScaleFactor();
+ ParentLayerCoord focus = aFocus * zoom;
+ ParentLayerCoord originAfterScale = (GetOrigin() + focus) - (focus / aScale);
+
+ bool both = ScaleWillOverscrollBothSides(aScale);
+ bool minus = GetPageStart() - originAfterScale > COORDINATE_EPSILON;
+ bool plus = (originAfterScale + (GetCompositionLength() / aScale)) - GetPageEnd() > COORDINATE_EPSILON;
+
+ if ((minus && plus) || both) {
+ // If we ever reach here it's a bug in the client code.
+ MOZ_ASSERT(false, "In an OVERSCROLL_BOTH condition in ScaleWillOverscrollAmount");
+ return 0;
+ }
+ if (minus) {
+ return (originAfterScale - GetPageStart()) / zoom;
+ }
+ if (plus) {
+ return (originAfterScale + (GetCompositionLength() / aScale) - GetPageEnd()) / zoom;
+ }
+ return 0;
+}
+
+bool Axis::IsAxisLocked() const {
+ return mAxisLocked;
+}
+
+float Axis::GetVelocity() const {
+ return mAxisLocked ? 0 : mVelocity;
+}
+
+void Axis::SetVelocity(float aVelocity) {
+ AXIS_LOG("%p|%s direct-setting velocity to %f\n",
+ mAsyncPanZoomController, Name(), aVelocity);
+ mVelocity = aVelocity;
+}
+
+ParentLayerCoord Axis::GetCompositionEnd() const {
+ return GetOrigin() + GetCompositionLength();
+}
+
+ParentLayerCoord Axis::GetPageEnd() const {
+ return GetPageStart() + GetPageLength();
+}
+
+ParentLayerCoord Axis::GetScrollRangeEnd() const {
+ return GetPageEnd() - GetCompositionLength();
+}
+
+ParentLayerCoord Axis::GetOrigin() const {
+ ParentLayerPoint origin = GetFrameMetrics().GetScrollOffset() * GetFrameMetrics().GetZoom();
+ return GetPointOffset(origin);
+}
+
+ParentLayerCoord Axis::GetCompositionLength() const {
+ return GetRectLength(GetFrameMetrics().GetCompositionBounds());
+}
+
+ParentLayerCoord Axis::GetPageStart() const {
+ ParentLayerRect pageRect = GetFrameMetrics().GetExpandedScrollableRect() * GetFrameMetrics().GetZoom();
+ return GetRectOffset(pageRect);
+}
+
+ParentLayerCoord Axis::GetPageLength() const {
+ ParentLayerRect pageRect = GetFrameMetrics().GetExpandedScrollableRect() * GetFrameMetrics().GetZoom();
+ return GetRectLength(pageRect);
+}
+
+bool Axis::ScaleWillOverscrollBothSides(float aScale) const {
+ const FrameMetrics& metrics = GetFrameMetrics();
+ ParentLayerRect screenCompositionBounds = metrics.GetCompositionBounds()
+ / ParentLayerToParentLayerScale(aScale);
+ return GetRectLength(screenCompositionBounds) - GetPageLength() > COORDINATE_EPSILON;
+}
+
+const FrameMetrics& Axis::GetFrameMetrics() const {
+ return mAsyncPanZoomController->GetFrameMetrics();
+}
+
+
+AxisX::AxisX(AsyncPanZoomController* aAsyncPanZoomController)
+ : Axis(aAsyncPanZoomController)
+{
+
+}
+
+ParentLayerCoord AxisX::GetPointOffset(const ParentLayerPoint& aPoint) const
+{
+ return aPoint.x;
+}
+
+ParentLayerCoord AxisX::GetRectLength(const ParentLayerRect& aRect) const
+{
+ return aRect.width;
+}
+
+ParentLayerCoord AxisX::GetRectOffset(const ParentLayerRect& aRect) const
+{
+ return aRect.x;
+}
+
+CSSToParentLayerScale AxisX::GetScaleForAxis(const CSSToParentLayerScale2D& aScale) const
+{
+ return CSSToParentLayerScale(aScale.xScale);
+}
+
+ScreenPoint AxisX::MakePoint(ScreenCoord aCoord) const
+{
+ return ScreenPoint(aCoord, 0);
+}
+
+const char* AxisX::Name() const
+{
+ return "X";
+}
+
+AxisY::AxisY(AsyncPanZoomController* aAsyncPanZoomController)
+ : Axis(aAsyncPanZoomController)
+{
+
+}
+
+ParentLayerCoord AxisY::GetPointOffset(const ParentLayerPoint& aPoint) const
+{
+ return aPoint.y;
+}
+
+ParentLayerCoord AxisY::GetRectLength(const ParentLayerRect& aRect) const
+{
+ return aRect.height;
+}
+
+ParentLayerCoord AxisY::GetRectOffset(const ParentLayerRect& aRect) const
+{
+ return aRect.y;
+}
+
+CSSToParentLayerScale AxisY::GetScaleForAxis(const CSSToParentLayerScale2D& aScale) const
+{
+ return CSSToParentLayerScale(aScale.yScale);
+}
+
+ScreenPoint AxisY::MakePoint(ScreenCoord aCoord) const
+{
+ return ScreenPoint(0, aCoord);
+}
+
+const char* AxisY::Name() const
+{
+ return "Y";
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/apz/src/Axis.h b/gfx/layers/apz/src/Axis.h
new file mode 100644
index 000000000..e4c6b5644
--- /dev/null
+++ b/gfx/layers/apz/src/Axis.h
@@ -0,0 +1,336 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set sw=2 ts=8 et tw=80 : */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_layers_Axis_h
+#define mozilla_layers_Axis_h
+
+#include <sys/types.h> // for int32_t
+#include "APZUtils.h"
+#include "Units.h"
+#include "mozilla/TimeStamp.h" // for TimeDuration
+#include "nsTArray.h" // for nsTArray
+
+namespace mozilla {
+namespace layers {
+
+const float EPSILON = 0.0001f;
+
+/**
+ * Compare two coordinates for equality, accounting for rounding error.
+ * Use both FuzzyEqualsAdditive() with COORDINATE_EPISLON, which accounts for
+ * things like the error introduced by rounding during a round-trip to app
+ * units, and FuzzyEqualsMultiplicative(), which accounts for accumulated error
+ * due to floating-point operations (which can be larger than COORDINATE_EPISLON
+ * for sufficiently large coordinate values).
+ */
+bool FuzzyEqualsCoordinate(float aValue1, float aValue2);
+
+struct FrameMetrics;
+class AsyncPanZoomController;
+
+/**
+ * Helper class to maintain each axis of movement (X,Y) for panning and zooming.
+ * Note that everything here is specific to one axis; that is, the X axis knows
+ * nothing about the Y axis and vice versa.
+ */
+class Axis {
+public:
+ explicit Axis(AsyncPanZoomController* aAsyncPanZoomController);
+
+ /**
+ * Notify this Axis that a new touch has been received, including a timestamp
+ * for when the touch was received. This triggers a recalculation of velocity.
+ * This can also used for pan gesture events. For those events, the "touch"
+ * location is stationary and the scroll displacement is passed in as
+ * aAdditionalDelta.
+ */
+ void UpdateWithTouchAtDevicePoint(ParentLayerCoord aPos, ParentLayerCoord aAdditionalDelta, uint32_t aTimestampMs);
+
+protected:
+ float ApplyFlingCurveToVelocity(float aVelocity) const;
+ void AddVelocityToQueue(uint32_t aTimestampMs, float aVelocity);
+
+public:
+ void HandleTouchVelocity(uint32_t aTimestampMs, float aSpeed);
+
+ /**
+ * Notify this Axis that a touch has begun, i.e. the user has put their finger
+ * on the screen but has not yet tried to pan.
+ */
+ void StartTouch(ParentLayerCoord aPos, uint32_t aTimestampMs);
+
+ /**
+ * Notify this Axis that a touch has ended gracefully. This may perform
+ * recalculations of the axis velocity.
+ */
+ void EndTouch(uint32_t aTimestampMs);
+
+ /**
+ * Notify this Axis that the gesture has ended forcefully. Useful for stopping
+ * flings when a user puts their finger down in the middle of one (i.e. to
+ * stop a previous touch including its fling so that a new one can take its
+ * place).
+ */
+ void CancelGesture();
+
+ /**
+ * Takes a requested displacement to the position of this axis, and adjusts it
+ * to account for overscroll (which might decrease the displacement; this is
+ * to prevent the viewport from overscrolling the page rect), and axis locking
+ * (which might prevent any displacement from happening). If overscroll
+ * ocurred, its amount is written to |aOverscrollAmountOut|.
+ * The |aDisplacementOut| parameter is set to the adjusted
+ * displacement, and the function returns true iff internal overscroll amounts
+ * were changed.
+ */
+ bool AdjustDisplacement(ParentLayerCoord aDisplacement,
+ /* ParentLayerCoord */ float& aDisplacementOut,
+ /* ParentLayerCoord */ float& aOverscrollAmountOut,
+ bool aForceOverscroll = false);
+
+ /**
+ * Overscrolls this axis by the requested amount in the requested direction.
+ * The axis must be at the end of its scroll range in this direction.
+ */
+ void OverscrollBy(ParentLayerCoord aOverscroll);
+
+ /**
+ * Return the amount of overscroll on this axis, in ParentLayer pixels.
+ *
+ * If this amount is nonzero, the relevant component of
+ * mAsyncPanZoomController->mFrameMetrics.mScrollOffset must be at its
+ * extreme allowed value in the relevant direction (that is, it must be at
+ * its maximum value if we are overscrolled at our composition length, and
+ * at its minimum value if we are overscrolled at the origin).
+ */
+ ParentLayerCoord GetOverscroll() const;
+
+ /**
+ * Start an overscroll animation with the given initial velocity.
+ */
+ void StartOverscrollAnimation(float aVelocity);
+
+ /**
+ * Sample the snap-back animation to relieve overscroll.
+ * |aDelta| is the time since the last sample.
+ */
+ bool SampleOverscrollAnimation(const TimeDuration& aDelta);
+
+ /**
+ * Stop an overscroll animation.
+ */
+ void EndOverscrollAnimation();
+
+ /**
+ * Return whether this axis is overscrolled in either direction.
+ */
+ bool IsOverscrolled() const;
+
+ /**
+ * Clear any overscroll amount on this axis.
+ */
+ void ClearOverscroll();
+
+ /**
+ * Gets the starting position of the touch supplied in StartTouch().
+ */
+ ParentLayerCoord PanStart() const;
+
+ /**
+ * Gets the distance between the starting position of the touch supplied in
+ * StartTouch() and the current touch from the last
+ * UpdateWithTouchAtDevicePoint().
+ */
+ ParentLayerCoord PanDistance() const;
+
+ /**
+ * Gets the distance between the starting position of the touch supplied in
+ * StartTouch() and the supplied position.
+ */
+ ParentLayerCoord PanDistance(ParentLayerCoord aPos) const;
+
+ /**
+ * Applies friction during a fling, or cancels the fling if the velocity is
+ * too low. Returns true if the fling should continue to another frame, or
+ * false if it should end.
+ * |aDelta| is the amount of time that has passed since the last time
+ * friction was applied.
+ * |aFriction| is the amount of friction to apply.
+ * |aThreshold| is the velocity below which the fling is cancelled.
+ */
+ bool FlingApplyFrictionOrCancel(const TimeDuration& aDelta,
+ float aFriction,
+ float aThreshold);
+
+ /**
+ * Returns true if the page has room to be scrolled along this axis.
+ */
+ bool CanScroll() const;
+
+ /**
+ * Returns whether this axis can scroll any more in a particular direction.
+ */
+ bool CanScroll(ParentLayerCoord aDelta) const;
+
+ /**
+ * Returns true if the page has room to be scrolled along this axis
+ * and this axis is not scroll-locked.
+ */
+ bool CanScrollNow() const;
+
+ /**
+ * Clamp a point to the page's scrollable bounds. That is, a scroll
+ * destination to the returned point will not contain any overscroll.
+ */
+ CSSCoord ClampOriginToScrollableRect(CSSCoord aOrigin) const;
+
+ void SetAxisLocked(bool aAxisLocked) { mAxisLocked = aAxisLocked; }
+
+ /**
+ * Gets the raw velocity of this axis at this moment.
+ */
+ float GetVelocity() const;
+
+ /**
+ * Sets the raw velocity of this axis at this moment.
+ * Intended to be called only when the axis "takes over" a velocity from
+ * another APZC, in which case there are no touch points available to call
+ * UpdateWithTouchAtDevicePoint. In other circumstances,
+ * UpdateWithTouchAtDevicePoint should be used and the velocity calculated
+ * there.
+ */
+ void SetVelocity(float aVelocity);
+
+ /**
+ * If a displacement will overscroll the axis, this returns the amount and in
+ * what direction.
+ */
+ ParentLayerCoord DisplacementWillOverscrollAmount(ParentLayerCoord aDisplacement) const;
+
+ /**
+ * If a scale will overscroll the axis, this returns the amount and in what
+ * direction.
+ *
+ * |aFocus| is the point at which the scale is focused at. We will offset the
+ * scroll offset in such a way that it remains in the same place on the page
+ * relative.
+ *
+ * Note: Unlike most other functions in Axis, this functions operates in
+ * CSS coordinates so there is no confusion as to whether the ParentLayer
+ * coordinates it operates in are before or after the scale is applied.
+ */
+ CSSCoord ScaleWillOverscrollAmount(float aScale, CSSCoord aFocus) const;
+
+ /**
+ * Checks if an axis will overscroll in both directions by computing the
+ * content rect and checking that its height/width (depending on the axis)
+ * does not overextend past the viewport.
+ *
+ * This gets called by ScaleWillOverscroll().
+ */
+ bool ScaleWillOverscrollBothSides(float aScale) const;
+
+ /**
+ * Returns true if movement on this axis is locked.
+ */
+ bool IsAxisLocked() const;
+
+ ParentLayerCoord GetOrigin() const;
+ ParentLayerCoord GetCompositionLength() const;
+ ParentLayerCoord GetPageStart() const;
+ ParentLayerCoord GetPageLength() const;
+ ParentLayerCoord GetCompositionEnd() const;
+ ParentLayerCoord GetPageEnd() const;
+ ParentLayerCoord GetScrollRangeEnd() const;
+
+ ParentLayerCoord GetPos() const { return mPos; }
+
+ virtual ParentLayerCoord GetPointOffset(const ParentLayerPoint& aPoint) const = 0;
+ virtual ParentLayerCoord GetRectLength(const ParentLayerRect& aRect) const = 0;
+ virtual ParentLayerCoord GetRectOffset(const ParentLayerRect& aRect) const = 0;
+ virtual CSSToParentLayerScale GetScaleForAxis(const CSSToParentLayerScale2D& aScale) const = 0;
+
+ virtual ScreenPoint MakePoint(ScreenCoord aCoord) const = 0;
+
+ virtual const char* Name() const = 0;
+
+protected:
+ ParentLayerCoord mPos;
+
+ // mVelocitySampleTimeMs and mVelocitySamplePos are the time and position
+ // used in the last velocity sampling. They get updated when a new sample is
+ // taken (which may not happen on every input event, if the time delta is too
+ // small).
+ uint32_t mVelocitySampleTimeMs;
+ ParentLayerCoord mVelocitySamplePos;
+
+ ParentLayerCoord mStartPos;
+ float mVelocity; // Units: ParentLayerCoords per millisecond
+ bool mAxisLocked; // Whether movement on this axis is locked.
+ AsyncPanZoomController* mAsyncPanZoomController;
+
+ // mOverscroll is the displacement of an oscillating spring from its resting
+ // state. The resting state moves as the overscroll animation progresses.
+ ParentLayerCoord mOverscroll;
+ // Used to record the initial overscroll when we start sampling for animation.
+ ParentLayerCoord mFirstOverscrollAnimationSample;
+ // These two variables are used in combination to make sure that
+ // GetOverscroll() never changes sign during animation. This is necessary,
+ // as mOverscroll itself oscillates around zero during animation.
+ // If we're not sampling overscroll animation, mOverscrollScale will be 1.0
+ // and mLastOverscrollPeak will be zero.
+ // If we are animating, after the overscroll reaches its peak,
+ // mOverscrollScale will be 2.0 and mLastOverscrollPeak will store the amount
+ // of overscroll at the last peak of the oscillation. Together, these values
+ // guarantee that the result of GetOverscroll() never changes sign.
+ ParentLayerCoord mLastOverscrollPeak;
+ float mOverscrollScale;
+
+ // A queue of (timestamp, velocity) pairs; these are the historical
+ // velocities at the given timestamps. Timestamps are in milliseconds,
+ // velocities are in screen pixels per ms. This member can only be
+ // accessed on the controller/UI thread.
+ nsTArray<std::pair<uint32_t, float> > mVelocityQueue;
+
+ const FrameMetrics& GetFrameMetrics() const;
+
+ // Adjust a requested overscroll amount for resistance, yielding a smaller
+ // actual overscroll amount.
+ ParentLayerCoord ApplyResistance(ParentLayerCoord aOverscroll) const;
+
+ // Helper function for SampleOverscrollAnimation().
+ void StepOverscrollAnimation(double aStepDurationMilliseconds);
+
+ // Convert a velocity from global inches/ms into ParentLayerCoords/ms.
+ float ToLocalVelocity(float aVelocityInchesPerMs) const;
+};
+
+class AxisX : public Axis {
+public:
+ explicit AxisX(AsyncPanZoomController* mAsyncPanZoomController);
+ virtual ParentLayerCoord GetPointOffset(const ParentLayerPoint& aPoint) const override;
+ virtual ParentLayerCoord GetRectLength(const ParentLayerRect& aRect) const override;
+ virtual ParentLayerCoord GetRectOffset(const ParentLayerRect& aRect) const override;
+ virtual CSSToParentLayerScale GetScaleForAxis(const CSSToParentLayerScale2D& aScale) const override;
+ virtual ScreenPoint MakePoint(ScreenCoord aCoord) const override;
+ virtual const char* Name() const override;
+};
+
+class AxisY : public Axis {
+public:
+ explicit AxisY(AsyncPanZoomController* mAsyncPanZoomController);
+ virtual ParentLayerCoord GetPointOffset(const ParentLayerPoint& aPoint) const override;
+ virtual ParentLayerCoord GetRectLength(const ParentLayerRect& aRect) const override;
+ virtual ParentLayerCoord GetRectOffset(const ParentLayerRect& aRect) const override;
+ virtual CSSToParentLayerScale GetScaleForAxis(const CSSToParentLayerScale2D& aScale) const override;
+ virtual ScreenPoint MakePoint(ScreenCoord aCoord) const override;
+ virtual const char* Name() const override;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif
diff --git a/gfx/layers/apz/src/CheckerboardEvent.cpp b/gfx/layers/apz/src/CheckerboardEvent.cpp
new file mode 100644
index 000000000..ea40a5fa7
--- /dev/null
+++ b/gfx/layers/apz/src/CheckerboardEvent.cpp
@@ -0,0 +1,230 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set sw=2 ts=8 et tw=80 : */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "CheckerboardEvent.h"
+
+#include <algorithm> // for std::sort
+
+namespace mozilla {
+namespace layers {
+
+// Relatively arbitrary limit to prevent a perma-checkerboard event from
+// eating up gobs of memory. Ideally we shouldn't have perma-checkerboarding
+// but better to guard against it.
+#define LOG_LENGTH_LIMIT (50 * 1024)
+
+const char* CheckerboardEvent::sDescriptions[] = {
+ "page",
+ "painted critical displayport",
+ "painted displayport",
+ "requested displayport",
+ "viewport",
+};
+
+const char* CheckerboardEvent::sColors[] = {
+ "brown",
+ "darkgreen",
+ "lightgreen",
+ "yellow",
+ "red",
+};
+
+CheckerboardEvent::CheckerboardEvent(bool aRecordTrace)
+ : mRecordTrace(aRecordTrace)
+ , mOriginTime(TimeStamp::Now())
+ , mCheckerboardingActive(false)
+ , mLastSampleTime(mOriginTime)
+ , mFrameCount(0)
+ , mTotalPixelMs(0)
+ , mPeakPixels(0)
+ , mRendertraceLock("Rendertrace")
+{
+}
+
+uint32_t
+CheckerboardEvent::GetSeverity()
+{
+ // Scale the total into a 32-bit value
+ return (uint32_t)sqrt((double)mTotalPixelMs);
+}
+
+uint32_t
+CheckerboardEvent::GetPeak()
+{
+ return mPeakPixels;
+}
+
+TimeDuration
+CheckerboardEvent::GetDuration()
+{
+ return mEndTime - mStartTime;
+}
+
+std::string
+CheckerboardEvent::GetLog()
+{
+ MonitorAutoLock lock(mRendertraceLock);
+ return mRendertraceInfo.str();
+}
+
+bool
+CheckerboardEvent::IsRecordingTrace()
+{
+ return mRecordTrace;
+}
+
+void
+CheckerboardEvent::UpdateRendertraceProperty(RendertraceProperty aProperty,
+ const CSSRect& aRect,
+ const std::string& aExtraInfo)
+{
+ if (!mRecordTrace) {
+ return;
+ }
+ MonitorAutoLock lock(mRendertraceLock);
+ if (!mCheckerboardingActive) {
+ mBufferedProperties[aProperty].Update(aProperty, aRect, aExtraInfo, lock);
+ } else {
+ LogInfo(aProperty, TimeStamp::Now(), aRect, aExtraInfo, lock);
+ }
+}
+
+void
+CheckerboardEvent::LogInfo(RendertraceProperty aProperty,
+ const TimeStamp& aTimestamp,
+ const CSSRect& aRect,
+ const std::string& aExtraInfo,
+ const MonitorAutoLock& aProofOfLock)
+{
+ MOZ_ASSERT(mRecordTrace);
+ if (mRendertraceInfo.tellp() >= LOG_LENGTH_LIMIT) {
+ // The log is already long enough, don't put more things into it. We'll
+ // append a truncation message when this event ends.
+ return;
+ }
+ // The log is consumed by the page at http://people.mozilla.org/~kgupta/rendertrace.html
+ // and will move to about:checkerboard in bug 1238042. The format is not
+ // formally specced, but an informal description can be found at
+ // https://github.com/staktrace/rendertrace/blob/master/index.html#L30
+ mRendertraceInfo << "RENDERTRACE "
+ << (aTimestamp - mOriginTime).ToMilliseconds() << " rect "
+ << sColors[aProperty] << " "
+ << aRect.x << " "
+ << aRect.y << " "
+ << aRect.width << " "
+ << aRect.height << " "
+ << "// " << sDescriptions[aProperty]
+ << aExtraInfo << std::endl;
+}
+
+bool
+CheckerboardEvent::RecordFrameInfo(uint32_t aCssPixelsCheckerboarded)
+{
+ TimeStamp sampleTime = TimeStamp::Now();
+ bool eventEnding = false;
+ if (aCssPixelsCheckerboarded > 0) {
+ if (!mCheckerboardingActive) {
+ StartEvent();
+ }
+ MOZ_ASSERT(mCheckerboardingActive);
+ MOZ_ASSERT(sampleTime >= mLastSampleTime);
+ mTotalPixelMs += (uint64_t)((sampleTime - mLastSampleTime).ToMilliseconds() * aCssPixelsCheckerboarded);
+ if (aCssPixelsCheckerboarded > mPeakPixels) {
+ mPeakPixels = aCssPixelsCheckerboarded;
+ }
+ mFrameCount++;
+ } else {
+ if (mCheckerboardingActive) {
+ StopEvent();
+ eventEnding = true;
+ }
+ MOZ_ASSERT(!mCheckerboardingActive);
+ }
+ mLastSampleTime = sampleTime;
+ return eventEnding;
+}
+
+void
+CheckerboardEvent::StartEvent()
+{
+ MOZ_ASSERT(!mCheckerboardingActive);
+ mCheckerboardingActive = true;
+ mStartTime = TimeStamp::Now();
+
+ if (!mRecordTrace) {
+ return;
+ }
+ MonitorAutoLock lock(mRendertraceLock);
+ std::vector<PropertyValue> history;
+ for (int i = 0; i < MAX_RendertraceProperty; i++) {
+ mBufferedProperties[i].Flush(history, lock);
+ }
+ std::sort(history.begin(), history.end());
+ for (const PropertyValue& p : history) {
+ LogInfo(p.mProperty, p.mTimeStamp, p.mRect, p.mExtraInfo, lock);
+ }
+ mRendertraceInfo << " -- checkerboarding starts below --" << std::endl;
+}
+
+void
+CheckerboardEvent::StopEvent()
+{
+ mCheckerboardingActive = false;
+ mEndTime = TimeStamp::Now();
+
+ if (!mRecordTrace) {
+ return;
+ }
+ MonitorAutoLock lock(mRendertraceLock);
+ if (mRendertraceInfo.tellp() >= LOG_LENGTH_LIMIT) {
+ mRendertraceInfo << "[logging aborted due to length limitations]\n";
+ }
+ mRendertraceInfo << "Checkerboarded for " << mFrameCount << " frames ("
+ << (mEndTime - mStartTime).ToMilliseconds() << " ms), "
+ << mPeakPixels << " peak, " << GetSeverity() << " severity." << std::endl;
+}
+
+bool
+CheckerboardEvent::PropertyValue::operator<(const PropertyValue& aOther) const
+{
+ if (mTimeStamp < aOther.mTimeStamp) {
+ return true;
+ } else if (mTimeStamp > aOther.mTimeStamp) {
+ return false;
+ }
+ return mProperty < aOther.mProperty;
+}
+
+CheckerboardEvent::PropertyBuffer::PropertyBuffer()
+ : mIndex(0)
+{
+}
+
+void
+CheckerboardEvent::PropertyBuffer::Update(RendertraceProperty aProperty,
+ const CSSRect& aRect,
+ const std::string& aExtraInfo,
+ const MonitorAutoLock& aProofOfLock)
+{
+ mValues[mIndex] = { aProperty, TimeStamp::Now(), aRect, aExtraInfo };
+ mIndex = (mIndex + 1) % BUFFER_SIZE;
+}
+
+void
+CheckerboardEvent::PropertyBuffer::Flush(std::vector<PropertyValue>& aOut,
+ const MonitorAutoLock& aProofOfLock)
+{
+ for (uint32_t i = 0; i < BUFFER_SIZE; i++) {
+ uint32_t ix = (mIndex + i) % BUFFER_SIZE;
+ if (!mValues[ix].mTimeStamp.IsNull()) {
+ aOut.push_back(mValues[ix]);
+ mValues[ix].mTimeStamp = TimeStamp();
+ }
+ }
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/apz/src/CheckerboardEvent.h b/gfx/layers/apz/src/CheckerboardEvent.h
new file mode 100644
index 000000000..c71611d89
--- /dev/null
+++ b/gfx/layers/apz/src/CheckerboardEvent.h
@@ -0,0 +1,221 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_layers_CheckerboardEvent_h
+#define mozilla_layers_CheckerboardEvent_h
+
+#include "mozilla/Monitor.h"
+#include "mozilla/TimeStamp.h"
+#include <sstream>
+#include "Units.h"
+#include <vector>
+
+namespace mozilla {
+namespace layers {
+
+/**
+ * This class records information relevant to one "checkerboard event", which is
+ * a contiguous set of frames where a given APZC was checkerboarding. The intent
+ * of this class is to record enough information that it can provide actionable
+ * steps to reduce the occurrence of checkerboarding. Furthermore, it records
+ * information about the severity of the checkerboarding so as to allow
+ * prioritizing the debugging of some checkerboarding events over others.
+ */
+class CheckerboardEvent {
+public:
+ enum RendertraceProperty {
+ Page,
+ PaintedCriticalDisplayPort,
+ PaintedDisplayPort,
+ RequestedDisplayPort,
+ UserVisible,
+
+ // sentinel final value
+ MAX_RendertraceProperty
+ };
+
+ static const char* sDescriptions[MAX_RendertraceProperty];
+ static const char* sColors[MAX_RendertraceProperty];
+
+public:
+ explicit CheckerboardEvent(bool aRecordTrace);
+
+ /**
+ * Gets the "severity" of the checkerboard event. This doesn't have units,
+ * it's just useful for comparing two checkerboard events to see which one
+ * is worse, for some implementation-specific definition of "worse".
+ */
+ uint32_t GetSeverity();
+
+ /**
+ * Gets the number of CSS pixels that were checkerboarded at the peak of the
+ * checkerboard event.
+ */
+ uint32_t GetPeak();
+
+ /**
+ * Gets the length of the checkerboard event.
+ */
+ TimeDuration GetDuration();
+
+ /**
+ * Gets the raw log of the checkerboard event. This can be called any time,
+ * although it really only makes sense to pull once the event is done, after
+ * RecordFrameInfo returns true.
+ */
+ std::string GetLog();
+
+ /**
+ * Returns true iff this event is recording a detailed trace of the event.
+ * This is the argument passed in to the constructor.
+ */
+ bool IsRecordingTrace();
+
+ /**
+ * Provide a new value for one of the rects that is tracked for
+ * checkerboard events.
+ */
+ void UpdateRendertraceProperty(RendertraceProperty aProperty,
+ const CSSRect& aRect,
+ const std::string& aExtraInfo = std::string());
+
+ /**
+ * Provide the number of CSS pixels that are checkerboarded in a composite
+ * at the current time.
+ * @return true if the checkerboard event has completed. The caller should
+ * stop updating this object once this happens.
+ */
+ bool RecordFrameInfo(uint32_t aCssPixelsCheckerboarded);
+
+private:
+ /**
+ * Helper method to do stuff when checkeboarding starts.
+ */
+ void StartEvent();
+ /**
+ * Helper method to do stuff when checkerboarding stops.
+ */
+ void StopEvent();
+
+ /**
+ * Helper method to log a rendertrace property and its value to the
+ * rendertrace info buffer (mRendertraceInfo).
+ */
+ void LogInfo(RendertraceProperty aProperty,
+ const TimeStamp& aTimestamp,
+ const CSSRect& aRect,
+ const std::string& aExtraInfo,
+ const MonitorAutoLock& aProofOfLock);
+
+ /**
+ * Helper struct that holds a single rendertrace property value.
+ */
+ struct PropertyValue
+ {
+ RendertraceProperty mProperty;
+ TimeStamp mTimeStamp;
+ CSSRect mRect;
+ std::string mExtraInfo;
+
+ bool operator<(const PropertyValue& aOther) const;
+ };
+
+ /**
+ * A circular buffer that stores the most recent BUFFER_SIZE values of a
+ * given property.
+ */
+ class PropertyBuffer
+ {
+ public:
+ PropertyBuffer();
+ /**
+ * Add a new value to the buffer, overwriting the oldest one if needed.
+ */
+ void Update(RendertraceProperty aProperty, const CSSRect& aRect,
+ const std::string& aExtraInfo,
+ const MonitorAutoLock& aProofOfLock);
+ /**
+ * Dump the recorded values, oldest to newest, to the given vector, and
+ * remove them from this buffer.
+ */
+ void Flush(std::vector<PropertyValue>& aOut,
+ const MonitorAutoLock& aProofOfLock);
+
+ private:
+ static const uint32_t BUFFER_SIZE = 5;
+
+ /**
+ * The index of the oldest value in the buffer. This is the next index
+ * that will be written to.
+ */
+ uint32_t mIndex;
+ PropertyValue mValues[BUFFER_SIZE];
+ };
+
+private:
+ /**
+ * If true, we should log the various properties during the checkerboard
+ * event. If false, we only need to record things we need for telemetry
+ * measures.
+ */
+ const bool mRecordTrace;
+ /**
+ * A base time so that the other timestamps can be turned into durations.
+ */
+ const TimeStamp mOriginTime;
+ /**
+ * Whether or not a checkerboard event is currently occurring.
+ */
+ bool mCheckerboardingActive;
+
+ /**
+ * The start time of the checkerboard event.
+ */
+ TimeStamp mStartTime;
+ /**
+ * The end time of the checkerboard event.
+ */
+ TimeStamp mEndTime;
+ /**
+ * The sample time of the last frame recorded.
+ */
+ TimeStamp mLastSampleTime;
+ /**
+ * The number of contiguous frames with checkerboard.
+ */
+ uint32_t mFrameCount;
+ /**
+ * The total number of pixel-milliseconds of checkerboarding visible to
+ * the user during the checkerboarding event.
+ */
+ uint64_t mTotalPixelMs;
+ /**
+ * The largest number of pixels of checkerboarding visible to the user
+ * during any one frame, during this checkerboarding event.
+ */
+ uint32_t mPeakPixels;
+
+ /**
+ * Monitor that needs to be acquired before touching mBufferedProperties
+ * or mRendertraceInfo.
+ */
+ mutable Monitor mRendertraceLock;
+ /**
+ * A circular buffer to store some properties. This is used before the
+ * checkerboarding actually starts, so that we have some data on what
+ * was happening before the checkerboarding started.
+ */
+ PropertyBuffer mBufferedProperties[MAX_RendertraceProperty];
+ /**
+ * The rendertrace info buffer that gives us info on what was happening
+ * during the checkerboard event.
+ */
+ std::ostringstream mRendertraceInfo;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif // mozilla_layers_CheckerboardEvent_h
diff --git a/gfx/layers/apz/src/DragTracker.cpp b/gfx/layers/apz/src/DragTracker.cpp
new file mode 100644
index 000000000..ecd3ff16f
--- /dev/null
+++ b/gfx/layers/apz/src/DragTracker.cpp
@@ -0,0 +1,70 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "DragTracker.h"
+
+#include "InputData.h"
+
+#define DRAG_LOG(...)
+// #define DRAG_LOG(...) printf_stderr("DRAG: " __VA_ARGS__)
+
+namespace mozilla {
+namespace layers {
+
+DragTracker::DragTracker()
+ : mInDrag(false)
+{
+}
+
+/*static*/ bool
+DragTracker::StartsDrag(const MouseInput& aInput)
+{
+ return aInput.IsLeftButton() && aInput.mType == MouseInput::MOUSE_DOWN;
+}
+
+/*static*/ bool
+DragTracker::EndsDrag(const MouseInput& aInput)
+{
+ // On Windows, we don't receive a MOUSE_UP at the end of a drag if an
+ // actual drag session took place. As a backup, we detect the end of the
+ // drag using the MOUSE_DRAG_END event, which normally is routed directly
+ // to content, but we're specially routing to APZ for this purpose. Bug
+ // 1265105 tracks a solution to this at the Windows widget layer; once
+ // that is implemented, this workaround can be removed.
+ return (aInput.IsLeftButton() && aInput.mType == MouseInput::MOUSE_UP)
+ || aInput.mType == MouseInput::MOUSE_DRAG_END;
+}
+
+void
+DragTracker::Update(const MouseInput& aInput)
+{
+ if (StartsDrag(aInput)) {
+ DRAG_LOG("Starting drag\n");
+ mInDrag = true;
+ } else if (EndsDrag(aInput)) {
+ DRAG_LOG("Ending drag\n");
+ mInDrag = false;
+ mOnScrollbar = Nothing();
+ }
+}
+
+bool
+DragTracker::InDrag() const
+{
+ return mInDrag;
+}
+
+bool
+DragTracker::IsOnScrollbar(bool aOnScrollbar)
+{
+ if (!mOnScrollbar) {
+ DRAG_LOG("Setting hitscrollbar %d\n", aOnScrollbar);
+ mOnScrollbar = Some(aOnScrollbar);
+ }
+ return mOnScrollbar.value();
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/apz/src/DragTracker.h b/gfx/layers/apz/src/DragTracker.h
new file mode 100644
index 000000000..9f7ff1222
--- /dev/null
+++ b/gfx/layers/apz/src/DragTracker.h
@@ -0,0 +1,39 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_layers_DragTracker_h
+#define mozilla_layers_DragTracker_h
+
+#include "mozilla/EventForwards.h"
+#include "mozilla/Maybe.h"
+
+namespace mozilla {
+
+class MouseInput;
+
+namespace layers {
+
+// DragTracker simply tracks a sequence of mouse inputs and allows us to tell
+// if we are in a drag or not (i.e. the left mouse button went down and hasn't
+// gone up yet).
+class DragTracker
+{
+public:
+ DragTracker();
+ static bool StartsDrag(const MouseInput& aInput);
+ static bool EndsDrag(const MouseInput& aInput);
+ void Update(const MouseInput& aInput);
+ bool InDrag() const;
+ bool IsOnScrollbar(bool aOnScrollbar);
+
+private:
+ Maybe<bool> mOnScrollbar;
+ bool mInDrag;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif /* mozilla_layers_DragTracker_h */
diff --git a/gfx/layers/apz/src/GenericFlingAnimation.h b/gfx/layers/apz/src/GenericFlingAnimation.h
new file mode 100644
index 000000000..deec37b47
--- /dev/null
+++ b/gfx/layers/apz/src/GenericFlingAnimation.h
@@ -0,0 +1,207 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set sw=2 ts=8 et tw=80 : */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_layers_GenericFlingAnimation_h_
+#define mozilla_layers_GenericFlingAnimation_h_
+
+#include "APZUtils.h"
+#include "AsyncPanZoomAnimation.h"
+#include "AsyncPanZoomController.h"
+#include "FrameMetrics.h"
+#include "Layers.h"
+#include "Units.h"
+#include "OverscrollHandoffState.h"
+#include "gfxPrefs.h"
+#include "mozilla/Assertions.h"
+#include "mozilla/Monitor.h"
+#include "mozilla/RefPtr.h"
+#include "mozilla/TimeStamp.h"
+#include "nsThreadUtils.h"
+
+#define FLING_LOG(...)
+// #define FLING_LOG(...) printf_stderr("FLING: " __VA_ARGS__)
+
+namespace mozilla {
+namespace layers {
+
+class GenericFlingAnimation: public AsyncPanZoomAnimation {
+public:
+ GenericFlingAnimation(AsyncPanZoomController& aApzc,
+ PlatformSpecificStateBase* aPlatformSpecificState,
+ const RefPtr<const OverscrollHandoffChain>& aOverscrollHandoffChain,
+ bool aFlingIsHandedOff,
+ const RefPtr<const AsyncPanZoomController>& aScrolledApzc)
+ : mApzc(aApzc)
+ , mOverscrollHandoffChain(aOverscrollHandoffChain)
+ , mScrolledApzc(aScrolledApzc)
+ {
+ MOZ_ASSERT(mOverscrollHandoffChain);
+ TimeStamp now = aApzc.GetFrameTime();
+
+ // Drop any velocity on axes where we don't have room to scroll anyways
+ // (in this APZC, or an APZC further in the handoff chain).
+ // This ensures that we don't take the 'overscroll' path in Sample()
+ // on account of one axis which can't scroll having a velocity.
+ if (!mOverscrollHandoffChain->CanScrollInDirection(&mApzc, Layer::HORIZONTAL)) {
+ ReentrantMonitorAutoEnter lock(mApzc.mMonitor);
+ mApzc.mX.SetVelocity(0);
+ }
+ if (!mOverscrollHandoffChain->CanScrollInDirection(&mApzc, Layer::VERTICAL)) {
+ ReentrantMonitorAutoEnter lock(mApzc.mMonitor);
+ mApzc.mY.SetVelocity(0);
+ }
+
+ ParentLayerPoint velocity = mApzc.GetVelocityVector();
+
+ // If the last fling was very recent and in the same direction as this one,
+ // boost the velocity to be the sum of the two. Check separate axes separately
+ // because we could have two vertical flings with small horizontal components
+ // on the opposite side of zero, and we still want the y-fling to get accelerated.
+ // Note that the acceleration code is only applied on the APZC that initiates
+ // the fling; the accelerated velocities are then handed off using the
+ // normal DispatchFling codepath.
+ // Acceleration is only applied in the APZC that originated the fling,
+ // not in APZCs further down the handoff chain during handoff.
+ bool applyAcceleration = !aFlingIsHandedOff;
+ if (applyAcceleration && !mApzc.mLastFlingTime.IsNull()
+ && (now - mApzc.mLastFlingTime).ToMilliseconds() < gfxPrefs::APZFlingAccelInterval()
+ && velocity.Length() >= gfxPrefs::APZFlingAccelMinVelocity()) {
+ if (SameDirection(velocity.x, mApzc.mLastFlingVelocity.x)) {
+ velocity.x = Accelerate(velocity.x, mApzc.mLastFlingVelocity.x);
+ FLING_LOG("%p Applying fling x-acceleration from %f to %f (delta %f)\n",
+ &mApzc, mApzc.mX.GetVelocity(), velocity.x, mApzc.mLastFlingVelocity.x);
+ mApzc.mX.SetVelocity(velocity.x);
+ }
+ if (SameDirection(velocity.y, mApzc.mLastFlingVelocity.y)) {
+ velocity.y = Accelerate(velocity.y, mApzc.mLastFlingVelocity.y);
+ FLING_LOG("%p Applying fling y-acceleration from %f to %f (delta %f)\n",
+ &mApzc, mApzc.mY.GetVelocity(), velocity.y, mApzc.mLastFlingVelocity.y);
+ mApzc.mY.SetVelocity(velocity.y);
+ }
+ }
+
+ mApzc.mLastFlingTime = now;
+ mApzc.mLastFlingVelocity = velocity;
+ }
+
+ /**
+ * Advances a fling by an interpolated amount based on the passed in |aDelta|.
+ * This should be called whenever sampling the content transform for this
+ * frame. Returns true if the fling animation should be advanced by one frame,
+ * or false if there is no fling or the fling has ended.
+ */
+ virtual bool DoSample(FrameMetrics& aFrameMetrics,
+ const TimeDuration& aDelta) override
+ {
+ float friction = gfxPrefs::APZFlingFriction();
+ float threshold = gfxPrefs::APZFlingStoppedThreshold();
+
+ bool shouldContinueFlingX = mApzc.mX.FlingApplyFrictionOrCancel(aDelta, friction, threshold),
+ shouldContinueFlingY = mApzc.mY.FlingApplyFrictionOrCancel(aDelta, friction, threshold);
+ // If we shouldn't continue the fling, let's just stop and repaint.
+ if (!shouldContinueFlingX && !shouldContinueFlingY) {
+ FLING_LOG("%p ending fling animation. overscrolled=%d\n", &mApzc, mApzc.IsOverscrolled());
+ // This APZC or an APZC further down the handoff chain may be be overscrolled.
+ // Start a snap-back animation on the overscrolled APZC.
+ // Note:
+ // This needs to be a deferred task even though it can safely run
+ // while holding mMonitor, because otherwise, if the overscrolled APZC
+ // is this one, then the SetState(NOTHING) in UpdateAnimation will
+ // stomp on the SetState(SNAP_BACK) it does.
+ mDeferredTasks.AppendElement(
+ NewRunnableMethod<AsyncPanZoomController*>(mOverscrollHandoffChain.get(),
+ &OverscrollHandoffChain::SnapBackOverscrolledApzc,
+ &mApzc));
+ return false;
+ }
+
+ // AdjustDisplacement() zeroes out the Axis velocity if we're in overscroll.
+ // Since we need to hand off the velocity to the tree manager in such a case,
+ // we save it here. Would be ParentLayerVector instead of ParentLayerPoint
+ // if we had vector classes.
+ ParentLayerPoint velocity = mApzc.GetVelocityVector();
+
+ ParentLayerPoint offset = velocity * aDelta.ToMilliseconds();
+
+ // Ordinarily we might need to do a ScheduleComposite if either of
+ // the following AdjustDisplacement calls returns true, but this
+ // is already running as part of a FlingAnimation, so we'll be compositing
+ // per frame of animation anyway.
+ ParentLayerPoint overscroll;
+ ParentLayerPoint adjustedOffset;
+ mApzc.mX.AdjustDisplacement(offset.x, adjustedOffset.x, overscroll.x);
+ mApzc.mY.AdjustDisplacement(offset.y, adjustedOffset.y, overscroll.y);
+
+ aFrameMetrics.ScrollBy(adjustedOffset / aFrameMetrics.GetZoom());
+
+ // The fling may have caused us to reach the end of our scroll range.
+ if (!IsZero(overscroll)) {
+ // Hand off the fling to the next APZC in the overscroll handoff chain.
+
+ // We may have reached the end of the scroll range along one axis but
+ // not the other. In such a case we only want to hand off the relevant
+ // component of the fling.
+ if (FuzzyEqualsAdditive(overscroll.x, 0.0f, COORDINATE_EPSILON)) {
+ velocity.x = 0;
+ } else if (FuzzyEqualsAdditive(overscroll.y, 0.0f, COORDINATE_EPSILON)) {
+ velocity.y = 0;
+ }
+
+ // To hand off the fling, we attempt to find a target APZC and start a new
+ // fling with the same velocity on that APZC. For simplicity, the actual
+ // overscroll of the current sample is discarded rather than being handed
+ // off. The compositor should sample animations sufficiently frequently
+ // that this is not noticeable. The target APZC is chosen by seeing if
+ // there is an APZC further in the handoff chain which is pannable; if
+ // there isn't, we take the new fling ourselves, entering an overscrolled
+ // state.
+ // Note: APZC is holding mMonitor, so directly calling
+ // HandleFlingOverscroll() (which acquires the tree lock) would violate
+ // the lock ordering. Instead we schedule HandleFlingOverscroll() to be
+ // called after mMonitor is released.
+ FLING_LOG("%p fling went into overscroll, handing off with velocity %s\n", &mApzc, Stringify(velocity).c_str());
+ mDeferredTasks.AppendElement(
+ NewRunnableMethod<ParentLayerPoint,
+ RefPtr<const OverscrollHandoffChain>,
+ RefPtr<const AsyncPanZoomController>>(&mApzc,
+ &AsyncPanZoomController::HandleFlingOverscroll,
+ velocity,
+ mOverscrollHandoffChain,
+ mScrolledApzc));
+
+ // If there is a remaining velocity on this APZC, continue this fling
+ // as well. (This fling and the handed-off fling will run concurrently.)
+ // Note that AdjustDisplacement() will have zeroed out the velocity
+ // along the axes where we're overscrolled.
+ return !IsZero(mApzc.GetVelocityVector());
+ }
+
+ return true;
+ }
+
+private:
+ static bool SameDirection(float aVelocity1, float aVelocity2)
+ {
+ return (aVelocity1 == 0.0f)
+ || (aVelocity2 == 0.0f)
+ || (IsNegative(aVelocity1) == IsNegative(aVelocity2));
+ }
+
+ static float Accelerate(float aBase, float aSupplemental)
+ {
+ return (aBase * gfxPrefs::APZFlingAccelBaseMultiplier())
+ + (aSupplemental * gfxPrefs::APZFlingAccelSupplementalMultiplier());
+ }
+
+ AsyncPanZoomController& mApzc;
+ RefPtr<const OverscrollHandoffChain> mOverscrollHandoffChain;
+ RefPtr<const AsyncPanZoomController> mScrolledApzc;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif // mozilla_layers_GenericFlingAnimation_h_
diff --git a/gfx/layers/apz/src/GestureEventListener.cpp b/gfx/layers/apz/src/GestureEventListener.cpp
new file mode 100644
index 000000000..7fd07f3ff
--- /dev/null
+++ b/gfx/layers/apz/src/GestureEventListener.cpp
@@ -0,0 +1,552 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set sw=2 ts=8 et tw=80 : */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "GestureEventListener.h"
+#include <math.h> // for fabsf
+#include <stddef.h> // for size_t
+#include "AsyncPanZoomController.h" // for AsyncPanZoomController
+#include "base/task.h" // for CancelableTask, etc
+#include "gfxPrefs.h" // for gfxPrefs
+#include "mozilla/SizePrintfMacros.h" // for PRIuSIZE
+#include "nsDebug.h" // for NS_WARNING
+#include "nsMathUtils.h" // for NS_hypot
+
+#define GEL_LOG(...)
+// #define GEL_LOG(...) printf_stderr("GEL: " __VA_ARGS__)
+
+namespace mozilla {
+namespace layers {
+
+/**
+ * Maximum time for a touch on the screen and corresponding lift of the finger
+ * to be considered a tap. This also applies to double taps, except that it is
+ * used twice.
+ */
+static const uint32_t MAX_TAP_TIME = 300;
+
+/**
+ * Amount of span or focus change needed to take us from the GESTURE_WAITING_PINCH
+ * state to the GESTURE_PINCH state. This is measured as either a change in distance
+ * between the fingers used to compute the span ratio, or the a change in
+ * position of the focus point between the two fingers.
+ */
+static const float PINCH_START_THRESHOLD = 35.0f;
+
+static bool sLongTapEnabled = true;
+
+ParentLayerPoint GetCurrentFocus(const MultiTouchInput& aEvent)
+{
+ const ParentLayerPoint& firstTouch = aEvent.mTouches[0].mLocalScreenPoint;
+ const ParentLayerPoint& secondTouch = aEvent.mTouches[1].mLocalScreenPoint;
+ return (firstTouch + secondTouch) / 2;
+}
+
+ParentLayerCoord GetCurrentSpan(const MultiTouchInput& aEvent)
+{
+ const ParentLayerPoint& firstTouch = aEvent.mTouches[0].mLocalScreenPoint;
+ const ParentLayerPoint& secondTouch = aEvent.mTouches[1].mLocalScreenPoint;
+ ParentLayerPoint delta = secondTouch - firstTouch;
+ return delta.Length();
+}
+
+TapGestureInput CreateTapEvent(const MultiTouchInput& aTouch, TapGestureInput::TapGestureType aType)
+{
+ return TapGestureInput(aType,
+ aTouch.mTime,
+ aTouch.mTimeStamp,
+ aTouch.mTouches[0].mScreenPoint,
+ aTouch.modifiers);
+}
+
+GestureEventListener::GestureEventListener(AsyncPanZoomController* aAsyncPanZoomController)
+ : mAsyncPanZoomController(aAsyncPanZoomController),
+ mState(GESTURE_NONE),
+ mSpanChange(0.0f),
+ mPreviousSpan(0.0f),
+ mFocusChange(0.0f),
+ mLastTouchInput(MultiTouchInput::MULTITOUCH_START, 0, TimeStamp(), 0),
+ mLastTapInput(MultiTouchInput::MULTITOUCH_START, 0, TimeStamp(), 0),
+ mLongTapTimeoutTask(nullptr),
+ mMaxTapTimeoutTask(nullptr)
+{
+}
+
+GestureEventListener::~GestureEventListener()
+{
+}
+
+nsEventStatus GestureEventListener::HandleInputEvent(const MultiTouchInput& aEvent)
+{
+ GEL_LOG("Receiving event type %d with %" PRIuSIZE " touches in state %d\n", aEvent.mType, aEvent.mTouches.Length(), mState);
+
+ nsEventStatus rv = nsEventStatus_eIgnore;
+
+ // Cache the current event since it may become the single or long tap that we
+ // send.
+ mLastTouchInput = aEvent;
+
+ switch (aEvent.mType) {
+ case MultiTouchInput::MULTITOUCH_START:
+ mTouches.Clear();
+ for (size_t i = 0; i < aEvent.mTouches.Length(); i++) {
+ mTouches.AppendElement(aEvent.mTouches[i]);
+ }
+
+ if (aEvent.mTouches.Length() == 1) {
+ rv = HandleInputTouchSingleStart();
+ } else {
+ rv = HandleInputTouchMultiStart();
+ }
+ break;
+ case MultiTouchInput::MULTITOUCH_MOVE:
+ for (size_t i = 0; i < aEvent.mTouches.Length(); i++) {
+ for (size_t j = 0; j < mTouches.Length(); j++) {
+ if (aEvent.mTouches[i].mIdentifier == mTouches[j].mIdentifier) {
+ mTouches[j].mScreenPoint = aEvent.mTouches[i].mScreenPoint;
+ mTouches[j].mLocalScreenPoint = aEvent.mTouches[i].mLocalScreenPoint;
+ }
+ }
+ }
+ rv = HandleInputTouchMove();
+ break;
+ case MultiTouchInput::MULTITOUCH_END:
+ for (size_t i = 0; i < aEvent.mTouches.Length(); i++) {
+ for (size_t j = 0; j < mTouches.Length(); j++) {
+ if (aEvent.mTouches[i].mIdentifier == mTouches[j].mIdentifier) {
+ mTouches.RemoveElementAt(j);
+ break;
+ }
+ }
+ }
+
+ rv = HandleInputTouchEnd();
+ break;
+ case MultiTouchInput::MULTITOUCH_CANCEL:
+ mTouches.Clear();
+ rv = HandleInputTouchCancel();
+ break;
+ case MultiTouchInput::MULTITOUCH_SENTINEL:
+ MOZ_ASSERT_UNREACHABLE("Invalid MultTouchInput.");
+ break;
+ }
+
+ return rv;
+}
+
+int32_t GestureEventListener::GetLastTouchIdentifier() const
+{
+ if (mTouches.Length() != 1) {
+ NS_WARNING("GetLastTouchIdentifier() called when last touch event "
+ "did not have one touch");
+ }
+ return mTouches.IsEmpty() ? -1 : mTouches[0].mIdentifier;
+}
+
+/* static */
+void GestureEventListener::SetLongTapEnabled(bool aLongTapEnabled)
+{
+ sLongTapEnabled = aLongTapEnabled;
+}
+
+nsEventStatus GestureEventListener::HandleInputTouchSingleStart()
+{
+ switch (mState) {
+ case GESTURE_NONE:
+ SetState(GESTURE_FIRST_SINGLE_TOUCH_DOWN);
+ mTouchStartPosition = mLastTouchInput.mTouches[0].mLocalScreenPoint;
+
+ if (sLongTapEnabled) {
+ CreateLongTapTimeoutTask();
+ }
+ CreateMaxTapTimeoutTask();
+ break;
+ case GESTURE_FIRST_SINGLE_TOUCH_UP:
+ SetState(GESTURE_SECOND_SINGLE_TOUCH_DOWN);
+ break;
+ default:
+ NS_WARNING("Unhandled state upon single touch start");
+ SetState(GESTURE_NONE);
+ break;
+ }
+
+ return nsEventStatus_eIgnore;
+}
+
+nsEventStatus GestureEventListener::HandleInputTouchMultiStart()
+{
+ nsEventStatus rv = nsEventStatus_eIgnore;
+
+ switch (mState) {
+ case GESTURE_NONE:
+ SetState(GESTURE_MULTI_TOUCH_DOWN);
+ break;
+ case GESTURE_FIRST_SINGLE_TOUCH_DOWN:
+ CancelLongTapTimeoutTask();
+ CancelMaxTapTimeoutTask();
+ SetState(GESTURE_MULTI_TOUCH_DOWN);
+ // Prevent APZC::OnTouchStart() from handling MULTITOUCH_START event
+ rv = nsEventStatus_eConsumeNoDefault;
+ break;
+ case GESTURE_FIRST_SINGLE_TOUCH_MAX_TAP_DOWN:
+ CancelLongTapTimeoutTask();
+ SetState(GESTURE_MULTI_TOUCH_DOWN);
+ // Prevent APZC::OnTouchStart() from handling MULTITOUCH_START event
+ rv = nsEventStatus_eConsumeNoDefault;
+ break;
+ case GESTURE_FIRST_SINGLE_TOUCH_UP:
+ case GESTURE_SECOND_SINGLE_TOUCH_DOWN:
+ // Cancel wait for double tap
+ CancelMaxTapTimeoutTask();
+ MOZ_ASSERT(mSingleTapSent.isSome());
+ if (!mSingleTapSent.value()) {
+ TriggerSingleTapConfirmedEvent();
+ }
+ mSingleTapSent = Nothing();
+ SetState(GESTURE_MULTI_TOUCH_DOWN);
+ // Prevent APZC::OnTouchStart() from handling MULTITOUCH_START event
+ rv = nsEventStatus_eConsumeNoDefault;
+ break;
+ case GESTURE_LONG_TOUCH_DOWN:
+ SetState(GESTURE_MULTI_TOUCH_DOWN);
+ break;
+ case GESTURE_MULTI_TOUCH_DOWN:
+ case GESTURE_PINCH:
+ // Prevent APZC::OnTouchStart() from handling MULTITOUCH_START event
+ rv = nsEventStatus_eConsumeNoDefault;
+ break;
+ default:
+ NS_WARNING("Unhandled state upon multitouch start");
+ SetState(GESTURE_NONE);
+ break;
+ }
+
+ return rv;
+}
+
+bool GestureEventListener::MoveDistanceIsLarge()
+{
+ const ParentLayerPoint start = mLastTouchInput.mTouches[0].mLocalScreenPoint;
+ ParentLayerPoint delta = start - mTouchStartPosition;
+ ScreenPoint screenDelta = mAsyncPanZoomController->ToScreenCoordinates(delta, start);
+ return (screenDelta.Length() > AsyncPanZoomController::GetTouchStartTolerance());
+}
+
+nsEventStatus GestureEventListener::HandleInputTouchMove()
+{
+ nsEventStatus rv = nsEventStatus_eIgnore;
+
+ switch (mState) {
+ case GESTURE_NONE:
+ // Ignore this input signal as the corresponding events get handled by APZC
+ break;
+
+ case GESTURE_LONG_TOUCH_DOWN:
+ if (MoveDistanceIsLarge()) {
+ // So that we don't fire a long-tap-up if the user moves around after a
+ // long-tap
+ SetState(GESTURE_NONE);
+ }
+ break;
+
+ case GESTURE_FIRST_SINGLE_TOUCH_DOWN:
+ case GESTURE_FIRST_SINGLE_TOUCH_MAX_TAP_DOWN:
+ case GESTURE_SECOND_SINGLE_TOUCH_DOWN: {
+ // If we move too much, bail out of the tap.
+ if (MoveDistanceIsLarge()) {
+ CancelLongTapTimeoutTask();
+ CancelMaxTapTimeoutTask();
+ mSingleTapSent = Nothing();
+ SetState(GESTURE_NONE);
+ }
+ break;
+ }
+
+ case GESTURE_MULTI_TOUCH_DOWN: {
+ if (mLastTouchInput.mTouches.Length() < 2) {
+ NS_WARNING("Wrong input: less than 2 moving points in GESTURE_MULTI_TOUCH_DOWN state");
+ break;
+ }
+
+ ParentLayerCoord currentSpan = GetCurrentSpan(mLastTouchInput);
+ ParentLayerPoint currentFocus = GetCurrentFocus(mLastTouchInput);
+
+ mSpanChange += fabsf(currentSpan - mPreviousSpan);
+ mFocusChange += (currentFocus - mPreviousFocus).Length();
+ if (mSpanChange > PINCH_START_THRESHOLD ||
+ mFocusChange > PINCH_START_THRESHOLD) {
+ SetState(GESTURE_PINCH);
+ PinchGestureInput pinchEvent(PinchGestureInput::PINCHGESTURE_START,
+ mLastTouchInput.mTime,
+ mLastTouchInput.mTimeStamp,
+ currentFocus,
+ currentSpan,
+ currentSpan,
+ mLastTouchInput.modifiers);
+
+ rv = mAsyncPanZoomController->HandleGestureEvent(pinchEvent);
+ } else {
+ // Prevent APZC::OnTouchMove from processing a move event when two
+ // touches are active
+ rv = nsEventStatus_eConsumeNoDefault;
+ }
+
+ mPreviousSpan = currentSpan;
+ mPreviousFocus = currentFocus;
+ break;
+ }
+
+ case GESTURE_PINCH: {
+ if (mLastTouchInput.mTouches.Length() < 2) {
+ NS_WARNING("Wrong input: less than 2 moving points in GESTURE_PINCH state");
+ // Prevent APZC::OnTouchMove() from handling this wrong input
+ rv = nsEventStatus_eConsumeNoDefault;
+ break;
+ }
+
+ ParentLayerCoord currentSpan = GetCurrentSpan(mLastTouchInput);
+
+ PinchGestureInput pinchEvent(PinchGestureInput::PINCHGESTURE_SCALE,
+ mLastTouchInput.mTime,
+ mLastTouchInput.mTimeStamp,
+ GetCurrentFocus(mLastTouchInput),
+ currentSpan,
+ mPreviousSpan,
+ mLastTouchInput.modifiers);
+
+ rv = mAsyncPanZoomController->HandleGestureEvent(pinchEvent);
+ mPreviousSpan = currentSpan;
+
+ break;
+ }
+
+ default:
+ NS_WARNING("Unhandled state upon touch move");
+ SetState(GESTURE_NONE);
+ break;
+ }
+
+ return rv;
+}
+
+nsEventStatus GestureEventListener::HandleInputTouchEnd()
+{
+ // We intentionally do not pass apzc return statuses up since
+ // it may cause apzc stay in the touching state even after
+ // gestures are completed (please see Bug 1013378 for reference).
+
+ nsEventStatus rv = nsEventStatus_eIgnore;
+
+ switch (mState) {
+ case GESTURE_NONE:
+ // GEL doesn't have a dedicated state for PANNING handled in APZC thus ignore.
+ break;
+
+ case GESTURE_FIRST_SINGLE_TOUCH_DOWN: {
+ CancelLongTapTimeoutTask();
+ CancelMaxTapTimeoutTask();
+ nsEventStatus tapupStatus = mAsyncPanZoomController->HandleGestureEvent(
+ CreateTapEvent(mLastTouchInput, TapGestureInput::TAPGESTURE_UP));
+ mSingleTapSent = Some(tapupStatus != nsEventStatus_eIgnore);
+ SetState(GESTURE_FIRST_SINGLE_TOUCH_UP);
+ CreateMaxTapTimeoutTask();
+ break;
+ }
+
+ case GESTURE_SECOND_SINGLE_TOUCH_DOWN: {
+ CancelMaxTapTimeoutTask();
+ MOZ_ASSERT(mSingleTapSent.isSome());
+ mAsyncPanZoomController->HandleGestureEvent(
+ CreateTapEvent(mLastTouchInput,
+ mSingleTapSent.value() ? TapGestureInput::TAPGESTURE_SECOND
+ : TapGestureInput::TAPGESTURE_DOUBLE));
+ mSingleTapSent = Nothing();
+ SetState(GESTURE_NONE);
+ break;
+ }
+
+ case GESTURE_FIRST_SINGLE_TOUCH_MAX_TAP_DOWN:
+ CancelLongTapTimeoutTask();
+ SetState(GESTURE_NONE);
+ TriggerSingleTapConfirmedEvent();
+ break;
+
+ case GESTURE_LONG_TOUCH_DOWN: {
+ SetState(GESTURE_NONE);
+ mAsyncPanZoomController->HandleGestureEvent(
+ CreateTapEvent(mLastTouchInput, TapGestureInput::TAPGESTURE_LONG_UP));
+ break;
+ }
+
+ case GESTURE_MULTI_TOUCH_DOWN:
+ if (mTouches.Length() < 2) {
+ SetState(GESTURE_NONE);
+ }
+ break;
+
+ case GESTURE_PINCH:
+ if (mTouches.Length() < 2) {
+ SetState(GESTURE_NONE);
+ ParentLayerPoint point(-1, -1);
+ if (mTouches.Length() == 1) {
+ // As user still keeps one finger down the event's focus point should
+ // contain meaningful data.
+ point = mTouches[0].mLocalScreenPoint;
+ }
+ PinchGestureInput pinchEvent(PinchGestureInput::PINCHGESTURE_END,
+ mLastTouchInput.mTime,
+ mLastTouchInput.mTimeStamp,
+ point,
+ 1.0f,
+ 1.0f,
+ mLastTouchInput.modifiers);
+ mAsyncPanZoomController->HandleGestureEvent(pinchEvent);
+ }
+
+ rv = nsEventStatus_eConsumeNoDefault;
+
+ break;
+
+ default:
+ NS_WARNING("Unhandled state upon touch end");
+ SetState(GESTURE_NONE);
+ break;
+ }
+
+ return rv;
+}
+
+nsEventStatus GestureEventListener::HandleInputTouchCancel()
+{
+ mSingleTapSent = Nothing();
+ SetState(GESTURE_NONE);
+ CancelMaxTapTimeoutTask();
+ CancelLongTapTimeoutTask();
+ return nsEventStatus_eIgnore;
+}
+
+void GestureEventListener::HandleInputTimeoutLongTap()
+{
+ GEL_LOG("Running long-tap timeout task in state %d\n", mState);
+
+ mLongTapTimeoutTask = nullptr;
+
+ switch (mState) {
+ case GESTURE_FIRST_SINGLE_TOUCH_DOWN:
+ // just in case MAX_TAP_TIME > ContextMenuDelay cancel MAX_TAP timer
+ // and fall through
+ CancelMaxTapTimeoutTask();
+ MOZ_FALLTHROUGH;
+ case GESTURE_FIRST_SINGLE_TOUCH_MAX_TAP_DOWN: {
+ SetState(GESTURE_LONG_TOUCH_DOWN);
+ mAsyncPanZoomController->HandleGestureEvent(
+ CreateTapEvent(mLastTouchInput, TapGestureInput::TAPGESTURE_LONG));
+ break;
+ }
+ default:
+ NS_WARNING("Unhandled state upon long tap timeout");
+ SetState(GESTURE_NONE);
+ break;
+ }
+}
+
+void GestureEventListener::HandleInputTimeoutMaxTap(bool aDuringFastFling)
+{
+ GEL_LOG("Running max-tap timeout task in state %d\n", mState);
+
+ mMaxTapTimeoutTask = nullptr;
+
+ if (mState == GESTURE_FIRST_SINGLE_TOUCH_DOWN) {
+ SetState(GESTURE_FIRST_SINGLE_TOUCH_MAX_TAP_DOWN);
+ } else if (mState == GESTURE_FIRST_SINGLE_TOUCH_UP ||
+ mState == GESTURE_SECOND_SINGLE_TOUCH_DOWN) {
+ MOZ_ASSERT(mSingleTapSent.isSome());
+ if (!aDuringFastFling && !mSingleTapSent.value()) {
+ TriggerSingleTapConfirmedEvent();
+ }
+ mSingleTapSent = Nothing();
+ SetState(GESTURE_NONE);
+ } else {
+ NS_WARNING("Unhandled state upon MAX_TAP timeout");
+ SetState(GESTURE_NONE);
+ }
+}
+
+void GestureEventListener::TriggerSingleTapConfirmedEvent()
+{
+ mAsyncPanZoomController->HandleGestureEvent(
+ CreateTapEvent(mLastTapInput, TapGestureInput::TAPGESTURE_CONFIRMED));
+}
+
+void GestureEventListener::SetState(GestureState aState)
+{
+ mState = aState;
+
+ if (mState == GESTURE_NONE) {
+ mSpanChange = 0.0f;
+ mPreviousSpan = 0.0f;
+ mFocusChange = 0.0f;
+ } else if (mState == GESTURE_MULTI_TOUCH_DOWN) {
+ mPreviousSpan = GetCurrentSpan(mLastTouchInput);
+ mPreviousFocus = GetCurrentFocus(mLastTouchInput);
+ }
+}
+
+void GestureEventListener::CancelLongTapTimeoutTask()
+{
+ if (mState == GESTURE_SECOND_SINGLE_TOUCH_DOWN) {
+ // being in this state means the task has been canceled already
+ return;
+ }
+
+ if (mLongTapTimeoutTask) {
+ mLongTapTimeoutTask->Cancel();
+ mLongTapTimeoutTask = nullptr;
+ }
+}
+
+void GestureEventListener::CreateLongTapTimeoutTask()
+{
+ RefPtr<CancelableRunnable> task =
+ NewCancelableRunnableMethod(this, &GestureEventListener::HandleInputTimeoutLongTap);
+
+ mLongTapTimeoutTask = task;
+ mAsyncPanZoomController->PostDelayedTask(
+ task.forget(),
+ gfxPrefs::UiClickHoldContextMenusDelay());
+}
+
+void GestureEventListener::CancelMaxTapTimeoutTask()
+{
+ if (mState == GESTURE_FIRST_SINGLE_TOUCH_MAX_TAP_DOWN) {
+ // being in this state means the timer has just been triggered
+ return;
+ }
+
+ if (mMaxTapTimeoutTask) {
+ mMaxTapTimeoutTask->Cancel();
+ mMaxTapTimeoutTask = nullptr;
+ }
+}
+
+void GestureEventListener::CreateMaxTapTimeoutTask()
+{
+ mLastTapInput = mLastTouchInput;
+
+ TouchBlockState* block = mAsyncPanZoomController->GetInputQueue()->GetCurrentTouchBlock();
+ MOZ_ASSERT(block);
+ RefPtr<CancelableRunnable> task =
+ NewCancelableRunnableMethod<bool>(this,
+ &GestureEventListener::HandleInputTimeoutMaxTap,
+ block->IsDuringFastFling());
+
+ mMaxTapTimeoutTask = task;
+ mAsyncPanZoomController->PostDelayedTask(
+ task.forget(),
+ MAX_TAP_TIME);
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/apz/src/GestureEventListener.h b/gfx/layers/apz/src/GestureEventListener.h
new file mode 100644
index 000000000..d025ed0d1
--- /dev/null
+++ b/gfx/layers/apz/src/GestureEventListener.h
@@ -0,0 +1,252 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set sw=2 ts=8 et tw=80 : */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_layers_GestureEventListener_h
+#define mozilla_layers_GestureEventListener_h
+
+#include "InputData.h" // for MultiTouchInput, etc
+#include "Units.h"
+#include "mozilla/EventForwards.h" // for nsEventStatus
+#include "mozilla/RefPtr.h" // for RefPtr
+#include "nsISupportsImpl.h"
+#include "nsTArray.h" // for nsTArray
+
+namespace mozilla {
+
+class CancelableRunnable;
+
+namespace layers {
+
+class AsyncPanZoomController;
+
+/**
+ * Platform-non-specific, generalized gesture event listener. This class
+ * intercepts all touches events on their way to AsyncPanZoomController and
+ * determines whether or not they are part of a gesture.
+ *
+ * For example, seeing that two fingers are on the screen means that the user
+ * wants to do a pinch gesture, so we don't forward the touches along to
+ * AsyncPanZoomController since it will think that they are just trying to pan
+ * the screen. Instead, we generate a PinchGestureInput and send that. If the
+ * touch event is not part of a gesture, we just return nsEventStatus_eIgnore
+ * and AsyncPanZoomController is expected to handle it.
+ *
+ * Android doesn't use this class because it has its own built-in gesture event
+ * listeners that should generally be preferred.
+ */
+class GestureEventListener final {
+public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(GestureEventListener)
+
+ explicit GestureEventListener(AsyncPanZoomController* aAsyncPanZoomController);
+
+ // --------------------------------------------------------------------------
+ // These methods must only be called on the controller/UI thread.
+ //
+
+ /**
+ * General input handler for a touch event. If the touch event is not a part
+ * of a gesture, then we pass it along to AsyncPanZoomController. Otherwise,
+ * it gets consumed here and never forwarded along.
+ */
+ nsEventStatus HandleInputEvent(const MultiTouchInput& aEvent);
+
+ /**
+ * Returns the identifier of the touch in the last touch event processed by
+ * this GestureEventListener. This should only be called when the last touch
+ * event contained only one touch.
+ */
+ int32_t GetLastTouchIdentifier() const;
+
+ /**
+ * Function used to disable long tap gestures.
+ *
+ * On slow running tests, drags and touch events can be misinterpreted
+ * as a long tap. This allows tests to disable long tap gesture detection.
+ */
+ static void SetLongTapEnabled(bool aLongTapEnabled);
+
+private:
+ // Private destructor, to discourage deletion outside of Release():
+ ~GestureEventListener();
+
+ /**
+ * States of GEL finite-state machine.
+ */
+ enum GestureState {
+ // This is the initial and final state of any gesture.
+ // In this state there's no gesture going on, and we don't think we're
+ // about to enter one.
+ // Allowed next states: GESTURE_FIRST_SINGLE_TOUCH_DOWN, GESTURE_MULTI_TOUCH_DOWN.
+ GESTURE_NONE,
+
+ // A touch start with a single touch point has just happened.
+ // After having gotten into this state we start timers for MAX_TAP_TIME and
+ // gfxPrefs::UiClickHoldContextMenusDelay().
+ // Allowed next states: GESTURE_MULTI_TOUCH_DOWN, GESTURE_NONE,
+ // GESTURE_FIRST_SINGLE_TOUCH_UP, GESTURE_LONG_TOUCH_DOWN,
+ // GESTURE_FIRST_SINGLE_TOUCH_MAX_TAP_DOWN.
+ GESTURE_FIRST_SINGLE_TOUCH_DOWN,
+
+ // While in GESTURE_FIRST_SINGLE_TOUCH_DOWN state a MAX_TAP_TIME timer got
+ // triggered. Now we'll trigger either a single tap if a user lifts her
+ // finger or a long tap if gfxPrefs::UiClickHoldContextMenusDelay() happens
+ // first.
+ // Allowed next states: GESTURE_MULTI_TOUCH_DOWN, GESTURE_NONE,
+ // GESTURE_LONG_TOUCH_DOWN.
+ GESTURE_FIRST_SINGLE_TOUCH_MAX_TAP_DOWN,
+
+ // A user put her finger down and lifted it up quickly enough.
+ // After having gotten into this state we clear the timer for MAX_TAP_TIME.
+ // Allowed next states: GESTURE_SECOND_SINGLE_TOUCH_DOWN, GESTURE_NONE,
+ // GESTURE_MULTI_TOUCH_DOWN.
+ GESTURE_FIRST_SINGLE_TOUCH_UP,
+
+ // A user put down her finger again right after a single tap thus the
+ // gesture can't be a single tap, but rather a double tap. But we're
+ // still not sure about that until the user lifts her finger again.
+ // Allowed next states: GESTURE_MULTI_TOUCH_DOWN, GESTURE_NONE.
+ GESTURE_SECOND_SINGLE_TOUCH_DOWN,
+
+ // A long touch has happened, but the user still keeps her finger down.
+ // We'll trigger a "long tap up" event when the finger is up.
+ // Allowed next states: GESTURE_NONE, GESTURE_MULTI_TOUCH_DOWN.
+ GESTURE_LONG_TOUCH_DOWN,
+
+ // We have detected that two or more fingers are on the screen, but there
+ // hasn't been enough movement yet to make us start actually zooming the
+ // screen.
+ // Allowed next states: GESTURE_PINCH, GESTURE_NONE
+ GESTURE_MULTI_TOUCH_DOWN,
+
+ // There are two or more fingers on the screen, and the user has already
+ // pinched enough for us to start zooming the screen.
+ // Allowed next states: GESTURE_NONE
+ GESTURE_PINCH
+ };
+
+ /**
+ * These HandleInput* functions comprise input alphabet of the GEL
+ * finite-state machine triggering state transitions.
+ */
+ nsEventStatus HandleInputTouchSingleStart();
+ nsEventStatus HandleInputTouchMultiStart();
+ nsEventStatus HandleInputTouchEnd();
+ nsEventStatus HandleInputTouchMove();
+ nsEventStatus HandleInputTouchCancel();
+ void HandleInputTimeoutLongTap();
+ void HandleInputTimeoutMaxTap(bool aDuringFastFling);
+
+ void TriggerSingleTapConfirmedEvent();
+
+ bool MoveDistanceIsLarge();
+
+ /**
+ * Do actual state transition and reset substates.
+ */
+ void SetState(GestureState aState);
+
+ RefPtr<AsyncPanZoomController> mAsyncPanZoomController;
+
+ /**
+ * Array containing all active touches. When a touch happens it, gets added to
+ * this array, even if we choose not to handle it. When it ends, we remove it.
+ * We need to maintain this array in order to detect the end of the
+ * "multitouch" states because touch start events contain all current touches,
+ * but touch end events contain only those touches that have gone.
+ */
+ nsTArray<SingleTouchData> mTouches;
+
+ /**
+ * Current state we're dealing with.
+ */
+ GestureState mState;
+
+ /**
+ * Total change in span since we detected a pinch gesture. Only used when we
+ * are in the |GESTURE_WAITING_PINCH| state and need to know how far zoomed
+ * out we are compared to our original pinch span. Note that this does _not_
+ * continue to be updated once we jump into the |GESTURE_PINCH| state.
+ */
+ ParentLayerCoord mSpanChange;
+
+ /**
+ * Previous span calculated for the purposes of setting inside a
+ * PinchGestureInput.
+ */
+ ParentLayerCoord mPreviousSpan;
+
+ /* Properties similar to mSpanChange and mPreviousSpan, but for the focus */
+ ParentLayerCoord mFocusChange;
+ ParentLayerPoint mPreviousFocus;
+
+ /**
+ * Cached copy of the last touch input.
+ */
+ MultiTouchInput mLastTouchInput;
+
+ /**
+ * Cached copy of the last tap gesture input.
+ * In the situation when we have a tap followed by a pinch we lose info
+ * about tap since we keep only last input and to dispatch it correctly
+ * we save last tap copy into this variable.
+ * For more info see bug 947892.
+ */
+ MultiTouchInput mLastTapInput;
+
+ /**
+ * Position of the last touch starting. This is only valid during an attempt
+ * to determine if a touch is a tap. If a touch point moves away from
+ * mTouchStartPosition to the distance greater than
+ * AsyncPanZoomController::GetTouchStartTolerance() while in
+ * GESTURE_FIRST_SINGLE_TOUCH_DOWN, GESTURE_FIRST_SINGLE_TOUCH_MAX_TAP_DOWN
+ * or GESTURE_SECOND_SINGLE_TOUCH_DOWN then we're certain the gesture is
+ * not tap.
+ */
+ ParentLayerPoint mTouchStartPosition;
+
+ /**
+ * Task used to timeout a long tap. This gets posted to the UI thread such
+ * that it runs a time when a single tap happens. We cache it so that
+ * we can cancel it if any other touch event happens.
+ *
+ * The task is supposed to be non-null if in GESTURE_FIRST_SINGLE_TOUCH_DOWN
+ * and GESTURE_FIRST_SINGLE_TOUCH_MAX_TAP_DOWN states.
+ *
+ * CancelLongTapTimeoutTask: Cancel the mLongTapTimeoutTask and also set
+ * it to null.
+ */
+ RefPtr<CancelableRunnable> mLongTapTimeoutTask;
+ void CancelLongTapTimeoutTask();
+ void CreateLongTapTimeoutTask();
+
+ /**
+ * Task used to timeout a single tap or a double tap.
+ *
+ * The task is supposed to be non-null if in GESTURE_FIRST_SINGLE_TOUCH_DOWN,
+ * GESTURE_FIRST_SINGLE_TOUCH_UP and GESTURE_SECOND_SINGLE_TOUCH_DOWN states.
+ *
+ * CancelMaxTapTimeoutTask: Cancel the mMaxTapTimeoutTask and also set
+ * it to null.
+ */
+ RefPtr<CancelableRunnable> mMaxTapTimeoutTask;
+ void CancelMaxTapTimeoutTask();
+ void CreateMaxTapTimeoutTask();
+
+ /**
+ * Tracks whether the single-tap event was already sent to content. This is
+ * needed because it affects how the double-tap gesture, if detected, is
+ * handled. The value is only valid in states GESTURE_FIRST_SINGLE_TOUCH_UP and
+ * GESTURE_SECOND_SINGLE_TOUCH_DOWN; to more easily catch violations it is
+ * stored in a Maybe which is set to Nothing() at all other times.
+ */
+ Maybe<bool> mSingleTapSent;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif
diff --git a/gfx/layers/apz/src/HitTestingTreeNode.cpp b/gfx/layers/apz/src/HitTestingTreeNode.cpp
new file mode 100644
index 000000000..acedcde5d
--- /dev/null
+++ b/gfx/layers/apz/src/HitTestingTreeNode.cpp
@@ -0,0 +1,336 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set sw=2 ts=8 et tw=80 : */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "HitTestingTreeNode.h"
+
+#include "AsyncPanZoomController.h" // for AsyncPanZoomController
+#include "LayersLogging.h" // for Stringify
+#include "mozilla/gfx/Point.h" // for Point4D
+#include "mozilla/layers/APZThreadUtils.h" // for AssertOnCompositorThread
+#include "mozilla/layers/APZUtils.h" // for CompleteAsyncTransform
+#include "mozilla/layers/AsyncCompositionManager.h" // for ViewTransform::operator Matrix4x4()
+#include "mozilla/layers/AsyncDragMetrics.h" // for AsyncDragMetrics
+#include "nsPrintfCString.h" // for nsPrintfCString
+#include "UnitTransforms.h" // for ViewAs
+
+namespace mozilla {
+namespace layers {
+
+HitTestingTreeNode::HitTestingTreeNode(AsyncPanZoomController* aApzc,
+ bool aIsPrimaryHolder,
+ uint64_t aLayersId)
+ : mApzc(aApzc)
+ , mIsPrimaryApzcHolder(aIsPrimaryHolder)
+ , mLayersId(aLayersId)
+ , mScrollViewId(FrameMetrics::NULL_SCROLL_ID)
+ , mScrollDir(Layer::NONE)
+ , mScrollSize(0)
+ , mIsScrollbarContainer(false)
+ , mFixedPosTarget(FrameMetrics::NULL_SCROLL_ID)
+ , mOverride(EventRegionsOverride::NoOverride)
+{
+if (mIsPrimaryApzcHolder) {
+ MOZ_ASSERT(mApzc);
+ }
+ MOZ_ASSERT(!mApzc || mApzc->GetLayersId() == mLayersId);
+}
+
+void
+HitTestingTreeNode::RecycleWith(AsyncPanZoomController* aApzc,
+ uint64_t aLayersId)
+{
+ MOZ_ASSERT(!mIsPrimaryApzcHolder);
+ Destroy(); // clear out tree pointers
+ mApzc = aApzc;
+ mLayersId = aLayersId;
+ MOZ_ASSERT(!mApzc || mApzc->GetLayersId() == mLayersId);
+ // The caller is expected to call SetHitTestData to repopulate the hit-test
+ // fields.
+}
+
+HitTestingTreeNode::~HitTestingTreeNode()
+{
+}
+
+void
+HitTestingTreeNode::Destroy()
+{
+ APZThreadUtils::AssertOnCompositorThread();
+
+ mPrevSibling = nullptr;
+ mLastChild = nullptr;
+ mParent = nullptr;
+
+ if (mApzc) {
+ if (mIsPrimaryApzcHolder) {
+ mApzc->Destroy();
+ }
+ mApzc = nullptr;
+ }
+
+ mLayersId = 0;
+}
+
+void
+HitTestingTreeNode::SetLastChild(HitTestingTreeNode* aChild)
+{
+ mLastChild = aChild;
+ if (aChild) {
+ aChild->mParent = this;
+
+ if (aChild->GetApzc()) {
+ AsyncPanZoomController* parent = GetNearestContainingApzc();
+ // We assume that HitTestingTreeNodes with an ancestor/descendant
+ // relationship cannot both point to the same APZC instance. This
+ // assertion only covers a subset of cases in which that might occur,
+ // but it's better than nothing.
+ MOZ_ASSERT(aChild->GetApzc() != parent);
+ aChild->SetApzcParent(parent);
+ }
+ }
+}
+
+void
+HitTestingTreeNode::SetScrollbarData(FrameMetrics::ViewID aScrollViewId,
+ Layer::ScrollDirection aDir,
+ int32_t aScrollSize,
+ bool aIsScrollContainer)
+{
+ mScrollViewId = aScrollViewId;
+ mScrollDir = aDir;
+ mScrollSize = aScrollSize;;
+ mIsScrollbarContainer = aIsScrollContainer;
+}
+
+bool
+HitTestingTreeNode::MatchesScrollDragMetrics(const AsyncDragMetrics& aDragMetrics) const
+{
+ return ((mScrollDir == Layer::HORIZONTAL &&
+ aDragMetrics.mDirection == AsyncDragMetrics::HORIZONTAL) ||
+ (mScrollDir == Layer::VERTICAL &&
+ aDragMetrics.mDirection == AsyncDragMetrics::VERTICAL)) &&
+ mScrollViewId == aDragMetrics.mViewId;
+}
+
+int32_t
+HitTestingTreeNode::GetScrollSize() const
+{
+ return mScrollSize;
+}
+
+bool
+HitTestingTreeNode::IsScrollbarNode() const
+{
+ return mIsScrollbarContainer || (mScrollDir != Layer::NONE);
+}
+
+void
+HitTestingTreeNode::SetFixedPosData(FrameMetrics::ViewID aFixedPosTarget)
+{
+ mFixedPosTarget = aFixedPosTarget;
+}
+
+FrameMetrics::ViewID
+HitTestingTreeNode::GetFixedPosTarget() const
+{
+ return mFixedPosTarget;
+}
+
+void
+HitTestingTreeNode::SetPrevSibling(HitTestingTreeNode* aSibling)
+{
+ mPrevSibling = aSibling;
+ if (aSibling) {
+ aSibling->mParent = mParent;
+
+ if (aSibling->GetApzc()) {
+ AsyncPanZoomController* parent = mParent ? mParent->GetNearestContainingApzc() : nullptr;
+ aSibling->SetApzcParent(parent);
+ }
+ }
+}
+
+void
+HitTestingTreeNode::MakeRoot()
+{
+ mParent = nullptr;
+
+ if (GetApzc()) {
+ SetApzcParent(nullptr);
+ }
+}
+
+HitTestingTreeNode*
+HitTestingTreeNode::GetFirstChild() const
+{
+ HitTestingTreeNode* child = GetLastChild();
+ while (child && child->GetPrevSibling()) {
+ child = child->GetPrevSibling();
+ }
+ return child;
+}
+
+HitTestingTreeNode*
+HitTestingTreeNode::GetLastChild() const
+{
+ return mLastChild;
+}
+
+HitTestingTreeNode*
+HitTestingTreeNode::GetPrevSibling() const
+{
+ return mPrevSibling;
+}
+
+HitTestingTreeNode*
+HitTestingTreeNode::GetParent() const
+{
+ return mParent;
+}
+
+AsyncPanZoomController*
+HitTestingTreeNode::GetApzc() const
+{
+ return mApzc;
+}
+
+AsyncPanZoomController*
+HitTestingTreeNode::GetNearestContainingApzc() const
+{
+ for (const HitTestingTreeNode* n = this; n; n = n->GetParent()) {
+ if (n->GetApzc()) {
+ return n->GetApzc();
+ }
+ }
+ return nullptr;
+}
+
+bool
+HitTestingTreeNode::IsPrimaryHolder() const
+{
+ return mIsPrimaryApzcHolder;
+}
+
+uint64_t
+HitTestingTreeNode::GetLayersId() const
+{
+ return mLayersId;
+}
+
+void
+HitTestingTreeNode::SetHitTestData(const EventRegions& aRegions,
+ const CSSTransformMatrix& aTransform,
+ const Maybe<ParentLayerIntRegion>& aClipRegion,
+ const EventRegionsOverride& aOverride)
+{
+ mEventRegions = aRegions;
+ mTransform = aTransform;
+ mClipRegion = aClipRegion;
+ mOverride = aOverride;
+}
+
+bool
+HitTestingTreeNode::IsOutsideClip(const ParentLayerPoint& aPoint) const
+{
+ // test against clip rect in ParentLayer coordinate space
+ return (mClipRegion.isSome() && !mClipRegion->Contains(aPoint.x, aPoint.y));
+}
+
+Maybe<LayerPoint>
+HitTestingTreeNode::Untransform(const ParentLayerPoint& aPoint) const
+{
+ // convert into Layer coordinate space
+ LayerToParentLayerMatrix4x4 transform = mTransform *
+ CompleteAsyncTransform(
+ mApzc
+ ? mApzc->GetCurrentAsyncTransformWithOverscroll(AsyncPanZoomController::NORMAL)
+ : AsyncTransformComponentMatrix());
+ return UntransformBy(transform.Inverse(), aPoint);
+}
+
+HitTestResult
+HitTestingTreeNode::HitTest(const ParentLayerPoint& aPoint) const
+{
+ // This should only ever get called if the point is inside the clip region
+ // for this node.
+ MOZ_ASSERT(!IsOutsideClip(aPoint));
+
+ if (mOverride & EventRegionsOverride::ForceEmptyHitRegion) {
+ return HitTestResult::HitNothing;
+ }
+
+ // convert into Layer coordinate space
+ Maybe<LayerPoint> pointInLayerPixels = Untransform(aPoint);
+ if (!pointInLayerPixels) {
+ return HitTestResult::HitNothing;
+ }
+ auto point = LayerIntPoint::Round(pointInLayerPixels.ref());
+
+ // test against event regions in Layer coordinate space
+ if (!mEventRegions.mHitRegion.Contains(point.x, point.y)) {
+ return HitTestResult::HitNothing;
+ }
+ if ((mOverride & EventRegionsOverride::ForceDispatchToContent) ||
+ mEventRegions.mDispatchToContentHitRegion.Contains(point.x, point.y))
+ {
+ return HitTestResult::HitDispatchToContentRegion;
+ }
+ if (gfxPrefs::TouchActionEnabled()) {
+ if (mEventRegions.mNoActionRegion.Contains(point.x, point.y)) {
+ return HitTestResult::HitLayerTouchActionNone;
+ }
+ bool panX = mEventRegions.mHorizontalPanRegion.Contains(point.x, point.y);
+ bool panY = mEventRegions.mVerticalPanRegion.Contains(point.x, point.y);
+ if (panX && panY) {
+ return HitTestResult::HitLayerTouchActionPanXY;
+ } else if (panX) {
+ return HitTestResult::HitLayerTouchActionPanX;
+ } else if (panY) {
+ return HitTestResult::HitLayerTouchActionPanY;
+ }
+ }
+ return HitTestResult::HitLayer;
+}
+
+EventRegionsOverride
+HitTestingTreeNode::GetEventRegionsOverride() const
+{
+ return mOverride;
+}
+
+void
+HitTestingTreeNode::Dump(const char* aPrefix) const
+{
+ if (mPrevSibling) {
+ mPrevSibling->Dump(aPrefix);
+ }
+ printf_stderr("%sHitTestingTreeNode (%p) APZC (%p) g=(%s) %s%s%sr=(%s) t=(%s) c=(%s)\n",
+ aPrefix, this, mApzc.get(),
+ mApzc ? Stringify(mApzc->GetGuid()).c_str() : nsPrintfCString("l=%" PRIu64, mLayersId).get(),
+ (mOverride & EventRegionsOverride::ForceDispatchToContent) ? "fdtc " : "",
+ (mOverride & EventRegionsOverride::ForceEmptyHitRegion) ? "fehr " : "",
+ (mFixedPosTarget != FrameMetrics::NULL_SCROLL_ID) ? nsPrintfCString("fixed=%" PRIu64 " ", mFixedPosTarget).get() : "",
+ Stringify(mEventRegions).c_str(), Stringify(mTransform).c_str(),
+ mClipRegion ? Stringify(mClipRegion.ref()).c_str() : "none");
+ if (mLastChild) {
+ mLastChild->Dump(nsPrintfCString("%s ", aPrefix).get());
+ }
+}
+
+void
+HitTestingTreeNode::SetApzcParent(AsyncPanZoomController* aParent)
+{
+ // precondition: GetApzc() is non-null
+ MOZ_ASSERT(GetApzc() != nullptr);
+ if (IsPrimaryHolder()) {
+ GetApzc()->SetParent(aParent);
+ } else {
+ MOZ_ASSERT(GetApzc()->GetParent() == aParent);
+ }
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/apz/src/HitTestingTreeNode.h b/gfx/layers/apz/src/HitTestingTreeNode.h
new file mode 100644
index 000000000..442751a8d
--- /dev/null
+++ b/gfx/layers/apz/src/HitTestingTreeNode.h
@@ -0,0 +1,166 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set sw=2 ts=8 et tw=80 : */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_layers_HitTestingTreeNode_h
+#define mozilla_layers_HitTestingTreeNode_h
+
+#include "APZUtils.h" // for HitTestResult
+#include "FrameMetrics.h" // for ScrollableLayerGuid
+#include "Layers.h"
+#include "mozilla/gfx/Matrix.h" // for Matrix4x4
+#include "mozilla/layers/LayersTypes.h" // for EventRegions
+#include "mozilla/Maybe.h" // for Maybe
+#include "mozilla/RefPtr.h" // for nsRefPtr
+
+namespace mozilla {
+namespace layers {
+
+class AsyncDragMetrics;
+class AsyncPanZoomController;
+
+/**
+ * This class represents a node in a tree that is used by the APZCTreeManager
+ * to do hit testing. The tree is roughly a copy of the layer tree, but will
+ * contain multiple nodes in cases where the layer has multiple FrameMetrics.
+ * In other words, the structure of this tree should be identical to the
+ * LayerMetrics tree (see documentation in LayerMetricsWrapper.h).
+ *
+ * Not all HitTestingTreeNode instances will have an APZC associated with them;
+ * only HitTestingTreeNodes that correspond to layers with scrollable metrics
+ * have APZCs.
+ * Multiple HitTestingTreeNode instances may share the same underlying APZC
+ * instance if the layers they represent share the same scrollable metrics (i.e.
+ * are part of the same animated geometry root). If this happens, exactly one of
+ * the HitTestingTreeNode instances will be designated as the "primary holder"
+ * of the APZC. When this primary holder is destroyed, it will destroy the APZC
+ * along with it; in contrast, destroying non-primary-holder nodes will not
+ * destroy the APZC.
+ * Code should not make assumptions about which of the nodes will be the
+ * primary holder, only that that there will be exactly one for each APZC in
+ * the tree.
+ *
+ * The reason this tree exists at all is so that we can do hit-testing on the
+ * thread that we receive input on (referred to the as the controller thread in
+ * APZ terminology), which may be different from the compositor thread.
+ * Accessing the compositor layer tree can only be done on the compositor
+ * thread, and so it is simpler to make a copy of the hit-testing related
+ * properties into a separate tree.
+ */
+class HitTestingTreeNode {
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(HitTestingTreeNode);
+
+private:
+ ~HitTestingTreeNode();
+public:
+ HitTestingTreeNode(AsyncPanZoomController* aApzc, bool aIsPrimaryHolder,
+ uint64_t aLayersId);
+ void RecycleWith(AsyncPanZoomController* aApzc, uint64_t aLayersId);
+ void Destroy();
+
+ /* Tree construction methods */
+
+ void SetLastChild(HitTestingTreeNode* aChild);
+ void SetPrevSibling(HitTestingTreeNode* aSibling);
+ void MakeRoot();
+
+ /* Tree walking methods. GetFirstChild is O(n) in the number of children. The
+ * other tree walking methods are all O(1). */
+
+ HitTestingTreeNode* GetFirstChild() const;
+ HitTestingTreeNode* GetLastChild() const;
+ HitTestingTreeNode* GetPrevSibling() const;
+ HitTestingTreeNode* GetParent() const;
+
+ /* APZC related methods */
+
+ AsyncPanZoomController* GetApzc() const;
+ AsyncPanZoomController* GetNearestContainingApzc() const;
+ bool IsPrimaryHolder() const;
+ uint64_t GetLayersId() const;
+
+ /* Hit test related methods */
+
+ void SetHitTestData(const EventRegions& aRegions,
+ const CSSTransformMatrix& aTransform,
+ const Maybe<ParentLayerIntRegion>& aClipRegion,
+ const EventRegionsOverride& aOverride);
+ bool IsOutsideClip(const ParentLayerPoint& aPoint) const;
+
+ /* Scrollbar info */
+
+ void SetScrollbarData(FrameMetrics::ViewID aScrollViewId,
+ Layer::ScrollDirection aDir,
+ int32_t aScrollSize,
+ bool aIsScrollContainer);
+ bool MatchesScrollDragMetrics(const AsyncDragMetrics& aDragMetrics) const;
+ int32_t GetScrollSize() const;
+ bool IsScrollbarNode() const;
+
+ /* Fixed pos info */
+
+ void SetFixedPosData(FrameMetrics::ViewID aFixedPosTarget);
+ FrameMetrics::ViewID GetFixedPosTarget() const;
+
+ /* Convert aPoint into the LayerPixel space for the layer corresponding to
+ * this node. */
+ Maybe<LayerPoint> Untransform(const ParentLayerPoint& aPoint) const;
+ /* Assuming aPoint is inside the clip region for this node, check which of the
+ * event region spaces it falls inside. */
+ HitTestResult HitTest(const ParentLayerPoint& aPoint) const;
+ /* Returns the mOverride flag. */
+ EventRegionsOverride GetEventRegionsOverride() const;
+
+ /* Debug helpers */
+ void Dump(const char* aPrefix = "") const;
+
+private:
+ void SetApzcParent(AsyncPanZoomController* aApzc);
+
+ RefPtr<HitTestingTreeNode> mLastChild;
+ RefPtr<HitTestingTreeNode> mPrevSibling;
+ RefPtr<HitTestingTreeNode> mParent;
+
+ RefPtr<AsyncPanZoomController> mApzc;
+ bool mIsPrimaryApzcHolder;
+
+ uint64_t mLayersId;
+
+ FrameMetrics::ViewID mScrollViewId;
+ Layer::ScrollDirection mScrollDir;
+ int32_t mScrollSize;
+ bool mIsScrollbarContainer;
+
+ FrameMetrics::ViewID mFixedPosTarget;
+
+ /* Let {L,M} be the {layer, scrollable metrics} pair that this node
+ * corresponds to in the layer tree. mEventRegions contains the event regions
+ * from L, in the case where event-regions are enabled. If event-regions are
+ * disabled, it will contain the visible region of L, which we use as an
+ * approximation to the hit region for the purposes of obscuring other layers.
+ * This value is in L's LayerPixels.
+ */
+ EventRegions mEventRegions;
+
+ /* This is the transform from layer L. This does NOT include any async
+ * transforms. */
+ CSSTransformMatrix mTransform;
+
+ /* This is clip rect for L that we wish to use for hit-testing purposes. Note
+ * that this may not be exactly the same as the clip rect on layer L because
+ * of the touch-sensitive region provided by the GeckoContentController, or
+ * because we may use the composition bounds of the layer if the clip is not
+ * present. This value is in L's ParentLayerPixels. */
+ Maybe<ParentLayerIntRegion> mClipRegion;
+
+ /* Indicates whether or not the event regions on this node need to be
+ * overridden in a certain way. */
+ EventRegionsOverride mOverride;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif // mozilla_layers_HitTestingTreeNode_h
diff --git a/gfx/layers/apz/src/InputBlockState.cpp b/gfx/layers/apz/src/InputBlockState.cpp
new file mode 100644
index 000000000..f1310c031
--- /dev/null
+++ b/gfx/layers/apz/src/InputBlockState.cpp
@@ -0,0 +1,868 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set sw=2 ts=8 et tw=80 : */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "InputBlockState.h"
+#include "AsyncPanZoomController.h" // for AsyncPanZoomController
+#include "AsyncScrollBase.h" // for kScrollSeriesTimeoutMs
+#include "gfxPrefs.h" // for gfxPrefs
+#include "mozilla/MouseEvents.h"
+#include "mozilla/SizePrintfMacros.h" // for PRIuSIZE
+#include "mozilla/Telemetry.h" // for Telemetry
+#include "mozilla/layers/APZCTreeManager.h" // for AllowedTouchBehavior
+#include "OverscrollHandoffState.h"
+#include "QueuedInput.h"
+
+#define TBS_LOG(...)
+// #define TBS_LOG(...) printf_stderr("TBS: " __VA_ARGS__)
+
+namespace mozilla {
+namespace layers {
+
+static uint64_t sBlockCounter = InputBlockState::NO_BLOCK_ID + 1;
+
+InputBlockState::InputBlockState(const RefPtr<AsyncPanZoomController>& aTargetApzc,
+ bool aTargetConfirmed)
+ : mTargetApzc(aTargetApzc)
+ , mTargetConfirmed(aTargetConfirmed ? TargetConfirmationState::eConfirmed
+ : TargetConfirmationState::eUnconfirmed)
+ , mBlockId(sBlockCounter++)
+ , mTransformToApzc(aTargetApzc->GetTransformToThis())
+{
+ // We should never be constructed with a nullptr target.
+ MOZ_ASSERT(mTargetApzc);
+ mOverscrollHandoffChain = mTargetApzc->BuildOverscrollHandoffChain();
+}
+
+bool
+InputBlockState::SetConfirmedTargetApzc(const RefPtr<AsyncPanZoomController>& aTargetApzc,
+ TargetConfirmationState aState,
+ InputData* aFirstInput)
+{
+ MOZ_ASSERT(aState == TargetConfirmationState::eConfirmed
+ || aState == TargetConfirmationState::eTimedOut);
+
+ if (mTargetConfirmed == TargetConfirmationState::eTimedOut &&
+ aState == TargetConfirmationState::eConfirmed) {
+ // The main thread finally responded. We had already timed out the
+ // confirmation, but we want to update the state internally so that we
+ // can record the time for telemetry purposes.
+ mTargetConfirmed = TargetConfirmationState::eTimedOutAndMainThreadResponded;
+ }
+ if (mTargetConfirmed != TargetConfirmationState::eUnconfirmed) {
+ return false;
+ }
+ mTargetConfirmed = aState;
+
+ TBS_LOG("%p got confirmed target APZC %p\n", this, mTargetApzc.get());
+ if (mTargetApzc == aTargetApzc) {
+ // The confirmed target is the same as the tentative one, so we're done.
+ return true;
+ }
+
+ TBS_LOG("%p replacing unconfirmed target %p with real target %p\n",
+ this, mTargetApzc.get(), aTargetApzc.get());
+
+ UpdateTargetApzc(aTargetApzc);
+ return true;
+}
+
+void
+InputBlockState::UpdateTargetApzc(const RefPtr<AsyncPanZoomController>& aTargetApzc)
+{
+ // note that aTargetApzc MAY be null here.
+ mTargetApzc = aTargetApzc;
+ mTransformToApzc = aTargetApzc ? aTargetApzc->GetTransformToThis() : ScreenToParentLayerMatrix4x4();
+ mOverscrollHandoffChain = (mTargetApzc ? mTargetApzc->BuildOverscrollHandoffChain() : nullptr);
+}
+
+const RefPtr<AsyncPanZoomController>&
+InputBlockState::GetTargetApzc() const
+{
+ return mTargetApzc;
+}
+
+const RefPtr<const OverscrollHandoffChain>&
+InputBlockState::GetOverscrollHandoffChain() const
+{
+ return mOverscrollHandoffChain;
+}
+
+uint64_t
+InputBlockState::GetBlockId() const
+{
+ return mBlockId;
+}
+
+bool
+InputBlockState::IsTargetConfirmed() const
+{
+ return mTargetConfirmed != TargetConfirmationState::eUnconfirmed;
+}
+
+bool
+InputBlockState::HasReceivedRealConfirmedTarget() const
+{
+ return mTargetConfirmed == TargetConfirmationState::eConfirmed ||
+ mTargetConfirmed == TargetConfirmationState::eTimedOutAndMainThreadResponded;
+}
+
+bool
+InputBlockState::IsDownchainOf(AsyncPanZoomController* aA, AsyncPanZoomController* aB) const
+{
+ if (aA == aB) {
+ return true;
+ }
+
+ bool seenA = false;
+ for (size_t i = 0; i < mOverscrollHandoffChain->Length(); ++i) {
+ AsyncPanZoomController* apzc = mOverscrollHandoffChain->GetApzcAtIndex(i);
+ if (apzc == aB) {
+ return seenA;
+ }
+ if (apzc == aA) {
+ seenA = true;
+ }
+ }
+ return false;
+}
+
+
+void
+InputBlockState::SetScrolledApzc(AsyncPanZoomController* aApzc)
+{
+ // An input block should only have one scrolled APZC.
+ MOZ_ASSERT(!mScrolledApzc || (gfxPrefs::APZAllowImmediateHandoff() ? IsDownchainOf(mScrolledApzc, aApzc) : mScrolledApzc == aApzc));
+
+ mScrolledApzc = aApzc;
+}
+
+AsyncPanZoomController*
+InputBlockState::GetScrolledApzc() const
+{
+ return mScrolledApzc;
+}
+
+bool
+InputBlockState::IsDownchainOfScrolledApzc(AsyncPanZoomController* aApzc) const
+{
+ MOZ_ASSERT(aApzc && mScrolledApzc);
+
+ return IsDownchainOf(mScrolledApzc, aApzc);
+}
+
+CancelableBlockState::CancelableBlockState(const RefPtr<AsyncPanZoomController>& aTargetApzc,
+ bool aTargetConfirmed)
+ : InputBlockState(aTargetApzc, aTargetConfirmed)
+ , mPreventDefault(false)
+ , mContentResponded(false)
+ , mContentResponseTimerExpired(false)
+{
+}
+
+bool
+CancelableBlockState::SetContentResponse(bool aPreventDefault)
+{
+ if (mContentResponded) {
+ return false;
+ }
+ TBS_LOG("%p got content response %d with timer expired %d\n",
+ this, aPreventDefault, mContentResponseTimerExpired);
+ mPreventDefault = aPreventDefault;
+ mContentResponded = true;
+ return true;
+}
+
+void
+CancelableBlockState::StartContentResponseTimer()
+{
+ MOZ_ASSERT(mContentResponseTimer.IsNull());
+ mContentResponseTimer = TimeStamp::Now();
+}
+
+bool
+CancelableBlockState::TimeoutContentResponse()
+{
+ if (mContentResponseTimerExpired) {
+ return false;
+ }
+ TBS_LOG("%p got content timer expired with response received %d\n",
+ this, mContentResponded);
+ if (!mContentResponded) {
+ mPreventDefault = false;
+ }
+ mContentResponseTimerExpired = true;
+ return true;
+}
+
+bool
+CancelableBlockState::IsContentResponseTimerExpired() const
+{
+ return mContentResponseTimerExpired;
+}
+
+bool
+CancelableBlockState::IsDefaultPrevented() const
+{
+ MOZ_ASSERT(mContentResponded || mContentResponseTimerExpired);
+ return mPreventDefault;
+}
+
+bool
+CancelableBlockState::HasReceivedAllContentNotifications() const
+{
+ return HasReceivedRealConfirmedTarget() && mContentResponded;
+}
+
+bool
+CancelableBlockState::IsReadyForHandling() const
+{
+ if (!IsTargetConfirmed()) {
+ return false;
+ }
+ return mContentResponded || mContentResponseTimerExpired;
+}
+
+void
+CancelableBlockState::DispatchEvent(const InputData& aEvent) const
+{
+ GetTargetApzc()->HandleInputEvent(aEvent, mTransformToApzc);
+}
+
+void
+CancelableBlockState::RecordContentResponseTime()
+{
+ if (!mContentResponseTimer) {
+ // We might get responses from content even though we didn't wait for them.
+ // In that case, ignore the time on them, because they're not relevant for
+ // tuning our timeout value. Also this function might get called multiple
+ // times on the same input block, so we should only record the time from the
+ // first successful call.
+ return;
+ }
+ if (!HasReceivedAllContentNotifications()) {
+ // Not done yet, we'll get called again
+ return;
+ }
+ mozilla::Telemetry::Accumulate(mozilla::Telemetry::CONTENT_RESPONSE_DURATION,
+ (uint32_t)(TimeStamp::Now() - mContentResponseTimer).ToMilliseconds());
+ mContentResponseTimer = TimeStamp();
+}
+
+DragBlockState::DragBlockState(const RefPtr<AsyncPanZoomController>& aTargetApzc,
+ bool aTargetConfirmed,
+ const MouseInput& aInitialEvent)
+ : CancelableBlockState(aTargetApzc, aTargetConfirmed)
+ , mReceivedMouseUp(false)
+{
+}
+
+bool
+DragBlockState::HasReceivedMouseUp()
+{
+ return mReceivedMouseUp;
+}
+
+void
+DragBlockState::MarkMouseUpReceived()
+{
+ mReceivedMouseUp = true;
+}
+
+void
+DragBlockState::SetDragMetrics(const AsyncDragMetrics& aDragMetrics)
+{
+ mDragMetrics = aDragMetrics;
+}
+
+void
+DragBlockState::DispatchEvent(const InputData& aEvent) const
+{
+ MouseInput mouseInput = aEvent.AsMouseInput();
+ if (!mouseInput.TransformToLocal(mTransformToApzc)) {
+ return;
+ }
+
+ GetTargetApzc()->HandleDragEvent(mouseInput, mDragMetrics);
+}
+
+bool
+DragBlockState::MustStayActive()
+{
+ return !mReceivedMouseUp;
+}
+
+const char*
+DragBlockState::Type()
+{
+ return "drag";
+}
+// This is used to track the current wheel transaction.
+static uint64_t sLastWheelBlockId = InputBlockState::NO_BLOCK_ID;
+
+WheelBlockState::WheelBlockState(const RefPtr<AsyncPanZoomController>& aTargetApzc,
+ bool aTargetConfirmed,
+ const ScrollWheelInput& aInitialEvent)
+ : CancelableBlockState(aTargetApzc, aTargetConfirmed)
+ , mScrollSeriesCounter(0)
+ , mTransactionEnded(false)
+{
+ 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);
+
+ // If nothing is scrollable, we don't consider this block as starting a
+ // transaction.
+ if (!apzc) {
+ EndTransaction();
+ return;
+ }
+
+ if (apzc != GetTargetApzc()) {
+ UpdateTargetApzc(apzc);
+ }
+ }
+}
+
+bool
+WheelBlockState::SetContentResponse(bool aPreventDefault)
+{
+ if (aPreventDefault) {
+ EndTransaction();
+ }
+ return CancelableBlockState::SetContentResponse(aPreventDefault);
+}
+
+bool
+WheelBlockState::SetConfirmedTargetApzc(const RefPtr<AsyncPanZoomController>& aTargetApzc,
+ 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);
+ }
+
+ InputBlockState::SetConfirmedTargetApzc(apzc, aState, aFirstInput);
+ return true;
+}
+
+void
+WheelBlockState::Update(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;
+ }
+
+ // The current "scroll series" is a like a sub-transaction. It has a separate
+ // timeout of 80ms. Since we need to compute wheel deltas at different phases
+ // of a transaction (for example, when it is updated, and later when the
+ // event action is taken), we affix the scroll series counter to the event.
+ // This makes GetScrollWheelDelta() consistent.
+ if (!mLastEventTime.IsNull() &&
+ (aEvent.mTimeStamp - mLastEventTime).ToMilliseconds() > kScrollSeriesTimeoutMs)
+ {
+ mScrollSeriesCounter = 0;
+ }
+ aEvent.mScrollSeriesNumber = ++mScrollSeriesCounter;
+
+ // 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.
+ RefPtr<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();
+}
+
+bool
+WheelBlockState::MustStayActive()
+{
+ return !mTransactionEnded;
+}
+
+const char*
+WheelBlockState::Type()
+{
+ return "scroll wheel";
+}
+
+bool
+WheelBlockState::ShouldAcceptNewEvent() const
+{
+ if (!InTransaction()) {
+ // If we're not in a transaction, start a new one.
+ return false;
+ }
+
+ RefPtr<AsyncPanZoomController> apzc = GetTargetApzc();
+ if (apzc->IsDestroyed()) {
+ return false;
+ }
+
+ return true;
+}
+
+bool
+WheelBlockState::MaybeTimeout(const ScrollWheelInput& aEvent)
+{
+ MOZ_ASSERT(InTransaction());
+
+ 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)
+{
+ MOZ_ASSERT(InTransaction());
+
+ // End the transaction if the event occurred > 1.5s after the most recently
+ // seen wheel event.
+ TimeDuration duration = aTimeStamp - mLastEventTime;
+ if (duration.ToMilliseconds() < gfxPrefs::MouseWheelTransactionTimeoutMs()) {
+ return false;
+ }
+
+ TBS_LOG("%p wheel transaction timed out\n", this);
+
+ if (gfxPrefs::MouseScrollTestingEnabled()) {
+ RefPtr<AsyncPanZoomController> apzc = GetTargetApzc();
+ apzc->NotifyMozMouseScrollEvent(NS_LITERAL_STRING("MozMouseScrollTransactionTimeout"));
+ }
+
+ EndTransaction();
+ return true;
+}
+
+void
+WheelBlockState::OnMouseMove(const ScreenIntPoint& aPoint)
+{
+ MOZ_ASSERT(InTransaction());
+
+ 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;
+ }
+ }
+}
+
+void
+WheelBlockState::UpdateTargetApzc(const RefPtr<AsyncPanZoomController>& aTargetApzc)
+{
+ InputBlockState::UpdateTargetApzc(aTargetApzc);
+
+ // If we found there was no target apzc, then we end the transaction.
+ if (!GetTargetApzc()) {
+ EndTransaction();
+ }
+}
+
+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;
+ }
+
+ if (mTransactionEnded) {
+ return false;
+ }
+
+ MOZ_ASSERT(GetTargetApzc());
+ return true;
+}
+
+bool
+WheelBlockState::AllowScrollHandoff() const
+{
+ // 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;
+}
+
+PanGestureBlockState::PanGestureBlockState(const RefPtr<AsyncPanZoomController>& aTargetApzc,
+ bool aTargetConfirmed,
+ const PanGestureInput& aInitialEvent)
+ : CancelableBlockState(aTargetApzc, aTargetConfirmed)
+ , mInterrupted(false)
+ , 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);
+
+ if (apzc && apzc != GetTargetApzc()) {
+ UpdateTargetApzc(apzc);
+ }
+ }
+}
+
+bool
+PanGestureBlockState::SetConfirmedTargetApzc(const RefPtr<AsyncPanZoomController>& aTargetApzc,
+ 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) {
+ RefPtr<AsyncPanZoomController> scrollableApzc =
+ apzc->BuildOverscrollHandoffChain()->FindFirstScrollable(*aFirstInput);
+ if (scrollableApzc) {
+ apzc = scrollableApzc;
+ }
+ }
+
+ InputBlockState::SetConfirmedTargetApzc(apzc, aState, aFirstInput);
+ return true;
+}
+
+bool
+PanGestureBlockState::MustStayActive()
+{
+ return !mInterrupted;
+}
+
+const char*
+PanGestureBlockState::Type()
+{
+ return "pan gesture";
+}
+
+bool
+PanGestureBlockState::SetContentResponse(bool aPreventDefault)
+{
+ if (aPreventDefault) {
+ TBS_LOG("%p setting interrupted flag\n", this);
+ mInterrupted = true;
+ }
+ bool stateChanged = CancelableBlockState::SetContentResponse(aPreventDefault);
+ if (mWaitingForContentResponse) {
+ mWaitingForContentResponse = false;
+ stateChanged = true;
+ }
+ return stateChanged;
+}
+
+bool
+PanGestureBlockState::HasReceivedAllContentNotifications() const
+{
+ return CancelableBlockState::HasReceivedAllContentNotifications()
+ && !mWaitingForContentResponse;
+}
+
+bool
+PanGestureBlockState::IsReadyForHandling() const
+{
+ if (!CancelableBlockState::IsReadyForHandling()) {
+ return false;
+ }
+ return !mWaitingForContentResponse ||
+ IsContentResponseTimerExpired();
+}
+
+bool
+PanGestureBlockState::AllowScrollHandoff() const
+{
+ return false;
+}
+
+void
+PanGestureBlockState::SetNeedsToWaitForContentResponse(bool aWaitForContentResponse)
+{
+ mWaitingForContentResponse = aWaitForContentResponse;
+}
+
+TouchBlockState::TouchBlockState(const RefPtr<AsyncPanZoomController>& aTargetApzc,
+ bool aTargetConfirmed, TouchCounter& aCounter)
+ : CancelableBlockState(aTargetApzc, aTargetConfirmed)
+ , mAllowedTouchBehaviorSet(false)
+ , mDuringFastFling(false)
+ , mSingleTapOccurred(false)
+ , mInSlop(false)
+ , mTouchCounter(aCounter)
+{
+ TBS_LOG("Creating %p\n", this);
+ if (!gfxPrefs::TouchActionEnabled()) {
+ mAllowedTouchBehaviorSet = true;
+ }
+}
+
+bool
+TouchBlockState::SetAllowedTouchBehaviors(const nsTArray<TouchBehaviorFlags>& aBehaviors)
+{
+ if (mAllowedTouchBehaviorSet) {
+ return false;
+ }
+ TBS_LOG("%p got allowed touch behaviours for %" PRIuSIZE " points\n", this, aBehaviors.Length());
+ mAllowedTouchBehaviors.AppendElements(aBehaviors);
+ mAllowedTouchBehaviorSet = true;
+ return true;
+}
+
+bool
+TouchBlockState::GetAllowedTouchBehaviors(nsTArray<TouchBehaviorFlags>& aOutBehaviors) const
+{
+ if (!mAllowedTouchBehaviorSet) {
+ return false;
+ }
+ aOutBehaviors.AppendElements(mAllowedTouchBehaviors);
+ return true;
+}
+
+void
+TouchBlockState::CopyPropertiesFrom(const TouchBlockState& aOther)
+{
+ TBS_LOG("%p copying properties from %p\n", this, &aOther);
+ if (gfxPrefs::TouchActionEnabled()) {
+ MOZ_ASSERT(aOther.mAllowedTouchBehaviorSet || aOther.IsContentResponseTimerExpired());
+ SetAllowedTouchBehaviors(aOther.mAllowedTouchBehaviors);
+ }
+ mTransformToApzc = aOther.mTransformToApzc;
+}
+
+bool
+TouchBlockState::HasReceivedAllContentNotifications() const
+{
+ return CancelableBlockState::HasReceivedAllContentNotifications()
+ // See comment in TouchBlockState::IsReadyforHandling()
+ && (!gfxPrefs::TouchActionEnabled() || mAllowedTouchBehaviorSet);
+}
+
+bool
+TouchBlockState::IsReadyForHandling() const
+{
+ if (!CancelableBlockState::IsReadyForHandling()) {
+ return false;
+ }
+
+ if (!gfxPrefs::TouchActionEnabled()) {
+ // If TouchActionEnabled() was false when this block was created, then
+ // mAllowedTouchBehaviorSet is guaranteed to the true. However, the pref
+ // may have been flipped to false after the block was created. In that case,
+ // we should eventually get the touch-behaviour notification, or expire the
+ // content response timeout, but we don't really need to wait for those,
+ // since we don't care about the touch-behaviour values any more.
+ return true;
+ }
+
+ return mAllowedTouchBehaviorSet || IsContentResponseTimerExpired();
+}
+
+void
+TouchBlockState::SetDuringFastFling()
+{
+ TBS_LOG("%p setting fast-motion flag\n", this);
+ mDuringFastFling = true;
+}
+
+bool
+TouchBlockState::IsDuringFastFling() const
+{
+ return mDuringFastFling;
+}
+
+void
+TouchBlockState::SetSingleTapOccurred()
+{
+ TBS_LOG("%p setting single-tap-occurred flag\n", this);
+ mSingleTapOccurred = true;
+}
+
+bool
+TouchBlockState::SingleTapOccurred() const
+{
+ return mSingleTapOccurred;
+}
+
+bool
+TouchBlockState::MustStayActive()
+{
+ return true;
+}
+
+const char*
+TouchBlockState::Type()
+{
+ return "touch";
+}
+
+void
+TouchBlockState::DispatchEvent(const InputData& aEvent) const
+{
+ MOZ_ASSERT(aEvent.mInputType == MULTITOUCH_INPUT);
+ mTouchCounter.Update(aEvent.AsMultiTouchInput());
+ CancelableBlockState::DispatchEvent(aEvent);
+}
+
+bool
+TouchBlockState::TouchActionAllowsPinchZoom() const
+{
+ if (!gfxPrefs::TouchActionEnabled()) {
+ return true;
+ }
+ // Pointer events specification requires that all touch points allow zoom.
+ for (size_t i = 0; i < mAllowedTouchBehaviors.Length(); i++) {
+ if (!(mAllowedTouchBehaviors[i] & AllowedTouchBehavior::PINCH_ZOOM)) {
+ return false;
+ }
+ }
+ return true;
+}
+
+bool
+TouchBlockState::TouchActionAllowsDoubleTapZoom() const
+{
+ if (!gfxPrefs::TouchActionEnabled()) {
+ return true;
+ }
+ for (size_t i = 0; i < mAllowedTouchBehaviors.Length(); i++) {
+ if (!(mAllowedTouchBehaviors[i] & AllowedTouchBehavior::DOUBLE_TAP_ZOOM)) {
+ return false;
+ }
+ }
+ return true;
+}
+
+bool
+TouchBlockState::TouchActionAllowsPanningX() const
+{
+ if (!gfxPrefs::TouchActionEnabled()) {
+ return true;
+ }
+ if (mAllowedTouchBehaviors.IsEmpty()) {
+ // Default to allowed
+ return true;
+ }
+ TouchBehaviorFlags flags = mAllowedTouchBehaviors[0];
+ return (flags & AllowedTouchBehavior::HORIZONTAL_PAN);
+}
+
+bool
+TouchBlockState::TouchActionAllowsPanningY() const
+{
+ if (!gfxPrefs::TouchActionEnabled()) {
+ return true;
+ }
+ if (mAllowedTouchBehaviors.IsEmpty()) {
+ // Default to allowed
+ return true;
+ }
+ TouchBehaviorFlags flags = mAllowedTouchBehaviors[0];
+ return (flags & AllowedTouchBehavior::VERTICAL_PAN);
+}
+
+bool
+TouchBlockState::TouchActionAllowsPanningXY() const
+{
+ if (!gfxPrefs::TouchActionEnabled()) {
+ return true;
+ }
+ if (mAllowedTouchBehaviors.IsEmpty()) {
+ // Default to allowed
+ return true;
+ }
+ TouchBehaviorFlags flags = mAllowedTouchBehaviors[0];
+ return (flags & AllowedTouchBehavior::HORIZONTAL_PAN)
+ && (flags & AllowedTouchBehavior::VERTICAL_PAN);
+}
+
+bool
+TouchBlockState::UpdateSlopState(const MultiTouchInput& aInput,
+ bool aApzcCanConsumeEvents)
+{
+ if (aInput.mType == MultiTouchInput::MULTITOUCH_START) {
+ // this is by definition the first event in this block. If it's the first
+ // touch, then we enter a slop state.
+ mInSlop = (aInput.mTouches.Length() == 1);
+ if (mInSlop) {
+ mSlopOrigin = aInput.mTouches[0].mScreenPoint;
+ TBS_LOG("%p entering slop with origin %s\n", this, Stringify(mSlopOrigin).c_str());
+ }
+ return false;
+ }
+ if (mInSlop) {
+ ScreenCoord threshold = aApzcCanConsumeEvents
+ ? AsyncPanZoomController::GetTouchStartTolerance()
+ : ScreenCoord(gfxPrefs::APZTouchMoveTolerance() * APZCTreeManager::GetDPI());
+ bool stayInSlop = (aInput.mType == MultiTouchInput::MULTITOUCH_MOVE) &&
+ (aInput.mTouches.Length() == 1) &&
+ ((aInput.mTouches[0].mScreenPoint - mSlopOrigin).Length() < threshold);
+ if (!stayInSlop) {
+ // we're out of the slop zone, and will stay out for the remainder of
+ // this block
+ TBS_LOG("%p exiting slop\n", this);
+ mInSlop = false;
+ }
+ }
+ return mInSlop;
+}
+
+uint32_t
+TouchBlockState::GetActiveTouchCount() const
+{
+ return mTouchCounter.GetActiveTouchCount();
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/apz/src/InputBlockState.h b/gfx/layers/apz/src/InputBlockState.h
new file mode 100644
index 000000000..86fb0d03f
--- /dev/null
+++ b/gfx/layers/apz/src/InputBlockState.h
@@ -0,0 +1,483 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set sw=2 ts=8 et tw=80 : */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_layers_InputBlockState_h
+#define mozilla_layers_InputBlockState_h
+
+#include "InputData.h" // for MultiTouchInput
+#include "mozilla/RefCounted.h" // for RefCounted
+#include "mozilla/RefPtr.h" // for RefPtr
+#include "mozilla/gfx/Matrix.h" // for Matrix4x4
+#include "mozilla/layers/APZUtils.h" // for TouchBehaviorFlags
+#include "mozilla/layers/AsyncDragMetrics.h"
+#include "mozilla/TimeStamp.h" // for TimeStamp
+#include "nsTArray.h" // for nsTArray
+#include "TouchCounter.h"
+
+namespace mozilla {
+namespace layers {
+
+class AsyncPanZoomController;
+class OverscrollHandoffChain;
+class CancelableBlockState;
+class TouchBlockState;
+class WheelBlockState;
+class DragBlockState;
+class PanGestureBlockState;
+
+/**
+ * A base class that stores state common to various input blocks.
+ * Note that the InputBlockState constructor acquires the tree lock, so callers
+ * from inside AsyncPanZoomController should ensure that the APZC lock is not
+ * held.
+ */
+class InputBlockState : public RefCounted<InputBlockState>
+{
+public:
+ MOZ_DECLARE_REFCOUNTED_TYPENAME(InputBlockState)
+
+ static const uint64_t NO_BLOCK_ID = 0;
+
+ enum class TargetConfirmationState {
+ eUnconfirmed,
+ eTimedOut,
+ eTimedOutAndMainThreadResponded,
+ eConfirmed
+ };
+
+ explicit InputBlockState(const RefPtr<AsyncPanZoomController>& aTargetApzc,
+ bool aTargetConfirmed);
+ virtual ~InputBlockState()
+ {}
+
+ virtual bool SetConfirmedTargetApzc(const RefPtr<AsyncPanZoomController>& aTargetApzc,
+ TargetConfirmationState aState,
+ InputData* aFirstInput);
+ const RefPtr<AsyncPanZoomController>& GetTargetApzc() const;
+ const RefPtr<const OverscrollHandoffChain>& GetOverscrollHandoffChain() const;
+ uint64_t GetBlockId() const;
+
+ bool IsTargetConfirmed() const;
+ bool HasReceivedRealConfirmedTarget() const;
+
+ void SetScrolledApzc(AsyncPanZoomController* aApzc);
+ AsyncPanZoomController* GetScrolledApzc() const;
+ bool IsDownchainOfScrolledApzc(AsyncPanZoomController* aApzc) const;
+
+protected:
+ virtual void UpdateTargetApzc(const RefPtr<AsyncPanZoomController>& aTargetApzc);
+
+private:
+ // Checks whether |aA| is an ancestor of |aB| (or the same as |aB|) in
+ // |mOverscrollHandoffChain|.
+ bool IsDownchainOf(AsyncPanZoomController* aA, AsyncPanZoomController* aB) const;
+
+private:
+ RefPtr<AsyncPanZoomController> mTargetApzc;
+ TargetConfirmationState mTargetConfirmed;
+ const uint64_t mBlockId;
+
+ // The APZC that was actually scrolled by events in this input block.
+ // This is used in configurations where a single input block is only
+ // allowed to scroll a single APZC (configurations where gfxPrefs::
+ // APZAllowImmediateHandoff() is false).
+ // Set the first time an input event in this block scrolls an APZC.
+ RefPtr<AsyncPanZoomController> mScrolledApzc;
+protected:
+ RefPtr<const OverscrollHandoffChain> mOverscrollHandoffChain;
+
+ // Used to transform events from global screen space to |mTargetApzc|'s
+ // screen space. It's cached at the beginning of the input block so that
+ // all events in the block are in the same coordinate space.
+ ScreenToParentLayerMatrix4x4 mTransformToApzc;
+};
+
+/**
+ * This class represents a set of events that can be cancelled by web content
+ * via event listeners.
+ *
+ * Each cancelable input block can be cancelled by web content, and
+ * this information is stored in the mPreventDefault flag. Because web
+ * content runs on the Gecko main thread, we cannot always wait for web content's
+ * response. Instead, there is a timeout that sets this flag in the case
+ * where web content doesn't respond in time. The mContentResponded
+ * and mContentResponseTimerExpired flags indicate which of these scenarios
+ * occurred.
+ */
+class CancelableBlockState : public InputBlockState
+{
+public:
+ CancelableBlockState(const RefPtr<AsyncPanZoomController>& aTargetApzc,
+ bool aTargetConfirmed);
+
+ virtual TouchBlockState *AsTouchBlock() {
+ return nullptr;
+ }
+ virtual WheelBlockState *AsWheelBlock() {
+ return nullptr;
+ }
+ virtual DragBlockState *AsDragBlock() {
+ return nullptr;
+ }
+ virtual PanGestureBlockState *AsPanGestureBlock() {
+ return nullptr;
+ }
+
+ /**
+ * Record whether or not content cancelled this block of events.
+ * @param aPreventDefault true iff the block is cancelled.
+ * @return false if this block has already received a response from
+ * web content, true if not.
+ */
+ virtual bool SetContentResponse(bool aPreventDefault);
+
+ /**
+ * This should be called when this block is starting to wait for the
+ * necessary content response notifications. It is used to gather data
+ * on how long the content response notifications take.
+ */
+ void StartContentResponseTimer();
+
+ /**
+ * This should be called when a content response notification has been
+ * delivered to this block. If all the notifications have arrived, this
+ * will report the total time take to telemetry.
+ */
+ void RecordContentResponseTime();
+
+ /**
+ * Record that content didn't respond in time.
+ * @return false if this block already timed out, true if not.
+ */
+ bool TimeoutContentResponse();
+
+ /**
+ * Checks if the content response timer has already expired.
+ */
+ bool IsContentResponseTimerExpired() const;
+
+ /**
+ * @return true iff web content cancelled this block of events.
+ */
+ bool IsDefaultPrevented() const;
+
+ /**
+ * Dispatch the event to the target APZC. Mostly this is a hook for
+ * subclasses to do any per-event processing they need to.
+ */
+ virtual void DispatchEvent(const InputData& aEvent) const;
+
+ /**
+ * @return true iff this block has received all the information it could
+ * have gotten from the content thread.
+ */
+ virtual bool HasReceivedAllContentNotifications() const;
+
+ /**
+ * @return true iff this block has received all the information needed
+ * to properly dispatch the events in the block.
+ */
+ virtual bool IsReadyForHandling() const;
+
+ /**
+ * Return true if this input block must stay active if it would otherwise
+ * be removed as the last item in the pending queue.
+ */
+ virtual bool MustStayActive() = 0;
+
+ /**
+ * Return a descriptive name for the block kind.
+ */
+ virtual const char* Type() = 0;
+
+private:
+ TimeStamp mContentResponseTimer;
+ bool mPreventDefault;
+ bool mContentResponded;
+ bool mContentResponseTimerExpired;
+};
+
+/**
+ * A single block of wheel events.
+ */
+class WheelBlockState : public CancelableBlockState
+{
+public:
+ WheelBlockState(const RefPtr<AsyncPanZoomController>& aTargetApzc,
+ bool aTargetConfirmed,
+ const ScrollWheelInput& aEvent);
+
+ bool SetContentResponse(bool aPreventDefault) override;
+ bool MustStayActive() override;
+ const char* Type() override;
+ bool SetConfirmedTargetApzc(const RefPtr<AsyncPanZoomController>& aTargetApzc,
+ TargetConfirmationState aState,
+ InputData* aFirstInput) override;
+
+ WheelBlockState *AsWheelBlock() override {
+ return this;
+ }
+
+ /**
+ * 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 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.
+ */
+ bool InTransaction() const;
+
+ /**
+ * Mark the block as no longer participating in a wheel transaction. This
+ * will force future wheel events to begin a new input block.
+ */
+ void EndTransaction();
+
+ /**
+ * @return Whether or not overscrolling is prevented for this wheel block.
+ */
+ bool AllowScrollHandoff() const;
+
+ /**
+ * 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(ScrollWheelInput& aEvent);
+
+protected:
+ void UpdateTargetApzc(const RefPtr<AsyncPanZoomController>& aTargetApzc) override;
+
+private:
+ TimeStamp mLastEventTime;
+ TimeStamp mLastMouseMove;
+ uint32_t mScrollSeriesCounter;
+ bool mTransactionEnded;
+};
+
+/**
+ * A block of mouse events that are part of a drag
+ */
+class DragBlockState : public CancelableBlockState
+{
+public:
+ DragBlockState(const RefPtr<AsyncPanZoomController>& aTargetApzc,
+ bool aTargetConfirmed,
+ const MouseInput& aEvent);
+
+ bool MustStayActive() override;
+ const char* Type() override;
+
+ bool HasReceivedMouseUp();
+ void MarkMouseUpReceived();
+
+ DragBlockState *AsDragBlock() override {
+ return this;
+ }
+
+ void SetDragMetrics(const AsyncDragMetrics& aDragMetrics);
+
+ void DispatchEvent(const InputData& aEvent) const override;
+private:
+ AsyncDragMetrics mDragMetrics;
+ bool mReceivedMouseUp;
+};
+
+/**
+ * A single block of pan gesture events.
+ */
+class PanGestureBlockState : public CancelableBlockState
+{
+public:
+ PanGestureBlockState(const RefPtr<AsyncPanZoomController>& aTargetApzc,
+ bool aTargetConfirmed,
+ const PanGestureInput& aEvent);
+
+ bool SetContentResponse(bool aPreventDefault) override;
+ bool HasReceivedAllContentNotifications() const override;
+ bool IsReadyForHandling() const override;
+ bool MustStayActive() override;
+ const char* Type() override;
+ bool SetConfirmedTargetApzc(const RefPtr<AsyncPanZoomController>& aTargetApzc,
+ TargetConfirmationState aState,
+ InputData* aFirstInput) override;
+
+ PanGestureBlockState *AsPanGestureBlock() override {
+ return this;
+ }
+
+ /**
+ * @return Whether or not overscrolling is prevented for this block.
+ */
+ bool AllowScrollHandoff() const;
+
+ bool WasInterrupted() const { return mInterrupted; }
+
+ void SetNeedsToWaitForContentResponse(bool aWaitForContentResponse);
+
+private:
+ bool mInterrupted;
+ bool mWaitingForContentResponse;
+};
+
+/**
+ * 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
+ * touch block consists of the touch-start, followed by all touch events
+ * up to but not including the next touch-start (except in the case where
+ * a long-tap happens, see below). Note that in particular we cannot know
+ * when a touch block ends until the next one is started. Most touch
+ * blocks are created by receipt of a touch-start event.
+ *
+ * Every long-tap event also creates a new touch block, since it can also
+ * be consumed by web content. In this case, when the long-tap event is
+ * dispatched to web content, a new touch block is started to hold the remaining
+ * touch events, up to but not including the next touch start (or long-tap).
+ *
+ * Additionally, if touch-action is enabled, each touch block should
+ * have a set of allowed touch behavior flags; one for each touch point.
+ * This also requires running code on the Gecko main thread, and so may
+ * be populated with some latency. The mAllowedTouchBehaviorSet and
+ * mAllowedTouchBehaviors variables track this information.
+ */
+class TouchBlockState : public CancelableBlockState
+{
+public:
+ explicit TouchBlockState(const RefPtr<AsyncPanZoomController>& aTargetApzc,
+ bool aTargetConfirmed, TouchCounter& aTouchCounter);
+
+ TouchBlockState *AsTouchBlock() override {
+ return this;
+ }
+
+ /**
+ * Set the allowed touch behavior flags for this block.
+ * @return false if this block already has these flags set, true if not.
+ */
+ bool SetAllowedTouchBehaviors(const nsTArray<TouchBehaviorFlags>& aBehaviors);
+ /**
+ * If the allowed touch behaviors have been set, populate them into
+ * |aOutBehaviors| and return true. Else, return false.
+ */
+ bool GetAllowedTouchBehaviors(nsTArray<TouchBehaviorFlags>& aOutBehaviors) const;
+
+ /**
+ * Copy various properties from another block.
+ */
+ void CopyPropertiesFrom(const TouchBlockState& aOther);
+
+ /*
+ * @return true iff this block has received all the information it could
+ * have gotten from the content thread.
+ */
+ bool HasReceivedAllContentNotifications() const override;
+
+ /**
+ * @return true iff this block has received all the information needed
+ * to properly dispatch the events in the block.
+ */
+ bool IsReadyForHandling() const override;
+
+ /**
+ * Sets a flag that indicates this input block occurred while the APZ was
+ * in a state of fast flinging. This affects gestures that may be produced
+ * from input events in this block.
+ */
+ void SetDuringFastFling();
+ /**
+ * @return true iff SetDuringFastFling was called on this block.
+ */
+ bool IsDuringFastFling() const;
+ /**
+ * Set the single-tap-occurred flag that indicates that this touch block
+ * triggered a single tap event.
+ */
+ void SetSingleTapOccurred();
+ /**
+ * @return true iff the single-tap-occurred flag is set on this block.
+ */
+ bool SingleTapOccurred() const;
+
+ /**
+ * @return false iff touch-action is enabled and the allowed touch behaviors for
+ * this touch block do not allow pinch-zooming.
+ */
+ bool TouchActionAllowsPinchZoom() const;
+ /**
+ * @return false iff touch-action is enabled and the allowed touch behaviors for
+ * this touch block do not allow double-tap zooming.
+ */
+ bool TouchActionAllowsDoubleTapZoom() const;
+ /**
+ * @return false iff touch-action is enabled and the allowed touch behaviors for
+ * the first touch point do not allow panning in the specified direction(s).
+ */
+ bool TouchActionAllowsPanningX() const;
+ bool TouchActionAllowsPanningY() const;
+ bool TouchActionAllowsPanningXY() const;
+
+ /**
+ * Notifies the input block of an incoming touch event so that the block can
+ * update its internal slop state. "Slop" refers to the area around the
+ * initial touchstart where we drop touchmove events so that content doesn't
+ * see them. The |aApzcCanConsumeEvents| parameter is factored into how large
+ * the slop area is - if this is true the slop area is larger.
+ * @return true iff the provided event is a touchmove in the slop area and
+ * so should not be sent to content.
+ */
+ bool UpdateSlopState(const MultiTouchInput& aInput,
+ bool aApzcCanConsumeEvents);
+
+ /**
+ * Returns the number of touch points currently active.
+ */
+ uint32_t GetActiveTouchCount() const;
+
+ void DispatchEvent(const InputData& aEvent) const override;
+ bool MustStayActive() override;
+ const char* Type() override;
+
+private:
+ nsTArray<TouchBehaviorFlags> mAllowedTouchBehaviors;
+ bool mAllowedTouchBehaviorSet;
+ bool mDuringFastFling;
+ bool mSingleTapOccurred;
+ bool mInSlop;
+ ScreenIntPoint mSlopOrigin;
+ // A reference to the InputQueue's touch counter
+ TouchCounter& mTouchCounter;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif // mozilla_layers_InputBlockState_h
diff --git a/gfx/layers/apz/src/InputQueue.cpp b/gfx/layers/apz/src/InputQueue.cpp
new file mode 100644
index 000000000..820526d52
--- /dev/null
+++ b/gfx/layers/apz/src/InputQueue.cpp
@@ -0,0 +1,731 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set sw=2 ts=8 et tw=80 : */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "InputQueue.h"
+
+#include "AsyncPanZoomController.h"
+#include "gfxPrefs.h"
+#include "InputBlockState.h"
+#include "LayersLogging.h"
+#include "mozilla/layers/APZThreadUtils.h"
+#include "OverscrollHandoffState.h"
+#include "QueuedInput.h"
+
+#define INPQ_LOG(...)
+// #define INPQ_LOG(...) printf_stderr("INPQ: " __VA_ARGS__)
+
+namespace mozilla {
+namespace layers {
+
+InputQueue::InputQueue()
+{
+}
+
+InputQueue::~InputQueue() {
+ mQueuedInputs.Clear();
+}
+
+nsEventStatus
+InputQueue::ReceiveInputEvent(const RefPtr<AsyncPanZoomController>& aTarget,
+ bool aTargetConfirmed,
+ const InputData& aEvent,
+ uint64_t* aOutInputBlockId) {
+ APZThreadUtils::AssertOnControllerThread();
+
+ switch (aEvent.mInputType) {
+ case MULTITOUCH_INPUT: {
+ const MultiTouchInput& event = aEvent.AsMultiTouchInput();
+ return ReceiveTouchInput(aTarget, aTargetConfirmed, event, aOutInputBlockId);
+ }
+
+ case SCROLLWHEEL_INPUT: {
+ const ScrollWheelInput& event = aEvent.AsScrollWheelInput();
+ return ReceiveScrollWheelInput(aTarget, aTargetConfirmed, event, aOutInputBlockId);
+ }
+
+ case PANGESTURE_INPUT: {
+ const PanGestureInput& event = aEvent.AsPanGestureInput();
+ return ReceivePanGestureInput(aTarget, aTargetConfirmed, event, aOutInputBlockId);
+ }
+
+ case MOUSE_INPUT: {
+ const MouseInput& event = aEvent.AsMouseInput();
+ return ReceiveMouseInput(aTarget, aTargetConfirmed, event, aOutInputBlockId);
+ }
+
+ default:
+ // The return value for non-touch input is only used by tests, so just pass
+ // through the return value for now. This can be changed later if needed.
+ // TODO (bug 1098430): we will eventually need to have smarter handling for
+ // non-touch events as well.
+ return aTarget->HandleInputEvent(aEvent, aTarget->GetTransformToThis());
+ }
+}
+
+nsEventStatus
+InputQueue::ReceiveTouchInput(const RefPtr<AsyncPanZoomController>& aTarget,
+ bool aTargetConfirmed,
+ const MultiTouchInput& aEvent,
+ uint64_t* aOutInputBlockId) {
+ TouchBlockState* block = nullptr;
+ if (aEvent.mType == MultiTouchInput::MULTITOUCH_START) {
+ nsTArray<TouchBehaviorFlags> currentBehaviors;
+ bool haveBehaviors = false;
+ if (!gfxPrefs::TouchActionEnabled()) {
+ haveBehaviors = true;
+ } else if (mActiveTouchBlock) {
+ haveBehaviors = mActiveTouchBlock->GetAllowedTouchBehaviors(currentBehaviors);
+ // If the behaviours aren't set, but the main-thread response timer on
+ // the block is expired we still treat it as though it has behaviors,
+ // because in that case we still want to interrupt the fast-fling and
+ // use the default behaviours.
+ haveBehaviors |= mActiveTouchBlock->IsContentResponseTimerExpired();
+ }
+
+ block = StartNewTouchBlock(aTarget, aTargetConfirmed, false);
+ INPQ_LOG("started new touch block %p id %" PRIu64 " for target %p\n",
+ block, block->GetBlockId(), aTarget.get());
+
+ // XXX using the chain from |block| here may be wrong in cases where the
+ // target isn't confirmed and the real target turns out to be something
+ // else. For now assume this is rare enough that it's not an issue.
+ if (mQueuedInputs.IsEmpty() &&
+ aEvent.mTouches.Length() == 1 &&
+ block->GetOverscrollHandoffChain()->HasFastFlungApzc() &&
+ haveBehaviors) {
+ // If we're already in a fast fling, and a single finger goes down, then
+ // we want special handling for the touch event, because it shouldn't get
+ // delivered to content. Note that we don't set this flag when going
+ // from a fast fling to a pinch state (i.e. second finger goes down while
+ // the first finger is moving).
+ block->SetDuringFastFling();
+ block->SetConfirmedTargetApzc(aTarget,
+ InputBlockState::TargetConfirmationState::eConfirmed,
+ nullptr /* the block was just created so it has no events */);
+ if (gfxPrefs::TouchActionEnabled()) {
+ block->SetAllowedTouchBehaviors(currentBehaviors);
+ }
+ INPQ_LOG("block %p tagged as fast-motion\n", block);
+ }
+
+ CancelAnimationsForNewBlock(block);
+
+ MaybeRequestContentResponse(aTarget, block);
+ } else {
+ block = mActiveTouchBlock.get();
+ if (!block) {
+ NS_WARNING("Received a non-start touch event while no touch blocks active!");
+ return nsEventStatus_eIgnore;
+ }
+
+ INPQ_LOG("received new event in block %p\n", block);
+ }
+
+ if (aOutInputBlockId) {
+ *aOutInputBlockId = block->GetBlockId();
+ }
+
+ // 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.
+ RefPtr<AsyncPanZoomController> target = block->GetTargetApzc();
+
+ nsEventStatus result = nsEventStatus_eIgnore;
+
+ // XXX calling ArePointerEventsConsumable on |target| may be wrong here if
+ // the target isn't confirmed and the real target turns out to be something
+ // else. For now assume this is rare enough that it's not an issue.
+ if (block->IsDuringFastFling()) {
+ INPQ_LOG("dropping event due to block %p being in fast motion\n", block);
+ result = nsEventStatus_eConsumeNoDefault;
+ } else if (target && target->ArePointerEventsConsumable(block, aEvent.mTouches.Length())) {
+ if (block->UpdateSlopState(aEvent, true)) {
+ INPQ_LOG("dropping event due to block %p being in slop\n", block);
+ result = nsEventStatus_eConsumeNoDefault;
+ } else {
+ result = nsEventStatus_eConsumeDoDefault;
+ }
+ } else if (block->UpdateSlopState(aEvent, false)) {
+ INPQ_LOG("dropping event due to block %p being in mini-slop\n", block);
+ result = nsEventStatus_eConsumeNoDefault;
+ }
+ mQueuedInputs.AppendElement(MakeUnique<QueuedInput>(aEvent, *block));
+ ProcessQueue();
+ return result;
+}
+
+nsEventStatus
+InputQueue::ReceiveMouseInput(const RefPtr<AsyncPanZoomController>& aTarget,
+ bool aTargetConfirmed,
+ const MouseInput& aEvent,
+ uint64_t* aOutInputBlockId) {
+ // On a new mouse down we can have a new target so we must force a new block
+ // with a new target.
+ bool newBlock = DragTracker::StartsDrag(aEvent);
+
+ DragBlockState* block = newBlock ? nullptr : mActiveDragBlock.get();
+ if (block && block->HasReceivedMouseUp()) {
+ block = nullptr;
+ }
+
+ if (!block && mDragTracker.InDrag()) {
+ // If there's no current drag block, but we're getting a move with a button
+ // down, we need to start a new drag block because we're obviously already
+ // in the middle of a drag (it probably got interrupted by something else).
+ INPQ_LOG("got a drag event outside a drag block, need to create a block to hold it\n");
+ newBlock = true;
+ }
+
+ mDragTracker.Update(aEvent);
+
+ if (!newBlock && !block) {
+ // This input event is not in a drag block, so we're not doing anything
+ // with it, return eIgnore.
+ return nsEventStatus_eIgnore;
+ }
+
+ if (!block) {
+ MOZ_ASSERT(newBlock);
+ block = new DragBlockState(aTarget, aTargetConfirmed, aEvent);
+
+ INPQ_LOG("started new drag block %p id %" PRIu64 " for %sconfirmed target %p\n",
+ block, block->GetBlockId(), aTargetConfirmed ? "" : "un", aTarget.get());
+
+ mActiveDragBlock = block;
+
+ CancelAnimationsForNewBlock(block);
+ MaybeRequestContentResponse(aTarget, block);
+ }
+
+ if (aOutInputBlockId) {
+ *aOutInputBlockId = block->GetBlockId();
+ }
+
+ mQueuedInputs.AppendElement(MakeUnique<QueuedInput>(aEvent, *block));
+ ProcessQueue();
+
+ if (DragTracker::EndsDrag(aEvent)) {
+ block->MarkMouseUpReceived();
+ }
+
+ // The event is part of a drag block and could potentially cause
+ // scrolling, so return DoDefault.
+ return nsEventStatus_eConsumeDoDefault;
+}
+
+nsEventStatus
+InputQueue::ReceiveScrollWheelInput(const RefPtr<AsyncPanZoomController>& aTarget,
+ bool aTargetConfirmed,
+ const ScrollWheelInput& aEvent,
+ uint64_t* aOutInputBlockId) {
+ WheelBlockState* block = mActiveWheelBlock.get();
+ // 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() ||
+ block->MaybeTimeout(aEvent)))
+ {
+ block = nullptr;
+ }
+
+ MOZ_ASSERT(!block || block->InTransaction());
+
+ if (!block) {
+ block = new WheelBlockState(aTarget, aTargetConfirmed, aEvent);
+ INPQ_LOG("started new scroll wheel block %p id %" PRIu64 " for target %p\n",
+ block, block->GetBlockId(), aTarget.get());
+
+ mActiveWheelBlock = block;
+
+ CancelAnimationsForNewBlock(block);
+ MaybeRequestContentResponse(aTarget, block);
+ } else {
+ INPQ_LOG("received new event in block %p\n", block);
+ }
+
+ if (aOutInputBlockId) {
+ *aOutInputBlockId = block->GetBlockId();
+ }
+
+ // 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
+ // ProcessQueue() does.
+ mQueuedInputs.AppendElement(MakeUnique<QueuedInput>(aEvent, *block));
+
+ // The WheelBlockState needs to affix a counter to the event before we process
+ // it. Note that the counter is affixed to the copy in the queue rather than
+ // |aEvent|.
+ block->Update(mQueuedInputs.LastElement()->Input()->AsScrollWheelInput());
+
+ ProcessQueue();
+
+ return nsEventStatus_eConsumeDoDefault;
+}
+
+static bool
+CanScrollTargetHorizontally(const PanGestureInput& aInitialEvent,
+ PanGestureBlockState* aBlock)
+{
+ PanGestureInput horizontalComponent = aInitialEvent;
+ horizontalComponent.mPanDisplacement.y = 0;
+ RefPtr<AsyncPanZoomController> horizontallyScrollableAPZC =
+ aBlock->GetOverscrollHandoffChain()->FindFirstScrollable(horizontalComponent);
+ return horizontallyScrollableAPZC && horizontallyScrollableAPZC == aBlock->GetTargetApzc();
+}
+
+nsEventStatus
+InputQueue::ReceivePanGestureInput(const RefPtr<AsyncPanZoomController>& aTarget,
+ bool aTargetConfirmed,
+ const PanGestureInput& aEvent,
+ uint64_t* aOutInputBlockId) {
+ if (aEvent.mType == PanGestureInput::PANGESTURE_MAYSTART ||
+ aEvent.mType == PanGestureInput::PANGESTURE_CANCELLED) {
+ // Ignore these events for now.
+ return nsEventStatus_eConsumeDoDefault;
+ }
+
+ PanGestureBlockState* block = nullptr;
+ if (aEvent.mType != PanGestureInput::PANGESTURE_START) {
+ block = mActivePanGestureBlock.get();
+ }
+
+ PanGestureInput event = aEvent;
+ nsEventStatus result = nsEventStatus_eConsumeDoDefault;
+
+ if (!block || block->WasInterrupted()) {
+ if (event.mType != PanGestureInput::PANGESTURE_START) {
+ // Only PANGESTURE_START events are allowed to start a new pan gesture
+ // block, but we really want to start a new block here, so we magically
+ // turn this input into a PANGESTURE_START.
+ INPQ_LOG("transmogrifying pan input %d to PANGESTURE_START for new block\n",
+ event.mType);
+ event.mType = PanGestureInput::PANGESTURE_START;
+ }
+ block = new PanGestureBlockState(aTarget, aTargetConfirmed, event);
+ INPQ_LOG("started new pan gesture block %p id %" PRIu64 " for target %p\n",
+ block, block->GetBlockId(), aTarget.get());
+
+ if (aTargetConfirmed &&
+ event.mRequiresContentResponseIfCannotScrollHorizontallyInStartDirection &&
+ !CanScrollTargetHorizontally(event, block)) {
+ // This event may trigger a swipe gesture, depending on what our caller
+ // wants to do it. We need to suspend handling of this block until we get
+ // a content response which will tell us whether to proceed or abort the
+ // block.
+ block->SetNeedsToWaitForContentResponse(true);
+
+ // Inform our caller that we haven't scrolled in response to the event
+ // and that a swipe can be started from this event if desired.
+ result = nsEventStatus_eIgnore;
+ }
+
+ mActivePanGestureBlock = block;
+
+ CancelAnimationsForNewBlock(block);
+ MaybeRequestContentResponse(aTarget, block);
+ } else {
+ INPQ_LOG("received new event in block %p\n", block);
+ }
+
+ if (aOutInputBlockId) {
+ *aOutInputBlockId = block->GetBlockId();
+ }
+
+ // 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
+ // ProcessQueue() does.
+ mQueuedInputs.AppendElement(MakeUnique<QueuedInput>(event, *block));
+ ProcessQueue();
+
+ return result;
+}
+
+void
+InputQueue::CancelAnimationsForNewBlock(CancelableBlockState* aBlock)
+{
+ // We want to cancel animations here as soon as possible (i.e. without waiting for
+ // content responses) because a finger has gone down and we don't want to keep moving
+ // the content under the finger. However, to prevent "future" touchstart events from
+ // interfering with "past" animations (i.e. from a previous touch block that is still
+ // being processed) we only do this animation-cancellation if there are no older
+ // touch blocks still in the queue.
+ if (mQueuedInputs.IsEmpty()) {
+ aBlock->GetOverscrollHandoffChain()->CancelAnimations(ExcludeOverscroll | ScrollSnap);
+ }
+}
+
+void
+InputQueue::MaybeRequestContentResponse(const RefPtr<AsyncPanZoomController>& aTarget,
+ CancelableBlockState* aBlock)
+{
+ bool waitForMainThread = false;
+ if (aBlock->IsTargetConfirmed()) {
+ // Content won't prevent-default this, so we can just set the flag directly.
+ INPQ_LOG("not waiting for content response on block %p\n", aBlock);
+ aBlock->SetContentResponse(false);
+ } else {
+ waitForMainThread = true;
+ }
+ if (aBlock->AsTouchBlock() && gfxPrefs::TouchActionEnabled()) {
+ // waitForMainThread is set to true unconditionally here, but if the APZCTM
+ // has the touch-action behaviours for this block, it will set it
+ // immediately after we unwind out of this ReceiveInputEvent call. So even
+ // though we are scheduling the main-thread timeout, we might end up not
+ // waiting.
+ INPQ_LOG("waiting for main thread touch-action info on block %p\n", aBlock);
+ waitForMainThread = true;
+ }
+ if (waitForMainThread) {
+ // We either don't know for sure if aTarget is the right APZC, or we may
+ // need to wait to give content the opportunity to prevent-default the
+ // touch events. Either way we schedule a timeout so the main thread stuff
+ // can run.
+ ScheduleMainThreadTimeout(aTarget, aBlock);
+ }
+}
+
+uint64_t
+InputQueue::InjectNewTouchBlock(AsyncPanZoomController* aTarget)
+{
+ TouchBlockState* block = StartNewTouchBlock(aTarget,
+ /* aTargetConfirmed = */ true,
+ /* aCopyPropertiesFromCurrent = */ true);
+ INPQ_LOG("injecting new touch block %p with id %" PRIu64 " and target %p\n",
+ block, block->GetBlockId(), aTarget);
+ ScheduleMainThreadTimeout(aTarget, block);
+ return block->GetBlockId();
+}
+
+TouchBlockState*
+InputQueue::StartNewTouchBlock(const RefPtr<AsyncPanZoomController>& aTarget,
+ bool aTargetConfirmed,
+ bool aCopyPropertiesFromCurrent)
+{
+ TouchBlockState* newBlock = new TouchBlockState(aTarget, aTargetConfirmed,
+ mTouchCounter);
+ if (aCopyPropertiesFromCurrent) {
+ // We should never enter here without a current touch block, because this
+ // codepath is invoked from the OnLongPress handler in
+ // AsyncPanZoomController, which should bail out if there is no current
+ // touch block.
+ MOZ_ASSERT(GetCurrentTouchBlock());
+ newBlock->CopyPropertiesFrom(*GetCurrentTouchBlock());
+ }
+
+ mActiveTouchBlock = newBlock;
+ return newBlock;
+}
+
+CancelableBlockState*
+InputQueue::GetCurrentBlock() const
+{
+ APZThreadUtils::AssertOnControllerThread();
+ return mQueuedInputs.IsEmpty() ? nullptr : mQueuedInputs[0]->Block();
+}
+
+TouchBlockState*
+InputQueue::GetCurrentTouchBlock() const
+{
+ CancelableBlockState* block = GetCurrentBlock();
+ return block ? block->AsTouchBlock() : mActiveTouchBlock.get();
+}
+
+WheelBlockState*
+InputQueue::GetCurrentWheelBlock() const
+{
+ CancelableBlockState* block = GetCurrentBlock();
+ return block ? block->AsWheelBlock() : mActiveWheelBlock.get();
+}
+
+DragBlockState*
+InputQueue::GetCurrentDragBlock() const
+{
+ CancelableBlockState* block = GetCurrentBlock();
+ return block ? block->AsDragBlock() : mActiveDragBlock.get();
+}
+
+PanGestureBlockState*
+InputQueue::GetCurrentPanGestureBlock() const
+{
+ CancelableBlockState* block = GetCurrentBlock();
+ return block ? block->AsPanGestureBlock() : mActivePanGestureBlock.get();
+}
+
+WheelBlockState*
+InputQueue::GetActiveWheelTransaction() const
+{
+ WheelBlockState* block = mActiveWheelBlock.get();
+ if (!block || !block->InTransaction()) {
+ return nullptr;
+ }
+ return block;
+}
+
+bool
+InputQueue::HasReadyTouchBlock() const
+{
+ return !mQueuedInputs.IsEmpty() &&
+ mQueuedInputs[0]->Block()->AsTouchBlock() &&
+ mQueuedInputs[0]->Block()->IsReadyForHandling();
+}
+
+bool
+InputQueue::AllowScrollHandoff() const
+{
+ if (GetCurrentWheelBlock()) {
+ return GetCurrentWheelBlock()->AllowScrollHandoff();
+ }
+ if (GetCurrentPanGestureBlock()) {
+ return GetCurrentPanGestureBlock()->AllowScrollHandoff();
+ }
+ return true;
+}
+
+bool
+InputQueue::IsDragOnScrollbar(bool aHitScrollbar)
+{
+ if (!mDragTracker.InDrag()) {
+ return false;
+ }
+ // Now that we know we are in a drag, get the info from the drag tracker.
+ // We keep it in the tracker rather than the block because the block can get
+ // interrupted by something else (like a wheel event) and then a new block
+ // will get created without the info we want. The tracker will persist though.
+ return mDragTracker.IsOnScrollbar(aHitScrollbar);
+}
+
+void
+InputQueue::ScheduleMainThreadTimeout(const RefPtr<AsyncPanZoomController>& aTarget,
+ CancelableBlockState* aBlock) {
+ INPQ_LOG("scheduling main thread timeout for target %p\n", aTarget.get());
+ aBlock->StartContentResponseTimer();
+ aTarget->PostDelayedTask(NewRunnableMethod<uint64_t>(this,
+ &InputQueue::MainThreadTimeout,
+ aBlock->GetBlockId()),
+ gfxPrefs::APZContentResponseTimeout());
+}
+
+CancelableBlockState*
+InputQueue::FindBlockForId(uint64_t aInputBlockId,
+ InputData** aOutFirstInput)
+{
+ for (const auto& queuedInput : mQueuedInputs) {
+ if (queuedInput->Block()->GetBlockId() == aInputBlockId) {
+ if (aOutFirstInput) {
+ *aOutFirstInput = queuedInput->Input();
+ }
+ return queuedInput->Block();
+ }
+ }
+
+ CancelableBlockState* block = nullptr;
+ if (mActiveTouchBlock && mActiveTouchBlock->GetBlockId() == aInputBlockId) {
+ block = mActiveTouchBlock.get();
+ } else if (mActiveWheelBlock && mActiveWheelBlock->GetBlockId() == aInputBlockId) {
+ block = mActiveWheelBlock.get();
+ } else if (mActiveDragBlock && mActiveDragBlock->GetBlockId() == aInputBlockId) {
+ block = mActiveDragBlock.get();
+ } else if (mActivePanGestureBlock && mActivePanGestureBlock->GetBlockId() == aInputBlockId) {
+ block = mActivePanGestureBlock.get();
+ }
+ // Since we didn't encounter this block while iterating through mQueuedInputs,
+ // it must have no events associated with it at the moment.
+ if (aOutFirstInput) {
+ *aOutFirstInput = nullptr;
+ }
+ return block;
+}
+
+void
+InputQueue::MainThreadTimeout(uint64_t aInputBlockId) {
+ APZThreadUtils::AssertOnControllerThread();
+
+ INPQ_LOG("got a main thread timeout; block=%" PRIu64 "\n", aInputBlockId);
+ bool success = false;
+ InputData* firstInput = nullptr;
+ CancelableBlockState* block = FindBlockForId(aInputBlockId, &firstInput);
+ if (block) {
+ // time out the touch-listener response and also confirm the existing
+ // target apzc in the case where the main thread doesn't get back to us
+ // fast enough.
+ success = block->TimeoutContentResponse();
+ success |= block->SetConfirmedTargetApzc(
+ block->GetTargetApzc(),
+ InputBlockState::TargetConfirmationState::eTimedOut,
+ firstInput);
+ }
+ if (success) {
+ ProcessQueue();
+ }
+}
+
+void
+InputQueue::ContentReceivedInputBlock(uint64_t aInputBlockId, bool aPreventDefault) {
+ APZThreadUtils::AssertOnControllerThread();
+
+ INPQ_LOG("got a content response; block=%" PRIu64 "\n", aInputBlockId);
+ bool success = false;
+ CancelableBlockState* block = FindBlockForId(aInputBlockId, nullptr);
+ if (block) {
+ success = block->SetContentResponse(aPreventDefault);
+ block->RecordContentResponseTime();
+ }
+ if (success) {
+ ProcessQueue();
+ }
+}
+
+void
+InputQueue::SetConfirmedTargetApzc(uint64_t aInputBlockId, const RefPtr<AsyncPanZoomController>& aTargetApzc) {
+ APZThreadUtils::AssertOnControllerThread();
+
+ INPQ_LOG("got a target apzc; block=%" PRIu64 " guid=%s\n",
+ aInputBlockId, aTargetApzc ? Stringify(aTargetApzc->GetGuid()).c_str() : "");
+ bool success = false;
+ InputData* firstInput = nullptr;
+ CancelableBlockState* block = FindBlockForId(aInputBlockId, &firstInput);
+ if (block) {
+ success = block->SetConfirmedTargetApzc(aTargetApzc,
+ InputBlockState::TargetConfirmationState::eConfirmed,
+ firstInput);
+ block->RecordContentResponseTime();
+ }
+ if (success) {
+ ProcessQueue();
+ }
+}
+
+void
+InputQueue::ConfirmDragBlock(uint64_t aInputBlockId, const RefPtr<AsyncPanZoomController>& aTargetApzc,
+ const AsyncDragMetrics& aDragMetrics)
+{
+ APZThreadUtils::AssertOnControllerThread();
+
+ INPQ_LOG("got a target apzc; block=%" PRIu64 " guid=%s\n",
+ aInputBlockId, aTargetApzc ? Stringify(aTargetApzc->GetGuid()).c_str() : "");
+ bool success = false;
+ InputData* firstInput = nullptr;
+ CancelableBlockState* block = FindBlockForId(aInputBlockId, &firstInput);
+ if (block && block->AsDragBlock()) {
+ block->AsDragBlock()->SetDragMetrics(aDragMetrics);
+ success = block->SetConfirmedTargetApzc(aTargetApzc,
+ InputBlockState::TargetConfirmationState::eConfirmed,
+ firstInput);
+ block->RecordContentResponseTime();
+ }
+ if (success) {
+ ProcessQueue();
+ }
+}
+
+void
+InputQueue::SetAllowedTouchBehavior(uint64_t aInputBlockId, const nsTArray<TouchBehaviorFlags>& aBehaviors) {
+ APZThreadUtils::AssertOnControllerThread();
+
+ INPQ_LOG("got allowed touch behaviours; block=%" PRIu64 "\n", aInputBlockId);
+ bool success = false;
+ CancelableBlockState* block = FindBlockForId(aInputBlockId, nullptr);
+ if (block && block->AsTouchBlock()) {
+ success = block->AsTouchBlock()->SetAllowedTouchBehaviors(aBehaviors);
+ block->RecordContentResponseTime();
+ } else if (block) {
+ NS_WARNING("input block is not a touch block");
+ }
+ if (success) {
+ ProcessQueue();
+ }
+}
+
+void
+InputQueue::ProcessQueue() {
+ APZThreadUtils::AssertOnControllerThread();
+
+ while (!mQueuedInputs.IsEmpty()) {
+ CancelableBlockState* curBlock = mQueuedInputs[0]->Block();
+ if (!curBlock->IsReadyForHandling()) {
+ break;
+ }
+
+ INPQ_LOG("processing input from block %p; preventDefault %d target %p\n",
+ curBlock, curBlock->IsDefaultPrevented(),
+ curBlock->GetTargetApzc().get());
+ RefPtr<AsyncPanZoomController> target = curBlock->GetTargetApzc();
+ // target may be null here if the initial target was unconfirmed and then
+ // we later got a confirmed null target. in that case drop the events.
+ if (target) {
+ if (curBlock->IsDefaultPrevented()) {
+ if (curBlock->AsTouchBlock()) {
+ target->ResetTouchInputState();
+ }
+ } else {
+ UpdateActiveApzc(target);
+ curBlock->DispatchEvent(*(mQueuedInputs[0]->Input()));
+ }
+ }
+ mQueuedInputs.RemoveElementAt(0);
+ }
+
+ if (CanDiscardBlock(mActiveTouchBlock)) {
+ mActiveTouchBlock = nullptr;
+ }
+ if (CanDiscardBlock(mActiveWheelBlock)) {
+ mActiveWheelBlock = nullptr;
+ }
+ if (CanDiscardBlock(mActiveDragBlock)) {
+ mActiveDragBlock = nullptr;
+ }
+ if (CanDiscardBlock(mActivePanGestureBlock)) {
+ mActivePanGestureBlock = nullptr;
+ }
+}
+
+bool
+InputQueue::CanDiscardBlock(CancelableBlockState* aBlock)
+{
+ if (!aBlock ||
+ !aBlock->IsReadyForHandling() ||
+ aBlock->MustStayActive()) {
+ return false;
+ }
+ InputData* firstInput = nullptr;
+ FindBlockForId(aBlock->GetBlockId(), &firstInput);
+ if (firstInput) {
+ // The block has at least one input event still in the queue, so it's
+ // not depleted
+ return false;
+ }
+ return true;
+}
+
+void
+InputQueue::UpdateActiveApzc(const RefPtr<AsyncPanZoomController>& aNewActive) {
+ if (mLastActiveApzc && mLastActiveApzc != aNewActive
+ && mTouchCounter.GetActiveTouchCount() > 0) {
+ mLastActiveApzc->ResetTouchInputState();
+ }
+ mLastActiveApzc = aNewActive;
+}
+
+void
+InputQueue::Clear()
+{
+ APZThreadUtils::AssertOnControllerThread();
+
+ mQueuedInputs.Clear();
+ mActiveTouchBlock = nullptr;
+ mActiveWheelBlock = nullptr;
+ mActiveDragBlock = nullptr;
+ mActivePanGestureBlock = nullptr;
+ mLastActiveApzc = nullptr;
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/apz/src/InputQueue.h b/gfx/layers/apz/src/InputQueue.h
new file mode 100644
index 000000000..eaf9b20bc
--- /dev/null
+++ b/gfx/layers/apz/src/InputQueue.h
@@ -0,0 +1,217 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_layers_InputQueue_h
+#define mozilla_layers_InputQueue_h
+
+#include "APZUtils.h"
+#include "DragTracker.h"
+#include "InputData.h"
+#include "mozilla/EventForwards.h"
+#include "mozilla/RefPtr.h"
+#include "mozilla/UniquePtr.h"
+#include "nsTArray.h"
+#include "TouchCounter.h"
+
+namespace mozilla {
+
+class InputData;
+class MultiTouchInput;
+class ScrollWheelInput;
+
+namespace layers {
+
+class AsyncPanZoomController;
+class CancelableBlockState;
+class TouchBlockState;
+class WheelBlockState;
+class DragBlockState;
+class PanGestureBlockState;
+class AsyncDragMetrics;
+class QueuedInput;
+
+/**
+ * This class stores incoming input events, associated with "input blocks", until
+ * they are ready for handling.
+ */
+class InputQueue {
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(InputQueue)
+
+public:
+ InputQueue();
+
+ /**
+ * Notifies the InputQueue of a new incoming input event. The APZC that the
+ * input event was targeted to should be provided in the |aTarget| parameter.
+ * See the documentation on APZCTreeManager::ReceiveInputEvent for info on
+ * return values from this function, including |aOutInputBlockId|.
+ */
+ nsEventStatus ReceiveInputEvent(const RefPtr<AsyncPanZoomController>& aTarget,
+ bool aTargetConfirmed,
+ const InputData& aEvent,
+ uint64_t* aOutInputBlockId);
+ /**
+ * This function should be invoked to notify the InputQueue when web content
+ * decides whether or not it wants to cancel a block of events. The block
+ * id to which this applies should be provided in |aInputBlockId|.
+ */
+ void ContentReceivedInputBlock(uint64_t aInputBlockId, bool aPreventDefault);
+ /**
+ * This function should be invoked to notify the InputQueue once the target
+ * APZC to handle an input block has been confirmed. In practice this should
+ * generally be decidable upon receipt of the input event, but in some cases
+ * we may need to query the layout engine to know for sure. The input block
+ * this applies to should be specified via the |aInputBlockId| parameter.
+ */
+ void SetConfirmedTargetApzc(uint64_t aInputBlockId, const RefPtr<AsyncPanZoomController>& aTargetApzc);
+ /**
+ * This function is invoked to confirm that the drag block should be handled
+ * by the APZ.
+ */
+ void ConfirmDragBlock(uint64_t aInputBlockId,
+ const RefPtr<AsyncPanZoomController>& aTargetApzc,
+ const AsyncDragMetrics& aDragMetrics);
+ /**
+ * This function should be invoked to notify the InputQueue of the touch-
+ * action properties for the different touch points in an input block. The
+ * input block this applies to should be specified by the |aInputBlockId|
+ * parameter. If touch-action is not enabled on the platform, this function
+ * does nothing and need not be called.
+ */
+ void SetAllowedTouchBehavior(uint64_t aInputBlockId, const nsTArray<TouchBehaviorFlags>& aBehaviors);
+ /**
+ * Adds a new touch block at the end of the input queue that has the same
+ * allowed touch behaviour flags as the the touch block currently being
+ * processed. This should only be called when processing of a touch block
+ * triggers the creation of a new touch block. Returns the input block id
+ * of the the newly-created block.
+ */
+ uint64_t InjectNewTouchBlock(AsyncPanZoomController* aTarget);
+ /**
+ * Returns the pending input block at the head of the queue, if there is one.
+ * This may return null if there all input events have been processed.
+ */
+ CancelableBlockState* GetCurrentBlock() const;
+ /*
+ * Returns the current pending input block as a specific kind of block. If
+ * GetCurrentBlock() returns null, these functions additionally check the
+ * mActiveXXXBlock field of the corresponding input type to see if there is
+ * a depleted but still active input block, and returns that if found. These
+ * functions may return null if no block is found.
+ */
+ TouchBlockState* GetCurrentTouchBlock() const;
+ WheelBlockState* GetCurrentWheelBlock() const;
+ DragBlockState* GetCurrentDragBlock() const;
+ PanGestureBlockState* GetCurrentPanGestureBlock() const;
+ /**
+ * Returns true iff the pending block at the head of the queue is a touch
+ * block and is ready for handling.
+ */
+ bool HasReadyTouchBlock() const;
+ /**
+ * If there is an active wheel transaction, returns the WheelBlockState
+ * representing the transaction. Otherwise, returns null. "Active" in this
+ * function name is the same kind of "active" as in mActiveWheelBlock - that
+ * is, new incoming wheel events will go into the "active" block.
+ */
+ WheelBlockState* GetActiveWheelTransaction() const;
+ /**
+ * Remove all input blocks from the input queue.
+ */
+ void Clear();
+ /**
+ * Whether the current pending block allows scroll handoff.
+ */
+ bool AllowScrollHandoff() const;
+ /**
+ * If there is currently a drag in progress, return whether or not it was
+ * targeted at a scrollbar. If the drag was newly-created and doesn't know,
+ * use the provided |aOnScrollbar| to populate that information.
+ */
+ bool IsDragOnScrollbar(bool aOnScrollbar);
+
+private:
+ ~InputQueue();
+
+ TouchBlockState* StartNewTouchBlock(const RefPtr<AsyncPanZoomController>& aTarget,
+ bool aTargetConfirmed,
+ bool aCopyPropertiesFromCurrent);
+
+ /**
+ * If animations are present for the current pending input block, cancel
+ * them as soon as possible.
+ */
+ void CancelAnimationsForNewBlock(CancelableBlockState* aBlock);
+
+ /**
+ * If we need to wait for a content response, schedule that now.
+ */
+ void MaybeRequestContentResponse(const RefPtr<AsyncPanZoomController>& aTarget,
+ CancelableBlockState* aBlock);
+
+ nsEventStatus ReceiveTouchInput(const RefPtr<AsyncPanZoomController>& aTarget,
+ bool aTargetConfirmed,
+ const MultiTouchInput& aEvent,
+ uint64_t* aOutInputBlockId);
+ nsEventStatus ReceiveMouseInput(const RefPtr<AsyncPanZoomController>& aTarget,
+ bool aTargetConfirmed,
+ const MouseInput& aEvent,
+ uint64_t* aOutInputBlockId);
+ nsEventStatus ReceiveScrollWheelInput(const RefPtr<AsyncPanZoomController>& aTarget,
+ bool aTargetConfirmed,
+ const ScrollWheelInput& aEvent,
+ uint64_t* aOutInputBlockId);
+ nsEventStatus ReceivePanGestureInput(const RefPtr<AsyncPanZoomController>& aTarget,
+ bool aTargetConfirmed,
+ const PanGestureInput& aEvent,
+ uint64_t* aOutInputBlockId);
+
+ /**
+ * Helper function that searches mQueuedInputs for the first block matching
+ * the given id, and returns it. If |aOutFirstInput| is non-null, it is
+ * populated with a pointer to the first input in mQueuedInputs that
+ * corresponds to the block, or null if no such input was found. Note that
+ * even if there are no inputs in mQueuedInputs, this function can return
+ * non-null if the block id provided matches one of the depleted-but-still-
+ * active blocks (mActiveTouchBlock, mActiveWheelBlock, etc.).
+ */
+ CancelableBlockState* FindBlockForId(uint64_t aInputBlockId,
+ InputData** aOutFirstInput);
+ void ScheduleMainThreadTimeout(const RefPtr<AsyncPanZoomController>& aTarget,
+ CancelableBlockState* aBlock);
+ void MainThreadTimeout(uint64_t aInputBlockId);
+ void ProcessQueue();
+ bool CanDiscardBlock(CancelableBlockState* aBlock);
+ void UpdateActiveApzc(const RefPtr<AsyncPanZoomController>& aNewActive);
+
+private:
+ // The queue of input events that have not yet been fully processed.
+ // This member must only be accessed on the controller/UI thread.
+ nsTArray<UniquePtr<QueuedInput>> mQueuedInputs;
+
+ // These are the most recently created blocks of each input type. They are
+ // "active" in the sense that new inputs of that type are associated with
+ // them. Note that these pointers may be null if no inputs of the type have
+ // arrived, or if the inputs for the type formed a complete block that was
+ // then discarded.
+ RefPtr<TouchBlockState> mActiveTouchBlock;
+ RefPtr<WheelBlockState> mActiveWheelBlock;
+ RefPtr<DragBlockState> mActiveDragBlock;
+ RefPtr<PanGestureBlockState> mActivePanGestureBlock;
+
+ // The APZC to which the last event was delivered
+ RefPtr<AsyncPanZoomController> mLastActiveApzc;
+
+ // Track touches so we know when to clear mLastActiveApzc
+ TouchCounter mTouchCounter;
+
+ // Track mouse inputs so we know if we're in a drag or not
+ DragTracker mDragTracker;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif // mozilla_layers_InputQueue_h
diff --git a/gfx/layers/apz/src/Overscroll.h b/gfx/layers/apz/src/Overscroll.h
new file mode 100644
index 000000000..586f104cc
--- /dev/null
+++ b/gfx/layers/apz/src/Overscroll.h
@@ -0,0 +1,137 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set sw=2 ts=8 et tw=80 : */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_layers_Overscroll_h
+#define mozilla_layers_Overscroll_h
+
+#include "AsyncPanZoomAnimation.h"
+#include "AsyncPanZoomController.h"
+#include "FrameMetrics.h"
+#include "mozilla/TimeStamp.h"
+#include "nsThreadUtils.h"
+
+namespace mozilla {
+namespace layers {
+
+// Animation used by GenericOverscrollEffect.
+class OverscrollAnimation: public AsyncPanZoomAnimation {
+public:
+ explicit OverscrollAnimation(AsyncPanZoomController& aApzc, const ParentLayerPoint& aVelocity)
+ : mApzc(aApzc)
+ {
+ mApzc.mX.StartOverscrollAnimation(aVelocity.x);
+ mApzc.mY.StartOverscrollAnimation(aVelocity.y);
+ }
+ ~OverscrollAnimation()
+ {
+ mApzc.mX.EndOverscrollAnimation();
+ mApzc.mY.EndOverscrollAnimation();
+ }
+
+ virtual bool DoSample(FrameMetrics& aFrameMetrics,
+ const TimeDuration& aDelta) override
+ {
+ // Can't inline these variables due to short-circuit evaluation.
+ bool continueX = mApzc.mX.SampleOverscrollAnimation(aDelta);
+ bool continueY = mApzc.mY.SampleOverscrollAnimation(aDelta);
+ if (!continueX && !continueY) {
+ // If we got into overscroll from a fling, that fling did not request a
+ // fling snap to avoid a resulting scrollTo from cancelling the overscroll
+ // animation too early. We do still want to request a fling snap, though,
+ // in case the end of the axis at which we're overscrolled is not a valid
+ // snap point, so we request one now. If there are no snap points, this will
+ // do nothing. If there are snap points, we'll get a scrollTo that snaps us
+ // back to the nearest valid snap point.
+ // The scroll snapping is done in a deferred task, otherwise the state
+ // change to NOTHING caused by the overscroll animation ending would
+ // clobber a possible state change to SMOOTH_SCROLL in ScrollSnap().
+ mDeferredTasks.AppendElement(NewRunnableMethod(&mApzc, &AsyncPanZoomController::ScrollSnap));
+ return false;
+ }
+ return true;
+ }
+
+ virtual bool WantsRepaints() override
+ {
+ return false;
+ }
+
+private:
+ AsyncPanZoomController& mApzc;
+};
+
+// Base class for different overscroll effects;
+class OverscrollEffectBase {
+public:
+ virtual ~OverscrollEffectBase() {}
+ virtual void ConsumeOverscroll(ParentLayerPoint& aOverscroll,
+ bool aShouldOverscrollX,
+ bool aShouldOverscrollY) = 0;
+ virtual void HandleFlingOverscroll(const ParentLayerPoint& aVelocity) = 0;
+};
+
+// A generic overscroll effect, implemented by AsyncPanZoomController itself.
+class GenericOverscrollEffect : public OverscrollEffectBase {
+public:
+ explicit GenericOverscrollEffect(AsyncPanZoomController& aApzc) : mApzc(aApzc) {}
+
+ void ConsumeOverscroll(ParentLayerPoint& aOverscroll,
+ bool aShouldOverscrollX,
+ bool aShouldOverscrollY) override {
+ if (aShouldOverscrollX) {
+ mApzc.mX.OverscrollBy(aOverscroll.x);
+ aOverscroll.x = 0;
+ }
+
+ if (aShouldOverscrollY) {
+ mApzc.mY.OverscrollBy(aOverscroll.y);
+ aOverscroll.y = 0;
+ }
+
+ if (aShouldOverscrollX || aShouldOverscrollY) {
+ mApzc.ScheduleComposite();
+ }
+ }
+
+ void HandleFlingOverscroll(const ParentLayerPoint& aVelocity) override {
+ mApzc.StartOverscrollAnimation(aVelocity);
+ }
+
+private:
+ AsyncPanZoomController& mApzc;
+};
+
+// A widget-specific overscroll effect, implemented by the widget via
+// GeckoContentController.
+class WidgetOverscrollEffect : public OverscrollEffectBase {
+public:
+ explicit WidgetOverscrollEffect(AsyncPanZoomController& aApzc) : mApzc(aApzc) {}
+
+ void ConsumeOverscroll(ParentLayerPoint& aOverscroll,
+ bool aShouldOverscrollX,
+ bool aShouldOverscrollY) override {
+ RefPtr<GeckoContentController> controller = mApzc.GetGeckoContentController();
+ if (controller && (aShouldOverscrollX || aShouldOverscrollY)) {
+ controller->UpdateOverscrollOffset(aOverscroll.x, aOverscroll.y, mApzc.IsRootContent());
+ aOverscroll = ParentLayerPoint();
+ }
+ }
+
+ void HandleFlingOverscroll(const ParentLayerPoint& aVelocity) override {
+ RefPtr<GeckoContentController> controller = mApzc.GetGeckoContentController();
+ if (controller) {
+ controller->UpdateOverscrollVelocity(aVelocity.x, aVelocity.y, mApzc.IsRootContent());
+ }
+ }
+
+private:
+ AsyncPanZoomController& mApzc;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif // mozilla_layers_Overscroll_h
diff --git a/gfx/layers/apz/src/OverscrollHandoffState.cpp b/gfx/layers/apz/src/OverscrollHandoffState.cpp
new file mode 100644
index 000000000..577303fdd
--- /dev/null
+++ b/gfx/layers/apz/src/OverscrollHandoffState.cpp
@@ -0,0 +1,175 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set sw=2 ts=8 et tw=80 : */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "OverscrollHandoffState.h"
+
+#include <algorithm> // for std::stable_sort
+#include "mozilla/Assertions.h"
+#include "AsyncPanZoomController.h"
+
+namespace mozilla {
+namespace layers {
+
+OverscrollHandoffChain::~OverscrollHandoffChain() {}
+
+void
+OverscrollHandoffChain::Add(AsyncPanZoomController* aApzc)
+{
+ mChain.push_back(aApzc);
+}
+
+struct CompareByScrollPriority
+{
+ bool operator()(const RefPtr<AsyncPanZoomController>& a,
+ const RefPtr<AsyncPanZoomController>& b) const
+ {
+ return a->HasScrollgrab() && !b->HasScrollgrab();
+ }
+};
+
+void
+OverscrollHandoffChain::SortByScrollPriority()
+{
+ // The sorting being stable ensures that the relative order between
+ // non-scrollgrabbing APZCs remains child -> parent.
+ // (The relative order between scrollgrabbing APZCs will also remain
+ // child -> parent, though that's just an artefact of the implementation
+ // and users of 'scrollgrab' should not rely on this.)
+ std::stable_sort(mChain.begin(), mChain.end(), CompareByScrollPriority());
+}
+
+const RefPtr<AsyncPanZoomController>&
+OverscrollHandoffChain::GetApzcAtIndex(uint32_t aIndex) const
+{
+ MOZ_ASSERT(aIndex < Length());
+ return mChain[aIndex];
+}
+
+uint32_t
+OverscrollHandoffChain::IndexOf(const AsyncPanZoomController* aApzc) const
+{
+ uint32_t i;
+ for (i = 0; i < Length(); ++i) {
+ if (mChain[i] == aApzc) {
+ break;
+ }
+ }
+ return i;
+}
+
+void
+OverscrollHandoffChain::ForEachApzc(APZCMethod aMethod) const
+{
+ for (uint32_t i = 0; i < Length(); ++i) {
+ (mChain[i]->*aMethod)();
+ }
+}
+
+bool
+OverscrollHandoffChain::AnyApzc(APZCPredicate aPredicate) const
+{
+ MOZ_ASSERT(Length() > 0);
+ for (uint32_t i = 0; i < Length(); ++i) {
+ if ((mChain[i]->*aPredicate)()) {
+ return true;
+ }
+ }
+ return false;
+}
+
+void
+OverscrollHandoffChain::FlushRepaints() const
+{
+ ForEachApzc(&AsyncPanZoomController::FlushRepaintForOverscrollHandoff);
+}
+
+void
+OverscrollHandoffChain::CancelAnimations(CancelAnimationFlags aFlags) const
+{
+ MOZ_ASSERT(Length() > 0);
+ for (uint32_t i = 0; i < Length(); ++i) {
+ mChain[i]->CancelAnimation(aFlags);
+ }
+}
+
+void
+OverscrollHandoffChain::ClearOverscroll() const
+{
+ ForEachApzc(&AsyncPanZoomController::ClearOverscroll);
+}
+
+void
+OverscrollHandoffChain::SnapBackOverscrolledApzc(const AsyncPanZoomController* aStart) const
+{
+ uint32_t i = IndexOf(aStart);
+ for (; i < Length(); ++i) {
+ AsyncPanZoomController* apzc = mChain[i];
+ if (!apzc->IsDestroyed()) {
+ apzc->SnapBackIfOverscrolled();
+ }
+ }
+}
+
+bool
+OverscrollHandoffChain::CanBePanned(const AsyncPanZoomController* aApzc) const
+{
+ // Find |aApzc| in the handoff chain.
+ uint32_t i = IndexOf(aApzc);
+
+ // See whether any APZC in the handoff chain starting from |aApzc|
+ // has room to be panned.
+ for (uint32_t j = i; j < Length(); ++j) {
+ if (mChain[j]->IsPannable()) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+bool
+OverscrollHandoffChain::CanScrollInDirection(const AsyncPanZoomController* aApzc,
+ Layer::ScrollDirection aDirection) const
+{
+ // Find |aApzc| in the handoff chain.
+ uint32_t i = IndexOf(aApzc);
+
+ // See whether any APZC in the handoff chain starting from |aApzc|
+ // has room to scroll in the given direction.
+ for (uint32_t j = i; j < Length(); ++j) {
+ if (mChain[j]->CanScroll(aDirection)) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+bool
+OverscrollHandoffChain::HasOverscrolledApzc() const
+{
+ return AnyApzc(&AsyncPanZoomController::IsOverscrolled);
+}
+
+bool
+OverscrollHandoffChain::HasFastFlungApzc() const
+{
+ return AnyApzc(&AsyncPanZoomController::IsFlingingFast);
+}
+
+RefPtr<AsyncPanZoomController>
+OverscrollHandoffChain::FindFirstScrollable(const InputData& aInput) const
+{
+ for (size_t i = 0; i < Length(); i++) {
+ if (mChain[i]->CanScroll(aInput)) {
+ return mChain[i];
+ }
+ }
+ return nullptr;
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/apz/src/OverscrollHandoffState.h b/gfx/layers/apz/src/OverscrollHandoffState.h
new file mode 100644
index 000000000..173d6bddd
--- /dev/null
+++ b/gfx/layers/apz/src/OverscrollHandoffState.h
@@ -0,0 +1,159 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set sw=2 ts=8 et tw=80 : */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_layers_OverscrollHandoffChain_h
+#define mozilla_layers_OverscrollHandoffChain_h
+
+#include <vector>
+#include "mozilla/RefPtr.h" // for RefPtr
+#include "nsISupportsImpl.h" // for NS_INLINE_DECL_THREADSAFE_REFCOUNTING
+#include "APZUtils.h" // for CancelAnimationFlags
+#include "Layers.h" // for Layer::ScrollDirection
+#include "Units.h" // for ScreenPoint
+
+namespace mozilla {
+
+class InputData;
+
+namespace layers {
+
+class AsyncPanZoomController;
+
+/**
+ * This class represents the chain of APZCs along which overscroll is handed off.
+ * It is created by APZCTreeManager by starting from an initial APZC which is
+ * the target for input events, and following the scroll parent ID links (often
+ * but not always corresponding to parent pointers in the APZC tree), then
+ * adjusting for scrollgrab.
+ */
+class OverscrollHandoffChain
+{
+protected:
+ // Reference-counted classes cannot have public destructors.
+ ~OverscrollHandoffChain();
+public:
+ // Threadsafe so that the controller and compositor threads can both maintain
+ // nsRefPtrs to the same handoff chain.
+ // Mutable so that we can pass around the class by
+ // RefPtr<const OverscrollHandoffChain> and thus enforce that, once built,
+ // the chain is not modified.
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(OverscrollHandoffChain)
+
+ /*
+ * Methods for building the handoff chain.
+ * These should be used only by AsyncPanZoomController::BuildOverscrollHandoffChain().
+ */
+ void Add(AsyncPanZoomController* aApzc);
+ void SortByScrollPriority();
+
+ /*
+ * Methods for accessing the handoff chain.
+ */
+ uint32_t Length() const { return mChain.size(); }
+ const RefPtr<AsyncPanZoomController>& GetApzcAtIndex(uint32_t aIndex) const;
+ // Returns Length() if |aApzc| is not on this chain.
+ uint32_t IndexOf(const AsyncPanZoomController* aApzc) const;
+
+ /*
+ * Convenience methods for performing operations on APZCs in the chain.
+ */
+
+ // Flush repaints all the way up the chain.
+ void FlushRepaints() const;
+
+ // Cancel animations all the way up the chain.
+ void CancelAnimations(CancelAnimationFlags aFlags = Default) const;
+
+ // Clear overscroll all the way up the chain.
+ void ClearOverscroll() const;
+
+ // Snap back the APZC that is overscrolled on the subset of the chain from
+ // |aStart| onwards, if any.
+ void SnapBackOverscrolledApzc(const AsyncPanZoomController* aStart) const;
+
+ // Determine whether the given APZC, or any APZC further in the chain,
+ // has room to be panned.
+ bool CanBePanned(const AsyncPanZoomController* aApzc) const;
+
+ // Determine whether the given APZC, or any APZC further in the chain,
+ // can scroll in the given direction.
+ bool CanScrollInDirection(const AsyncPanZoomController* aApzc,
+ Layer::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;
+
+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;
+};
+
+/**
+ * This class groups the state maintained during overscroll handoff.
+ */
+struct OverscrollHandoffState {
+ OverscrollHandoffState(const OverscrollHandoffChain& aChain,
+ const ScreenPoint& aPanDistance,
+ ScrollSource aScrollSource)
+ : mChain(aChain),
+ mChainIndex(0),
+ mPanDistance(aPanDistance),
+ mScrollSource(aScrollSource)
+ {}
+
+ // The chain of APZCs along which we hand off scroll.
+ // This is const to indicate that the chain does not change over the
+ // course of handoff.
+ const OverscrollHandoffChain& mChain;
+
+ // The index of the APZC in the chain that we are currently giving scroll to.
+ // This is non-const to indicate that this changes over the course of handoff.
+ uint32_t mChainIndex;
+
+ // The total distance since touch-start of the pan that triggered the
+ // handoff. This is const to indicate that it does not change over the
+ // course of handoff.
+ // The x/y components of this are non-negative.
+ const ScreenPoint mPanDistance;
+
+ ScrollSource mScrollSource;
+};
+
+/*
+ * This class groups the state maintained during fling handoff.
+ */
+struct FlingHandoffState {
+ // The velocity of the fling being handed off.
+ ParentLayerPoint mVelocity;
+
+ // The chain of APZCs along which we hand off the fling.
+ // Unlike in OverscrollHandoffState, this is stored by RefPtr because
+ // otherwise it may not stay alive for the entire handoff.
+ RefPtr<const OverscrollHandoffChain> mChain;
+
+ // Whether handoff has happened by this point, or we're still process
+ // the original fling.
+ bool mIsHandoff;
+
+ // The single APZC that was scrolled by the pan that started this fling.
+ // The fling is only allowed to scroll this APZC, too.
+ // Used only if immediate scroll handoff is disallowed.
+ RefPtr<const AsyncPanZoomController> mScrolledApzc;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif /* mozilla_layers_OverscrollHandoffChain_h */
diff --git a/gfx/layers/apz/src/PotentialCheckerboardDurationTracker.cpp b/gfx/layers/apz/src/PotentialCheckerboardDurationTracker.cpp
new file mode 100644
index 000000000..c83b9f45c
--- /dev/null
+++ b/gfx/layers/apz/src/PotentialCheckerboardDurationTracker.cpp
@@ -0,0 +1,79 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set sw=2 ts=8 et tw=80 : */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "PotentialCheckerboardDurationTracker.h"
+
+#include "mozilla/Telemetry.h" // for Telemetry
+
+namespace mozilla {
+namespace layers {
+
+PotentialCheckerboardDurationTracker::PotentialCheckerboardDurationTracker()
+ : mInCheckerboard(false)
+ , mInTransform(false)
+{
+}
+
+void
+PotentialCheckerboardDurationTracker::CheckerboardSeen()
+{
+ // This might get called while mInCheckerboard is already true
+ if (!Tracking()) {
+ mCurrentPeriodStart = TimeStamp::Now();
+ }
+ mInCheckerboard = true;
+}
+
+void
+PotentialCheckerboardDurationTracker::CheckerboardDone()
+{
+ MOZ_ASSERT(Tracking());
+ mInCheckerboard = false;
+ if (!Tracking()) {
+ mozilla::Telemetry::AccumulateTimeDelta(
+ mozilla::Telemetry::CHECKERBOARD_POTENTIAL_DURATION,
+ mCurrentPeriodStart);
+ }
+}
+
+void
+PotentialCheckerboardDurationTracker::InTransform(bool aInTransform)
+{
+ if (aInTransform == mInTransform) {
+ // no-op
+ return;
+ }
+
+ if (!Tracking()) {
+ // Because !Tracking(), mInTransform must be false, and so aInTransform
+ // must be true (or we would have early-exited this function already).
+ // Therefore, we are starting a potential checkerboard period.
+ mInTransform = aInTransform;
+ mCurrentPeriodStart = TimeStamp::Now();
+ return;
+ }
+
+ mInTransform = aInTransform;
+
+ if (!Tracking()) {
+ // Tracking() must have been true at the start of this function, or we
+ // would have taken the other !Tracking branch above. If it's false now,
+ // it means we just stopped tracking, so we are ending a potential
+ // checkerboard period.
+ mozilla::Telemetry::AccumulateTimeDelta(
+ mozilla::Telemetry::CHECKERBOARD_POTENTIAL_DURATION,
+ mCurrentPeriodStart);
+ }
+}
+
+bool
+PotentialCheckerboardDurationTracker::Tracking() const
+{
+ return mInTransform || mInCheckerboard;
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/apz/src/PotentialCheckerboardDurationTracker.h b/gfx/layers/apz/src/PotentialCheckerboardDurationTracker.h
new file mode 100644
index 000000000..6154003ad
--- /dev/null
+++ b/gfx/layers/apz/src/PotentialCheckerboardDurationTracker.h
@@ -0,0 +1,60 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_layers_PotentialCheckerboardDurationTracker_h
+#define mozilla_layers_PotentialCheckerboardDurationTracker_h
+
+#include "mozilla/TimeStamp.h"
+
+namespace mozilla {
+namespace layers {
+
+/**
+ * This class allows the owner to track the duration of time considered
+ * "potentially checkerboarding". This is the union of two possibly-intersecting
+ * sets of time periods. The first set is that in which checkerboarding was
+ * actually happening, since by definition it could potentially be happening.
+ * The second set is that in which the APZC is actively transforming content
+ * in the compositor, since it could potentially transform it so as to display
+ * checkerboarding to the user.
+ * The caller of this class calls the appropriate methods to indicate the start
+ * and stop of these two sets, and this class manages accumulating the union
+ * of the various durations.
+ */
+class PotentialCheckerboardDurationTracker {
+public:
+ PotentialCheckerboardDurationTracker();
+
+ /**
+ * This should be called if checkerboarding is encountered. It can be called
+ * multiple times during a checkerboard event.
+ */
+ void CheckerboardSeen();
+ /**
+ * This should be called when checkerboarding is done. It must have been
+ * preceded by one or more calls to CheckerboardSeen().
+ */
+ void CheckerboardDone();
+
+ /**
+ * This should be called at composition time, to indicate if the APZC is in
+ * a transforming state or not.
+ */
+ void InTransform(bool aInTransform);
+
+private:
+ bool Tracking() const;
+
+private:
+ bool mInCheckerboard;
+ bool mInTransform;
+
+ TimeStamp mCurrentPeriodStart;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif // mozilla_layers_PotentialCheckerboardDurationTracker_h
diff --git a/gfx/layers/apz/src/QueuedInput.cpp b/gfx/layers/apz/src/QueuedInput.cpp
new file mode 100644
index 000000000..21dd8e1b4
--- /dev/null
+++ b/gfx/layers/apz/src/QueuedInput.cpp
@@ -0,0 +1,54 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set sw=2 ts=8 et tw=80 : */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "QueuedInput.h"
+
+#include "AsyncPanZoomController.h"
+#include "InputBlockState.h"
+#include "InputData.h"
+#include "OverscrollHandoffState.h"
+
+namespace mozilla {
+namespace layers {
+
+QueuedInput::QueuedInput(const MultiTouchInput& aInput, TouchBlockState& aBlock)
+ : mInput(MakeUnique<MultiTouchInput>(aInput))
+ , mBlock(&aBlock)
+{
+}
+
+QueuedInput::QueuedInput(const ScrollWheelInput& aInput, WheelBlockState& aBlock)
+ : mInput(MakeUnique<ScrollWheelInput>(aInput))
+ , mBlock(&aBlock)
+{
+}
+
+QueuedInput::QueuedInput(const MouseInput& aInput, DragBlockState& aBlock)
+ : mInput(MakeUnique<MouseInput>(aInput))
+ , mBlock(&aBlock)
+{
+}
+
+QueuedInput::QueuedInput(const PanGestureInput& aInput, PanGestureBlockState& aBlock)
+ : mInput(MakeUnique<PanGestureInput>(aInput))
+ , mBlock(&aBlock)
+{
+}
+
+InputData*
+QueuedInput::Input()
+{
+ return mInput.get();
+}
+
+CancelableBlockState*
+QueuedInput::Block()
+{
+ return mBlock.get();
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/apz/src/QueuedInput.h b/gfx/layers/apz/src/QueuedInput.h
new file mode 100644
index 000000000..68dfbc3c5
--- /dev/null
+++ b/gfx/layers/apz/src/QueuedInput.h
@@ -0,0 +1,58 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set sw=2 ts=8 et tw=80 : */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_layers_QueuedInput_h
+#define mozilla_layers_QueuedInput_h
+
+#include "mozilla/RefPtr.h"
+#include "mozilla/UniquePtr.h"
+
+namespace mozilla {
+
+class InputData;
+class MultiTouchInput;
+class ScrollWheelInput;
+class MouseInput;
+class PanGestureInput;
+
+namespace layers {
+
+class CancelableBlockState;
+class TouchBlockState;
+class WheelBlockState;
+class DragBlockState;
+class PanGestureBlockState;
+
+/**
+ * This lightweight class holds a pointer to an input event that has not yet
+ * been completely processed, along with the input block that the input event
+ * is associated with.
+ */
+class QueuedInput
+{
+public:
+ QueuedInput(const MultiTouchInput& aInput, TouchBlockState& aBlock);
+ QueuedInput(const ScrollWheelInput& aInput, WheelBlockState& aBlock);
+ QueuedInput(const MouseInput& aInput, DragBlockState& aBlock);
+ QueuedInput(const PanGestureInput& aInput, PanGestureBlockState& aBlock);
+
+ InputData* Input();
+ CancelableBlockState* Block();
+
+private:
+ // A copy of the input event that is provided to the constructor. This must
+ // be non-null, and is owned by this QueuedInput instance (hence the
+ // UniquePtr).
+ UniquePtr<InputData> mInput;
+ // A pointer to the block that the input event is associated with. This must
+ // be non-null.
+ RefPtr<CancelableBlockState> mBlock;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif // mozilla_layers_QueuedInput_h
diff --git a/gfx/layers/apz/src/TouchCounter.cpp b/gfx/layers/apz/src/TouchCounter.cpp
new file mode 100644
index 000000000..96dc35dc7
--- /dev/null
+++ b/gfx/layers/apz/src/TouchCounter.cpp
@@ -0,0 +1,50 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "TouchCounter.h"
+
+#include "InputData.h"
+
+namespace mozilla {
+namespace layers {
+
+TouchCounter::TouchCounter()
+ : mActiveTouchCount(0)
+{
+}
+
+void
+TouchCounter::Update(const MultiTouchInput& aInput)
+{
+ switch (aInput.mType) {
+ case MultiTouchInput::MULTITOUCH_START:
+ // touch-start event contains all active touches of the current session
+ mActiveTouchCount = aInput.mTouches.Length();
+ break;
+ case MultiTouchInput::MULTITOUCH_END:
+ if (mActiveTouchCount >= aInput.mTouches.Length()) {
+ // touch-end event contains only released touches
+ mActiveTouchCount -= aInput.mTouches.Length();
+ } else {
+ NS_WARNING("Got an unexpected touchend/touchcancel");
+ mActiveTouchCount = 0;
+ }
+ break;
+ case MultiTouchInput::MULTITOUCH_CANCEL:
+ mActiveTouchCount = 0;
+ break;
+ default:
+ break;
+ }
+}
+
+uint32_t
+TouchCounter::GetActiveTouchCount() const
+{
+ return mActiveTouchCount;
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/apz/src/TouchCounter.h b/gfx/layers/apz/src/TouchCounter.h
new file mode 100644
index 000000000..f2c45486e
--- /dev/null
+++ b/gfx/layers/apz/src/TouchCounter.h
@@ -0,0 +1,33 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_layers_TouchCounter_h
+#define mozilla_layers_TouchCounter_h
+
+#include "mozilla/EventForwards.h"
+
+namespace mozilla {
+
+class MultiTouchInput;
+
+namespace layers {
+
+// TouchCounter simply tracks the number of active touch points. Feed it
+// your input events to update the internal state.
+class TouchCounter
+{
+public:
+ TouchCounter();
+ void Update(const MultiTouchInput& aInput);
+ uint32_t GetActiveTouchCount() const;
+
+private:
+ uint32_t mActiveTouchCount;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif /* mozilla_layers_TouchCounter_h */
diff --git a/gfx/layers/apz/src/WheelScrollAnimation.cpp b/gfx/layers/apz/src/WheelScrollAnimation.cpp
new file mode 100644
index 000000000..d7cb338e6
--- /dev/null
+++ b/gfx/layers/apz/src/WheelScrollAnimation.cpp
@@ -0,0 +1,119 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set sw=2 ts=8 et tw=80 : */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "WheelScrollAnimation.h"
+
+#include "AsyncPanZoomController.h"
+#include "gfxPrefs.h"
+#include "nsPoint.h"
+
+namespace mozilla {
+namespace layers {
+
+WheelScrollAnimation::WheelScrollAnimation(AsyncPanZoomController& aApzc,
+ const nsPoint& aInitialPosition,
+ ScrollWheelInput::ScrollDeltaType aDeltaType)
+ : AsyncScrollBase(aInitialPosition)
+ , mApzc(aApzc)
+ , mFinalDestination(aInitialPosition)
+ , mDeltaType(aDeltaType)
+{
+}
+
+void
+WheelScrollAnimation::Update(TimeStamp aTime, nsPoint aDelta, const nsSize& aCurrentVelocity)
+{
+ InitPreferences(aTime);
+
+ mFinalDestination += aDelta;
+
+ // Clamp the final destination to the scrollable area.
+ CSSPoint clamped = CSSPoint::FromAppUnits(mFinalDestination);
+ clamped.x = mApzc.mX.ClampOriginToScrollableRect(clamped.x);
+ clamped.y = mApzc.mY.ClampOriginToScrollableRect(clamped.y);
+ mFinalDestination = CSSPoint::ToAppUnits(clamped);
+
+ AsyncScrollBase::Update(aTime, mFinalDestination, aCurrentVelocity);
+}
+
+bool
+WheelScrollAnimation::DoSample(FrameMetrics& aFrameMetrics, const TimeDuration& aDelta)
+{
+ TimeStamp now = mApzc.GetFrameTime();
+ CSSToParentLayerScale2D zoom = aFrameMetrics.GetZoom();
+
+ // If the animation is finished, make sure the final position is correct by
+ // using one last displacement. Otherwise, compute the delta via the timing
+ // function as normal.
+ bool finished = IsFinished(now);
+ nsPoint sampledDest = finished
+ ? mDestination
+ : PositionAt(now);
+ ParentLayerPoint displacement =
+ (CSSPoint::FromAppUnits(sampledDest) - aFrameMetrics.GetScrollOffset()) * zoom;
+
+ if (finished) {
+ mApzc.mX.SetVelocity(0);
+ mApzc.mY.SetVelocity(0);
+ } else if (!IsZero(displacement)) {
+ // Velocity is measured in ParentLayerCoords / Milliseconds
+ float xVelocity = displacement.x / aDelta.ToMilliseconds();
+ float yVelocity = displacement.y / aDelta.ToMilliseconds();
+ mApzc.mX.SetVelocity(xVelocity);
+ mApzc.mY.SetVelocity(yVelocity);
+ }
+
+ // Note: we ignore overscroll for wheel animations.
+ ParentLayerPoint adjustedOffset, overscroll;
+ mApzc.mX.AdjustDisplacement(displacement.x, adjustedOffset.x, overscroll.x);
+ mApzc.mY.AdjustDisplacement(displacement.y, adjustedOffset.y, overscroll.y,
+ !mApzc.mScrollMetadata.AllowVerticalScrollWithWheel());
+
+ // If we expected to scroll, but there's no more scroll range on either axis,
+ // then end the animation early. Note that the initial displacement could be 0
+ // if the compositor ran very quickly (<1ms) after the animation was created.
+ // When that happens we want to make sure the animation continues.
+ if (!IsZero(displacement) && IsZero(adjustedOffset)) {
+ // Nothing more to do - end the animation.
+ return false;
+ }
+
+ aFrameMetrics.ScrollBy(adjustedOffset / zoom);
+ return !finished;
+}
+
+void
+WheelScrollAnimation::InitPreferences(TimeStamp aTime)
+{
+ if (!mIsFirstIteration) {
+ return;
+ }
+
+ switch (mDeltaType) {
+ case ScrollWheelInput::SCROLLDELTA_PAGE:
+ mOriginMaxMS = clamped(gfxPrefs::PageSmoothScrollMaxDurationMs(), 0, 10000);
+ mOriginMinMS = clamped(gfxPrefs::PageSmoothScrollMinDurationMs(), 0, mOriginMaxMS);
+ break;
+ case ScrollWheelInput::SCROLLDELTA_PIXEL:
+ mOriginMaxMS = clamped(gfxPrefs::PixelSmoothScrollMaxDurationMs(), 0, 10000);
+ mOriginMinMS = clamped(gfxPrefs::PixelSmoothScrollMinDurationMs(), 0, mOriginMaxMS);
+ break;
+ case ScrollWheelInput::SCROLLDELTA_LINE:
+ default:
+ mOriginMaxMS = clamped(gfxPrefs::WheelSmoothScrollMaxDurationMs(), 0, 10000);
+ mOriginMinMS = clamped(gfxPrefs::WheelSmoothScrollMinDurationMs(), 0, mOriginMaxMS);
+ break;
+ }
+
+ // The pref is 100-based int percentage, while mIntervalRatio is 1-based ratio
+ mIntervalRatio = ((double)gfxPrefs::SmoothScrollDurationToIntervalRatio()) / 100.0;
+ mIntervalRatio = std::max(1.0, mIntervalRatio);
+
+ InitializeHistory(aTime);
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/apz/src/WheelScrollAnimation.h b/gfx/layers/apz/src/WheelScrollAnimation.h
new file mode 100644
index 000000000..79466c445
--- /dev/null
+++ b/gfx/layers/apz/src/WheelScrollAnimation.h
@@ -0,0 +1,51 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set sw=2 ts=8 et tw=80 : */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_layers_WheelScrollAnimation_h_
+#define mozilla_layers_WheelScrollAnimation_h_
+
+#include "AsyncPanZoomAnimation.h"
+#include "AsyncScrollBase.h"
+#include "InputData.h"
+
+namespace mozilla {
+namespace layers {
+
+class AsyncPanZoomController;
+
+class WheelScrollAnimation
+ : public AsyncPanZoomAnimation,
+ public AsyncScrollBase
+{
+public:
+ WheelScrollAnimation(AsyncPanZoomController& aApzc,
+ const nsPoint& aInitialPosition,
+ ScrollWheelInput::ScrollDeltaType aDeltaType);
+
+ bool DoSample(FrameMetrics& aFrameMetrics, const TimeDuration& aDelta) override;
+ void Update(TimeStamp aTime, nsPoint aDelta, const nsSize& aCurrentVelocity);
+
+ WheelScrollAnimation* AsWheelScrollAnimation() override {
+ return this;
+ }
+
+ CSSPoint GetDestination() const {
+ return CSSPoint::FromAppUnits(mFinalDestination);
+ }
+
+private:
+ void InitPreferences(TimeStamp aTime);
+
+private:
+ AsyncPanZoomController& mApzc;
+ nsPoint mFinalDestination;
+ ScrollWheelInput::ScrollDeltaType mDeltaType;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif // mozilla_layers_WheelScrollAnimation_h_
diff --git a/gfx/layers/apz/test/gtest/APZCBasicTester.h b/gfx/layers/apz/test/gtest/APZCBasicTester.h
new file mode 100644
index 000000000..79a69301f
--- /dev/null
+++ b/gfx/layers/apz/test/gtest/APZCBasicTester.h
@@ -0,0 +1,120 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set sw=2 ts=8 et tw=80 : */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_layers_APZCBasicTester_h
+#define mozilla_layers_APZCBasicTester_h
+
+/**
+ * Defines a test fixture used for testing a single APZC.
+ */
+
+#include "APZTestCommon.h"
+
+class APZCBasicTester : public APZCTesterBase {
+public:
+ explicit APZCBasicTester(AsyncPanZoomController::GestureBehavior aGestureBehavior = AsyncPanZoomController::DEFAULT_GESTURES)
+ : mGestureBehavior(aGestureBehavior)
+ {
+ }
+
+protected:
+ virtual void SetUp()
+ {
+ gfxPrefs::GetSingleton();
+ APZThreadUtils::SetThreadAssertionsEnabled(false);
+ APZThreadUtils::SetControllerThread(MessageLoop::current());
+
+ tm = new TestAPZCTreeManager(mcc);
+ apzc = new TestAsyncPanZoomController(0, mcc, tm, mGestureBehavior);
+ apzc->SetFrameMetrics(TestFrameMetrics());
+ apzc->GetScrollMetadata().SetIsLayersIdRoot(true);
+ }
+
+ /**
+ * Get the APZC's scroll range in CSS pixels.
+ */
+ CSSRect GetScrollRange() const
+ {
+ const FrameMetrics& metrics = apzc->GetFrameMetrics();
+ return CSSRect(
+ metrics.GetScrollableRect().TopLeft(),
+ metrics.GetScrollableRect().Size() - metrics.CalculateCompositedSizeInCssPixels());
+ }
+
+ virtual void TearDown()
+ {
+ while (mcc->RunThroughDelayedTasks());
+ apzc->Destroy();
+ tm->ClearTree();
+ tm->ClearContentController();
+ }
+
+ void MakeApzcWaitForMainThread()
+ {
+ apzc->SetWaitForMainThread();
+ }
+
+ void MakeApzcZoomable()
+ {
+ apzc->UpdateZoomConstraints(ZoomConstraints(true, true, CSSToParentLayerScale(0.25f), CSSToParentLayerScale(4.0f)));
+ }
+
+ void MakeApzcUnzoomable()
+ {
+ apzc->UpdateZoomConstraints(ZoomConstraints(false, false, CSSToParentLayerScale(1.0f), CSSToParentLayerScale(1.0f)));
+ }
+
+ void PanIntoOverscroll();
+
+ /**
+ * Sample animations once, 1 ms later than the last sample.
+ */
+ void SampleAnimationOnce()
+ {
+ const TimeDuration increment = TimeDuration::FromMilliseconds(1);
+ ParentLayerPoint pointOut;
+ AsyncTransform viewTransformOut;
+ mcc->AdvanceBy(increment);
+ apzc->SampleContentTransformForFrame(&viewTransformOut, pointOut);
+ }
+
+ /**
+ * Sample animations until we recover from overscroll.
+ * @param aExpectedScrollOffset the expected reported scroll offset
+ * throughout the animation
+ */
+ void SampleAnimationUntilRecoveredFromOverscroll(const ParentLayerPoint& aExpectedScrollOffset)
+ {
+ const TimeDuration increment = TimeDuration::FromMilliseconds(1);
+ bool recoveredFromOverscroll = false;
+ ParentLayerPoint pointOut;
+ AsyncTransform viewTransformOut;
+ while (apzc->SampleContentTransformForFrame(&viewTransformOut, pointOut)) {
+ // The reported scroll offset should be the same throughout.
+ EXPECT_EQ(aExpectedScrollOffset, pointOut);
+
+ // Trigger computation of the overscroll tranform, to make sure
+ // no assetions fire during the calculation.
+ apzc->GetOverscrollTransform(AsyncPanZoomController::NORMAL);
+
+ if (!apzc->IsOverscrolled()) {
+ recoveredFromOverscroll = true;
+ }
+
+ mcc->AdvanceBy(increment);
+ }
+ EXPECT_TRUE(recoveredFromOverscroll);
+ apzc->AssertStateIsReset();
+ }
+
+ void TestOverscroll();
+
+ AsyncPanZoomController::GestureBehavior mGestureBehavior;
+ RefPtr<TestAPZCTreeManager> tm;
+ RefPtr<TestAsyncPanZoomController> apzc;
+};
+
+#endif // mozilla_layers_APZCBasicTester_h
diff --git a/gfx/layers/apz/test/gtest/APZCTreeManagerTester.h b/gfx/layers/apz/test/gtest/APZCTreeManagerTester.h
new file mode 100644
index 000000000..4eeed1e7e
--- /dev/null
+++ b/gfx/layers/apz/test/gtest/APZCTreeManagerTester.h
@@ -0,0 +1,194 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set sw=2 ts=8 et tw=80 : */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_layers_APZCTreeManagerTester_h
+#define mozilla_layers_APZCTreeManagerTester_h
+
+/**
+ * Defines a test fixture used for testing multiple APZCs interacting in
+ * an APZCTreeManager.
+ */
+
+#include "APZTestCommon.h"
+#include "gfxPlatform.h"
+
+class APZCTreeManagerTester : public APZCTesterBase {
+protected:
+ virtual void SetUp() {
+ gfxPrefs::GetSingleton();
+ gfxPlatform::GetPlatform();
+ APZThreadUtils::SetThreadAssertionsEnabled(false);
+ APZThreadUtils::SetControllerThread(MessageLoop::current());
+
+ manager = new TestAPZCTreeManager(mcc);
+ }
+
+ virtual void TearDown() {
+ while (mcc->RunThroughDelayedTasks());
+ manager->ClearTree();
+ manager->ClearContentController();
+ }
+
+ /**
+ * Sample animations once for all APZCs, 1 ms later than the last sample.
+ */
+ void SampleAnimationsOnce() {
+ const TimeDuration increment = TimeDuration::FromMilliseconds(1);
+ ParentLayerPoint pointOut;
+ AsyncTransform viewTransformOut;
+ mcc->AdvanceBy(increment);
+
+ for (const RefPtr<Layer>& layer : layers) {
+ if (TestAsyncPanZoomController* apzc = ApzcOf(layer)) {
+ apzc->SampleContentTransformForFrame(&viewTransformOut, pointOut);
+ }
+ }
+ }
+
+ nsTArray<RefPtr<Layer> > layers;
+ RefPtr<LayerManager> lm;
+ RefPtr<Layer> root;
+
+ RefPtr<TestAPZCTreeManager> manager;
+
+protected:
+ static ScrollMetadata BuildScrollMetadata(FrameMetrics::ViewID aScrollId,
+ const CSSRect& aScrollableRect,
+ const ParentLayerRect& aCompositionBounds)
+ {
+ ScrollMetadata metadata;
+ FrameMetrics& metrics = metadata.GetMetrics();
+ metrics.SetScrollId(aScrollId);
+ // By convention in this test file, START_SCROLL_ID is the root, so mark it as such.
+ if (aScrollId == FrameMetrics::START_SCROLL_ID) {
+ metadata.SetIsLayersIdRoot(true);
+ }
+ metrics.SetCompositionBounds(aCompositionBounds);
+ metrics.SetScrollableRect(aScrollableRect);
+ metrics.SetScrollOffset(CSSPoint(0, 0));
+ metadata.SetPageScrollAmount(LayoutDeviceIntSize(50, 100));
+ metadata.SetLineScrollAmount(LayoutDeviceIntSize(5, 10));
+ metadata.SetAllowVerticalScrollWithWheel(true);
+ return metadata;
+ }
+
+ static void SetEventRegionsBasedOnBottommostMetrics(Layer* aLayer)
+ {
+ const FrameMetrics& metrics = aLayer->GetScrollMetadata(0).GetMetrics();
+ CSSRect scrollableRect = metrics.GetScrollableRect();
+ if (!scrollableRect.IsEqualEdges(CSSRect(-1, -1, -1, -1))) {
+ // The purpose of this is to roughly mimic what layout would do in the
+ // case of a scrollable frame with the event regions and clip. This lets
+ // us exercise the hit-testing code in APZCTreeManager
+ EventRegions er = aLayer->GetEventRegions();
+ IntRect scrollRect = RoundedToInt(
+ scrollableRect * metrics.LayersPixelsPerCSSPixel()).ToUnknownRect();
+ er.mHitRegion = nsIntRegion(IntRect(
+ RoundedToInt(metrics.GetCompositionBounds().TopLeft().ToUnknownPoint()),
+ scrollRect.Size()));
+ aLayer->SetEventRegions(er);
+ }
+ }
+
+ static void SetScrollableFrameMetrics(Layer* aLayer, FrameMetrics::ViewID aScrollId,
+ CSSRect aScrollableRect = CSSRect(-1, -1, -1, -1)) {
+ ParentLayerIntRect compositionBounds = ViewAs<ParentLayerPixel>(
+ aLayer->GetVisibleRegion().ToUnknownRegion().GetBounds());
+ ScrollMetadata metadata = BuildScrollMetadata(aScrollId, aScrollableRect,
+ ParentLayerRect(compositionBounds));
+ aLayer->SetScrollMetadata(metadata);
+ aLayer->SetClipRect(Some(compositionBounds));
+ SetEventRegionsBasedOnBottommostMetrics(aLayer);
+ }
+
+ void SetScrollHandoff(Layer* aChild, Layer* aParent) {
+ ScrollMetadata metadata = aChild->GetScrollMetadata(0);
+ metadata.SetScrollParentId(aParent->GetFrameMetrics(0).GetScrollId());
+ aChild->SetScrollMetadata(metadata);
+ }
+
+ static TestAsyncPanZoomController* ApzcOf(Layer* aLayer) {
+ EXPECT_EQ(1u, aLayer->GetScrollMetadataCount());
+ return (TestAsyncPanZoomController*)aLayer->GetAsyncPanZoomController(0);
+ }
+
+ static TestAsyncPanZoomController* ApzcOf(Layer* aLayer, uint32_t aIndex) {
+ EXPECT_LT(aIndex, aLayer->GetScrollMetadataCount());
+ return (TestAsyncPanZoomController*)aLayer->GetAsyncPanZoomController(aIndex);
+ }
+
+ void CreateSimpleScrollingLayer() {
+ const char* layerTreeSyntax = "t";
+ nsIntRegion layerVisibleRegion[] = {
+ nsIntRegion(IntRect(0,0,200,200)),
+ };
+ root = CreateLayerTree(layerTreeSyntax, layerVisibleRegion, nullptr, lm, layers);
+ SetScrollableFrameMetrics(root, FrameMetrics::START_SCROLL_ID, CSSRect(0, 0, 500, 500));
+ }
+
+ void CreateSimpleDTCScrollingLayer() {
+ const char* layerTreeSyntax = "t";
+ nsIntRegion layerVisibleRegion[] = {
+ nsIntRegion(IntRect(0,0,200,200)),
+ };
+ root = CreateLayerTree(layerTreeSyntax, layerVisibleRegion, nullptr, lm, layers);
+ SetScrollableFrameMetrics(root, FrameMetrics::START_SCROLL_ID, CSSRect(0, 0, 500, 500));
+
+ EventRegions regions;
+ regions.mHitRegion = nsIntRegion(IntRect(0, 0, 200, 200));
+ regions.mDispatchToContentHitRegion = regions.mHitRegion;
+ layers[0]->SetEventRegions(regions);
+ }
+
+ void CreateSimpleMultiLayerTree() {
+ const char* layerTreeSyntax = "c(tt)";
+ // LayerID 0 12
+ nsIntRegion layerVisibleRegion[] = {
+ nsIntRegion(IntRect(0,0,100,100)),
+ nsIntRegion(IntRect(0,0,100,50)),
+ nsIntRegion(IntRect(0,50,100,50)),
+ };
+ root = CreateLayerTree(layerTreeSyntax, layerVisibleRegion, nullptr, lm, layers);
+ }
+
+ void CreatePotentiallyLeakingTree() {
+ const char* layerTreeSyntax = "c(c(c(t))c(c(t)))";
+ // LayerID 0 1 2 3 4 5 6
+ root = CreateLayerTree(layerTreeSyntax, nullptr, nullptr, lm, layers);
+ SetScrollableFrameMetrics(layers[0], FrameMetrics::START_SCROLL_ID);
+ SetScrollableFrameMetrics(layers[2], FrameMetrics::START_SCROLL_ID + 1);
+ SetScrollableFrameMetrics(layers[5], FrameMetrics::START_SCROLL_ID + 1);
+ SetScrollableFrameMetrics(layers[3], FrameMetrics::START_SCROLL_ID + 2);
+ SetScrollableFrameMetrics(layers[6], FrameMetrics::START_SCROLL_ID + 3);
+ }
+
+ void CreateBug1194876Tree() {
+ const char* layerTreeSyntax = "c(t)";
+ // LayerID 0 1
+ nsIntRegion layerVisibleRegion[] = {
+ nsIntRegion(IntRect(0,0,100,100)),
+ nsIntRegion(IntRect(0,0,100,100)),
+ };
+ root = CreateLayerTree(layerTreeSyntax, layerVisibleRegion, nullptr, lm, layers);
+ SetScrollableFrameMetrics(layers[0], FrameMetrics::START_SCROLL_ID);
+ SetScrollableFrameMetrics(layers[1], FrameMetrics::START_SCROLL_ID + 1);
+ SetScrollHandoff(layers[1], layers[0]);
+
+ // Make layers[1] the root content
+ ScrollMetadata childMetadata = layers[1]->GetScrollMetadata(0);
+ childMetadata.GetMetrics().SetIsRootContent(true);
+ layers[1]->SetScrollMetadata(childMetadata);
+
+ // Both layers are fully dispatch-to-content
+ EventRegions regions;
+ regions.mHitRegion = nsIntRegion(IntRect(0, 0, 100, 100));
+ regions.mDispatchToContentHitRegion = regions.mHitRegion;
+ layers[0]->SetEventRegions(regions);
+ layers[1]->SetEventRegions(regions);
+ }
+};
+
+#endif // mozilla_layers_APZCTreeManagerTester_h
diff --git a/gfx/layers/apz/test/gtest/APZTestCommon.h b/gfx/layers/apz/test/gtest/APZTestCommon.h
new file mode 100644
index 000000000..6e259ab60
--- /dev/null
+++ b/gfx/layers/apz/test/gtest/APZTestCommon.h
@@ -0,0 +1,609 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set sw=2 ts=8 et tw=80 : */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_layers_APZTestCommon_h
+#define mozilla_layers_APZTestCommon_h
+
+/**
+ * Defines a set of mock classes and utility functions/classes for
+ * writing APZ gtests.
+ */
+
+#include "gtest/gtest.h"
+#include "gmock/gmock.h"
+
+#include "mozilla/Attributes.h"
+#include "mozilla/layers/AsyncCompositionManager.h" // for ViewTransform
+#include "mozilla/layers/GeckoContentController.h"
+#include "mozilla/layers/CompositorBridgeParent.h"
+#include "mozilla/layers/APZCTreeManager.h"
+#include "mozilla/layers/LayerMetricsWrapper.h"
+#include "mozilla/layers/APZThreadUtils.h"
+#include "mozilla/UniquePtr.h"
+#include "apz/src/AsyncPanZoomController.h"
+#include "apz/src/HitTestingTreeNode.h"
+#include "base/task.h"
+#include "Layers.h"
+#include "TestLayers.h"
+#include "UnitTransforms.h"
+#include "gfxPrefs.h"
+
+using namespace mozilla;
+using namespace mozilla::gfx;
+using namespace mozilla::layers;
+using ::testing::_;
+using ::testing::NiceMock;
+using ::testing::AtLeast;
+using ::testing::AtMost;
+using ::testing::MockFunction;
+using ::testing::InSequence;
+typedef mozilla::layers::GeckoContentController::TapType TapType;
+
+template<class T>
+class ScopedGfxPref {
+public:
+ ScopedGfxPref(T (*aGetPrefFunc)(void), void (*aSetPrefFunc)(T), T aVal)
+ : mSetPrefFunc(aSetPrefFunc)
+ {
+ mOldVal = aGetPrefFunc();
+ aSetPrefFunc(aVal);
+ }
+
+ ~ScopedGfxPref() {
+ mSetPrefFunc(mOldVal);
+ }
+
+private:
+ void (*mSetPrefFunc)(T);
+ T mOldVal;
+};
+
+#define SCOPED_GFX_PREF(prefBase, prefType, prefValue) \
+ ScopedGfxPref<prefType> pref_##prefBase( \
+ &(gfxPrefs::prefBase), \
+ &(gfxPrefs::Set##prefBase), \
+ prefValue)
+
+static TimeStamp GetStartupTime() {
+ static TimeStamp sStartupTime = TimeStamp::Now();
+ return sStartupTime;
+}
+
+class MockContentController : public GeckoContentController {
+public:
+ MOCK_METHOD1(RequestContentRepaint, void(const FrameMetrics&));
+ MOCK_METHOD2(RequestFlingSnap, void(const FrameMetrics::ViewID& aScrollId, const mozilla::CSSPoint& aDestination));
+ MOCK_METHOD2(AcknowledgeScrollUpdate, void(const FrameMetrics::ViewID&, const uint32_t& aScrollGeneration));
+ MOCK_METHOD5(HandleTap, void(TapType, const LayoutDevicePoint&, Modifiers, const ScrollableLayerGuid&, uint64_t));
+ MOCK_METHOD4(NotifyPinchGesture, void(PinchGestureInput::PinchGestureType, const ScrollableLayerGuid&, LayoutDeviceCoord, Modifiers));
+ // Can't use the macros with already_AddRefed :(
+ void PostDelayedTask(already_AddRefed<Runnable> aTask, int aDelayMs) {
+ RefPtr<Runnable> task = aTask;
+ }
+ bool IsRepaintThread() {
+ return NS_IsMainThread();
+ }
+ void DispatchToRepaintThread(already_AddRefed<Runnable> aTask) {
+ NS_DispatchToMainThread(Move(aTask));
+ }
+ MOCK_METHOD3(NotifyAPZStateChange, void(const ScrollableLayerGuid& aGuid, APZStateChange aChange, int aArg));
+ MOCK_METHOD0(NotifyFlushComplete, void());
+};
+
+class MockContentControllerDelayed : public MockContentController {
+public:
+ MockContentControllerDelayed()
+ : mTime(GetStartupTime())
+ {
+ }
+
+ const TimeStamp& Time() {
+ return mTime;
+ }
+
+ void AdvanceByMillis(int aMillis) {
+ AdvanceBy(TimeDuration::FromMilliseconds(aMillis));
+ }
+
+ void AdvanceBy(const TimeDuration& aIncrement) {
+ TimeStamp target = mTime + aIncrement;
+ while (mTaskQueue.Length() > 0 && mTaskQueue[0].second <= target) {
+ RunNextDelayedTask();
+ }
+ mTime = target;
+ }
+
+ void PostDelayedTask(already_AddRefed<Runnable> aTask, int aDelayMs) {
+ RefPtr<Runnable> task = aTask;
+ TimeStamp runAtTime = mTime + TimeDuration::FromMilliseconds(aDelayMs);
+ int insIndex = mTaskQueue.Length();
+ while (insIndex > 0) {
+ if (mTaskQueue[insIndex - 1].second <= runAtTime) {
+ break;
+ }
+ insIndex--;
+ }
+ mTaskQueue.InsertElementAt(insIndex, std::make_pair(task, runAtTime));
+ }
+
+ // Run all the tasks in the queue, returning the number of tasks
+ // run. Note that if a task queues another task while running, that
+ // new task will not be run. Therefore, there may be still be tasks
+ // in the queue after this function is called. Only when the return
+ // value is 0 is the queue guaranteed to be empty.
+ int RunThroughDelayedTasks() {
+ nsTArray<std::pair<RefPtr<Runnable>, TimeStamp>> runQueue;
+ runQueue.SwapElements(mTaskQueue);
+ int numTasks = runQueue.Length();
+ for (int i = 0; i < numTasks; i++) {
+ mTime = runQueue[i].second;
+ runQueue[i].first->Run();
+
+ // Deleting the task is important in order to release the reference to
+ // the callee object.
+ runQueue[i].first = nullptr;
+ }
+ return numTasks;
+ }
+
+private:
+ void RunNextDelayedTask() {
+ std::pair<RefPtr<Runnable>, TimeStamp> next = mTaskQueue[0];
+ mTaskQueue.RemoveElementAt(0);
+ mTime = next.second;
+ next.first->Run();
+ // Deleting the task is important in order to release the reference to
+ // the callee object.
+ next.first = nullptr;
+ }
+
+ // The following array is sorted by timestamp (tasks are inserted in order by
+ // timestamp).
+ nsTArray<std::pair<RefPtr<Runnable>, TimeStamp>> mTaskQueue;
+ TimeStamp mTime;
+};
+
+class TestAPZCTreeManager : public APZCTreeManager {
+public:
+ explicit TestAPZCTreeManager(MockContentControllerDelayed* aMcc) : mcc(aMcc) {}
+
+ RefPtr<InputQueue> GetInputQueue() const {
+ return mInputQueue;
+ }
+
+ void ClearContentController() {
+ mcc = nullptr;
+ }
+
+protected:
+ AsyncPanZoomController* NewAPZCInstance(uint64_t aLayersId,
+ GeckoContentController* aController) override;
+
+ TimeStamp GetFrameTime() override {
+ return mcc->Time();
+ }
+
+private:
+ RefPtr<MockContentControllerDelayed> mcc;
+};
+
+class TestAsyncPanZoomController : public AsyncPanZoomController {
+public:
+ TestAsyncPanZoomController(uint64_t aLayersId, MockContentControllerDelayed* aMcc,
+ TestAPZCTreeManager* aTreeManager,
+ GestureBehavior aBehavior = DEFAULT_GESTURES)
+ : AsyncPanZoomController(aLayersId, aTreeManager, aTreeManager->GetInputQueue(),
+ aMcc, aBehavior)
+ , mWaitForMainThread(false)
+ , mcc(aMcc)
+ {}
+
+ nsEventStatus ReceiveInputEvent(const InputData& aEvent, ScrollableLayerGuid* aDummy, uint64_t* aOutInputBlockId) {
+ // This is a function whose signature matches exactly the ReceiveInputEvent
+ // on APZCTreeManager. This allows us to templates for functions like
+ // TouchDown, TouchUp, etc so that we can reuse the code for dispatching
+ // events into both APZC and APZCTM.
+ return ReceiveInputEvent(aEvent, aOutInputBlockId);
+ }
+
+ nsEventStatus ReceiveInputEvent(const InputData& aEvent, uint64_t* aOutInputBlockId) {
+ return GetInputQueue()->ReceiveInputEvent(this, !mWaitForMainThread, aEvent, aOutInputBlockId);
+ }
+
+ void ContentReceivedInputBlock(uint64_t aInputBlockId, bool aPreventDefault) {
+ GetInputQueue()->ContentReceivedInputBlock(aInputBlockId, aPreventDefault);
+ }
+
+ void ConfirmTarget(uint64_t aInputBlockId) {
+ RefPtr<AsyncPanZoomController> target = this;
+ GetInputQueue()->SetConfirmedTargetApzc(aInputBlockId, target);
+ }
+
+ void SetAllowedTouchBehavior(uint64_t aInputBlockId, const nsTArray<TouchBehaviorFlags>& aBehaviors) {
+ GetInputQueue()->SetAllowedTouchBehavior(aInputBlockId, aBehaviors);
+ }
+
+ void SetFrameMetrics(const FrameMetrics& metrics) {
+ ReentrantMonitorAutoEnter lock(mMonitor);
+ mFrameMetrics = metrics;
+ }
+
+ FrameMetrics& GetFrameMetrics() {
+ ReentrantMonitorAutoEnter lock(mMonitor);
+ return mFrameMetrics;
+ }
+
+ ScrollMetadata& GetScrollMetadata() {
+ ReentrantMonitorAutoEnter lock(mMonitor);
+ return mScrollMetadata;
+ }
+
+ const FrameMetrics& GetFrameMetrics() const {
+ ReentrantMonitorAutoEnter lock(mMonitor);
+ return mFrameMetrics;
+ }
+
+ using AsyncPanZoomController::GetVelocityVector;
+
+ void AssertStateIsReset() const {
+ ReentrantMonitorAutoEnter lock(mMonitor);
+ EXPECT_EQ(NOTHING, mState);
+ }
+
+ void AssertStateIsFling() const {
+ ReentrantMonitorAutoEnter lock(mMonitor);
+ EXPECT_EQ(FLING, mState);
+ }
+
+ void AdvanceAnimationsUntilEnd(const TimeDuration& aIncrement = TimeDuration::FromMilliseconds(10)) {
+ while (AdvanceAnimations(mcc->Time())) {
+ mcc->AdvanceBy(aIncrement);
+ }
+ }
+
+ bool SampleContentTransformForFrame(AsyncTransform* aOutTransform,
+ ParentLayerPoint& aScrollOffset,
+ const TimeDuration& aIncrement = TimeDuration::FromMilliseconds(0)) {
+ mcc->AdvanceBy(aIncrement);
+ bool ret = AdvanceAnimations(mcc->Time());
+ if (aOutTransform) {
+ *aOutTransform = GetCurrentAsyncTransform(AsyncPanZoomController::NORMAL);
+ }
+ aScrollOffset = GetCurrentAsyncScrollOffset(AsyncPanZoomController::NORMAL);
+ return ret;
+ }
+
+ void SetWaitForMainThread() {
+ mWaitForMainThread = true;
+ }
+
+private:
+ bool mWaitForMainThread;
+ MockContentControllerDelayed* mcc;
+};
+
+class APZCTesterBase : public ::testing::Test {
+public:
+ APZCTesterBase() {
+ mcc = new NiceMock<MockContentControllerDelayed>();
+ }
+
+ template<class InputReceiver>
+ void Tap(const RefPtr<InputReceiver>& aTarget, const ScreenIntPoint& aPoint,
+ TimeDuration aTapLength,
+ nsEventStatus (*aOutEventStatuses)[2] = nullptr,
+ uint64_t* aOutInputBlockId = nullptr);
+
+ template<class InputReceiver>
+ void TapAndCheckStatus(const RefPtr<InputReceiver>& aTarget,
+ const ScreenIntPoint& aPoint, TimeDuration aTapLength);
+
+ template<class InputReceiver>
+ void Pan(const RefPtr<InputReceiver>& aTarget,
+ const ScreenIntPoint& aTouchStart,
+ const ScreenIntPoint& aTouchEnd,
+ bool aKeepFingerDown = false,
+ nsTArray<uint32_t>* aAllowedTouchBehaviors = nullptr,
+ nsEventStatus (*aOutEventStatuses)[4] = nullptr,
+ uint64_t* aOutInputBlockId = nullptr);
+
+ /*
+ * A version of Pan() that only takes y coordinates rather than (x, y) points
+ * for the touch start and end points, and uses 10 for the x coordinates.
+ * This is for convenience, as most tests only need to pan in one direction.
+ */
+ template<class InputReceiver>
+ void Pan(const RefPtr<InputReceiver>& aTarget, int aTouchStartY,
+ int aTouchEndY, bool aKeepFingerDown = false,
+ nsTArray<uint32_t>* aAllowedTouchBehaviors = nullptr,
+ nsEventStatus (*aOutEventStatuses)[4] = nullptr,
+ uint64_t* aOutInputBlockId = nullptr);
+
+ /*
+ * Dispatches mock touch events to the apzc and checks whether apzc properly
+ * consumed them and triggered scrolling behavior.
+ */
+ template<class InputReceiver>
+ void PanAndCheckStatus(const RefPtr<InputReceiver>& aTarget, int aTouchStartY,
+ int aTouchEndY,
+ bool aExpectConsumed,
+ nsTArray<uint32_t>* aAllowedTouchBehaviors,
+ uint64_t* aOutInputBlockId = nullptr);
+
+ void ApzcPanNoFling(const RefPtr<TestAsyncPanZoomController>& aApzc,
+ int aTouchStartY,
+ int aTouchEndY,
+ uint64_t* aOutInputBlockId = nullptr);
+
+ template<class InputReceiver>
+ void DoubleTap(const RefPtr<InputReceiver>& aTarget,
+ const ScreenIntPoint& aPoint,
+ nsEventStatus (*aOutEventStatuses)[4] = nullptr,
+ uint64_t (*aOutInputBlockIds)[2] = nullptr);
+
+ template<class InputReceiver>
+ void DoubleTapAndCheckStatus(const RefPtr<InputReceiver>& aTarget,
+ const ScreenIntPoint& aPoint,
+ uint64_t (*aOutInputBlockIds)[2] = nullptr);
+
+protected:
+ RefPtr<MockContentControllerDelayed> mcc;
+};
+
+template<class InputReceiver>
+void
+APZCTesterBase::Tap(const RefPtr<InputReceiver>& aTarget,
+ const ScreenIntPoint& aPoint, TimeDuration aTapLength,
+ nsEventStatus (*aOutEventStatuses)[2],
+ uint64_t* aOutInputBlockId)
+{
+ // Even if the caller doesn't care about the block id, we need it to set the
+ // allowed touch behaviour below, so make sure aOutInputBlockId is non-null.
+ uint64_t blockId;
+ if (!aOutInputBlockId) {
+ aOutInputBlockId = &blockId;
+ }
+
+ nsEventStatus status = TouchDown(aTarget, aPoint, mcc->Time(), aOutInputBlockId);
+ if (aOutEventStatuses) {
+ (*aOutEventStatuses)[0] = status;
+ }
+ mcc->AdvanceBy(aTapLength);
+
+ // If touch-action is enabled then simulate the allowed touch behaviour
+ // notification that the main thread is supposed to deliver.
+ if (gfxPrefs::TouchActionEnabled() && status != nsEventStatus_eConsumeNoDefault) {
+ SetDefaultAllowedTouchBehavior(aTarget, *aOutInputBlockId);
+ }
+
+ status = TouchUp(aTarget, aPoint, mcc->Time());
+ if (aOutEventStatuses) {
+ (*aOutEventStatuses)[1] = status;
+ }
+}
+
+template<class InputReceiver>
+void
+APZCTesterBase::TapAndCheckStatus(const RefPtr<InputReceiver>& aTarget,
+ const ScreenIntPoint& aPoint,
+ TimeDuration aTapLength)
+{
+ nsEventStatus statuses[2];
+ Tap(aTarget, aPoint, aTapLength, &statuses);
+ EXPECT_EQ(nsEventStatus_eConsumeDoDefault, statuses[0]);
+ EXPECT_EQ(nsEventStatus_eConsumeDoDefault, statuses[1]);
+}
+
+template<class InputReceiver>
+void
+APZCTesterBase::Pan(const RefPtr<InputReceiver>& aTarget,
+ const ScreenIntPoint& aTouchStart,
+ const ScreenIntPoint& aTouchEnd,
+ bool aKeepFingerDown,
+ nsTArray<uint32_t>* aAllowedTouchBehaviors,
+ nsEventStatus (*aOutEventStatuses)[4],
+ uint64_t* aOutInputBlockId)
+{
+ // Reduce the touch start and move tolerance to a tiny value.
+ // We can't use a scoped pref because this value might be read at some later
+ // time when the events are actually processed, rather than when we deliver
+ // them.
+ gfxPrefs::SetAPZTouchStartTolerance(1.0f / 1000.0f);
+ gfxPrefs::SetAPZTouchMoveTolerance(0.0f);
+ const int OVERCOME_TOUCH_TOLERANCE = 1;
+
+ const TimeDuration TIME_BETWEEN_TOUCH_EVENT = TimeDuration::FromMilliseconds(50);
+
+ // Even if the caller doesn't care about the block id, we need it to set the
+ // allowed touch behaviour below, so make sure aOutInputBlockId is non-null.
+ uint64_t blockId;
+ if (!aOutInputBlockId) {
+ aOutInputBlockId = &blockId;
+ }
+
+ // Make sure the move is large enough to not be handled as a tap
+ nsEventStatus status = TouchDown(aTarget,
+ ScreenIntPoint(aTouchStart.x, aTouchStart.y + OVERCOME_TOUCH_TOLERANCE),
+ mcc->Time(), aOutInputBlockId);
+ if (aOutEventStatuses) {
+ (*aOutEventStatuses)[0] = status;
+ }
+
+ mcc->AdvanceBy(TIME_BETWEEN_TOUCH_EVENT);
+
+ // Allowed touch behaviours must be set after sending touch-start.
+ if (status != nsEventStatus_eConsumeNoDefault) {
+ if (aAllowedTouchBehaviors) {
+ EXPECT_EQ(1UL, aAllowedTouchBehaviors->Length());
+ aTarget->SetAllowedTouchBehavior(*aOutInputBlockId, *aAllowedTouchBehaviors);
+ } else if (gfxPrefs::TouchActionEnabled()) {
+ SetDefaultAllowedTouchBehavior(aTarget, *aOutInputBlockId);
+ }
+ }
+
+ status = TouchMove(aTarget, aTouchStart, mcc->Time());
+ if (aOutEventStatuses) {
+ (*aOutEventStatuses)[1] = status;
+ }
+
+ mcc->AdvanceBy(TIME_BETWEEN_TOUCH_EVENT);
+
+ status = TouchMove(aTarget, aTouchEnd, mcc->Time());
+ if (aOutEventStatuses) {
+ (*aOutEventStatuses)[2] = status;
+ }
+
+ mcc->AdvanceBy(TIME_BETWEEN_TOUCH_EVENT);
+
+ if (!aKeepFingerDown) {
+ status = TouchUp(aTarget, aTouchEnd, mcc->Time());
+ } else {
+ status = nsEventStatus_eIgnore;
+ }
+ if (aOutEventStatuses) {
+ (*aOutEventStatuses)[3] = status;
+ }
+
+ // Don't increment the time here. Animations started on touch-up, such as
+ // flings, are affected by elapsed time, and we want to be able to sample
+ // them immediately after they start, without time having elapsed.
+}
+
+template<class InputReceiver>
+void
+APZCTesterBase::Pan(const RefPtr<InputReceiver>& aTarget,
+ int aTouchStartY, int aTouchEndY, bool aKeepFingerDown,
+ nsTArray<uint32_t>* aAllowedTouchBehaviors,
+ nsEventStatus (*aOutEventStatuses)[4],
+ uint64_t* aOutInputBlockId)
+{
+ Pan(aTarget, ScreenIntPoint(10, aTouchStartY), ScreenIntPoint(10, aTouchEndY),
+ aKeepFingerDown, aAllowedTouchBehaviors, aOutEventStatuses, aOutInputBlockId);
+}
+
+template<class InputReceiver>
+void
+APZCTesterBase::PanAndCheckStatus(const RefPtr<InputReceiver>& aTarget,
+ int aTouchStartY,
+ int aTouchEndY,
+ bool aExpectConsumed,
+ nsTArray<uint32_t>* aAllowedTouchBehaviors,
+ uint64_t* aOutInputBlockId)
+{
+ nsEventStatus statuses[4]; // down, move, move, up
+ Pan(aTarget, aTouchStartY, aTouchEndY, false, aAllowedTouchBehaviors, &statuses, aOutInputBlockId);
+
+ EXPECT_EQ(nsEventStatus_eConsumeDoDefault, statuses[0]);
+
+ nsEventStatus touchMoveStatus;
+ if (aExpectConsumed) {
+ touchMoveStatus = nsEventStatus_eConsumeDoDefault;
+ } else {
+ touchMoveStatus = nsEventStatus_eIgnore;
+ }
+ EXPECT_EQ(touchMoveStatus, statuses[1]);
+ EXPECT_EQ(touchMoveStatus, statuses[2]);
+}
+
+void
+APZCTesterBase::ApzcPanNoFling(const RefPtr<TestAsyncPanZoomController>& aApzc,
+ int aTouchStartY, int aTouchEndY,
+ uint64_t* aOutInputBlockId)
+{
+ Pan(aApzc, aTouchStartY, aTouchEndY, false, nullptr, nullptr, aOutInputBlockId);
+ aApzc->CancelAnimation();
+}
+
+template<class InputReceiver>
+void
+APZCTesterBase::DoubleTap(const RefPtr<InputReceiver>& aTarget,
+ const ScreenIntPoint& aPoint,
+ nsEventStatus (*aOutEventStatuses)[4],
+ uint64_t (*aOutInputBlockIds)[2])
+{
+ uint64_t blockId;
+ nsEventStatus status = TouchDown(aTarget, aPoint, mcc->Time(), &blockId);
+ if (aOutEventStatuses) {
+ (*aOutEventStatuses)[0] = status;
+ }
+ if (aOutInputBlockIds) {
+ (*aOutInputBlockIds)[0] = blockId;
+ }
+ mcc->AdvanceByMillis(10);
+
+ // If touch-action is enabled then simulate the allowed touch behaviour
+ // notification that the main thread is supposed to deliver.
+ if (gfxPrefs::TouchActionEnabled() && status != nsEventStatus_eConsumeNoDefault) {
+ SetDefaultAllowedTouchBehavior(aTarget, blockId);
+ }
+
+ status = TouchUp(aTarget, aPoint, mcc->Time());
+ if (aOutEventStatuses) {
+ (*aOutEventStatuses)[1] = status;
+ }
+ mcc->AdvanceByMillis(10);
+ status = TouchDown(aTarget, aPoint, mcc->Time(), &blockId);
+ if (aOutEventStatuses) {
+ (*aOutEventStatuses)[2] = status;
+ }
+ if (aOutInputBlockIds) {
+ (*aOutInputBlockIds)[1] = blockId;
+ }
+ mcc->AdvanceByMillis(10);
+
+ if (gfxPrefs::TouchActionEnabled() && status != nsEventStatus_eConsumeNoDefault) {
+ SetDefaultAllowedTouchBehavior(aTarget, blockId);
+ }
+
+ status = TouchUp(aTarget, aPoint, mcc->Time());
+ if (aOutEventStatuses) {
+ (*aOutEventStatuses)[3] = status;
+ }
+}
+
+template<class InputReceiver>
+void
+APZCTesterBase::DoubleTapAndCheckStatus(const RefPtr<InputReceiver>& aTarget,
+ const ScreenIntPoint& aPoint,
+ uint64_t (*aOutInputBlockIds)[2])
+{
+ nsEventStatus statuses[4];
+ DoubleTap(aTarget, aPoint, &statuses, aOutInputBlockIds);
+ EXPECT_EQ(nsEventStatus_eConsumeDoDefault, statuses[0]);
+ EXPECT_EQ(nsEventStatus_eConsumeDoDefault, statuses[1]);
+ EXPECT_EQ(nsEventStatus_eConsumeDoDefault, statuses[2]);
+ EXPECT_EQ(nsEventStatus_eConsumeDoDefault, statuses[3]);
+}
+
+AsyncPanZoomController*
+TestAPZCTreeManager::NewAPZCInstance(uint64_t aLayersId,
+ GeckoContentController* aController)
+{
+ MockContentControllerDelayed* mcc = static_cast<MockContentControllerDelayed*>(aController);
+ return new TestAsyncPanZoomController(aLayersId, mcc, this,
+ AsyncPanZoomController::USE_GESTURE_DETECTOR);
+}
+
+FrameMetrics
+TestFrameMetrics()
+{
+ FrameMetrics fm;
+
+ fm.SetDisplayPort(CSSRect(0, 0, 10, 10));
+ fm.SetCompositionBounds(ParentLayerRect(0, 0, 10, 10));
+ fm.SetCriticalDisplayPort(CSSRect(0, 0, 10, 10));
+ fm.SetScrollableRect(CSSRect(0, 0, 100, 100));
+
+ return fm;
+}
+
+uint32_t
+MillisecondsSinceStartup(TimeStamp aTime)
+{
+ return (aTime - GetStartupTime()).ToMilliseconds();
+}
+
+#endif // mozilla_layers_APZTestCommon_h
diff --git a/gfx/layers/apz/test/gtest/InputUtils.h b/gfx/layers/apz/test/gtest/InputUtils.h
new file mode 100644
index 000000000..a1bd2851e
--- /dev/null
+++ b/gfx/layers/apz/test/gtest/InputUtils.h
@@ -0,0 +1,297 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set sw=2 ts=8 et tw=80 : */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_layers_InputUtils_h
+#define mozilla_layers_InputUtils_h
+
+/**
+ * Defines a set of utility functions for generating input events
+ * to an APZC/APZCTM during APZ gtests.
+ */
+
+#include "APZTestCommon.h"
+
+/* The InputReceiver template parameter used in the helper functions below needs
+ * to be a class that implements functions with the signatures:
+ * nsEventStatus ReceiveInputEvent(const InputData& aEvent,
+ * ScrollableLayerGuid* aGuid,
+ * uint64_t* aOutInputBlockId);
+ * void SetAllowedTouchBehavior(uint64_t aInputBlockId,
+ * const nsTArray<uint32_t>& aBehaviours);
+ * The classes that currently implement these are APZCTreeManager and
+ * TestAsyncPanZoomController. Using this template allows us to test individual
+ * APZC instances in isolation and also an entire APZ tree, while using the same
+ * code to dispatch input events.
+ */
+
+// Some helper functions for constructing input event objects suitable to be
+// passed either to an APZC (which expects an transformed point), or to an APZTM
+// (which expects an untransformed point). We handle both cases by setting both
+// the transformed and untransformed fields to the same value.
+SingleTouchData
+CreateSingleTouchData(int32_t aIdentifier, const ScreenIntPoint& aPoint)
+{
+ SingleTouchData touch(aIdentifier, aPoint, ScreenSize(0, 0), 0, 0);
+ touch.mLocalScreenPoint = ParentLayerPoint(aPoint.x, aPoint.y);
+ return touch;
+}
+
+// Convenience wrapper for CreateSingleTouchData() that takes loose coordinates.
+SingleTouchData
+CreateSingleTouchData(int32_t aIdentifier, ScreenIntCoord aX, ScreenIntCoord aY)
+{
+ return CreateSingleTouchData(aIdentifier, ScreenIntPoint(aX, aY));
+}
+
+PinchGestureInput
+CreatePinchGestureInput(PinchGestureInput::PinchGestureType aType,
+ const ScreenIntPoint& aFocus,
+ float aCurrentSpan, float aPreviousSpan)
+{
+ ParentLayerPoint localFocus(aFocus.x, aFocus.y);
+ PinchGestureInput result(aType, 0, TimeStamp(), localFocus,
+ aCurrentSpan, aPreviousSpan, 0);
+ result.mFocusPoint = aFocus;
+ return result;
+}
+
+template<class InputReceiver>
+void
+SetDefaultAllowedTouchBehavior(const RefPtr<InputReceiver>& aTarget,
+ uint64_t aInputBlockId,
+ int touchPoints = 1)
+{
+ nsTArray<uint32_t> defaultBehaviors;
+ // use the default value where everything is allowed
+ for (int i = 0; i < touchPoints; i++) {
+ defaultBehaviors.AppendElement(mozilla::layers::AllowedTouchBehavior::HORIZONTAL_PAN
+ | mozilla::layers::AllowedTouchBehavior::VERTICAL_PAN
+ | mozilla::layers::AllowedTouchBehavior::PINCH_ZOOM
+ | mozilla::layers::AllowedTouchBehavior::DOUBLE_TAP_ZOOM);
+ }
+ aTarget->SetAllowedTouchBehavior(aInputBlockId, defaultBehaviors);
+}
+
+
+MultiTouchInput
+CreateMultiTouchInput(MultiTouchInput::MultiTouchType aType, TimeStamp aTime)
+{
+ return MultiTouchInput(aType, MillisecondsSinceStartup(aTime), aTime, 0);
+}
+
+template<class InputReceiver>
+nsEventStatus
+TouchDown(const RefPtr<InputReceiver>& aTarget, const ScreenIntPoint& aPoint,
+ TimeStamp aTime, uint64_t* aOutInputBlockId = nullptr)
+{
+ MultiTouchInput mti = CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_START, aTime);
+ mti.mTouches.AppendElement(CreateSingleTouchData(0, aPoint));
+ return aTarget->ReceiveInputEvent(mti, nullptr, aOutInputBlockId);
+}
+
+template<class InputReceiver>
+nsEventStatus
+TouchMove(const RefPtr<InputReceiver>& aTarget, const ScreenIntPoint& aPoint,
+ TimeStamp aTime)
+{
+ MultiTouchInput mti = CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_MOVE, aTime);
+ mti.mTouches.AppendElement(CreateSingleTouchData(0, aPoint));
+ return aTarget->ReceiveInputEvent(mti, nullptr, nullptr);
+}
+
+template<class InputReceiver>
+nsEventStatus
+TouchUp(const RefPtr<InputReceiver>& aTarget, const ScreenIntPoint& aPoint,
+ TimeStamp aTime)
+{
+ MultiTouchInput mti = CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_END, aTime);
+ mti.mTouches.AppendElement(CreateSingleTouchData(0, aPoint));
+ return aTarget->ReceiveInputEvent(mti, nullptr, nullptr);
+}
+
+template<class InputReceiver>
+void
+PinchWithPinchInput(const RefPtr<InputReceiver>& aTarget,
+ const ScreenIntPoint& aFocus,
+ const ScreenIntPoint& aSecondFocus, float aScale,
+ nsEventStatus (*aOutEventStatuses)[3] = nullptr)
+{
+ nsEventStatus actualStatus = aTarget->ReceiveInputEvent(
+ CreatePinchGestureInput(PinchGestureInput::PINCHGESTURE_START,
+ aFocus, 10.0, 10.0),
+ nullptr);
+ if (aOutEventStatuses) {
+ (*aOutEventStatuses)[0] = actualStatus;
+ }
+ actualStatus = aTarget->ReceiveInputEvent(
+ CreatePinchGestureInput(PinchGestureInput::PINCHGESTURE_SCALE,
+ aSecondFocus, 10.0 * aScale, 10.0),
+ nullptr);
+ if (aOutEventStatuses) {
+ (*aOutEventStatuses)[1] = actualStatus;
+ }
+ actualStatus = aTarget->ReceiveInputEvent(
+ CreatePinchGestureInput(PinchGestureInput::PINCHGESTURE_END,
+ // note: negative values here tell APZC
+ // not to turn the pinch into a pan
+ aFocus, -1.0, -1.0),
+ nullptr);
+ if (aOutEventStatuses) {
+ (*aOutEventStatuses)[2] = actualStatus;
+ }
+}
+
+template<class InputReceiver>
+void
+PinchWithPinchInputAndCheckStatus(const RefPtr<InputReceiver>& aTarget,
+ const ScreenIntPoint& aFocus, float aScale,
+ bool aShouldTriggerPinch)
+{
+ nsEventStatus statuses[3]; // scalebegin, scale, scaleend
+ PinchWithPinchInput(aTarget, aFocus, aFocus, aScale, &statuses);
+
+ nsEventStatus expectedStatus = aShouldTriggerPinch
+ ? nsEventStatus_eConsumeNoDefault
+ : nsEventStatus_eIgnore;
+ EXPECT_EQ(expectedStatus, statuses[0]);
+ EXPECT_EQ(expectedStatus, statuses[1]);
+}
+
+template<class InputReceiver>
+void
+PinchWithTouchInput(const RefPtr<InputReceiver>& aTarget,
+ const ScreenIntPoint& aFocus, float aScale,
+ int& inputId,
+ nsTArray<uint32_t>* aAllowedTouchBehaviors = nullptr,
+ nsEventStatus (*aOutEventStatuses)[4] = nullptr,
+ uint64_t* aOutInputBlockId = nullptr)
+{
+ // Having pinch coordinates in float type may cause problems with high-precision scale values
+ // since SingleTouchData accepts integer value. But for trivial tests it should be ok.
+ float pinchLength = 100.0;
+ float pinchLengthScaled = pinchLength * aScale;
+
+ // Even if the caller doesn't care about the block id, we need it to set the
+ // allowed touch behaviour below, so make sure aOutInputBlockId is non-null.
+ uint64_t blockId;
+ if (!aOutInputBlockId) {
+ aOutInputBlockId = &blockId;
+ }
+
+ MultiTouchInput mtiStart = MultiTouchInput(MultiTouchInput::MULTITOUCH_START, 0, TimeStamp(), 0);
+ mtiStart.mTouches.AppendElement(CreateSingleTouchData(inputId, aFocus));
+ mtiStart.mTouches.AppendElement(CreateSingleTouchData(inputId + 1, aFocus));
+ nsEventStatus status = aTarget->ReceiveInputEvent(mtiStart, aOutInputBlockId);
+ if (aOutEventStatuses) {
+ (*aOutEventStatuses)[0] = status;
+ }
+
+ if (aAllowedTouchBehaviors) {
+ EXPECT_EQ(2UL, aAllowedTouchBehaviors->Length());
+ aTarget->SetAllowedTouchBehavior(*aOutInputBlockId, *aAllowedTouchBehaviors);
+ } else if (gfxPrefs::TouchActionEnabled()) {
+ SetDefaultAllowedTouchBehavior(aTarget, *aOutInputBlockId, 2);
+ }
+
+ MultiTouchInput mtiMove1 = MultiTouchInput(MultiTouchInput::MULTITOUCH_MOVE, 0, TimeStamp(), 0);
+ mtiMove1.mTouches.AppendElement(CreateSingleTouchData(inputId, aFocus.x - pinchLength, aFocus.y));
+ mtiMove1.mTouches.AppendElement(CreateSingleTouchData(inputId + 1, aFocus.x + pinchLength, aFocus.y));
+ status = aTarget->ReceiveInputEvent(mtiMove1, nullptr);
+ if (aOutEventStatuses) {
+ (*aOutEventStatuses)[1] = status;
+ }
+
+ MultiTouchInput mtiMove2 = MultiTouchInput(MultiTouchInput::MULTITOUCH_MOVE, 0, TimeStamp(), 0);
+ mtiMove2.mTouches.AppendElement(CreateSingleTouchData(inputId, aFocus.x - pinchLengthScaled, aFocus.y));
+ mtiMove2.mTouches.AppendElement(CreateSingleTouchData(inputId + 1, aFocus.x + pinchLengthScaled, aFocus.y));
+ status = aTarget->ReceiveInputEvent(mtiMove2, nullptr);
+ if (aOutEventStatuses) {
+ (*aOutEventStatuses)[2] = status;
+ }
+
+ MultiTouchInput mtiEnd = MultiTouchInput(MultiTouchInput::MULTITOUCH_END, 0, TimeStamp(), 0);
+ mtiEnd.mTouches.AppendElement(CreateSingleTouchData(inputId, aFocus.x - pinchLengthScaled, aFocus.y));
+ mtiEnd.mTouches.AppendElement(CreateSingleTouchData(inputId + 1, aFocus.x + pinchLengthScaled, aFocus.y));
+ status = aTarget->ReceiveInputEvent(mtiEnd, nullptr);
+ if (aOutEventStatuses) {
+ (*aOutEventStatuses)[3] = status;
+ }
+
+ inputId += 2;
+}
+
+template<class InputReceiver>
+void
+PinchWithTouchInputAndCheckStatus(const RefPtr<InputReceiver>& aTarget,
+ const ScreenIntPoint& aFocus, float aScale,
+ int& inputId, bool aShouldTriggerPinch,
+ nsTArray<uint32_t>* aAllowedTouchBehaviors)
+{
+ nsEventStatus statuses[4]; // down, move, move, up
+ PinchWithTouchInput(aTarget, aFocus, aScale, inputId, aAllowedTouchBehaviors, &statuses);
+
+ nsEventStatus expectedMoveStatus = aShouldTriggerPinch
+ ? nsEventStatus_eConsumeDoDefault
+ : nsEventStatus_eIgnore;
+ EXPECT_EQ(nsEventStatus_eConsumeDoDefault, statuses[0]);
+ EXPECT_EQ(expectedMoveStatus, statuses[1]);
+ EXPECT_EQ(expectedMoveStatus, statuses[2]);
+}
+
+template<class InputReceiver>
+nsEventStatus
+Wheel(const RefPtr<InputReceiver>& aTarget, const ScreenIntPoint& aPoint,
+ const ScreenPoint& aDelta, TimeStamp aTime, uint64_t* aOutInputBlockId = nullptr)
+{
+ ScrollWheelInput input(MillisecondsSinceStartup(aTime), aTime, 0,
+ ScrollWheelInput::SCROLLMODE_INSTANT, ScrollWheelInput::SCROLLDELTA_PIXEL,
+ aPoint, aDelta.x, aDelta.y, false);
+ return aTarget->ReceiveInputEvent(input, nullptr, aOutInputBlockId);
+}
+
+template<class InputReceiver>
+nsEventStatus
+SmoothWheel(const RefPtr<InputReceiver>& aTarget, const ScreenIntPoint& aPoint,
+ const ScreenPoint& aDelta, TimeStamp aTime, uint64_t* aOutInputBlockId = nullptr)
+{
+ ScrollWheelInput input(MillisecondsSinceStartup(aTime), aTime, 0,
+ ScrollWheelInput::SCROLLMODE_SMOOTH, ScrollWheelInput::SCROLLDELTA_LINE,
+ aPoint, aDelta.x, aDelta.y, false);
+ return aTarget->ReceiveInputEvent(input, nullptr, aOutInputBlockId);
+}
+
+template<class InputReceiver>
+nsEventStatus
+MouseDown(const RefPtr<InputReceiver>& aTarget, const ScreenIntPoint& aPoint,
+ TimeStamp aTime, uint64_t* aOutInputBlockId = nullptr)
+{
+ MouseInput input(MouseInput::MOUSE_DOWN, MouseInput::ButtonType::LEFT_BUTTON,
+ 0, 0, aPoint, MillisecondsSinceStartup(aTime), aTime, 0);
+ return aTarget->ReceiveInputEvent(input, nullptr, aOutInputBlockId);
+}
+
+template<class InputReceiver>
+nsEventStatus
+MouseMove(const RefPtr<InputReceiver>& aTarget, const ScreenIntPoint& aPoint,
+ TimeStamp aTime, uint64_t* aOutInputBlockId = nullptr)
+{
+ MouseInput input(MouseInput::MOUSE_MOVE, MouseInput::ButtonType::LEFT_BUTTON,
+ 0, 0, aPoint, MillisecondsSinceStartup(aTime), aTime, 0);
+ return aTarget->ReceiveInputEvent(input, nullptr, aOutInputBlockId);
+}
+
+template<class InputReceiver>
+nsEventStatus
+MouseUp(const RefPtr<InputReceiver>& aTarget, const ScreenIntPoint& aPoint,
+ TimeStamp aTime, uint64_t* aOutInputBlockId = nullptr)
+{
+ MouseInput input(MouseInput::MOUSE_UP, MouseInput::ButtonType::LEFT_BUTTON,
+ 0, 0, aPoint, MillisecondsSinceStartup(aTime), aTime, 0);
+ return aTarget->ReceiveInputEvent(input, nullptr, aOutInputBlockId);
+}
+
+
+#endif // mozilla_layers_InputUtils_h
diff --git a/gfx/layers/apz/test/gtest/TestBasic.cpp b/gfx/layers/apz/test/gtest/TestBasic.cpp
new file mode 100644
index 000000000..921ea4080
--- /dev/null
+++ b/gfx/layers/apz/test/gtest/TestBasic.cpp
@@ -0,0 +1,356 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set sw=2 ts=8 et tw=80 : */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "APZCBasicTester.h"
+#include "APZTestCommon.h"
+#include "InputUtils.h"
+
+TEST_F(APZCBasicTester, Overzoom) {
+ // the visible area of the document in CSS pixels is x=10 y=0 w=100 h=100
+ FrameMetrics fm;
+ fm.SetCompositionBounds(ParentLayerRect(0, 0, 100, 100));
+ fm.SetScrollableRect(CSSRect(0, 0, 125, 150));
+ fm.SetScrollOffset(CSSPoint(10, 0));
+ fm.SetZoom(CSSToParentLayerScale2D(1.0, 1.0));
+ fm.SetIsRootContent(true);
+ apzc->SetFrameMetrics(fm);
+
+ MakeApzcZoomable();
+
+ EXPECT_CALL(*mcc, RequestContentRepaint(_)).Times(1);
+
+ PinchWithPinchInputAndCheckStatus(apzc, ScreenIntPoint(50, 50), 0.5, true);
+
+ fm = apzc->GetFrameMetrics();
+ EXPECT_EQ(0.8f, fm.GetZoom().ToScaleFactor().scale);
+ // bug 936721 - PGO builds introduce rounding error so
+ // use a fuzzy match instead
+ EXPECT_LT(std::abs(fm.GetScrollOffset().x), 1e-5);
+ EXPECT_LT(std::abs(fm.GetScrollOffset().y), 1e-5);
+}
+
+TEST_F(APZCBasicTester, SimpleTransform) {
+ ParentLayerPoint pointOut;
+ AsyncTransform viewTransformOut;
+ apzc->SampleContentTransformForFrame(&viewTransformOut, pointOut);
+
+ EXPECT_EQ(ParentLayerPoint(), pointOut);
+ EXPECT_EQ(AsyncTransform(), viewTransformOut);
+}
+
+
+TEST_F(APZCBasicTester, ComplexTransform) {
+ // This test assumes there is a page that gets rendered to
+ // two layers. In CSS pixels, the first layer is 50x50 and
+ // the second layer is 25x50. The widget scale factor is 3.0
+ // and the presShell resolution is 2.0. Therefore, these layers
+ // end up being 300x300 and 150x300 in layer pixels.
+ //
+ // The second (child) layer has an additional CSS transform that
+ // stretches it by 2.0 on the x-axis. Therefore, after applying
+ // CSS transforms, the two layers are the same size in screen
+ // pixels.
+ //
+ // The screen itself is 24x24 in screen pixels (therefore 4x4 in
+ // CSS pixels). The displayport is 1 extra CSS pixel on all
+ // sides.
+
+ RefPtr<TestAsyncPanZoomController> childApzc =
+ new TestAsyncPanZoomController(0, mcc, tm);
+
+ const char* layerTreeSyntax = "c(c)";
+ // LayerID 0 1
+ nsIntRegion layerVisibleRegion[] = {
+ nsIntRegion(IntRect(0, 0, 300, 300)),
+ nsIntRegion(IntRect(0, 0, 150, 300)),
+ };
+ Matrix4x4 transforms[] = {
+ Matrix4x4(),
+ Matrix4x4(),
+ };
+ transforms[0].PostScale(0.5f, 0.5f, 1.0f); // this results from the 2.0 resolution on the root layer
+ transforms[1].PostScale(2.0f, 1.0f, 1.0f); // this is the 2.0 x-axis CSS transform on the child layer
+
+ nsTArray<RefPtr<Layer> > layers;
+ RefPtr<LayerManager> lm;
+ RefPtr<Layer> root = CreateLayerTree(layerTreeSyntax, layerVisibleRegion, transforms, lm, layers);
+
+ ScrollMetadata metadata;
+ FrameMetrics& metrics = metadata.GetMetrics();
+ metrics.SetCompositionBounds(ParentLayerRect(0, 0, 24, 24));
+ metrics.SetDisplayPort(CSSRect(-1, -1, 6, 6));
+ metrics.SetScrollOffset(CSSPoint(10, 10));
+ metrics.SetScrollableRect(CSSRect(0, 0, 50, 50));
+ metrics.SetCumulativeResolution(LayoutDeviceToLayerScale2D(2, 2));
+ metrics.SetPresShellResolution(2.0f);
+ metrics.SetZoom(CSSToParentLayerScale2D(6, 6));
+ metrics.SetDevPixelsPerCSSPixel(CSSToLayoutDeviceScale(3));
+ metrics.SetScrollId(FrameMetrics::START_SCROLL_ID);
+
+ ScrollMetadata childMetadata = metadata;
+ FrameMetrics& childMetrics = childMetadata.GetMetrics();
+ childMetrics.SetScrollId(FrameMetrics::START_SCROLL_ID + 1);
+
+ layers[0]->SetScrollMetadata(metadata);
+ layers[1]->SetScrollMetadata(childMetadata);
+
+ ParentLayerPoint pointOut;
+ AsyncTransform viewTransformOut;
+
+ // Both the parent and child layer should behave exactly the same here, because
+ // the CSS transform on the child layer does not affect the SampleContentTransformForFrame code
+
+ // initial transform
+ apzc->SetFrameMetrics(metrics);
+ apzc->NotifyLayersUpdated(metadata, true, true);
+ apzc->SampleContentTransformForFrame(&viewTransformOut, pointOut);
+ EXPECT_EQ(AsyncTransform(LayerToParentLayerScale(1), ParentLayerPoint()), viewTransformOut);
+ EXPECT_EQ(ParentLayerPoint(60, 60), pointOut);
+
+ childApzc->SetFrameMetrics(childMetrics);
+ childApzc->NotifyLayersUpdated(childMetadata, true, true);
+ childApzc->SampleContentTransformForFrame(&viewTransformOut, pointOut);
+ EXPECT_EQ(AsyncTransform(LayerToParentLayerScale(1), ParentLayerPoint()), viewTransformOut);
+ EXPECT_EQ(ParentLayerPoint(60, 60), pointOut);
+
+ // do an async scroll by 5 pixels and check the transform
+ metrics.ScrollBy(CSSPoint(5, 0));
+ apzc->SetFrameMetrics(metrics);
+ apzc->SampleContentTransformForFrame(&viewTransformOut, pointOut);
+ EXPECT_EQ(AsyncTransform(LayerToParentLayerScale(1), ParentLayerPoint(-30, 0)), viewTransformOut);
+ EXPECT_EQ(ParentLayerPoint(90, 60), pointOut);
+
+ childMetrics.ScrollBy(CSSPoint(5, 0));
+ childApzc->SetFrameMetrics(childMetrics);
+ childApzc->SampleContentTransformForFrame(&viewTransformOut, pointOut);
+ EXPECT_EQ(AsyncTransform(LayerToParentLayerScale(1), ParentLayerPoint(-30, 0)), viewTransformOut);
+ EXPECT_EQ(ParentLayerPoint(90, 60), pointOut);
+
+ // do an async zoom of 1.5x and check the transform
+ metrics.ZoomBy(1.5f);
+ apzc->SetFrameMetrics(metrics);
+ apzc->SampleContentTransformForFrame(&viewTransformOut, pointOut);
+ EXPECT_EQ(AsyncTransform(LayerToParentLayerScale(1.5), ParentLayerPoint(-45, 0)), viewTransformOut);
+ EXPECT_EQ(ParentLayerPoint(135, 90), pointOut);
+
+ childMetrics.ZoomBy(1.5f);
+ childApzc->SetFrameMetrics(childMetrics);
+ childApzc->SampleContentTransformForFrame(&viewTransformOut, pointOut);
+ EXPECT_EQ(AsyncTransform(LayerToParentLayerScale(1.5), ParentLayerPoint(-45, 0)), viewTransformOut);
+ EXPECT_EQ(ParentLayerPoint(135, 90), pointOut);
+
+ childApzc->Destroy();
+}
+
+TEST_F(APZCBasicTester, Fling) {
+ SCOPED_GFX_PREF(APZFlingMinVelocityThreshold, float, 0.0f);
+ int touchStart = 50;
+ int touchEnd = 10;
+ ParentLayerPoint pointOut;
+ AsyncTransform viewTransformOut;
+
+ // Fling down. Each step scroll further down
+ Pan(apzc, touchStart, touchEnd);
+ ParentLayerPoint lastPoint;
+ for (int i = 1; i < 50; i+=1) {
+ apzc->SampleContentTransformForFrame(&viewTransformOut, pointOut, TimeDuration::FromMilliseconds(1));
+ EXPECT_GT(pointOut.y, lastPoint.y);
+ lastPoint = pointOut;
+ }
+}
+
+TEST_F(APZCBasicTester, FlingIntoOverscroll) {
+ // Enable overscrolling.
+ SCOPED_GFX_PREF(APZOverscrollEnabled, bool, true);
+ SCOPED_GFX_PREF(APZFlingMinVelocityThreshold, float, 0.0f);
+
+ // Scroll down by 25 px. Don't fling for simplicity.
+ ApzcPanNoFling(apzc, 50, 25);
+
+ // Now scroll back up by 20px, this time flinging after.
+ // The fling should cover the remaining 5 px of room to scroll, then
+ // go into overscroll, and finally snap-back to recover from overscroll.
+ Pan(apzc, 25, 45);
+ const TimeDuration increment = TimeDuration::FromMilliseconds(1);
+ bool reachedOverscroll = false;
+ bool recoveredFromOverscroll = false;
+ while (apzc->AdvanceAnimations(mcc->Time())) {
+ if (!reachedOverscroll && apzc->IsOverscrolled()) {
+ reachedOverscroll = true;
+ }
+ if (reachedOverscroll && !apzc->IsOverscrolled()) {
+ recoveredFromOverscroll = true;
+ }
+ mcc->AdvanceBy(increment);
+ }
+ EXPECT_TRUE(reachedOverscroll);
+ EXPECT_TRUE(recoveredFromOverscroll);
+}
+
+TEST_F(APZCBasicTester, PanningTransformNotifications) {
+ SCOPED_GFX_PREF(APZOverscrollEnabled, bool, true);
+
+ // Scroll down by 25 px. Ensure we only get one set of
+ // state change notifications.
+ //
+ // Then, scroll back up by 20px, this time flinging after.
+ // The fling should cover the remaining 5 px of room to scroll, then
+ // go into overscroll, and finally snap-back to recover from overscroll.
+ // Again, ensure we only get one set of state change notifications for
+ // this entire procedure.
+
+ MockFunction<void(std::string checkPointName)> check;
+ {
+ InSequence s;
+ EXPECT_CALL(check, Call("Simple pan"));
+ EXPECT_CALL(*mcc, NotifyAPZStateChange(_,GeckoContentController::APZStateChange::eStartTouch,_)).Times(1);
+ EXPECT_CALL(*mcc, NotifyAPZStateChange(_,GeckoContentController::APZStateChange::eTransformBegin,_)).Times(1);
+ EXPECT_CALL(*mcc, NotifyAPZStateChange(_,GeckoContentController::APZStateChange::eStartPanning,_)).Times(1);
+ EXPECT_CALL(*mcc, NotifyAPZStateChange(_,GeckoContentController::APZStateChange::eEndTouch,_)).Times(1);
+ EXPECT_CALL(*mcc, NotifyAPZStateChange(_,GeckoContentController::APZStateChange::eTransformEnd,_)).Times(1);
+ EXPECT_CALL(check, Call("Complex pan"));
+ EXPECT_CALL(*mcc, NotifyAPZStateChange(_,GeckoContentController::APZStateChange::eStartTouch,_)).Times(1);
+ EXPECT_CALL(*mcc, NotifyAPZStateChange(_,GeckoContentController::APZStateChange::eTransformBegin,_)).Times(1);
+ EXPECT_CALL(*mcc, NotifyAPZStateChange(_,GeckoContentController::APZStateChange::eStartPanning,_)).Times(1);
+ EXPECT_CALL(*mcc, NotifyAPZStateChange(_,GeckoContentController::APZStateChange::eEndTouch,_)).Times(1);
+ EXPECT_CALL(*mcc, NotifyAPZStateChange(_,GeckoContentController::APZStateChange::eTransformEnd,_)).Times(1);
+ EXPECT_CALL(check, Call("Done"));
+ }
+
+ check.Call("Simple pan");
+ ApzcPanNoFling(apzc, 50, 25);
+ check.Call("Complex pan");
+ Pan(apzc, 25, 45);
+ apzc->AdvanceAnimationsUntilEnd();
+ check.Call("Done");
+}
+
+void APZCBasicTester::PanIntoOverscroll()
+{
+ int touchStart = 500;
+ int touchEnd = 10;
+ Pan(apzc, touchStart, touchEnd);
+ EXPECT_TRUE(apzc->IsOverscrolled());
+}
+
+void APZCBasicTester::TestOverscroll()
+{
+ // Pan sufficiently to hit overscroll behavior
+ PanIntoOverscroll();
+
+ // Check that we recover from overscroll via an animation.
+ ParentLayerPoint expectedScrollOffset(0, GetScrollRange().YMost());
+ SampleAnimationUntilRecoveredFromOverscroll(expectedScrollOffset);
+}
+
+
+TEST_F(APZCBasicTester, OverScrollPanning) {
+ SCOPED_GFX_PREF(APZOverscrollEnabled, bool, true);
+
+ TestOverscroll();
+}
+
+// Tests that an overscroll animation doesn't trigger an assertion failure
+// in the case where a sample has a velocity of zero.
+TEST_F(APZCBasicTester, OverScroll_Bug1152051a) {
+ SCOPED_GFX_PREF(APZOverscrollEnabled, bool, true);
+
+ // Doctor the prefs to make the velocity zero at the end of the first sample.
+
+ // This ensures our incoming velocity to the overscroll animation is
+ // a round(ish) number, 4.9 (that being the distance of the pan before
+ // overscroll, which is 500 - 10 = 490 pixels, divided by the duration of
+ // the pan, which is 100 ms).
+ SCOPED_GFX_PREF(APZFlingFriction, float, 0);
+
+ // To ensure the velocity after the first sample is 0, set the spring
+ // stiffness to the incoming velocity (4.9) divided by the overscroll
+ // (400 pixels) times the step duration (1 ms).
+ SCOPED_GFX_PREF(APZOverscrollSpringStiffness, float, 0.01225f);
+
+ TestOverscroll();
+}
+
+// Tests that ending an overscroll animation doesn't leave around state that
+// confuses the next overscroll animation.
+TEST_F(APZCBasicTester, OverScroll_Bug1152051b) {
+ SCOPED_GFX_PREF(APZOverscrollEnabled, bool, true);
+
+ SCOPED_GFX_PREF(APZOverscrollStopDistanceThreshold, float, 0.1f);
+
+ // Pan sufficiently to hit overscroll behavior
+ PanIntoOverscroll();
+
+ // Sample animations once, to give the fling animation started on touch-up
+ // a chance to realize it's overscrolled, and schedule a call to
+ // HandleFlingOverscroll().
+ SampleAnimationOnce();
+
+ // This advances the time and runs the HandleFlingOverscroll task scheduled in
+ // the previous call, which starts an overscroll animation. It then samples
+ // the overscroll animation once, to get it to initialize the first overscroll
+ // sample.
+ SampleAnimationOnce();
+
+ // Do a touch-down to cancel the overscroll animation, and then a touch-up
+ // to schedule a new one since we're still overscrolled. We don't pan because
+ // panning can trigger functions that clear the overscroll animation state
+ // in other ways.
+ uint64_t blockId;
+ nsEventStatus status = TouchDown(apzc, ScreenIntPoint(10, 10), mcc->Time(), &blockId);
+ if (gfxPrefs::TouchActionEnabled() && status != nsEventStatus_eConsumeNoDefault) {
+ SetDefaultAllowedTouchBehavior(apzc, blockId);
+ }
+ TouchUp(apzc, ScreenIntPoint(10, 10), mcc->Time());
+
+ // Sample the second overscroll animation to its end.
+ // If the ending of the first overscroll animation fails to clear state
+ // properly, this will assert.
+ ParentLayerPoint expectedScrollOffset(0, GetScrollRange().YMost());
+ SampleAnimationUntilRecoveredFromOverscroll(expectedScrollOffset);
+}
+
+TEST_F(APZCBasicTester, OverScrollAbort) {
+ SCOPED_GFX_PREF(APZOverscrollEnabled, bool, true);
+
+ // Pan sufficiently to hit overscroll behavior
+ int touchStart = 500;
+ int touchEnd = 10;
+ Pan(apzc, touchStart, touchEnd);
+ EXPECT_TRUE(apzc->IsOverscrolled());
+
+ ParentLayerPoint pointOut;
+ AsyncTransform viewTransformOut;
+
+ // This sample call will run to the end of the fling animation
+ // and will schedule the overscroll animation.
+ apzc->SampleContentTransformForFrame(&viewTransformOut, pointOut, TimeDuration::FromMilliseconds(10000));
+ EXPECT_TRUE(apzc->IsOverscrolled());
+
+ // At this point, we have an active overscroll animation.
+ // Check that cancelling the animation clears the overscroll.
+ apzc->CancelAnimation();
+ EXPECT_FALSE(apzc->IsOverscrolled());
+ apzc->AssertStateIsReset();
+}
+
+TEST_F(APZCBasicTester, OverScrollPanningAbort) {
+ SCOPED_GFX_PREF(APZOverscrollEnabled, bool, true);
+
+ // Pan sufficiently to hit overscroll behaviour. Keep the finger down so
+ // the pan does not end.
+ int touchStart = 500;
+ int touchEnd = 10;
+ Pan(apzc, touchStart, touchEnd, true); // keep finger down
+ EXPECT_TRUE(apzc->IsOverscrolled());
+
+ // Check that calling CancelAnimation() while the user is still panning
+ // (and thus no fling or snap-back animation has had a chance to start)
+ // clears the overscroll.
+ apzc->CancelAnimation();
+ EXPECT_FALSE(apzc->IsOverscrolled());
+ apzc->AssertStateIsReset();
+}
diff --git a/gfx/layers/apz/test/gtest/TestEventRegions.cpp b/gfx/layers/apz/test/gtest/TestEventRegions.cpp
new file mode 100644
index 000000000..8b3aac348
--- /dev/null
+++ b/gfx/layers/apz/test/gtest/TestEventRegions.cpp
@@ -0,0 +1,272 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set sw=2 ts=8 et tw=80 : */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "APZCTreeManagerTester.h"
+#include "APZTestCommon.h"
+#include "InputUtils.h"
+
+class APZEventRegionsTester : public APZCTreeManagerTester {
+protected:
+ UniquePtr<ScopedLayerTreeRegistration> registration;
+ TestAsyncPanZoomController* rootApzc;
+
+ void CreateEventRegionsLayerTree1() {
+ const char* layerTreeSyntax = "c(tt)";
+ nsIntRegion layerVisibleRegions[] = {
+ nsIntRegion(IntRect(0, 0, 200, 200)), // root
+ nsIntRegion(IntRect(0, 0, 100, 200)), // left half
+ nsIntRegion(IntRect(0, 100, 200, 100)), // bottom half
+ };
+ root = CreateLayerTree(layerTreeSyntax, layerVisibleRegions, nullptr, lm, layers);
+ SetScrollableFrameMetrics(root, FrameMetrics::START_SCROLL_ID);
+ SetScrollableFrameMetrics(layers[1], FrameMetrics::START_SCROLL_ID + 1);
+ SetScrollableFrameMetrics(layers[2], FrameMetrics::START_SCROLL_ID + 2);
+ SetScrollHandoff(layers[1], root);
+ SetScrollHandoff(layers[2], root);
+
+ // Set up the event regions over a 200x200 area. The root layer has the
+ // whole 200x200 as the hit region; layers[1] has the left half and
+ // layers[2] has the bottom half. The bottom-left 100x100 area is also
+ // in the d-t-c region for both layers[1] and layers[2] (but layers[2] is
+ // on top so it gets the events by default if the main thread doesn't
+ // respond).
+ EventRegions regions(nsIntRegion(IntRect(0, 0, 200, 200)));
+ root->SetEventRegions(regions);
+ regions.mDispatchToContentHitRegion = nsIntRegion(IntRect(0, 100, 100, 100));
+ regions.mHitRegion = nsIntRegion(IntRect(0, 0, 100, 200));
+ layers[1]->SetEventRegions(regions);
+ regions.mHitRegion = nsIntRegion(IntRect(0, 100, 200, 100));
+ layers[2]->SetEventRegions(regions);
+
+ registration = MakeUnique<ScopedLayerTreeRegistration>(manager, 0, root, mcc);
+ manager->UpdateHitTestingTree(0, root, false, 0, 0);
+ rootApzc = ApzcOf(root);
+ }
+
+ void CreateEventRegionsLayerTree2() {
+ const char* layerTreeSyntax = "c(t)";
+ nsIntRegion layerVisibleRegions[] = {
+ nsIntRegion(IntRect(0, 0, 100, 500)),
+ nsIntRegion(IntRect(0, 150, 100, 100)),
+ };
+ root = CreateLayerTree(layerTreeSyntax, layerVisibleRegions, nullptr, lm, layers);
+ SetScrollableFrameMetrics(root, FrameMetrics::START_SCROLL_ID);
+
+ // Set up the event regions so that the child thebes layer is positioned far
+ // away from the scrolling container layer.
+ EventRegions regions(nsIntRegion(IntRect(0, 0, 100, 100)));
+ root->SetEventRegions(regions);
+ regions.mHitRegion = nsIntRegion(IntRect(0, 150, 100, 100));
+ layers[1]->SetEventRegions(regions);
+
+ registration = MakeUnique<ScopedLayerTreeRegistration>(manager, 0, root, mcc);
+ manager->UpdateHitTestingTree(0, root, false, 0, 0);
+ rootApzc = ApzcOf(root);
+ }
+
+ void CreateObscuringLayerTree() {
+ const char* layerTreeSyntax = "c(c(t)t)";
+ // LayerID 0 1 2 3
+ // 0 is the root.
+ // 1 is a parent scrollable layer.
+ // 2 is a child scrollable layer.
+ // 3 is the Obscurer, who ruins everything.
+ nsIntRegion layerVisibleRegions[] = {
+ // x coordinates are uninteresting
+ nsIntRegion(IntRect(0, 0, 200, 200)), // [0, 200]
+ nsIntRegion(IntRect(0, 0, 200, 200)), // [0, 200]
+ nsIntRegion(IntRect(0, 100, 200, 50)), // [100, 150]
+ nsIntRegion(IntRect(0, 100, 200, 100)) // [100, 200]
+ };
+ root = CreateLayerTree(layerTreeSyntax, layerVisibleRegions, nullptr, lm, layers);
+
+ SetScrollableFrameMetrics(root, FrameMetrics::START_SCROLL_ID, CSSRect(0, 0, 200, 200));
+ SetScrollableFrameMetrics(layers[1], FrameMetrics::START_SCROLL_ID + 1, CSSRect(0, 0, 200, 300));
+ SetScrollableFrameMetrics(layers[2], FrameMetrics::START_SCROLL_ID + 2, CSSRect(0, 0, 200, 100));
+ SetScrollHandoff(layers[2], layers[1]);
+ SetScrollHandoff(layers[1], root);
+
+ EventRegions regions(nsIntRegion(IntRect(0, 0, 200, 200)));
+ root->SetEventRegions(regions);
+ regions.mHitRegion = nsIntRegion(IntRect(0, 0, 200, 300));
+ layers[1]->SetEventRegions(regions);
+ regions.mHitRegion = nsIntRegion(IntRect(0, 100, 200, 100));
+ layers[2]->SetEventRegions(regions);
+
+ registration = MakeUnique<ScopedLayerTreeRegistration>(manager, 0, root, mcc);
+ manager->UpdateHitTestingTree(0, root, false, 0, 0);
+ rootApzc = ApzcOf(root);
+ }
+
+ void CreateBug1119497LayerTree() {
+ const char* layerTreeSyntax = "c(tt)";
+ // LayerID 0 12
+ // 0 is the root and has an APZC
+ // 1 is behind 2 and has an APZC
+ // 2 entirely covers 1 and should take all the input events, but has no APZC
+ // so hits to 2 should go to to the root APZC
+ nsIntRegion layerVisibleRegions[] = {
+ nsIntRegion(IntRect(0, 0, 100, 100)),
+ nsIntRegion(IntRect(0, 0, 100, 100)),
+ nsIntRegion(IntRect(0, 0, 100, 100)),
+ };
+ root = CreateLayerTree(layerTreeSyntax, layerVisibleRegions, nullptr, lm, layers);
+
+ SetScrollableFrameMetrics(root, FrameMetrics::START_SCROLL_ID);
+ SetScrollableFrameMetrics(layers[1], FrameMetrics::START_SCROLL_ID + 1);
+
+ registration = MakeUnique<ScopedLayerTreeRegistration>(manager, 0, root, mcc);
+ manager->UpdateHitTestingTree(0, root, false, 0, 0);
+ }
+
+ void CreateBug1117712LayerTree() {
+ const char* layerTreeSyntax = "c(c(t)t)";
+ // LayerID 0 1 2 3
+ // 0 is the root
+ // 1 is a container layer whose sole purpose to make a non-empty ancestor
+ // transform for 2, so that 2's screen-to-apzc and apzc-to-gecko
+ // transforms are different from 3's.
+ // 2 is a small layer that is the actual target
+ // 3 is a big layer obscuring 2 with a dispatch-to-content region
+ nsIntRegion layerVisibleRegions[] = {
+ nsIntRegion(IntRect(0, 0, 100, 100)),
+ nsIntRegion(IntRect(0, 0, 0, 0)),
+ nsIntRegion(IntRect(0, 0, 10, 10)),
+ nsIntRegion(IntRect(0, 0, 100, 100)),
+ };
+ Matrix4x4 layerTransforms[] = {
+ Matrix4x4(),
+ Matrix4x4::Translation(50, 0, 0),
+ Matrix4x4(),
+ Matrix4x4(),
+ };
+ root = CreateLayerTree(layerTreeSyntax, layerVisibleRegions, layerTransforms, lm, layers);
+
+ SetScrollableFrameMetrics(layers[2], FrameMetrics::START_SCROLL_ID, CSSRect(0, 0, 10, 10));
+ SetScrollableFrameMetrics(layers[3], FrameMetrics::START_SCROLL_ID + 1, CSSRect(0, 0, 100, 100));
+ SetScrollHandoff(layers[3], layers[2]);
+
+ EventRegions regions(nsIntRegion(IntRect(0, 0, 10, 10)));
+ layers[2]->SetEventRegions(regions);
+ regions.mHitRegion = nsIntRegion(IntRect(0, 0, 100, 100));
+ regions.mDispatchToContentHitRegion = nsIntRegion(IntRect(0, 0, 100, 100));
+ layers[3]->SetEventRegions(regions);
+
+ registration = MakeUnique<ScopedLayerTreeRegistration>(manager, 0, root, mcc);
+ manager->UpdateHitTestingTree(0, root, false, 0, 0);
+ }
+};
+
+TEST_F(APZEventRegionsTester, HitRegionImmediateResponse) {
+ CreateEventRegionsLayerTree1();
+
+ TestAsyncPanZoomController* root = ApzcOf(layers[0]);
+ TestAsyncPanZoomController* left = ApzcOf(layers[1]);
+ TestAsyncPanZoomController* bottom = ApzcOf(layers[2]);
+
+ MockFunction<void(std::string checkPointName)> check;
+ {
+ InSequence s;
+ EXPECT_CALL(*mcc, HandleTap(TapType::eSingleTap, _, _, left->GetGuid(), _)).Times(1);
+ EXPECT_CALL(check, Call("Tapped on left"));
+ EXPECT_CALL(*mcc, HandleTap(TapType::eSingleTap, _, _, bottom->GetGuid(), _)).Times(1);
+ EXPECT_CALL(check, Call("Tapped on bottom"));
+ EXPECT_CALL(*mcc, HandleTap(TapType::eSingleTap, _, _, root->GetGuid(), _)).Times(1);
+ EXPECT_CALL(check, Call("Tapped on root"));
+ EXPECT_CALL(check, Call("Tap pending on d-t-c region"));
+ EXPECT_CALL(*mcc, HandleTap(TapType::eSingleTap, _, _, bottom->GetGuid(), _)).Times(1);
+ EXPECT_CALL(check, Call("Tapped on bottom again"));
+ EXPECT_CALL(*mcc, HandleTap(TapType::eSingleTap, _, _, left->GetGuid(), _)).Times(1);
+ EXPECT_CALL(check, Call("Tapped on left this time"));
+ }
+
+ TimeDuration tapDuration = TimeDuration::FromMilliseconds(100);
+
+ // Tap in the exposed hit regions of each of the layers once and ensure
+ // the clicks are dispatched right away
+ Tap(manager, ScreenIntPoint(10, 10), tapDuration);
+ mcc->RunThroughDelayedTasks(); // this runs the tap event
+ check.Call("Tapped on left");
+ Tap(manager, ScreenIntPoint(110, 110), tapDuration);
+ mcc->RunThroughDelayedTasks(); // this runs the tap event
+ check.Call("Tapped on bottom");
+ Tap(manager, ScreenIntPoint(110, 10), tapDuration);
+ mcc->RunThroughDelayedTasks(); // this runs the tap event
+ check.Call("Tapped on root");
+
+ // Now tap on the dispatch-to-content region where the layers overlap
+ Tap(manager, ScreenIntPoint(10, 110), tapDuration);
+ mcc->RunThroughDelayedTasks(); // this runs the main-thread timeout
+ check.Call("Tap pending on d-t-c region");
+ mcc->RunThroughDelayedTasks(); // this runs the tap event
+ check.Call("Tapped on bottom again");
+
+ // Now let's do that again, but simulate a main-thread response
+ uint64_t inputBlockId = 0;
+ Tap(manager, ScreenIntPoint(10, 110), tapDuration, nullptr, &inputBlockId);
+ nsTArray<ScrollableLayerGuid> targets;
+ targets.AppendElement(left->GetGuid());
+ manager->SetTargetAPZC(inputBlockId, targets);
+ while (mcc->RunThroughDelayedTasks()); // this runs the tap event
+ check.Call("Tapped on left this time");
+}
+
+TEST_F(APZEventRegionsTester, HitRegionAccumulatesChildren) {
+ CreateEventRegionsLayerTree2();
+
+ // Tap in the area of the child layer that's not directly included in the
+ // parent layer's hit region. Verify that it comes out of the APZC's
+ // content controller, which indicates the input events got routed correctly
+ // to the APZC.
+ EXPECT_CALL(*mcc, HandleTap(TapType::eSingleTap, _, _, rootApzc->GetGuid(), _)).Times(1);
+ Tap(manager, ScreenIntPoint(10, 160), TimeDuration::FromMilliseconds(100));
+}
+
+TEST_F(APZEventRegionsTester, Obscuration) {
+ CreateObscuringLayerTree();
+ ScopedLayerTreeRegistration registration(manager, 0, root, mcc);
+
+ manager->UpdateHitTestingTree(0, root, false, 0, 0);
+
+ TestAsyncPanZoomController* parent = ApzcOf(layers[1]);
+ TestAsyncPanZoomController* child = ApzcOf(layers[2]);
+
+ ApzcPanNoFling(parent, 75, 25);
+
+ HitTestResult result;
+ RefPtr<AsyncPanZoomController> hit = manager->GetTargetAPZC(ScreenPoint(50, 75), &result);
+ EXPECT_EQ(child, hit.get());
+ EXPECT_EQ(HitTestResult::HitLayer, result);
+}
+
+TEST_F(APZEventRegionsTester, Bug1119497) {
+ CreateBug1119497LayerTree();
+
+ HitTestResult result;
+ RefPtr<AsyncPanZoomController> hit = manager->GetTargetAPZC(ScreenPoint(50, 50), &result);
+ // We should hit layers[2], so |result| will be HitLayer but there's no
+ // actual APZC on layers[2], so it will be the APZC of the root layer.
+ EXPECT_EQ(ApzcOf(layers[0]), hit.get());
+ EXPECT_EQ(HitTestResult::HitLayer, result);
+}
+
+TEST_F(APZEventRegionsTester, Bug1117712) {
+ CreateBug1117712LayerTree();
+
+ TestAsyncPanZoomController* apzc2 = ApzcOf(layers[2]);
+
+ // These touch events should hit the dispatch-to-content region of layers[3]
+ // and so get queued with that APZC as the tentative target.
+ uint64_t inputBlockId = 0;
+ Tap(manager, ScreenIntPoint(55, 5), TimeDuration::FromMilliseconds(100), nullptr, &inputBlockId);
+ // But now we tell the APZ that really it hit layers[2], and expect the tap
+ // to be delivered at the correct coordinates.
+ EXPECT_CALL(*mcc, HandleTap(TapType::eSingleTap, LayoutDevicePoint(55, 5), 0, apzc2->GetGuid(), _)).Times(1);
+
+ nsTArray<ScrollableLayerGuid> targets;
+ targets.AppendElement(apzc2->GetGuid());
+ manager->SetTargetAPZC(inputBlockId, targets);
+}
diff --git a/gfx/layers/apz/test/gtest/TestGestureDetector.cpp b/gfx/layers/apz/test/gtest/TestGestureDetector.cpp
new file mode 100644
index 000000000..fcbc250f7
--- /dev/null
+++ b/gfx/layers/apz/test/gtest/TestGestureDetector.cpp
@@ -0,0 +1,638 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set sw=2 ts=8 et tw=80 : */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "APZCBasicTester.h"
+#include "APZTestCommon.h"
+
+class APZCGestureDetectorTester : public APZCBasicTester {
+public:
+ APZCGestureDetectorTester()
+ : APZCBasicTester(AsyncPanZoomController::USE_GESTURE_DETECTOR)
+ {
+ }
+
+protected:
+ FrameMetrics GetPinchableFrameMetrics()
+ {
+ FrameMetrics fm;
+ fm.SetCompositionBounds(ParentLayerRect(200, 200, 100, 200));
+ fm.SetScrollableRect(CSSRect(0, 0, 980, 1000));
+ fm.SetScrollOffset(CSSPoint(300, 300));
+ fm.SetZoom(CSSToParentLayerScale2D(2.0, 2.0));
+ // APZC only allows zooming on the root scrollable frame.
+ fm.SetIsRootContent(true);
+ // the visible area of the document in CSS pixels is x=300 y=300 w=50 h=100
+ return fm;
+ }
+};
+
+TEST_F(APZCGestureDetectorTester, Pan_After_Pinch) {
+ SCOPED_GFX_PREF(TouchActionEnabled, bool, false);
+
+ FrameMetrics originalMetrics = GetPinchableFrameMetrics();
+ apzc->SetFrameMetrics(originalMetrics);
+
+ MakeApzcZoomable();
+
+ // Test parameters
+ float zoomAmount = 1.25;
+ float pinchLength = 100.0;
+ float pinchLengthScaled = pinchLength * zoomAmount;
+ int focusX = 250;
+ int focusY = 300;
+ int panDistance = 20;
+
+ int firstFingerId = 0;
+ int secondFingerId = firstFingerId + 1;
+
+ // Put fingers down
+ MultiTouchInput mti = MultiTouchInput(MultiTouchInput::MULTITOUCH_START, 0, TimeStamp(), 0);
+ mti.mTouches.AppendElement(CreateSingleTouchData(firstFingerId, focusX, focusY));
+ mti.mTouches.AppendElement(CreateSingleTouchData(secondFingerId, focusX, focusY));
+ apzc->ReceiveInputEvent(mti, nullptr);
+
+ // Spread fingers out to enter the pinch state
+ mti = MultiTouchInput(MultiTouchInput::MULTITOUCH_MOVE, 0, TimeStamp(), 0);
+ mti.mTouches.AppendElement(CreateSingleTouchData(firstFingerId, focusX - pinchLength, focusY));
+ mti.mTouches.AppendElement(CreateSingleTouchData(secondFingerId, focusX + pinchLength, focusY));
+ apzc->ReceiveInputEvent(mti, nullptr);
+
+ // Do the actual pinch of 1.25x
+ mti = MultiTouchInput(MultiTouchInput::MULTITOUCH_MOVE, 0, TimeStamp(), 0);
+ mti.mTouches.AppendElement(CreateSingleTouchData(firstFingerId, focusX - pinchLengthScaled, focusY));
+ mti.mTouches.AppendElement(CreateSingleTouchData(secondFingerId, focusX + pinchLengthScaled, focusY));
+ apzc->ReceiveInputEvent(mti, nullptr);
+
+ // Verify that the zoom changed, just to make sure our code above did what it
+ // was supposed to.
+ FrameMetrics zoomedMetrics = apzc->GetFrameMetrics();
+ float newZoom = zoomedMetrics.GetZoom().ToScaleFactor().scale;
+ EXPECT_EQ(originalMetrics.GetZoom().ToScaleFactor().scale * zoomAmount, newZoom);
+
+ // Now we lift one finger...
+ mti = MultiTouchInput(MultiTouchInput::MULTITOUCH_END, 0, TimeStamp(), 0);
+ mti.mTouches.AppendElement(CreateSingleTouchData(secondFingerId, focusX + pinchLengthScaled, focusY));
+ apzc->ReceiveInputEvent(mti, nullptr);
+
+ // ... and pan with the remaining finger. This pan just breaks through the
+ // distance threshold.
+ focusY += 40;
+ mti = MultiTouchInput(MultiTouchInput::MULTITOUCH_MOVE, 0, TimeStamp(), 0);
+ mti.mTouches.AppendElement(CreateSingleTouchData(firstFingerId, focusX - pinchLengthScaled, focusY));
+ apzc->ReceiveInputEvent(mti, nullptr);
+
+ // This one does an actual pan of 20 pixels
+ focusY += panDistance;
+ mti = MultiTouchInput(MultiTouchInput::MULTITOUCH_MOVE, 0, TimeStamp(), 0);
+ mti.mTouches.AppendElement(CreateSingleTouchData(firstFingerId, focusX - pinchLengthScaled, focusY));
+ apzc->ReceiveInputEvent(mti, nullptr);
+
+ // Lift the remaining finger
+ mti = MultiTouchInput(MultiTouchInput::MULTITOUCH_END, 0, TimeStamp(), 0);
+ mti.mTouches.AppendElement(CreateSingleTouchData(firstFingerId, focusX - pinchLengthScaled, focusY));
+ apzc->ReceiveInputEvent(mti, nullptr);
+
+ // Verify that we scrolled
+ FrameMetrics finalMetrics = apzc->GetFrameMetrics();
+ EXPECT_EQ(zoomedMetrics.GetScrollOffset().y - (panDistance / newZoom), finalMetrics.GetScrollOffset().y);
+
+ // Clear out any remaining fling animation and pending tasks
+ apzc->AdvanceAnimationsUntilEnd();
+ while (mcc->RunThroughDelayedTasks());
+ apzc->AssertStateIsReset();
+}
+
+TEST_F(APZCGestureDetectorTester, Pan_With_Tap) {
+ SCOPED_GFX_PREF(TouchActionEnabled, bool, false);
+
+ FrameMetrics originalMetrics = GetPinchableFrameMetrics();
+ apzc->SetFrameMetrics(originalMetrics);
+
+ // Making the APZC zoomable isn't really needed for the correct operation of
+ // this test, but it could help catch regressions where we accidentally enter
+ // a pinch state.
+ MakeApzcZoomable();
+
+ // Test parameters
+ int touchX = 250;
+ int touchY = 300;
+ int panDistance = 20;
+
+ int firstFingerId = 0;
+ int secondFingerId = firstFingerId + 1;
+
+ // Put finger down
+ MultiTouchInput mti = MultiTouchInput(MultiTouchInput::MULTITOUCH_START, 0, TimeStamp(), 0);
+ mti.mTouches.AppendElement(CreateSingleTouchData(firstFingerId, touchX, touchY));
+ apzc->ReceiveInputEvent(mti, nullptr);
+
+ // Start a pan, break through the threshold
+ touchY += 40;
+ mti = MultiTouchInput(MultiTouchInput::MULTITOUCH_MOVE, 0, TimeStamp(), 0);
+ mti.mTouches.AppendElement(CreateSingleTouchData(firstFingerId, touchX, touchY));
+ apzc->ReceiveInputEvent(mti, nullptr);
+
+ // Do an actual pan for a bit
+ touchY += panDistance;
+ mti = MultiTouchInput(MultiTouchInput::MULTITOUCH_MOVE, 0, TimeStamp(), 0);
+ mti.mTouches.AppendElement(CreateSingleTouchData(firstFingerId, touchX, touchY));
+ apzc->ReceiveInputEvent(mti, nullptr);
+
+ // Put a second finger down
+ mti = MultiTouchInput(MultiTouchInput::MULTITOUCH_START, 0, TimeStamp(), 0);
+ mti.mTouches.AppendElement(CreateSingleTouchData(firstFingerId, touchX, touchY));
+ mti.mTouches.AppendElement(CreateSingleTouchData(secondFingerId, touchX + 10, touchY));
+ apzc->ReceiveInputEvent(mti, nullptr);
+
+ // Lift the second finger
+ mti = MultiTouchInput(MultiTouchInput::MULTITOUCH_END, 0, TimeStamp(), 0);
+ mti.mTouches.AppendElement(CreateSingleTouchData(secondFingerId, touchX + 10, touchY));
+ apzc->ReceiveInputEvent(mti, nullptr);
+
+ // Bust through the threshold again
+ touchY += 40;
+ mti = MultiTouchInput(MultiTouchInput::MULTITOUCH_MOVE, 0, TimeStamp(), 0);
+ mti.mTouches.AppendElement(CreateSingleTouchData(firstFingerId, touchX, touchY));
+ apzc->ReceiveInputEvent(mti, nullptr);
+
+ // Do some more actual panning
+ touchY += panDistance;
+ mti = MultiTouchInput(MultiTouchInput::MULTITOUCH_MOVE, 0, TimeStamp(), 0);
+ mti.mTouches.AppendElement(CreateSingleTouchData(firstFingerId, touchX, touchY));
+ apzc->ReceiveInputEvent(mti, nullptr);
+
+ // Lift the first finger
+ mti = MultiTouchInput(MultiTouchInput::MULTITOUCH_END, 0, TimeStamp(), 0);
+ mti.mTouches.AppendElement(CreateSingleTouchData(firstFingerId, touchX, touchY));
+ apzc->ReceiveInputEvent(mti, nullptr);
+
+ // Verify that we scrolled
+ FrameMetrics finalMetrics = apzc->GetFrameMetrics();
+ float zoom = finalMetrics.GetZoom().ToScaleFactor().scale;
+ EXPECT_EQ(originalMetrics.GetScrollOffset().y - (panDistance * 2 / zoom), finalMetrics.GetScrollOffset().y);
+
+ // Clear out any remaining fling animation and pending tasks
+ apzc->AdvanceAnimationsUntilEnd();
+ while (mcc->RunThroughDelayedTasks());
+ apzc->AssertStateIsReset();
+}
+
+class APZCFlingStopTester : public APZCGestureDetectorTester {
+protected:
+ // Start a fling, and then tap while the fling is ongoing. When
+ // aSlow is false, the tap will happen while the fling is at a
+ // high velocity, and we check that the tap doesn't trigger sending a tap
+ // to content. If aSlow is true, the tap will happen while the fling
+ // is at a slow velocity, and we check that the tap does trigger sending
+ // a tap to content. See bug 1022956.
+ void DoFlingStopTest(bool aSlow) {
+ int touchStart = 50;
+ int touchEnd = 10;
+
+ // Start the fling down.
+ Pan(apzc, touchStart, touchEnd);
+ // The touchstart from the pan will leave some cancelled tasks in the queue, clear them out
+
+ // If we want to tap while the fling is fast, let the fling advance for 10ms only. If we want
+ // the fling to slow down more, advance to 2000ms. These numbers may need adjusting if our
+ // friction and threshold values change, but they should be deterministic at least.
+ int timeDelta = aSlow ? 2000 : 10;
+ int tapCallsExpected = aSlow ? 2 : 1;
+
+ // Advance the fling animation by timeDelta milliseconds.
+ ParentLayerPoint pointOut;
+ AsyncTransform viewTransformOut;
+ apzc->SampleContentTransformForFrame(&viewTransformOut, pointOut, TimeDuration::FromMilliseconds(timeDelta));
+
+ // Deliver a tap to abort the fling. Ensure that we get a SingleTap
+ // call out of it if and only if the fling is slow.
+ EXPECT_CALL(*mcc, HandleTap(TapType::eSingleTap, _, 0, apzc->GetGuid(), _)).Times(tapCallsExpected);
+ Tap(apzc, ScreenIntPoint(10, 10), 0);
+ while (mcc->RunThroughDelayedTasks());
+
+ // Deliver another tap, to make sure that taps are flowing properly once
+ // the fling is aborted.
+ Tap(apzc, ScreenIntPoint(100, 100), 0);
+ while (mcc->RunThroughDelayedTasks());
+
+ // Verify that we didn't advance any further after the fling was aborted, in either case.
+ ParentLayerPoint finalPointOut;
+ apzc->SampleContentTransformForFrame(&viewTransformOut, finalPointOut);
+ EXPECT_EQ(pointOut.x, finalPointOut.x);
+ EXPECT_EQ(pointOut.y, finalPointOut.y);
+
+ apzc->AssertStateIsReset();
+ }
+
+ void DoFlingStopWithSlowListener(bool aPreventDefault) {
+ MakeApzcWaitForMainThread();
+
+ int touchStart = 50;
+ int touchEnd = 10;
+ uint64_t blockId = 0;
+
+ // Start the fling down.
+ Pan(apzc, touchStart, touchEnd, false, nullptr, nullptr, &blockId);
+ apzc->ConfirmTarget(blockId);
+ apzc->ContentReceivedInputBlock(blockId, false);
+
+ // Sample the fling a couple of times to ensure it's going.
+ ParentLayerPoint point, finalPoint;
+ AsyncTransform viewTransform;
+ apzc->SampleContentTransformForFrame(&viewTransform, point, TimeDuration::FromMilliseconds(10));
+ apzc->SampleContentTransformForFrame(&viewTransform, finalPoint, TimeDuration::FromMilliseconds(10));
+ EXPECT_GT(finalPoint.y, point.y);
+
+ // Now we put our finger down to stop the fling
+ TouchDown(apzc, ScreenIntPoint(10, 10), mcc->Time(), &blockId);
+
+ // Re-sample to make sure it hasn't moved
+ apzc->SampleContentTransformForFrame(&viewTransform, point, TimeDuration::FromMilliseconds(10));
+ EXPECT_EQ(finalPoint.x, point.x);
+ EXPECT_EQ(finalPoint.y, point.y);
+
+ // respond to the touchdown that stopped the fling.
+ // even if we do a prevent-default on it, the animation should remain stopped.
+ apzc->ContentReceivedInputBlock(blockId, aPreventDefault);
+
+ // Verify the page hasn't moved
+ apzc->SampleContentTransformForFrame(&viewTransform, point, TimeDuration::FromMilliseconds(70));
+ EXPECT_EQ(finalPoint.x, point.x);
+ EXPECT_EQ(finalPoint.y, point.y);
+
+ // clean up
+ TouchUp(apzc, ScreenIntPoint(10, 10), mcc->Time());
+
+ apzc->AssertStateIsReset();
+ }
+};
+
+TEST_F(APZCFlingStopTester, FlingStop) {
+ SCOPED_GFX_PREF(APZFlingMinVelocityThreshold, float, 0.0f);
+ DoFlingStopTest(false);
+}
+
+TEST_F(APZCFlingStopTester, FlingStopTap) {
+ SCOPED_GFX_PREF(APZFlingMinVelocityThreshold, float, 0.0f);
+ DoFlingStopTest(true);
+}
+
+TEST_F(APZCFlingStopTester, FlingStopSlowListener) {
+ SCOPED_GFX_PREF(APZFlingMinVelocityThreshold, float, 0.0f);
+ DoFlingStopWithSlowListener(false);
+}
+
+TEST_F(APZCFlingStopTester, FlingStopPreventDefault) {
+ SCOPED_GFX_PREF(APZFlingMinVelocityThreshold, float, 0.0f);
+ DoFlingStopWithSlowListener(true);
+}
+
+TEST_F(APZCGestureDetectorTester, ShortPress) {
+ MakeApzcUnzoomable();
+
+ MockFunction<void(std::string checkPointName)> check;
+ {
+ InSequence s;
+ // This verifies that the single tap notification is sent after the
+ // touchup is fully processed. The ordering here is important.
+ EXPECT_CALL(check, Call("pre-tap"));
+ EXPECT_CALL(check, Call("post-tap"));
+ EXPECT_CALL(*mcc, HandleTap(TapType::eSingleTap, LayoutDevicePoint(10, 10), 0, apzc->GetGuid(), _)).Times(1);
+ }
+
+ check.Call("pre-tap");
+ TapAndCheckStatus(apzc, ScreenIntPoint(10, 10), TimeDuration::FromMilliseconds(100));
+ check.Call("post-tap");
+
+ apzc->AssertStateIsReset();
+}
+
+TEST_F(APZCGestureDetectorTester, MediumPress) {
+ MakeApzcUnzoomable();
+
+ MockFunction<void(std::string checkPointName)> check;
+ {
+ InSequence s;
+ // This verifies that the single tap notification is sent after the
+ // touchup is fully processed. The ordering here is important.
+ EXPECT_CALL(check, Call("pre-tap"));
+ EXPECT_CALL(check, Call("post-tap"));
+ EXPECT_CALL(*mcc, HandleTap(TapType::eSingleTap, LayoutDevicePoint(10, 10), 0, apzc->GetGuid(), _)).Times(1);
+ }
+
+ check.Call("pre-tap");
+ TapAndCheckStatus(apzc, ScreenIntPoint(10, 10), TimeDuration::FromMilliseconds(400));
+ check.Call("post-tap");
+
+ apzc->AssertStateIsReset();
+}
+
+class APZCLongPressTester : public APZCGestureDetectorTester {
+protected:
+ void DoLongPressTest(uint32_t aBehavior) {
+ MakeApzcUnzoomable();
+
+ uint64_t blockId = 0;
+
+ nsEventStatus status = TouchDown(apzc, ScreenIntPoint(10, 10), mcc->Time(), &blockId);
+ EXPECT_EQ(nsEventStatus_eConsumeDoDefault, status);
+
+ if (gfxPrefs::TouchActionEnabled() && status != nsEventStatus_eConsumeNoDefault) {
+ // SetAllowedTouchBehavior() must be called after sending touch-start.
+ nsTArray<uint32_t> allowedTouchBehaviors;
+ allowedTouchBehaviors.AppendElement(aBehavior);
+ apzc->SetAllowedTouchBehavior(blockId, allowedTouchBehaviors);
+ }
+ // Have content "respond" to the touchstart
+ apzc->ContentReceivedInputBlock(blockId, false);
+
+ MockFunction<void(std::string checkPointName)> check;
+
+ {
+ InSequence s;
+
+ EXPECT_CALL(check, Call("preHandleLongTap"));
+ blockId++;
+ EXPECT_CALL(*mcc, HandleTap(TapType::eLongTap, LayoutDevicePoint(10, 10), 0, apzc->GetGuid(), blockId)).Times(1);
+ EXPECT_CALL(check, Call("postHandleLongTap"));
+
+ EXPECT_CALL(check, Call("preHandleLongTapUp"));
+ EXPECT_CALL(*mcc, HandleTap(TapType::eLongTapUp, LayoutDevicePoint(10, 10), 0, apzc->GetGuid(), _)).Times(1);
+ EXPECT_CALL(check, Call("postHandleLongTapUp"));
+ }
+
+ // Manually invoke the longpress while the touch is currently down.
+ check.Call("preHandleLongTap");
+ mcc->RunThroughDelayedTasks();
+ check.Call("postHandleLongTap");
+
+ // Dispatching the longpress event starts a new touch block, which
+ // needs a new content response and also has a pending timeout task
+ // in the queue. Deal with those here. We do the content response first
+ // with preventDefault=false, and then we run the timeout task which
+ // "loses the race" and does nothing.
+ apzc->ContentReceivedInputBlock(blockId, false);
+ mcc->AdvanceByMillis(1000);
+
+ // Finally, simulate lifting the finger. Since the long-press wasn't
+ // prevent-defaulted, we should get a long-tap-up event.
+ check.Call("preHandleLongTapUp");
+ status = TouchUp(apzc, ScreenIntPoint(10, 10), mcc->Time());
+ mcc->RunThroughDelayedTasks();
+ EXPECT_EQ(nsEventStatus_eConsumeDoDefault, status);
+ check.Call("postHandleLongTapUp");
+
+ apzc->AssertStateIsReset();
+ }
+
+ void DoLongPressPreventDefaultTest(uint32_t aBehavior) {
+ MakeApzcUnzoomable();
+
+ EXPECT_CALL(*mcc, RequestContentRepaint(_)).Times(0);
+
+ int touchX = 10,
+ touchStartY = 10,
+ touchEndY = 50;
+
+ uint64_t blockId = 0;
+ nsEventStatus status = TouchDown(apzc, ScreenIntPoint(touchX, touchStartY), mcc->Time(), &blockId);
+ EXPECT_EQ(nsEventStatus_eConsumeDoDefault, status);
+
+ if (gfxPrefs::TouchActionEnabled() && status != nsEventStatus_eConsumeNoDefault) {
+ // SetAllowedTouchBehavior() must be called after sending touch-start.
+ nsTArray<uint32_t> allowedTouchBehaviors;
+ allowedTouchBehaviors.AppendElement(aBehavior);
+ apzc->SetAllowedTouchBehavior(blockId, allowedTouchBehaviors);
+ }
+ // Have content "respond" to the touchstart
+ apzc->ContentReceivedInputBlock(blockId, false);
+
+ MockFunction<void(std::string checkPointName)> check;
+
+ {
+ InSequence s;
+
+ EXPECT_CALL(check, Call("preHandleLongTap"));
+ blockId++;
+ EXPECT_CALL(*mcc, HandleTap(TapType::eLongTap, LayoutDevicePoint(touchX, touchStartY), 0, apzc->GetGuid(), blockId)).Times(1);
+ EXPECT_CALL(check, Call("postHandleLongTap"));
+ }
+
+ // Manually invoke the longpress while the touch is currently down.
+ check.Call("preHandleLongTap");
+ mcc->RunThroughDelayedTasks();
+ check.Call("postHandleLongTap");
+
+ // There should be a TimeoutContentResponse task in the queue still,
+ // waiting for the response from the longtap event dispatched above.
+ // Send the signal that content has handled the long-tap, and then run
+ // the timeout task (it will be a no-op because the content "wins" the
+ // race. This takes the place of the "contextmenu" event.
+ apzc->ContentReceivedInputBlock(blockId, true);
+ mcc->AdvanceByMillis(1000);
+
+ MultiTouchInput mti = CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_MOVE, mcc->Time());
+ mti.mTouches.AppendElement(SingleTouchData(0, ParentLayerPoint(touchX, touchEndY), ScreenSize(0, 0), 0, 0));
+ status = apzc->ReceiveInputEvent(mti, nullptr);
+ EXPECT_EQ(nsEventStatus_eConsumeDoDefault, status);
+
+ EXPECT_CALL(*mcc, HandleTap(TapType::eSingleTap, LayoutDevicePoint(touchX, touchEndY), 0, apzc->GetGuid(), _)).Times(0);
+ status = TouchUp(apzc, ScreenIntPoint(touchX, touchEndY), mcc->Time());
+ EXPECT_EQ(nsEventStatus_eConsumeDoDefault, status);
+
+ ParentLayerPoint pointOut;
+ AsyncTransform viewTransformOut;
+ apzc->SampleContentTransformForFrame(&viewTransformOut, pointOut);
+
+ EXPECT_EQ(ParentLayerPoint(), pointOut);
+ EXPECT_EQ(AsyncTransform(), viewTransformOut);
+
+ apzc->AssertStateIsReset();
+ }
+};
+
+TEST_F(APZCLongPressTester, LongPress) {
+ SCOPED_GFX_PREF(TouchActionEnabled, bool, false);
+ DoLongPressTest(mozilla::layers::AllowedTouchBehavior::NONE);
+}
+
+TEST_F(APZCLongPressTester, LongPressWithTouchAction) {
+ SCOPED_GFX_PREF(TouchActionEnabled, bool, true);
+ DoLongPressTest(mozilla::layers::AllowedTouchBehavior::HORIZONTAL_PAN
+ | mozilla::layers::AllowedTouchBehavior::VERTICAL_PAN
+ | mozilla::layers::AllowedTouchBehavior::PINCH_ZOOM);
+}
+
+TEST_F(APZCLongPressTester, LongPressPreventDefault) {
+ SCOPED_GFX_PREF(TouchActionEnabled, bool, false);
+ DoLongPressPreventDefaultTest(mozilla::layers::AllowedTouchBehavior::NONE);
+}
+
+TEST_F(APZCLongPressTester, LongPressPreventDefaultWithTouchAction) {
+ SCOPED_GFX_PREF(TouchActionEnabled, bool, true);
+ DoLongPressPreventDefaultTest(mozilla::layers::AllowedTouchBehavior::HORIZONTAL_PAN
+ | mozilla::layers::AllowedTouchBehavior::VERTICAL_PAN
+ | mozilla::layers::AllowedTouchBehavior::PINCH_ZOOM);
+}
+
+TEST_F(APZCGestureDetectorTester, DoubleTap) {
+ MakeApzcWaitForMainThread();
+ MakeApzcZoomable();
+
+ EXPECT_CALL(*mcc, HandleTap(TapType::eSingleTap, LayoutDevicePoint(10, 10), 0, apzc->GetGuid(), _)).Times(0);
+ EXPECT_CALL(*mcc, HandleTap(TapType::eDoubleTap, LayoutDevicePoint(10, 10), 0, apzc->GetGuid(), _)).Times(1);
+
+ uint64_t blockIds[2];
+ DoubleTapAndCheckStatus(apzc, ScreenIntPoint(10, 10), &blockIds);
+
+ // responses to the two touchstarts
+ apzc->ContentReceivedInputBlock(blockIds[0], false);
+ apzc->ContentReceivedInputBlock(blockIds[1], false);
+
+ apzc->AssertStateIsReset();
+}
+
+TEST_F(APZCGestureDetectorTester, DoubleTapNotZoomable) {
+ MakeApzcWaitForMainThread();
+ MakeApzcUnzoomable();
+
+ EXPECT_CALL(*mcc, HandleTap(TapType::eSingleTap, LayoutDevicePoint(10, 10), 0, apzc->GetGuid(), _)).Times(1);
+ EXPECT_CALL(*mcc, HandleTap(TapType::eSecondTap, LayoutDevicePoint(10, 10), 0, apzc->GetGuid(), _)).Times(1);
+ EXPECT_CALL(*mcc, HandleTap(TapType::eDoubleTap, LayoutDevicePoint(10, 10), 0, apzc->GetGuid(), _)).Times(0);
+
+ uint64_t blockIds[2];
+ DoubleTapAndCheckStatus(apzc, ScreenIntPoint(10, 10), &blockIds);
+
+ // responses to the two touchstarts
+ apzc->ContentReceivedInputBlock(blockIds[0], false);
+ apzc->ContentReceivedInputBlock(blockIds[1], false);
+
+ apzc->AssertStateIsReset();
+}
+
+TEST_F(APZCGestureDetectorTester, DoubleTapPreventDefaultFirstOnly) {
+ MakeApzcWaitForMainThread();
+ MakeApzcZoomable();
+
+ EXPECT_CALL(*mcc, HandleTap(TapType::eSingleTap, LayoutDevicePoint(10, 10), 0, apzc->GetGuid(), _)).Times(1);
+ EXPECT_CALL(*mcc, HandleTap(TapType::eDoubleTap, LayoutDevicePoint(10, 10), 0, apzc->GetGuid(), _)).Times(0);
+
+ uint64_t blockIds[2];
+ DoubleTapAndCheckStatus(apzc, ScreenIntPoint(10, 10), &blockIds);
+
+ // responses to the two touchstarts
+ apzc->ContentReceivedInputBlock(blockIds[0], true);
+ apzc->ContentReceivedInputBlock(blockIds[1], false);
+
+ apzc->AssertStateIsReset();
+}
+
+TEST_F(APZCGestureDetectorTester, DoubleTapPreventDefaultBoth) {
+ MakeApzcWaitForMainThread();
+ MakeApzcZoomable();
+
+ EXPECT_CALL(*mcc, HandleTap(TapType::eSingleTap, LayoutDevicePoint(10, 10), 0, apzc->GetGuid(), _)).Times(0);
+ EXPECT_CALL(*mcc, HandleTap(TapType::eDoubleTap, LayoutDevicePoint(10, 10), 0, apzc->GetGuid(), _)).Times(0);
+
+ uint64_t blockIds[2];
+ DoubleTapAndCheckStatus(apzc, ScreenIntPoint(10, 10), &blockIds);
+
+ // responses to the two touchstarts
+ apzc->ContentReceivedInputBlock(blockIds[0], true);
+ apzc->ContentReceivedInputBlock(blockIds[1], true);
+
+ apzc->AssertStateIsReset();
+}
+
+// Test for bug 947892
+// We test whether we dispatch tap event when the tap is followed by pinch.
+TEST_F(APZCGestureDetectorTester, TapFollowedByPinch) {
+ MakeApzcZoomable();
+
+ EXPECT_CALL(*mcc, HandleTap(TapType::eSingleTap, LayoutDevicePoint(10, 10), 0, apzc->GetGuid(), _)).Times(1);
+
+ Tap(apzc, ScreenIntPoint(10, 10), TimeDuration::FromMilliseconds(100));
+
+ int inputId = 0;
+ MultiTouchInput mti;
+ mti = CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_START, mcc->Time());
+ mti.mTouches.AppendElement(SingleTouchData(inputId, ParentLayerPoint(20, 20), ScreenSize(0, 0), 0, 0));
+ mti.mTouches.AppendElement(SingleTouchData(inputId + 1, ParentLayerPoint(10, 10), ScreenSize(0, 0), 0, 0));
+ apzc->ReceiveInputEvent(mti, nullptr);
+
+ mti = CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_END, mcc->Time());
+ mti.mTouches.AppendElement(SingleTouchData(inputId, ParentLayerPoint(20, 20), ScreenSize(0, 0), 0, 0));
+ mti.mTouches.AppendElement(SingleTouchData(inputId + 1, ParentLayerPoint(10, 10), ScreenSize(0, 0), 0, 0));
+ apzc->ReceiveInputEvent(mti, nullptr);
+
+ apzc->AssertStateIsReset();
+}
+
+TEST_F(APZCGestureDetectorTester, TapFollowedByMultipleTouches) {
+ MakeApzcZoomable();
+
+ EXPECT_CALL(*mcc, HandleTap(TapType::eSingleTap, LayoutDevicePoint(10, 10), 0, apzc->GetGuid(), _)).Times(1);
+
+ Tap(apzc, ScreenIntPoint(10, 10), TimeDuration::FromMilliseconds(100));
+
+ int inputId = 0;
+ MultiTouchInput mti;
+ mti = CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_START, mcc->Time());
+ mti.mTouches.AppendElement(SingleTouchData(inputId, ParentLayerPoint(20, 20), ScreenSize(0, 0), 0, 0));
+ apzc->ReceiveInputEvent(mti, nullptr);
+
+ mti = CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_START, mcc->Time());
+ mti.mTouches.AppendElement(SingleTouchData(inputId, ParentLayerPoint(20, 20), ScreenSize(0, 0), 0, 0));
+ mti.mTouches.AppendElement(SingleTouchData(inputId + 1, ParentLayerPoint(10, 10), ScreenSize(0, 0), 0, 0));
+ apzc->ReceiveInputEvent(mti, nullptr);
+
+ mti = CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_END, mcc->Time());
+ mti.mTouches.AppendElement(SingleTouchData(inputId, ParentLayerPoint(20, 20), ScreenSize(0, 0), 0, 0));
+ mti.mTouches.AppendElement(SingleTouchData(inputId + 1, ParentLayerPoint(10, 10), ScreenSize(0, 0), 0, 0));
+ apzc->ReceiveInputEvent(mti, nullptr);
+
+ apzc->AssertStateIsReset();
+}
+
+TEST_F(APZCGestureDetectorTester, LongPressInterruptedByWheel) {
+ // Since we try to allow concurrent input blocks of different types to
+ // co-exist, the wheel block shouldn't interrupt the long-press detection.
+ // But more importantly, this shouldn't crash, which is what it did at one
+ // point in time.
+ EXPECT_CALL(*mcc, HandleTap(TapType::eLongTap, _, _, _, _)).Times(1);
+
+ uint64_t touchBlockId = 0;
+ uint64_t wheelBlockId = 0;
+ nsEventStatus status = TouchDown(apzc, ScreenIntPoint(10, 10), mcc->Time(), &touchBlockId);
+ if (gfxPrefs::TouchActionEnabled() && status != nsEventStatus_eConsumeNoDefault) {
+ SetDefaultAllowedTouchBehavior(apzc, touchBlockId);
+ }
+ mcc->AdvanceByMillis(10);
+ Wheel(apzc, ScreenIntPoint(10, 10), ScreenPoint(0, -10), mcc->Time(), &wheelBlockId);
+ EXPECT_NE(touchBlockId, wheelBlockId);
+ mcc->AdvanceByMillis(1000);
+}
+
+TEST_F(APZCGestureDetectorTester, TapTimeoutInterruptedByWheel) {
+ // In this test, even though the wheel block comes right after the tap, the
+ // tap should still be dispatched because it completes fully before the wheel
+ // block arrived.
+ EXPECT_CALL(*mcc, HandleTap(TapType::eSingleTap, LayoutDevicePoint(10, 10), 0, apzc->GetGuid(), _)).Times(1);
+
+ // We make the APZC zoomable so the gesture detector needs to wait to
+ // distinguish between tap and double-tap. During that timeout is when we
+ // insert the wheel event.
+ MakeApzcZoomable();
+
+ uint64_t touchBlockId = 0;
+ uint64_t wheelBlockId = 0;
+ Tap(apzc, ScreenIntPoint(10, 10), TimeDuration::FromMilliseconds(100),
+ nullptr, &touchBlockId);
+ mcc->AdvanceByMillis(10);
+ Wheel(apzc, ScreenIntPoint(10, 10), ScreenPoint(0, -10), mcc->Time(), &wheelBlockId);
+ EXPECT_NE(touchBlockId, wheelBlockId);
+ while (mcc->RunThroughDelayedTasks());
+}
diff --git a/gfx/layers/apz/test/gtest/TestHitTesting.cpp b/gfx/layers/apz/test/gtest/TestHitTesting.cpp
new file mode 100644
index 000000000..182194208
--- /dev/null
+++ b/gfx/layers/apz/test/gtest/TestHitTesting.cpp
@@ -0,0 +1,578 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set sw=2 ts=8 et tw=80 : */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "APZCTreeManagerTester.h"
+#include "APZTestCommon.h"
+#include "InputUtils.h"
+
+class APZHitTestingTester : public APZCTreeManagerTester {
+protected:
+ ScreenToParentLayerMatrix4x4 transformToApzc;
+ ParentLayerToScreenMatrix4x4 transformToGecko;
+
+ already_AddRefed<AsyncPanZoomController> GetTargetAPZC(const ScreenPoint& aPoint) {
+ RefPtr<AsyncPanZoomController> hit = manager->GetTargetAPZC(aPoint, nullptr);
+ if (hit) {
+ transformToApzc = manager->GetScreenToApzcTransform(hit.get());
+ transformToGecko = manager->GetApzcToGeckoTransform(hit.get());
+ }
+ return hit.forget();
+ }
+
+protected:
+ void CreateHitTesting1LayerTree() {
+ const char* layerTreeSyntax = "c(tttt)";
+ // LayerID 0 1234
+ nsIntRegion layerVisibleRegion[] = {
+ nsIntRegion(IntRect(0,0,100,100)),
+ nsIntRegion(IntRect(0,0,100,100)),
+ nsIntRegion(IntRect(10,10,20,20)),
+ nsIntRegion(IntRect(10,10,20,20)),
+ nsIntRegion(IntRect(5,5,20,20)),
+ };
+ root = CreateLayerTree(layerTreeSyntax, layerVisibleRegion, nullptr, lm, layers);
+ }
+
+ void CreateHitTesting2LayerTree() {
+ const char* layerTreeSyntax = "c(tc(t))";
+ // LayerID 0 12 3
+ nsIntRegion layerVisibleRegion[] = {
+ nsIntRegion(IntRect(0,0,100,100)),
+ nsIntRegion(IntRect(10,10,40,40)),
+ nsIntRegion(IntRect(10,60,40,40)),
+ nsIntRegion(IntRect(10,60,40,40)),
+ };
+ Matrix4x4 transforms[] = {
+ Matrix4x4(),
+ Matrix4x4(),
+ Matrix4x4::Scaling(2, 1, 1),
+ Matrix4x4(),
+ };
+ root = CreateLayerTree(layerTreeSyntax, layerVisibleRegion, transforms, lm, layers);
+
+ SetScrollableFrameMetrics(root, FrameMetrics::START_SCROLL_ID, CSSRect(0, 0, 200, 200));
+ SetScrollableFrameMetrics(layers[1], FrameMetrics::START_SCROLL_ID + 1, CSSRect(0, 0, 80, 80));
+ SetScrollableFrameMetrics(layers[3], FrameMetrics::START_SCROLL_ID + 2, CSSRect(0, 0, 80, 80));
+ }
+
+ void DisableApzOn(Layer* aLayer) {
+ ScrollMetadata m = aLayer->GetScrollMetadata(0);
+ m.SetForceDisableApz(true);
+ aLayer->SetScrollMetadata(m);
+ }
+
+ void CreateComplexMultiLayerTree() {
+ const char* layerTreeSyntax = "c(tc(t)tc(c(t)tt))";
+ // LayerID 0 12 3 45 6 7 89
+ nsIntRegion layerVisibleRegion[] = {
+ nsIntRegion(IntRect(0,0,300,400)), // root(0)
+ nsIntRegion(IntRect(0,0,100,100)), // thebes(1) in top-left
+ nsIntRegion(IntRect(50,50,200,300)), // container(2) centered in root(0)
+ nsIntRegion(IntRect(50,50,200,300)), // thebes(3) fully occupying parent container(2)
+ nsIntRegion(IntRect(0,200,100,100)), // thebes(4) in bottom-left
+ nsIntRegion(IntRect(200,0,100,400)), // container(5) along the right 100px of root(0)
+ nsIntRegion(IntRect(200,0,100,200)), // container(6) taking up the top half of parent container(5)
+ nsIntRegion(IntRect(200,0,100,200)), // thebes(7) fully occupying parent container(6)
+ nsIntRegion(IntRect(200,200,100,100)), // thebes(8) in bottom-right (below (6))
+ nsIntRegion(IntRect(200,300,100,100)), // thebes(9) in bottom-right (below (8))
+ };
+ root = CreateLayerTree(layerTreeSyntax, layerVisibleRegion, nullptr, lm, layers);
+ SetScrollableFrameMetrics(layers[1], FrameMetrics::START_SCROLL_ID);
+ SetScrollableFrameMetrics(layers[2], FrameMetrics::START_SCROLL_ID);
+ SetScrollableFrameMetrics(layers[4], FrameMetrics::START_SCROLL_ID + 1);
+ SetScrollableFrameMetrics(layers[6], FrameMetrics::START_SCROLL_ID + 1);
+ SetScrollableFrameMetrics(layers[7], FrameMetrics::START_SCROLL_ID + 2);
+ SetScrollableFrameMetrics(layers[8], FrameMetrics::START_SCROLL_ID + 1);
+ SetScrollableFrameMetrics(layers[9], FrameMetrics::START_SCROLL_ID + 3);
+ }
+
+ void CreateBug1148350LayerTree() {
+ const char* layerTreeSyntax = "c(t)";
+ // LayerID 0 1
+ nsIntRegion layerVisibleRegion[] = {
+ nsIntRegion(IntRect(0,0,200,200)),
+ nsIntRegion(IntRect(0,0,200,200)),
+ };
+ root = CreateLayerTree(layerTreeSyntax, layerVisibleRegion, nullptr, lm, layers);
+ SetScrollableFrameMetrics(layers[1], FrameMetrics::START_SCROLL_ID);
+ }
+};
+
+// A simple hit testing test that doesn't involve any transforms on layers.
+TEST_F(APZHitTestingTester, HitTesting1) {
+ CreateHitTesting1LayerTree();
+ ScopedLayerTreeRegistration registration(manager, 0, root, mcc);
+
+ // No APZC attached so hit testing will return no APZC at (20,20)
+ RefPtr<AsyncPanZoomController> hit = GetTargetAPZC(ScreenPoint(20, 20));
+ TestAsyncPanZoomController* nullAPZC = nullptr;
+ EXPECT_EQ(nullAPZC, hit.get());
+ EXPECT_EQ(ScreenToParentLayerMatrix4x4(), transformToApzc);
+ EXPECT_EQ(ParentLayerToScreenMatrix4x4(), transformToGecko);
+
+ uint32_t paintSequenceNumber = 0;
+
+ // Now we have a root APZC that will match the page
+ SetScrollableFrameMetrics(root, FrameMetrics::START_SCROLL_ID);
+ manager->UpdateHitTestingTree(0, root, false, 0, paintSequenceNumber++);
+ hit = GetTargetAPZC(ScreenPoint(15, 15));
+ EXPECT_EQ(ApzcOf(root), hit.get());
+ // expect hit point at LayerIntPoint(15, 15)
+ EXPECT_EQ(ParentLayerPoint(15, 15), transformToApzc.TransformPoint(ScreenPoint(15, 15)));
+ EXPECT_EQ(ScreenPoint(15, 15), transformToGecko.TransformPoint(ParentLayerPoint(15, 15)));
+
+ // Now we have a sub APZC with a better fit
+ SetScrollableFrameMetrics(layers[3], FrameMetrics::START_SCROLL_ID + 1);
+ manager->UpdateHitTestingTree(0, root, false, 0, paintSequenceNumber++);
+ EXPECT_NE(ApzcOf(root), ApzcOf(layers[3]));
+ hit = GetTargetAPZC(ScreenPoint(25, 25));
+ EXPECT_EQ(ApzcOf(layers[3]), hit.get());
+ // expect hit point at LayerIntPoint(25, 25)
+ EXPECT_EQ(ParentLayerPoint(25, 25), transformToApzc.TransformPoint(ScreenPoint(25, 25)));
+ EXPECT_EQ(ScreenPoint(25, 25), transformToGecko.TransformPoint(ParentLayerPoint(25, 25)));
+
+ // At this point, layers[4] obscures layers[3] at the point (15, 15) so
+ // hitting there should hit the root APZC
+ hit = GetTargetAPZC(ScreenPoint(15, 15));
+ EXPECT_EQ(ApzcOf(root), hit.get());
+
+ // Now test hit testing when we have two scrollable layers
+ SetScrollableFrameMetrics(layers[4], FrameMetrics::START_SCROLL_ID + 2);
+ manager->UpdateHitTestingTree(0, root, false, 0, paintSequenceNumber++);
+ hit = GetTargetAPZC(ScreenPoint(15, 15));
+ EXPECT_EQ(ApzcOf(layers[4]), hit.get());
+ // expect hit point at LayerIntPoint(15, 15)
+ EXPECT_EQ(ParentLayerPoint(15, 15), transformToApzc.TransformPoint(ScreenPoint(15, 15)));
+ EXPECT_EQ(ScreenPoint(15, 15), transformToGecko.TransformPoint(ParentLayerPoint(15, 15)));
+
+ // Hit test ouside the reach of layer[3,4] but inside root
+ hit = GetTargetAPZC(ScreenPoint(90, 90));
+ EXPECT_EQ(ApzcOf(root), hit.get());
+ // expect hit point at LayerIntPoint(90, 90)
+ EXPECT_EQ(ParentLayerPoint(90, 90), transformToApzc.TransformPoint(ScreenPoint(90, 90)));
+ EXPECT_EQ(ScreenPoint(90, 90), transformToGecko.TransformPoint(ParentLayerPoint(90, 90)));
+
+ // Hit test ouside the reach of any layer
+ hit = GetTargetAPZC(ScreenPoint(1000, 10));
+ EXPECT_EQ(nullAPZC, hit.get());
+ EXPECT_EQ(ScreenToParentLayerMatrix4x4(), transformToApzc);
+ EXPECT_EQ(ParentLayerToScreenMatrix4x4(), transformToGecko);
+ hit = GetTargetAPZC(ScreenPoint(-1000, 10));
+ EXPECT_EQ(nullAPZC, hit.get());
+ EXPECT_EQ(ScreenToParentLayerMatrix4x4(), transformToApzc);
+ EXPECT_EQ(ParentLayerToScreenMatrix4x4(), transformToGecko);
+}
+
+// A more involved hit testing test that involves css and async transforms.
+TEST_F(APZHitTestingTester, HitTesting2) {
+ SCOPED_GFX_PREF(APZVelocityBias, float, 0.0); // Velocity bias can cause extra repaint requests
+
+ CreateHitTesting2LayerTree();
+ ScopedLayerTreeRegistration registration(manager, 0, root, mcc);
+
+ manager->UpdateHitTestingTree(0, root, false, 0, 0);
+
+ // At this point, the following holds (all coordinates in screen pixels):
+ // layers[0] has content from (0,0)-(200,200), clipped by composition bounds (0,0)-(100,100)
+ // layers[1] has content from (10,10)-(90,90), clipped by composition bounds (10,10)-(50,50)
+ // layers[2] has content from (20,60)-(100,100). no clipping as it's not a scrollable layer
+ // layers[3] has content from (20,60)-(180,140), clipped by composition bounds (20,60)-(100,100)
+
+ TestAsyncPanZoomController* apzcroot = ApzcOf(root);
+ TestAsyncPanZoomController* apzc1 = ApzcOf(layers[1]);
+ TestAsyncPanZoomController* apzc3 = ApzcOf(layers[3]);
+
+ // Hit an area that's clearly on the root layer but not any of the child layers.
+ RefPtr<AsyncPanZoomController> hit = GetTargetAPZC(ScreenPoint(75, 25));
+ EXPECT_EQ(apzcroot, hit.get());
+ EXPECT_EQ(ParentLayerPoint(75, 25), transformToApzc.TransformPoint(ScreenPoint(75, 25)));
+ EXPECT_EQ(ScreenPoint(75, 25), transformToGecko.TransformPoint(ParentLayerPoint(75, 25)));
+
+ // Hit an area on the root that would be on layers[3] if layers[2]
+ // weren't transformed.
+ // Note that if layers[2] were scrollable, then this would hit layers[2]
+ // because its composition bounds would be at (10,60)-(50,100) (and the
+ // scale-only transform that we set on layers[2] would be invalid because
+ // it would place the layer into overscroll, as its composition bounds
+ // start at x=10 but its content at x=20).
+ hit = GetTargetAPZC(ScreenPoint(15, 75));
+ EXPECT_EQ(apzcroot, hit.get());
+ EXPECT_EQ(ParentLayerPoint(15, 75), transformToApzc.TransformPoint(ScreenPoint(15, 75)));
+ EXPECT_EQ(ScreenPoint(15, 75), transformToGecko.TransformPoint(ParentLayerPoint(15, 75)));
+
+ // Hit an area on layers[1].
+ hit = GetTargetAPZC(ScreenPoint(25, 25));
+ EXPECT_EQ(apzc1, hit.get());
+ EXPECT_EQ(ParentLayerPoint(25, 25), transformToApzc.TransformPoint(ScreenPoint(25, 25)));
+ EXPECT_EQ(ScreenPoint(25, 25), transformToGecko.TransformPoint(ParentLayerPoint(25, 25)));
+
+ // Hit an area on layers[3].
+ hit = GetTargetAPZC(ScreenPoint(25, 75));
+ EXPECT_EQ(apzc3, hit.get());
+ // transformToApzc should unapply layers[2]'s transform
+ EXPECT_EQ(ParentLayerPoint(12.5, 75), transformToApzc.TransformPoint(ScreenPoint(25, 75)));
+ // and transformToGecko should reapply it
+ EXPECT_EQ(ScreenPoint(25, 75), transformToGecko.TransformPoint(ParentLayerPoint(12.5, 75)));
+
+ // Hit an area on layers[3] that would be on the root if layers[2]
+ // weren't transformed.
+ hit = GetTargetAPZC(ScreenPoint(75, 75));
+ EXPECT_EQ(apzc3, hit.get());
+ // transformToApzc should unapply layers[2]'s transform
+ EXPECT_EQ(ParentLayerPoint(37.5, 75), transformToApzc.TransformPoint(ScreenPoint(75, 75)));
+ // and transformToGecko should reapply it
+ EXPECT_EQ(ScreenPoint(75, 75), transformToGecko.TransformPoint(ParentLayerPoint(37.5, 75)));
+
+ // Pan the root layer upward by 50 pixels.
+ // This causes layers[1] to scroll out of view, and an async transform
+ // of -50 to be set on the root layer.
+ EXPECT_CALL(*mcc, RequestContentRepaint(_)).Times(1);
+
+ // This first pan will move the APZC by 50 pixels, and dispatch a paint request.
+ // Since this paint request is in the queue to Gecko, transformToGecko will
+ // take it into account.
+ ApzcPanNoFling(apzcroot, 100, 50);
+
+ // Hit where layers[3] used to be. It should now hit the root.
+ hit = GetTargetAPZC(ScreenPoint(75, 75));
+ EXPECT_EQ(apzcroot, hit.get());
+ // transformToApzc doesn't unapply the root's own async transform
+ EXPECT_EQ(ParentLayerPoint(75, 75), transformToApzc.TransformPoint(ScreenPoint(75, 75)));
+ // and transformToGecko unapplies it and then reapplies it, because by the
+ // time the event being transformed reaches Gecko the new paint request will
+ // have been handled.
+ EXPECT_EQ(ScreenPoint(75, 75), transformToGecko.TransformPoint(ParentLayerPoint(75, 75)));
+
+ // Hit where layers[1] used to be and where layers[3] should now be.
+ hit = GetTargetAPZC(ScreenPoint(25, 25));
+ EXPECT_EQ(apzc3, hit.get());
+ // transformToApzc unapplies both layers[2]'s css transform and the root's
+ // async transform
+ EXPECT_EQ(ParentLayerPoint(12.5, 75), transformToApzc.TransformPoint(ScreenPoint(25, 25)));
+ // transformToGecko reapplies both the css transform and the async transform
+ // because we have already issued a paint request with it.
+ EXPECT_EQ(ScreenPoint(25, 25), transformToGecko.TransformPoint(ParentLayerPoint(12.5, 75)));
+
+ // This second pan will move the APZC by another 50 pixels.
+ EXPECT_CALL(*mcc, RequestContentRepaint(_)).Times(1);
+ ApzcPanNoFling(apzcroot, 100, 50);
+
+ // Hit where layers[3] used to be. It should now hit the root.
+ hit = GetTargetAPZC(ScreenPoint(75, 75));
+ EXPECT_EQ(apzcroot, hit.get());
+ // transformToApzc doesn't unapply the root's own async transform
+ EXPECT_EQ(ParentLayerPoint(75, 75), transformToApzc.TransformPoint(ScreenPoint(75, 75)));
+ // transformToGecko unapplies the full async transform of -100 pixels
+ EXPECT_EQ(ScreenPoint(75, 75), transformToGecko.TransformPoint(ParentLayerPoint(75, 75)));
+
+ // Hit where layers[1] used to be. It should now hit the root.
+ hit = GetTargetAPZC(ScreenPoint(25, 25));
+ EXPECT_EQ(apzcroot, hit.get());
+ // transformToApzc doesn't unapply the root's own async transform
+ EXPECT_EQ(ParentLayerPoint(25, 25), transformToApzc.TransformPoint(ScreenPoint(25, 25)));
+ // transformToGecko unapplies the full async transform of -100 pixels
+ EXPECT_EQ(ScreenPoint(25, 25), transformToGecko.TransformPoint(ParentLayerPoint(25, 25)));
+}
+
+TEST_F(APZHitTestingTester, ComplexMultiLayerTree) {
+ CreateComplexMultiLayerTree();
+ ScopedLayerTreeRegistration registration(manager, 0, root, mcc);
+ manager->UpdateHitTestingTree(0, root, false, 0, 0);
+
+ /* The layer tree looks like this:
+
+ 0
+ |----|--+--|----|
+ 1 2 4 5
+ | /|\
+ 3 6 8 9
+ |
+ 7
+
+ Layers 1,2 have the same APZC
+ Layers 4,6,8 have the same APZC
+ Layer 7 has an APZC
+ Layer 9 has an APZC
+ */
+
+ TestAsyncPanZoomController* nullAPZC = nullptr;
+ // Ensure all the scrollable layers have an APZC
+ EXPECT_FALSE(layers[0]->HasScrollableFrameMetrics());
+ EXPECT_NE(nullAPZC, ApzcOf(layers[1]));
+ EXPECT_NE(nullAPZC, ApzcOf(layers[2]));
+ EXPECT_FALSE(layers[3]->HasScrollableFrameMetrics());
+ EXPECT_NE(nullAPZC, ApzcOf(layers[4]));
+ EXPECT_FALSE(layers[5]->HasScrollableFrameMetrics());
+ EXPECT_NE(nullAPZC, ApzcOf(layers[6]));
+ EXPECT_NE(nullAPZC, ApzcOf(layers[7]));
+ EXPECT_NE(nullAPZC, ApzcOf(layers[8]));
+ EXPECT_NE(nullAPZC, ApzcOf(layers[9]));
+ // Ensure those that scroll together have the same APZCs
+ EXPECT_EQ(ApzcOf(layers[1]), ApzcOf(layers[2]));
+ EXPECT_EQ(ApzcOf(layers[4]), ApzcOf(layers[6]));
+ EXPECT_EQ(ApzcOf(layers[8]), ApzcOf(layers[6]));
+ // Ensure those that don't scroll together have different APZCs
+ EXPECT_NE(ApzcOf(layers[1]), ApzcOf(layers[4]));
+ EXPECT_NE(ApzcOf(layers[1]), ApzcOf(layers[7]));
+ EXPECT_NE(ApzcOf(layers[1]), ApzcOf(layers[9]));
+ EXPECT_NE(ApzcOf(layers[4]), ApzcOf(layers[7]));
+ EXPECT_NE(ApzcOf(layers[4]), ApzcOf(layers[9]));
+ EXPECT_NE(ApzcOf(layers[7]), ApzcOf(layers[9]));
+ // Ensure the APZC parent chains are set up correctly
+ TestAsyncPanZoomController* layers1_2 = ApzcOf(layers[1]);
+ TestAsyncPanZoomController* layers4_6_8 = ApzcOf(layers[4]);
+ TestAsyncPanZoomController* layer7 = ApzcOf(layers[7]);
+ TestAsyncPanZoomController* layer9 = ApzcOf(layers[9]);
+ EXPECT_EQ(nullptr, layers1_2->GetParent());
+ EXPECT_EQ(nullptr, layers4_6_8->GetParent());
+ EXPECT_EQ(layers4_6_8, layer7->GetParent());
+ EXPECT_EQ(nullptr, layer9->GetParent());
+ // Ensure the hit-testing tree looks like the layer tree
+ RefPtr<HitTestingTreeNode> root = manager->GetRootNode();
+ RefPtr<HitTestingTreeNode> node5 = root->GetLastChild();
+ RefPtr<HitTestingTreeNode> node4 = node5->GetPrevSibling();
+ RefPtr<HitTestingTreeNode> node2 = node4->GetPrevSibling();
+ RefPtr<HitTestingTreeNode> node1 = node2->GetPrevSibling();
+ RefPtr<HitTestingTreeNode> node3 = node2->GetLastChild();
+ RefPtr<HitTestingTreeNode> node9 = node5->GetLastChild();
+ RefPtr<HitTestingTreeNode> node8 = node9->GetPrevSibling();
+ RefPtr<HitTestingTreeNode> node6 = node8->GetPrevSibling();
+ RefPtr<HitTestingTreeNode> node7 = node6->GetLastChild();
+ EXPECT_EQ(nullptr, node1->GetPrevSibling());
+ EXPECT_EQ(nullptr, node3->GetPrevSibling());
+ EXPECT_EQ(nullptr, node6->GetPrevSibling());
+ EXPECT_EQ(nullptr, node7->GetPrevSibling());
+ EXPECT_EQ(nullptr, node1->GetLastChild());
+ EXPECT_EQ(nullptr, node3->GetLastChild());
+ EXPECT_EQ(nullptr, node4->GetLastChild());
+ EXPECT_EQ(nullptr, node7->GetLastChild());
+ EXPECT_EQ(nullptr, node8->GetLastChild());
+ EXPECT_EQ(nullptr, node9->GetLastChild());
+
+ RefPtr<AsyncPanZoomController> hit = GetTargetAPZC(ScreenPoint(25, 25));
+ EXPECT_EQ(ApzcOf(layers[1]), hit.get());
+ hit = GetTargetAPZC(ScreenPoint(275, 375));
+ EXPECT_EQ(ApzcOf(layers[9]), hit.get());
+ hit = GetTargetAPZC(ScreenPoint(250, 100));
+ EXPECT_EQ(ApzcOf(layers[7]), hit.get());
+}
+
+TEST_F(APZHitTestingTester, TestRepaintFlushOnNewInputBlock) {
+ SCOPED_GFX_PREF(TouchActionEnabled, bool, false);
+
+ // The main purpose of this test is to verify that touch-start events (or anything
+ // that starts a new input block) don't ever get untransformed. This should always
+ // hold because the APZ code should flush repaints when we start a new input block
+ // and the transform to gecko space should be empty.
+
+ CreateSimpleScrollingLayer();
+ ScopedLayerTreeRegistration registration(manager, 0, root, mcc);
+ manager->UpdateHitTestingTree(0, root, false, 0, 0);
+ TestAsyncPanZoomController* apzcroot = ApzcOf(root);
+
+ // At this point, the following holds (all coordinates in screen pixels):
+ // layers[0] has content from (0,0)-(500,500), clipped by composition bounds (0,0)-(200,200)
+
+ MockFunction<void(std::string checkPointName)> check;
+
+ {
+ InSequence s;
+
+ EXPECT_CALL(*mcc, RequestContentRepaint(_)).Times(AtLeast(1));
+ EXPECT_CALL(check, Call("post-first-touch-start"));
+ EXPECT_CALL(*mcc, RequestContentRepaint(_)).Times(AtLeast(1));
+ EXPECT_CALL(check, Call("post-second-fling"));
+ EXPECT_CALL(*mcc, RequestContentRepaint(_)).Times(AtLeast(1));
+ EXPECT_CALL(check, Call("post-second-touch-start"));
+ }
+
+ // This first pan will move the APZC by 50 pixels, and dispatch a paint request.
+ ApzcPanNoFling(apzcroot, 100, 50);
+
+ // Verify that a touch start doesn't get untransformed
+ ScreenIntPoint touchPoint(50, 50);
+ MultiTouchInput mti = CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_START, mcc->Time());
+ mti.mTouches.AppendElement(SingleTouchData(0, touchPoint, ScreenSize(0, 0), 0, 0));
+
+ EXPECT_EQ(nsEventStatus_eConsumeDoDefault, manager->ReceiveInputEvent(mti, nullptr, nullptr));
+ EXPECT_EQ(touchPoint, mti.mTouches[0].mScreenPoint);
+ check.Call("post-first-touch-start");
+
+ // Send a touchend to clear state
+ mti.mType = MultiTouchInput::MULTITOUCH_END;
+ manager->ReceiveInputEvent(mti, nullptr, nullptr);
+
+ mcc->AdvanceByMillis(1000);
+
+ // Now do two pans. The first of these will dispatch a repaint request, as above.
+ // The second will get stuck in the paint throttler because the first one doesn't
+ // get marked as "completed", so this will result in a non-empty LD transform.
+ // (Note that any outstanding repaint requests from the first half of this test
+ // don't impact this half because we advance the time by 1 second, which will trigger
+ // the max-wait-exceeded codepath in the paint throttler).
+ ApzcPanNoFling(apzcroot, 100, 50);
+ check.Call("post-second-fling");
+ ApzcPanNoFling(apzcroot, 100, 50);
+
+ // Ensure that a touch start again doesn't get untransformed by flushing
+ // a repaint
+ mti.mType = MultiTouchInput::MULTITOUCH_START;
+ EXPECT_EQ(nsEventStatus_eConsumeDoDefault, manager->ReceiveInputEvent(mti, nullptr, nullptr));
+ EXPECT_EQ(touchPoint, mti.mTouches[0].mScreenPoint);
+ check.Call("post-second-touch-start");
+
+ mti.mType = MultiTouchInput::MULTITOUCH_END;
+ EXPECT_EQ(nsEventStatus_eConsumeDoDefault, manager->ReceiveInputEvent(mti, nullptr, nullptr));
+ EXPECT_EQ(touchPoint, mti.mTouches[0].mScreenPoint);
+}
+
+TEST_F(APZHitTestingTester, TestRepaintFlushOnWheelEvents) {
+ // The purpose of this test is to ensure that wheel events trigger a repaint
+ // flush as per bug 1166871, and that the wheel event untransform is a no-op.
+
+ CreateSimpleScrollingLayer();
+ ScopedLayerTreeRegistration registration(manager, 0, root, mcc);
+ manager->UpdateHitTestingTree(0, root, false, 0, 0);
+ TestAsyncPanZoomController* apzcroot = ApzcOf(root);
+
+ EXPECT_CALL(*mcc, RequestContentRepaint(_)).Times(AtLeast(3));
+ ScreenPoint origin(100, 50);
+ for (int i = 0; i < 3; i++) {
+ ScrollWheelInput swi(MillisecondsSinceStartup(mcc->Time()), mcc->Time(), 0,
+ ScrollWheelInput::SCROLLMODE_INSTANT, ScrollWheelInput::SCROLLDELTA_PIXEL,
+ origin, 0, 10, false);
+ EXPECT_EQ(nsEventStatus_eConsumeDoDefault, manager->ReceiveInputEvent(swi, nullptr, nullptr));
+ EXPECT_EQ(origin, swi.mOrigin);
+
+ AsyncTransform viewTransform;
+ ParentLayerPoint point;
+ apzcroot->SampleContentTransformForFrame(&viewTransform, point);
+ EXPECT_EQ(0, point.x);
+ EXPECT_EQ((i + 1) * 10, point.y);
+ EXPECT_EQ(0, viewTransform.mTranslation.x);
+ EXPECT_EQ((i + 1) * -10, viewTransform.mTranslation.y);
+
+ mcc->AdvanceByMillis(5);
+ }
+}
+
+TEST_F(APZHitTestingTester, TestForceDisableApz) {
+ CreateSimpleScrollingLayer();
+ DisableApzOn(root);
+ ScopedLayerTreeRegistration registration(manager, 0, root, mcc);
+ manager->UpdateHitTestingTree(0, root, false, 0, 0);
+ TestAsyncPanZoomController* apzcroot = ApzcOf(root);
+
+ ScreenPoint origin(100, 50);
+ ScrollWheelInput swi(MillisecondsSinceStartup(mcc->Time()), mcc->Time(), 0,
+ ScrollWheelInput::SCROLLMODE_INSTANT, ScrollWheelInput::SCROLLDELTA_PIXEL,
+ origin, 0, 10, false);
+ EXPECT_EQ(nsEventStatus_eConsumeDoDefault, manager->ReceiveInputEvent(swi, nullptr, nullptr));
+ EXPECT_EQ(origin, swi.mOrigin);
+
+ AsyncTransform viewTransform;
+ ParentLayerPoint point;
+ apzcroot->SampleContentTransformForFrame(&viewTransform, point);
+ // Since APZ is force-disabled, we expect to see the async transform via
+ // the NORMAL AsyncMode, but not via the RESPECT_FORCE_DISABLE AsyncMode.
+ EXPECT_EQ(0, point.x);
+ EXPECT_EQ(10, point.y);
+ EXPECT_EQ(0, viewTransform.mTranslation.x);
+ EXPECT_EQ(-10, viewTransform.mTranslation.y);
+ viewTransform = apzcroot->GetCurrentAsyncTransform(AsyncPanZoomController::RESPECT_FORCE_DISABLE);
+ point = apzcroot->GetCurrentAsyncScrollOffset(AsyncPanZoomController::RESPECT_FORCE_DISABLE);
+ EXPECT_EQ(0, point.x);
+ EXPECT_EQ(0, point.y);
+ EXPECT_EQ(0, viewTransform.mTranslation.x);
+ EXPECT_EQ(0, viewTransform.mTranslation.y);
+
+ mcc->AdvanceByMillis(10);
+
+ // With untransforming events we should get normal behaviour (in this case,
+ // no noticeable untransform, because the repaint request already got
+ // flushed).
+ swi = ScrollWheelInput(MillisecondsSinceStartup(mcc->Time()), mcc->Time(), 0,
+ ScrollWheelInput::SCROLLMODE_INSTANT, ScrollWheelInput::SCROLLDELTA_PIXEL,
+ origin, 0, 0, false);
+ EXPECT_EQ(nsEventStatus_eConsumeDoDefault, manager->ReceiveInputEvent(swi, nullptr, nullptr));
+ EXPECT_EQ(origin, swi.mOrigin);
+}
+
+TEST_F(APZHitTestingTester, Bug1148350) {
+ CreateBug1148350LayerTree();
+ ScopedLayerTreeRegistration registration(manager, 0, root, mcc);
+ manager->UpdateHitTestingTree(0, root, false, 0, 0);
+
+ MockFunction<void(std::string checkPointName)> check;
+ {
+ InSequence s;
+ EXPECT_CALL(*mcc, HandleTap(TapType::eSingleTap, LayoutDevicePoint(100, 100), 0, ApzcOf(layers[1])->GetGuid(), _)).Times(1);
+ EXPECT_CALL(check, Call("Tapped without transform"));
+ EXPECT_CALL(*mcc, HandleTap(TapType::eSingleTap, LayoutDevicePoint(100, 100), 0, ApzcOf(layers[1])->GetGuid(), _)).Times(1);
+ EXPECT_CALL(check, Call("Tapped with interleaved transform"));
+ }
+
+ Tap(manager, ScreenIntPoint(100, 100), TimeDuration::FromMilliseconds(100));
+ mcc->RunThroughDelayedTasks();
+ check.Call("Tapped without transform");
+
+ uint64_t blockId;
+ TouchDown(manager, ScreenIntPoint(100, 100), mcc->Time(), &blockId);
+ if (gfxPrefs::TouchActionEnabled()) {
+ SetDefaultAllowedTouchBehavior(manager, blockId);
+ }
+ mcc->AdvanceByMillis(100);
+
+ layers[0]->SetVisibleRegion(LayerIntRegion(LayerIntRect(0,50,200,150)));
+ layers[0]->SetBaseTransform(Matrix4x4::Translation(0, 50, 0));
+ manager->UpdateHitTestingTree(0, root, false, 0, 0);
+
+ TouchUp(manager, ScreenIntPoint(100, 100), mcc->Time());
+ mcc->RunThroughDelayedTasks();
+ check.Call("Tapped with interleaved transform");
+}
+
+TEST_F(APZHitTestingTester, HitTestingRespectsScrollClip_Bug1257288) {
+ // Create the layer tree.
+ const char* layerTreeSyntax = "c(tt)";
+ // LayerID 0 12
+ nsIntRegion layerVisibleRegion[] = {
+ nsIntRegion(IntRect(0,0,200,200)),
+ nsIntRegion(IntRect(0,0,200,200)),
+ nsIntRegion(IntRect(0,0,200,100))
+ };
+ root = CreateLayerTree(layerTreeSyntax, layerVisibleRegion, nullptr, lm, layers);
+
+ // Add root scroll metadata to the first painted layer.
+ SetScrollableFrameMetrics(layers[1], FrameMetrics::START_SCROLL_ID, CSSRect(0,0,200,200));
+
+ // Add root and subframe scroll metadata to the second painted layer.
+ // Give the subframe metadata a scroll clip corresponding to the subframe's
+ // composition bounds.
+ // Importantly, give the layer a layer clip which leaks outside of the
+ // subframe's composition bounds.
+ ScrollMetadata rootMetadata = BuildScrollMetadata(
+ FrameMetrics::START_SCROLL_ID, CSSRect(0,0,200,200),
+ ParentLayerRect(0,0,200,200));
+ ScrollMetadata subframeMetadata = BuildScrollMetadata(
+ FrameMetrics::START_SCROLL_ID + 1, CSSRect(0,0,200,200),
+ ParentLayerRect(0,0,200,100));
+ subframeMetadata.SetScrollClip(Some(LayerClip(ParentLayerIntRect(0,0,200,100))));
+ layers[2]->SetScrollMetadata({subframeMetadata, rootMetadata});
+ layers[2]->SetClipRect(Some(ParentLayerIntRect(0,0,200,200)));
+ SetEventRegionsBasedOnBottommostMetrics(layers[2]);
+
+ // Build the hit testing tree.
+ ScopedLayerTreeRegistration registration(manager, 0, root, mcc);
+ manager->UpdateHitTestingTree(0, root, false, 0, 0);
+
+ // Pan on a region that's inside layers[2]'s layer clip, but outside
+ // its subframe metadata's scroll clip.
+ Pan(manager, 120, 110);
+
+ // Test that the subframe hasn't scrolled.
+ EXPECT_EQ(CSSPoint(0,0), ApzcOf(layers[2], 0)->GetFrameMetrics().GetScrollOffset());
+}
diff --git a/gfx/layers/apz/test/gtest/TestInputQueue.cpp b/gfx/layers/apz/test/gtest/TestInputQueue.cpp
new file mode 100644
index 000000000..d05b6d66e
--- /dev/null
+++ b/gfx/layers/apz/test/gtest/TestInputQueue.cpp
@@ -0,0 +1,44 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set sw=2 ts=8 et tw=80 : */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "APZCTreeManagerTester.h"
+#include "APZTestCommon.h"
+#include "InputUtils.h"
+
+// Test of scenario described in bug 1269067 - that a continuing mouse drag
+// doesn't interrupt a wheel scrolling animation
+TEST_F(APZCTreeManagerTester, WheelInterruptedByMouseDrag) {
+ // Set up a scrollable layer
+ CreateSimpleScrollingLayer();
+ ScopedLayerTreeRegistration registration(manager, 0, root, mcc);
+ manager->UpdateHitTestingTree(0, root, false, 0, 0);
+ RefPtr<TestAsyncPanZoomController> apzc = ApzcOf(root);
+
+ uint64_t dragBlockId = 0;
+ uint64_t wheelBlockId = 0;
+ uint64_t tmpBlockId = 0;
+
+ // First start the mouse drag
+ MouseDown(apzc, ScreenIntPoint(5, 5), mcc->Time(), &dragBlockId);
+ MouseMove(apzc, ScreenIntPoint(6, 6), mcc->Time(), &tmpBlockId);
+ EXPECT_EQ(dragBlockId, tmpBlockId);
+
+ // Insert the wheel event, check that it has a new block id
+ SmoothWheel(apzc, ScreenIntPoint(6, 6), ScreenPoint(0, 1), mcc->Time(), &wheelBlockId);
+ EXPECT_NE(dragBlockId, wheelBlockId);
+
+ // Continue the drag, check that the block id is the same as before
+ MouseMove(apzc, ScreenIntPoint(7, 5), mcc->Time(), &tmpBlockId);
+ EXPECT_EQ(dragBlockId, tmpBlockId);
+
+ // Finish the wheel animation
+ apzc->AdvanceAnimationsUntilEnd();
+
+ // Check that it scrolled
+ ParentLayerPoint scroll = apzc->GetCurrentAsyncScrollOffset(AsyncPanZoomController::NORMAL);
+ EXPECT_EQ(scroll.x, 0);
+ EXPECT_EQ(scroll.y, 10); // We scrolled 1 "line" or 10 pixels
+}
diff --git a/gfx/layers/apz/test/gtest/TestPanning.cpp b/gfx/layers/apz/test/gtest/TestPanning.cpp
new file mode 100644
index 000000000..3ee19a58d
--- /dev/null
+++ b/gfx/layers/apz/test/gtest/TestPanning.cpp
@@ -0,0 +1,128 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set sw=2 ts=8 et tw=80 : */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "APZCBasicTester.h"
+#include "APZTestCommon.h"
+#include "InputUtils.h"
+
+class APZCPanningTester : public APZCBasicTester {
+protected:
+ void DoPanTest(bool aShouldTriggerScroll, bool aShouldBeConsumed, uint32_t aBehavior)
+ {
+ if (aShouldTriggerScroll) {
+ // One repaint request for each pan.
+ EXPECT_CALL(*mcc, RequestContentRepaint(_)).Times(2);
+ } else {
+ EXPECT_CALL(*mcc, RequestContentRepaint(_)).Times(0);
+ }
+
+ int touchStart = 50;
+ int touchEnd = 10;
+ ParentLayerPoint pointOut;
+ AsyncTransform viewTransformOut;
+
+ nsTArray<uint32_t> allowedTouchBehaviors;
+ allowedTouchBehaviors.AppendElement(aBehavior);
+
+ // Pan down
+ PanAndCheckStatus(apzc, touchStart, touchEnd, aShouldBeConsumed, &allowedTouchBehaviors);
+ apzc->SampleContentTransformForFrame(&viewTransformOut, pointOut);
+
+ if (aShouldTriggerScroll) {
+ EXPECT_EQ(ParentLayerPoint(0, -(touchEnd-touchStart)), pointOut);
+ EXPECT_NE(AsyncTransform(), viewTransformOut);
+ } else {
+ EXPECT_EQ(ParentLayerPoint(), pointOut);
+ EXPECT_EQ(AsyncTransform(), viewTransformOut);
+ }
+
+ // Clear the fling from the previous pan, or stopping it will
+ // consume the next touchstart
+ apzc->CancelAnimation();
+
+ // Pan back
+ PanAndCheckStatus(apzc, touchEnd, touchStart, aShouldBeConsumed, &allowedTouchBehaviors);
+ apzc->SampleContentTransformForFrame(&viewTransformOut, pointOut);
+
+ EXPECT_EQ(ParentLayerPoint(), pointOut);
+ EXPECT_EQ(AsyncTransform(), viewTransformOut);
+ }
+
+ void DoPanWithPreventDefaultTest()
+ {
+ MakeApzcWaitForMainThread();
+
+ int touchStart = 50;
+ int touchEnd = 10;
+ ParentLayerPoint pointOut;
+ AsyncTransform viewTransformOut;
+ uint64_t blockId = 0;
+
+ // Pan down
+ nsTArray<uint32_t> allowedTouchBehaviors;
+ allowedTouchBehaviors.AppendElement(mozilla::layers::AllowedTouchBehavior::VERTICAL_PAN);
+ PanAndCheckStatus(apzc, touchStart, touchEnd, true, &allowedTouchBehaviors, &blockId);
+
+ // Send the signal that content has handled and preventDefaulted the touch
+ // events. This flushes the event queue.
+ apzc->ContentReceivedInputBlock(blockId, true);
+
+ apzc->SampleContentTransformForFrame(&viewTransformOut, pointOut);
+ EXPECT_EQ(ParentLayerPoint(), pointOut);
+ EXPECT_EQ(AsyncTransform(), viewTransformOut);
+
+ apzc->AssertStateIsReset();
+ }
+};
+
+TEST_F(APZCPanningTester, Pan) {
+ SCOPED_GFX_PREF(TouchActionEnabled, bool, false);
+ SCOPED_GFX_PREF(APZVelocityBias, float, 0.0); // Velocity bias can cause extra repaint requests
+ DoPanTest(true, true, mozilla::layers::AllowedTouchBehavior::NONE);
+}
+
+// In the each of the following 4 pan tests we are performing two pan gestures: vertical pan from top
+// to bottom and back - from bottom to top.
+// According to the pointer-events/touch-action spec AUTO and PAN_Y touch-action values allow vertical
+// scrolling while NONE and PAN_X forbid it. The first parameter of DoPanTest method specifies this
+// behavior.
+// However, the events will be marked as consumed even if the behavior in PAN_X, because the user could
+// move their finger horizontally too - APZ has no way of knowing beforehand and so must consume the
+// events.
+TEST_F(APZCPanningTester, PanWithTouchActionAuto) {
+ SCOPED_GFX_PREF(TouchActionEnabled, bool, true);
+ SCOPED_GFX_PREF(APZVelocityBias, float, 0.0); // Velocity bias can cause extra repaint requests
+ DoPanTest(true, true, mozilla::layers::AllowedTouchBehavior::HORIZONTAL_PAN
+ | mozilla::layers::AllowedTouchBehavior::VERTICAL_PAN);
+}
+
+TEST_F(APZCPanningTester, PanWithTouchActionNone) {
+ SCOPED_GFX_PREF(TouchActionEnabled, bool, true);
+ SCOPED_GFX_PREF(APZVelocityBias, float, 0.0); // Velocity bias can cause extra repaint requests
+ DoPanTest(false, false, 0);
+}
+
+TEST_F(APZCPanningTester, PanWithTouchActionPanX) {
+ SCOPED_GFX_PREF(TouchActionEnabled, bool, true);
+ SCOPED_GFX_PREF(APZVelocityBias, float, 0.0); // Velocity bias can cause extra repaint requests
+ DoPanTest(false, true, mozilla::layers::AllowedTouchBehavior::HORIZONTAL_PAN);
+}
+
+TEST_F(APZCPanningTester, PanWithTouchActionPanY) {
+ SCOPED_GFX_PREF(TouchActionEnabled, bool, true);
+ SCOPED_GFX_PREF(APZVelocityBias, float, 0.0); // Velocity bias can cause extra repaint requests
+ DoPanTest(true, true, mozilla::layers::AllowedTouchBehavior::VERTICAL_PAN);
+}
+
+TEST_F(APZCPanningTester, PanWithPreventDefaultAndTouchAction) {
+ SCOPED_GFX_PREF(TouchActionEnabled, bool, true);
+ DoPanWithPreventDefaultTest();
+}
+
+TEST_F(APZCPanningTester, PanWithPreventDefault) {
+ SCOPED_GFX_PREF(TouchActionEnabled, bool, false);
+ DoPanWithPreventDefaultTest();
+}
diff --git a/gfx/layers/apz/test/gtest/TestPinching.cpp b/gfx/layers/apz/test/gtest/TestPinching.cpp
new file mode 100644
index 000000000..54fb7a757
--- /dev/null
+++ b/gfx/layers/apz/test/gtest/TestPinching.cpp
@@ -0,0 +1,294 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set sw=2 ts=8 et tw=80 : */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "APZCBasicTester.h"
+#include "APZTestCommon.h"
+#include "InputUtils.h"
+
+class APZCPinchTester : public APZCBasicTester {
+public:
+ explicit APZCPinchTester(AsyncPanZoomController::GestureBehavior aGestureBehavior = AsyncPanZoomController::DEFAULT_GESTURES)
+ : APZCBasicTester(aGestureBehavior)
+ {
+ }
+
+protected:
+ FrameMetrics GetPinchableFrameMetrics()
+ {
+ FrameMetrics fm;
+ fm.SetCompositionBounds(ParentLayerRect(200, 200, 100, 200));
+ fm.SetScrollableRect(CSSRect(0, 0, 980, 1000));
+ fm.SetScrollOffset(CSSPoint(300, 300));
+ fm.SetZoom(CSSToParentLayerScale2D(2.0, 2.0));
+ // APZC only allows zooming on the root scrollable frame.
+ fm.SetIsRootContent(true);
+ // the visible area of the document in CSS pixels is x=300 y=300 w=50 h=100
+ return fm;
+ }
+
+ void DoPinchTest(bool aShouldTriggerPinch,
+ nsTArray<uint32_t> *aAllowedTouchBehaviors = nullptr)
+ {
+ apzc->SetFrameMetrics(GetPinchableFrameMetrics());
+ MakeApzcZoomable();
+
+ if (aShouldTriggerPinch) {
+ // One repaint request for each gesture.
+ EXPECT_CALL(*mcc, RequestContentRepaint(_)).Times(2);
+ } else {
+ EXPECT_CALL(*mcc, RequestContentRepaint(_)).Times(0);
+ }
+
+ int touchInputId = 0;
+ if (mGestureBehavior == AsyncPanZoomController::USE_GESTURE_DETECTOR) {
+ PinchWithTouchInputAndCheckStatus(apzc, ScreenIntPoint(250, 300), 1.25,
+ touchInputId, aShouldTriggerPinch, aAllowedTouchBehaviors);
+ } else {
+ PinchWithPinchInputAndCheckStatus(apzc, ScreenIntPoint(250, 300), 1.25,
+ aShouldTriggerPinch);
+ }
+
+ FrameMetrics fm = apzc->GetFrameMetrics();
+
+ if (aShouldTriggerPinch) {
+ // the visible area of the document in CSS pixels is now x=305 y=310 w=40 h=80
+ EXPECT_EQ(2.5f, fm.GetZoom().ToScaleFactor().scale);
+ EXPECT_EQ(305, fm.GetScrollOffset().x);
+ EXPECT_EQ(310, fm.GetScrollOffset().y);
+ } else {
+ // The frame metrics should stay the same since touch-action:none makes
+ // apzc ignore pinch gestures.
+ EXPECT_EQ(2.0f, fm.GetZoom().ToScaleFactor().scale);
+ EXPECT_EQ(300, fm.GetScrollOffset().x);
+ EXPECT_EQ(300, fm.GetScrollOffset().y);
+ }
+
+ // part 2 of the test, move to the top-right corner of the page and pinch and
+ // make sure we stay in the correct spot
+ fm.SetZoom(CSSToParentLayerScale2D(2.0, 2.0));
+ fm.SetScrollOffset(CSSPoint(930, 5));
+ apzc->SetFrameMetrics(fm);
+ // the visible area of the document in CSS pixels is x=930 y=5 w=50 h=100
+
+ if (mGestureBehavior == AsyncPanZoomController::USE_GESTURE_DETECTOR) {
+ PinchWithTouchInputAndCheckStatus(apzc, ScreenIntPoint(250, 300), 0.5,
+ touchInputId, aShouldTriggerPinch, aAllowedTouchBehaviors);
+ } else {
+ PinchWithPinchInputAndCheckStatus(apzc, ScreenIntPoint(250, 300), 0.5,
+ aShouldTriggerPinch);
+ }
+
+ fm = apzc->GetFrameMetrics();
+
+ if (aShouldTriggerPinch) {
+ // the visible area of the document in CSS pixels is now x=880 y=0 w=100 h=200
+ EXPECT_EQ(1.0f, fm.GetZoom().ToScaleFactor().scale);
+ EXPECT_EQ(880, fm.GetScrollOffset().x);
+ EXPECT_EQ(0, fm.GetScrollOffset().y);
+ } else {
+ EXPECT_EQ(2.0f, fm.GetZoom().ToScaleFactor().scale);
+ EXPECT_EQ(930, fm.GetScrollOffset().x);
+ EXPECT_EQ(5, fm.GetScrollOffset().y);
+ }
+ }
+};
+
+class APZCPinchGestureDetectorTester : public APZCPinchTester {
+public:
+ APZCPinchGestureDetectorTester()
+ : APZCPinchTester(AsyncPanZoomController::USE_GESTURE_DETECTOR)
+ {
+ }
+
+ void DoPinchWithPreventDefaultTest() {
+ FrameMetrics originalMetrics = GetPinchableFrameMetrics();
+ apzc->SetFrameMetrics(originalMetrics);
+
+ MakeApzcWaitForMainThread();
+ MakeApzcZoomable();
+
+ int touchInputId = 0;
+ uint64_t blockId = 0;
+ PinchWithTouchInput(apzc, ScreenIntPoint(250, 300), 1.25, touchInputId,
+ nullptr, nullptr, &blockId);
+
+ // Send the prevent-default notification for the touch block
+ apzc->ContentReceivedInputBlock(blockId, true);
+
+ // verify the metrics didn't change (i.e. the pinch was ignored)
+ FrameMetrics fm = apzc->GetFrameMetrics();
+ EXPECT_EQ(originalMetrics.GetZoom(), fm.GetZoom());
+ EXPECT_EQ(originalMetrics.GetScrollOffset().x, fm.GetScrollOffset().x);
+ EXPECT_EQ(originalMetrics.GetScrollOffset().y, fm.GetScrollOffset().y);
+
+ apzc->AssertStateIsReset();
+ }
+};
+
+TEST_F(APZCPinchTester, Pinch_DefaultGestures_NoTouchAction) {
+ SCOPED_GFX_PREF(TouchActionEnabled, bool, false);
+ DoPinchTest(true);
+}
+
+TEST_F(APZCPinchGestureDetectorTester, Pinch_UseGestureDetector_NoTouchAction) {
+ SCOPED_GFX_PREF(TouchActionEnabled, bool, false);
+ DoPinchTest(true);
+}
+
+TEST_F(APZCPinchGestureDetectorTester, Pinch_UseGestureDetector_TouchActionNone) {
+ SCOPED_GFX_PREF(TouchActionEnabled, bool, true);
+ nsTArray<uint32_t> behaviors = { mozilla::layers::AllowedTouchBehavior::NONE,
+ mozilla::layers::AllowedTouchBehavior::NONE };
+ DoPinchTest(false, &behaviors);
+}
+
+TEST_F(APZCPinchGestureDetectorTester, Pinch_UseGestureDetector_TouchActionZoom) {
+ SCOPED_GFX_PREF(TouchActionEnabled, bool, true);
+ nsTArray<uint32_t> behaviors;
+ behaviors.AppendElement(mozilla::layers::AllowedTouchBehavior::PINCH_ZOOM);
+ behaviors.AppendElement(mozilla::layers::AllowedTouchBehavior::PINCH_ZOOM);
+ DoPinchTest(true, &behaviors);
+}
+
+TEST_F(APZCPinchGestureDetectorTester, Pinch_UseGestureDetector_TouchActionNotAllowZoom) {
+ SCOPED_GFX_PREF(TouchActionEnabled, bool, true);
+ nsTArray<uint32_t> behaviors;
+ behaviors.AppendElement(mozilla::layers::AllowedTouchBehavior::VERTICAL_PAN);
+ behaviors.AppendElement(mozilla::layers::AllowedTouchBehavior::PINCH_ZOOM);
+ DoPinchTest(false, &behaviors);
+}
+
+TEST_F(APZCPinchGestureDetectorTester, Pinch_UseGestureDetector_TouchActionNone_NoAPZZoom) {
+ SCOPED_GFX_PREF(TouchActionEnabled, bool, true);
+ SCOPED_GFX_PREF(APZAllowZooming, bool, false);
+
+ // Since we are preventing the pinch action via touch-action we should not be
+ // sending the pinch gesture notifications that would normally be sent when
+ // APZAllowZooming is false.
+ EXPECT_CALL(*mcc, NotifyPinchGesture(_, _, _, _)).Times(0);
+ nsTArray<uint32_t> behaviors = { mozilla::layers::AllowedTouchBehavior::NONE,
+ mozilla::layers::AllowedTouchBehavior::NONE };
+ DoPinchTest(false, &behaviors);
+}
+
+TEST_F(APZCPinchGestureDetectorTester, Pinch_PreventDefault) {
+ DoPinchWithPreventDefaultTest();
+}
+
+TEST_F(APZCPinchGestureDetectorTester, Pinch_PreventDefault_NoAPZZoom) {
+ SCOPED_GFX_PREF(APZAllowZooming, bool, false);
+
+ // Since we are preventing the pinch action we should not be sending the pinch
+ // gesture notifications that would normally be sent when APZAllowZooming is
+ // false.
+ EXPECT_CALL(*mcc, NotifyPinchGesture(_, _, _, _)).Times(0);
+
+ DoPinchWithPreventDefaultTest();
+}
+
+TEST_F(APZCPinchTester, Panning_TwoFinger_ZoomDisabled) {
+ // set up APZ
+ apzc->SetFrameMetrics(GetPinchableFrameMetrics());
+ MakeApzcUnzoomable();
+
+ nsEventStatus statuses[3]; // scalebegin, scale, scaleend
+ PinchWithPinchInput(apzc, ScreenIntPoint(250, 350), ScreenIntPoint(200, 300),
+ 10, &statuses);
+
+ FrameMetrics fm = apzc->GetFrameMetrics();
+
+ // It starts from (300, 300), then moves the focus point from (250, 350) to
+ // (200, 300) pans by (50, 50) screen pixels, but there is a 2x zoom, which
+ // causes the scroll offset to change by half of that (25, 25) pixels.
+ EXPECT_EQ(325, fm.GetScrollOffset().x);
+ EXPECT_EQ(325, fm.GetScrollOffset().y);
+ EXPECT_EQ(2.0, fm.GetZoom().ToScaleFactor().scale);
+}
+
+TEST_F(APZCPinchGestureDetectorTester, Pinch_APZZoom_Disabled) {
+ SCOPED_GFX_PREF(APZAllowZooming, bool, false);
+
+ FrameMetrics originalMetrics = GetPinchableFrameMetrics();
+ apzc->SetFrameMetrics(originalMetrics);
+
+ // When APZAllowZooming is false, the ZoomConstraintsClient produces
+ // ZoomConstraints with mAllowZoom set to false.
+ MakeApzcUnzoomable();
+
+ // With APZAllowZooming false, we expect the NotifyPinchGesture function to
+ // get called as the pinch progresses, but the metrics shouldn't change.
+ EXPECT_CALL(*mcc, NotifyPinchGesture(PinchGestureInput::PINCHGESTURE_START, apzc->GetGuid(), LayoutDeviceCoord(0), _)).Times(1);
+ EXPECT_CALL(*mcc, NotifyPinchGesture(PinchGestureInput::PINCHGESTURE_SCALE, apzc->GetGuid(), _, _)).Times(AtLeast(1));
+ EXPECT_CALL(*mcc, NotifyPinchGesture(PinchGestureInput::PINCHGESTURE_END, apzc->GetGuid(), LayoutDeviceCoord(0), _)).Times(1);
+
+ int touchInputId = 0;
+ uint64_t blockId = 0;
+ PinchWithTouchInput(apzc, ScreenIntPoint(250, 300), 1.25, touchInputId,
+ nullptr, nullptr, &blockId);
+
+ // verify the metrics didn't change (i.e. the pinch was ignored inside APZ)
+ FrameMetrics fm = apzc->GetFrameMetrics();
+ EXPECT_EQ(originalMetrics.GetZoom(), fm.GetZoom());
+ EXPECT_EQ(originalMetrics.GetScrollOffset().x, fm.GetScrollOffset().x);
+ EXPECT_EQ(originalMetrics.GetScrollOffset().y, fm.GetScrollOffset().y);
+
+ apzc->AssertStateIsReset();
+}
+
+TEST_F(APZCPinchGestureDetectorTester, Pinch_NoSpan) {
+ SCOPED_GFX_PREF(APZAllowZooming, bool, false);
+ SCOPED_GFX_PREF(TouchActionEnabled, bool, false);
+
+ FrameMetrics originalMetrics = GetPinchableFrameMetrics();
+ apzc->SetFrameMetrics(originalMetrics);
+
+ // When APZAllowZooming is false, the ZoomConstraintsClient produces
+ // ZoomConstraints with mAllowZoom set to false.
+ MakeApzcUnzoomable();
+
+ // With APZAllowZooming false, we expect the NotifyPinchGesture function to
+ // get called as the pinch progresses, but the metrics shouldn't change.
+ EXPECT_CALL(*mcc, NotifyPinchGesture(PinchGestureInput::PINCHGESTURE_START, apzc->GetGuid(), LayoutDeviceCoord(0), _)).Times(1);
+ EXPECT_CALL(*mcc, NotifyPinchGesture(PinchGestureInput::PINCHGESTURE_SCALE, apzc->GetGuid(), _, _)).Times(AtLeast(1));
+ EXPECT_CALL(*mcc, NotifyPinchGesture(PinchGestureInput::PINCHGESTURE_END, apzc->GetGuid(), LayoutDeviceCoord(0), _)).Times(1);
+
+ int inputId = 0;
+ ScreenIntPoint focus(250, 300);
+
+ // Do a pinch holding a zero span and moving the focus by y=100
+
+ MultiTouchInput mtiStart = MultiTouchInput(MultiTouchInput::MULTITOUCH_START, 0, TimeStamp(), 0);
+ mtiStart.mTouches.AppendElement(CreateSingleTouchData(inputId, focus));
+ mtiStart.mTouches.AppendElement(CreateSingleTouchData(inputId + 1, focus));
+ apzc->ReceiveInputEvent(mtiStart, nullptr);
+
+ focus.y -= 35 + 1; // this is to get over the PINCH_START_THRESHOLD in GestureEventListener.cpp
+ MultiTouchInput mtiMove1 = MultiTouchInput(MultiTouchInput::MULTITOUCH_MOVE, 0, TimeStamp(), 0);
+ mtiMove1.mTouches.AppendElement(CreateSingleTouchData(inputId, focus));
+ mtiMove1.mTouches.AppendElement(CreateSingleTouchData(inputId + 1, focus));
+ apzc->ReceiveInputEvent(mtiMove1, nullptr);
+
+ focus.y -= 100; // do a two-finger scroll of 100 screen pixels
+ MultiTouchInput mtiMove2 = MultiTouchInput(MultiTouchInput::MULTITOUCH_MOVE, 0, TimeStamp(), 0);
+ mtiMove2.mTouches.AppendElement(CreateSingleTouchData(inputId, focus));
+ mtiMove2.mTouches.AppendElement(CreateSingleTouchData(inputId + 1, focus));
+ apzc->ReceiveInputEvent(mtiMove2, nullptr);
+
+ MultiTouchInput mtiEnd = MultiTouchInput(MultiTouchInput::MULTITOUCH_END, 0, TimeStamp(), 0);
+ mtiEnd.mTouches.AppendElement(CreateSingleTouchData(inputId, focus));
+ mtiEnd.mTouches.AppendElement(CreateSingleTouchData(inputId + 1, focus));
+ apzc->ReceiveInputEvent(mtiEnd, nullptr);
+
+ // Done, check the metrics to make sure we scrolled by 100 screen pixels,
+ // which is 50 CSS pixels for the pinchable frame metrics.
+
+ FrameMetrics fm = apzc->GetFrameMetrics();
+ EXPECT_EQ(originalMetrics.GetZoom(), fm.GetZoom());
+ EXPECT_EQ(originalMetrics.GetScrollOffset().x, fm.GetScrollOffset().x);
+ EXPECT_EQ(originalMetrics.GetScrollOffset().y + 50, fm.GetScrollOffset().y);
+
+ apzc->AssertStateIsReset();
+}
diff --git a/gfx/layers/apz/test/gtest/TestScrollHandoff.cpp b/gfx/layers/apz/test/gtest/TestScrollHandoff.cpp
new file mode 100644
index 000000000..d57d09ead
--- /dev/null
+++ b/gfx/layers/apz/test/gtest/TestScrollHandoff.cpp
@@ -0,0 +1,521 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set sw=2 ts=8 et tw=80 : */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "APZCTreeManagerTester.h"
+#include "APZTestCommon.h"
+#include "InputUtils.h"
+
+class APZScrollHandoffTester : public APZCTreeManagerTester {
+protected:
+ UniquePtr<ScopedLayerTreeRegistration> registration;
+ TestAsyncPanZoomController* rootApzc;
+
+ void CreateScrollHandoffLayerTree1() {
+ const char* layerTreeSyntax = "c(t)";
+ nsIntRegion layerVisibleRegion[] = {
+ nsIntRegion(IntRect(0, 0, 100, 100)),
+ nsIntRegion(IntRect(0, 50, 100, 50))
+ };
+ root = CreateLayerTree(layerTreeSyntax, layerVisibleRegion, nullptr, lm, layers);
+ SetScrollableFrameMetrics(root, FrameMetrics::START_SCROLL_ID, CSSRect(0, 0, 200, 200));
+ SetScrollableFrameMetrics(layers[1], FrameMetrics::START_SCROLL_ID + 1, CSSRect(0, 0, 100, 100));
+ SetScrollHandoff(layers[1], root);
+ registration = MakeUnique<ScopedLayerTreeRegistration>(manager, 0, root, mcc);
+ manager->UpdateHitTestingTree(0, root, false, 0, 0);
+ rootApzc = ApzcOf(root);
+ rootApzc->GetFrameMetrics().SetIsRootContent(true); // make root APZC zoomable
+ }
+
+ void CreateScrollHandoffLayerTree2() {
+ const char* layerTreeSyntax = "c(c(t))";
+ nsIntRegion layerVisibleRegion[] = {
+ nsIntRegion(IntRect(0, 0, 100, 100)),
+ nsIntRegion(IntRect(0, 0, 100, 100)),
+ nsIntRegion(IntRect(0, 50, 100, 50))
+ };
+ root = CreateLayerTree(layerTreeSyntax, layerVisibleRegion, nullptr, lm, layers);
+ SetScrollableFrameMetrics(root, FrameMetrics::START_SCROLL_ID, CSSRect(0, 0, 200, 200));
+ SetScrollableFrameMetrics(layers[1], FrameMetrics::START_SCROLL_ID + 2, CSSRect(-100, -100, 200, 200));
+ SetScrollableFrameMetrics(layers[2], FrameMetrics::START_SCROLL_ID + 1, CSSRect(0, 0, 100, 100));
+ SetScrollHandoff(layers[1], root);
+ SetScrollHandoff(layers[2], layers[1]);
+ // No ScopedLayerTreeRegistration as that just needs to be done once per test
+ // and this is the second layer tree for a particular test.
+ MOZ_ASSERT(registration);
+ manager->UpdateHitTestingTree(0, root, false, 0, 0);
+ rootApzc = ApzcOf(root);
+ }
+
+ void CreateScrollHandoffLayerTree3() {
+ const char* layerTreeSyntax = "c(c(t)c(t))";
+ nsIntRegion layerVisibleRegion[] = {
+ nsIntRegion(IntRect(0, 0, 100, 100)), // root
+ nsIntRegion(IntRect(0, 0, 100, 50)), // scrolling parent 1
+ nsIntRegion(IntRect(0, 0, 100, 50)), // scrolling child 1
+ nsIntRegion(IntRect(0, 50, 100, 50)), // scrolling parent 2
+ nsIntRegion(IntRect(0, 50, 100, 50)) // scrolling child 2
+ };
+ root = CreateLayerTree(layerTreeSyntax, layerVisibleRegion, nullptr, lm, layers);
+ SetScrollableFrameMetrics(layers[0], FrameMetrics::START_SCROLL_ID, CSSRect(0, 0, 100, 100));
+ SetScrollableFrameMetrics(layers[1], FrameMetrics::START_SCROLL_ID + 1, CSSRect(0, 0, 100, 100));
+ SetScrollableFrameMetrics(layers[2], FrameMetrics::START_SCROLL_ID + 2, CSSRect(0, 0, 100, 100));
+ SetScrollableFrameMetrics(layers[3], FrameMetrics::START_SCROLL_ID + 3, CSSRect(0, 50, 100, 100));
+ SetScrollableFrameMetrics(layers[4], FrameMetrics::START_SCROLL_ID + 4, CSSRect(0, 50, 100, 100));
+ SetScrollHandoff(layers[1], layers[0]);
+ SetScrollHandoff(layers[3], layers[0]);
+ SetScrollHandoff(layers[2], layers[1]);
+ SetScrollHandoff(layers[4], layers[3]);
+ registration = MakeUnique<ScopedLayerTreeRegistration>(manager, 0, root, mcc);
+ manager->UpdateHitTestingTree(0, root, false, 0, 0);
+ }
+
+ void CreateScrollgrabLayerTree(bool makeParentScrollable = true) {
+ const char* layerTreeSyntax = "c(t)";
+ nsIntRegion layerVisibleRegion[] = {
+ nsIntRegion(IntRect(0, 0, 100, 100)), // scroll-grabbing parent
+ nsIntRegion(IntRect(0, 20, 100, 80)) // child
+ };
+ root = CreateLayerTree(layerTreeSyntax, layerVisibleRegion, nullptr, lm, layers);
+ float parentHeight = makeParentScrollable ? 120 : 100;
+ SetScrollableFrameMetrics(root, FrameMetrics::START_SCROLL_ID, CSSRect(0, 0, 100, parentHeight));
+ SetScrollableFrameMetrics(layers[1], FrameMetrics::START_SCROLL_ID + 1, CSSRect(0, 0, 100, 200));
+ SetScrollHandoff(layers[1], root);
+ registration = MakeUnique<ScopedLayerTreeRegistration>(manager, 0, root, mcc);
+ manager->UpdateHitTestingTree(0, root, false, 0, 0);
+ rootApzc = ApzcOf(root);
+ rootApzc->GetScrollMetadata().SetHasScrollgrab(true);
+ }
+
+ void TestFlingAcceleration() {
+ // Jack up the fling acceleration multiplier so we can easily determine
+ // whether acceleration occured.
+ const float kAcceleration = 100.0f;
+ SCOPED_GFX_PREF(APZFlingAccelBaseMultiplier, float, kAcceleration);
+ SCOPED_GFX_PREF(APZFlingAccelMinVelocity, float, 0.0);
+
+ RefPtr<TestAsyncPanZoomController> childApzc = ApzcOf(layers[1]);
+
+ // Pan once, enough to fully scroll the scrollgrab parent and then scroll
+ // and fling the child.
+ Pan(manager, 70, 40);
+
+ // Give the fling animation a chance to start.
+ SampleAnimationsOnce();
+
+ float childVelocityAfterFling1 = childApzc->GetVelocityVector().y;
+
+ // Pan again.
+ Pan(manager, 70, 40);
+
+ // Give the fling animation a chance to start.
+ // This time it should be accelerated.
+ SampleAnimationsOnce();
+
+ float childVelocityAfterFling2 = childApzc->GetVelocityVector().y;
+
+ // We should have accelerated once.
+ // The division by 2 is to account for friction.
+ EXPECT_GT(childVelocityAfterFling2,
+ childVelocityAfterFling1 * kAcceleration / 2);
+
+ // We should not have accelerated twice.
+ // The division by 4 is to account for friction.
+ EXPECT_LE(childVelocityAfterFling2,
+ childVelocityAfterFling1 * kAcceleration * kAcceleration / 4);
+ }
+};
+
+// Here we test that if the processing of a touch block is deferred while we
+// wait for content to send a prevent-default message, overscroll is still
+// handed off correctly when the block is processed.
+TEST_F(APZScrollHandoffTester, DeferredInputEventProcessing) {
+ // Set up the APZC tree.
+ CreateScrollHandoffLayerTree1();
+
+ TestAsyncPanZoomController* childApzc = ApzcOf(layers[1]);
+
+ // Enable touch-listeners so that we can separate the queueing of input
+ // events from them being processed.
+ childApzc->SetWaitForMainThread();
+
+ // Queue input events for a pan.
+ uint64_t blockId = 0;
+ ApzcPanNoFling(childApzc, 90, 30, &blockId);
+
+ // Allow the pan to be processed.
+ childApzc->ContentReceivedInputBlock(blockId, false);
+ childApzc->ConfirmTarget(blockId);
+
+ // Make sure overscroll was handed off correctly.
+ EXPECT_EQ(50, childApzc->GetFrameMetrics().GetScrollOffset().y);
+ EXPECT_EQ(10, rootApzc->GetFrameMetrics().GetScrollOffset().y);
+}
+
+// Here we test that if the layer structure changes in between two input
+// blocks being queued, and the first block is only processed after the second
+// one has been queued, overscroll handoff for the first block follows
+// the original layer structure while overscroll handoff for the second block
+// follows the new layer structure.
+TEST_F(APZScrollHandoffTester, LayerStructureChangesWhileEventsArePending) {
+ // Set up an initial APZC tree.
+ CreateScrollHandoffLayerTree1();
+
+ TestAsyncPanZoomController* childApzc = ApzcOf(layers[1]);
+
+ // Enable touch-listeners so that we can separate the queueing of input
+ // events from them being processed.
+ childApzc->SetWaitForMainThread();
+
+ // Queue input events for a pan.
+ uint64_t blockId = 0;
+ ApzcPanNoFling(childApzc, 90, 30, &blockId);
+
+ // Modify the APZC tree to insert a new APZC 'middle' into the handoff chain
+ // between the child and the root.
+ CreateScrollHandoffLayerTree2();
+ RefPtr<Layer> middle = layers[1];
+ childApzc->SetWaitForMainThread();
+ TestAsyncPanZoomController* middleApzc = ApzcOf(middle);
+
+ // Queue input events for another pan.
+ uint64_t secondBlockId = 0;
+ ApzcPanNoFling(childApzc, 30, 90, &secondBlockId);
+
+ // Allow the first pan to be processed.
+ childApzc->ContentReceivedInputBlock(blockId, false);
+ childApzc->ConfirmTarget(blockId);
+
+ // Make sure things have scrolled according to the handoff chain in
+ // place at the time the touch-start of the first pan was queued.
+ EXPECT_EQ(50, childApzc->GetFrameMetrics().GetScrollOffset().y);
+ EXPECT_EQ(10, rootApzc->GetFrameMetrics().GetScrollOffset().y);
+ EXPECT_EQ(0, middleApzc->GetFrameMetrics().GetScrollOffset().y);
+
+ // Allow the second pan to be processed.
+ childApzc->ContentReceivedInputBlock(secondBlockId, false);
+ childApzc->ConfirmTarget(secondBlockId);
+
+ // Make sure things have scrolled according to the handoff chain in
+ // place at the time the touch-start of the second pan was queued.
+ EXPECT_EQ(0, childApzc->GetFrameMetrics().GetScrollOffset().y);
+ EXPECT_EQ(10, rootApzc->GetFrameMetrics().GetScrollOffset().y);
+ EXPECT_EQ(-10, middleApzc->GetFrameMetrics().GetScrollOffset().y);
+}
+
+// Test that putting a second finger down on an APZC while a down-chain APZC
+// is overscrolled doesn't result in being stuck in overscroll.
+TEST_F(APZScrollHandoffTester, StuckInOverscroll_Bug1073250) {
+ // Enable overscrolling.
+ SCOPED_GFX_PREF(APZOverscrollEnabled, bool, true);
+ SCOPED_GFX_PREF(APZFlingMinVelocityThreshold, float, 0.0f);
+
+ CreateScrollHandoffLayerTree1();
+
+ TestAsyncPanZoomController* child = ApzcOf(layers[1]);
+
+ // Pan, causing the parent APZC to overscroll.
+ Pan(manager, 10, 40, true /* keep finger down */);
+ EXPECT_FALSE(child->IsOverscrolled());
+ EXPECT_TRUE(rootApzc->IsOverscrolled());
+
+ // Put a second finger down.
+ MultiTouchInput secondFingerDown(MultiTouchInput::MULTITOUCH_START, 0, TimeStamp(), 0);
+ // Use the same touch identifier for the first touch (0) as Pan(). (A bit hacky.)
+ secondFingerDown.mTouches.AppendElement(SingleTouchData(0, ScreenIntPoint(10, 40), ScreenSize(0, 0), 0, 0));
+ secondFingerDown.mTouches.AppendElement(SingleTouchData(1, ScreenIntPoint(30, 20), ScreenSize(0, 0), 0, 0));
+ manager->ReceiveInputEvent(secondFingerDown, nullptr, nullptr);
+
+ // Release the fingers.
+ MultiTouchInput fingersUp = secondFingerDown;
+ fingersUp.mType = MultiTouchInput::MULTITOUCH_END;
+ manager->ReceiveInputEvent(fingersUp, nullptr, nullptr);
+
+ // Allow any animations to run their course.
+ child->AdvanceAnimationsUntilEnd();
+ rootApzc->AdvanceAnimationsUntilEnd();
+
+ // Make sure nothing is overscrolled.
+ EXPECT_FALSE(child->IsOverscrolled());
+ EXPECT_FALSE(rootApzc->IsOverscrolled());
+}
+
+// This is almost exactly like StuckInOverscroll_Bug1073250, except the
+// APZC receiving the input events for the first touch block is the child
+// (and thus not the same APZC that overscrolls, which is the parent).
+TEST_F(APZScrollHandoffTester, StuckInOverscroll_Bug1231228) {
+ // Enable overscrolling.
+ SCOPED_GFX_PREF(APZOverscrollEnabled, bool, true);
+ SCOPED_GFX_PREF(APZFlingMinVelocityThreshold, float, 0.0f);
+
+ CreateScrollHandoffLayerTree1();
+
+ TestAsyncPanZoomController* child = ApzcOf(layers[1]);
+
+ // Pan, causing the parent APZC to overscroll.
+ Pan(manager, 60, 90, true /* keep finger down */);
+ EXPECT_FALSE(child->IsOverscrolled());
+ EXPECT_TRUE(rootApzc->IsOverscrolled());
+
+ // Put a second finger down.
+ MultiTouchInput secondFingerDown(MultiTouchInput::MULTITOUCH_START, 0, TimeStamp(), 0);
+ // Use the same touch identifier for the first touch (0) as Pan(). (A bit hacky.)
+ secondFingerDown.mTouches.AppendElement(SingleTouchData(0, ScreenIntPoint(10, 40), ScreenSize(0, 0), 0, 0));
+ secondFingerDown.mTouches.AppendElement(SingleTouchData(1, ScreenIntPoint(30, 20), ScreenSize(0, 0), 0, 0));
+ manager->ReceiveInputEvent(secondFingerDown, nullptr, nullptr);
+
+ // Release the fingers.
+ MultiTouchInput fingersUp = secondFingerDown;
+ fingersUp.mType = MultiTouchInput::MULTITOUCH_END;
+ manager->ReceiveInputEvent(fingersUp, nullptr, nullptr);
+
+ // Allow any animations to run their course.
+ child->AdvanceAnimationsUntilEnd();
+ rootApzc->AdvanceAnimationsUntilEnd();
+
+ // Make sure nothing is overscrolled.
+ EXPECT_FALSE(child->IsOverscrolled());
+ EXPECT_FALSE(rootApzc->IsOverscrolled());
+}
+
+TEST_F(APZScrollHandoffTester, StuckInOverscroll_Bug1240202a) {
+ // Enable overscrolling.
+ SCOPED_GFX_PREF(APZOverscrollEnabled, bool, true);
+
+ CreateScrollHandoffLayerTree1();
+
+ TestAsyncPanZoomController* child = ApzcOf(layers[1]);
+
+ // Pan, causing the parent APZC to overscroll.
+ Pan(manager, 60, 90, true /* keep finger down */);
+ EXPECT_FALSE(child->IsOverscrolled());
+ EXPECT_TRUE(rootApzc->IsOverscrolled());
+
+ // Lift the finger, triggering an overscroll animation
+ // (but don't allow it to run).
+ TouchUp(manager, ScreenIntPoint(10, 90), mcc->Time());
+
+ // Put the finger down again, interrupting the animation
+ // and entering the TOUCHING state.
+ TouchDown(manager, ScreenIntPoint(10, 90), mcc->Time());
+
+ // Lift the finger once again.
+ TouchUp(manager, ScreenIntPoint(10, 90), mcc->Time());
+
+ // Allow any animations to run their course.
+ child->AdvanceAnimationsUntilEnd();
+ rootApzc->AdvanceAnimationsUntilEnd();
+
+ // Make sure nothing is overscrolled.
+ EXPECT_FALSE(child->IsOverscrolled());
+ EXPECT_FALSE(rootApzc->IsOverscrolled());
+}
+
+TEST_F(APZScrollHandoffTester, StuckInOverscroll_Bug1240202b) {
+ // Enable overscrolling.
+ SCOPED_GFX_PREF(APZOverscrollEnabled, bool, true);
+
+ CreateScrollHandoffLayerTree1();
+
+ TestAsyncPanZoomController* child = ApzcOf(layers[1]);
+
+ // Pan, causing the parent APZC to overscroll.
+ Pan(manager, 60, 90, true /* keep finger down */);
+ EXPECT_FALSE(child->IsOverscrolled());
+ EXPECT_TRUE(rootApzc->IsOverscrolled());
+
+ // Lift the finger, triggering an overscroll animation
+ // (but don't allow it to run).
+ TouchUp(manager, ScreenIntPoint(10, 90), mcc->Time());
+
+ // Put the finger down again, interrupting the animation
+ // and entering the TOUCHING state.
+ TouchDown(manager, ScreenIntPoint(10, 90), mcc->Time());
+
+ // Put a second finger down. Since we're in the TOUCHING state,
+ // the "are we panned into overscroll" check will fail and we
+ // will not ignore the second finger, instead entering the
+ // PINCHING state.
+ MultiTouchInput secondFingerDown(MultiTouchInput::MULTITOUCH_START, 0, TimeStamp(), 0);
+ // Use the same touch identifier for the first touch (0) as TouchDown(). (A bit hacky.)
+ secondFingerDown.mTouches.AppendElement(SingleTouchData(0, ScreenIntPoint(10, 90), ScreenSize(0, 0), 0, 0));
+ secondFingerDown.mTouches.AppendElement(SingleTouchData(1, ScreenIntPoint(10, 80), ScreenSize(0, 0), 0, 0));
+ manager->ReceiveInputEvent(secondFingerDown, nullptr, nullptr);
+
+ // Release the fingers.
+ MultiTouchInput fingersUp = secondFingerDown;
+ fingersUp.mType = MultiTouchInput::MULTITOUCH_END;
+ manager->ReceiveInputEvent(fingersUp, nullptr, nullptr);
+
+ // Allow any animations to run their course.
+ child->AdvanceAnimationsUntilEnd();
+ rootApzc->AdvanceAnimationsUntilEnd();
+
+ // Make sure nothing is overscrolled.
+ EXPECT_FALSE(child->IsOverscrolled());
+ EXPECT_FALSE(rootApzc->IsOverscrolled());
+}
+
+// Test that flinging in a direction where one component of the fling goes into
+// overscroll but the other doesn't, results in just the one component being
+// handed off to the parent, while the original APZC continues flinging in the
+// other direction.
+TEST_F(APZScrollHandoffTester, PartialFlingHandoff) {
+ CreateScrollHandoffLayerTree1();
+
+ // Fling up and to the left. The child APZC has room to scroll up, but not
+ // to the left, so the horizontal component of the fling should be handed
+ // off to the parent APZC.
+ Pan(manager, ScreenIntPoint(90, 90), ScreenIntPoint(55, 55));
+
+ RefPtr<TestAsyncPanZoomController> parent = ApzcOf(root);
+ RefPtr<TestAsyncPanZoomController> child = ApzcOf(layers[1]);
+
+ // Advance the child's fling animation once to give the partial handoff
+ // a chance to occur.
+ mcc->AdvanceByMillis(10);
+ child->AdvanceAnimations(mcc->Time());
+
+ // Assert that partial handoff has occurred.
+ child->AssertStateIsFling();
+ parent->AssertStateIsFling();
+}
+
+// Here we test that if two flings are happening simultaneously, overscroll
+// is handed off correctly for each.
+TEST_F(APZScrollHandoffTester, SimultaneousFlings) {
+ SCOPED_GFX_PREF(APZFlingMinVelocityThreshold, float, 0.0f);
+
+ // Set up an initial APZC tree.
+ CreateScrollHandoffLayerTree3();
+
+ RefPtr<TestAsyncPanZoomController> parent1 = ApzcOf(layers[1]);
+ RefPtr<TestAsyncPanZoomController> child1 = ApzcOf(layers[2]);
+ RefPtr<TestAsyncPanZoomController> parent2 = ApzcOf(layers[3]);
+ RefPtr<TestAsyncPanZoomController> child2 = ApzcOf(layers[4]);
+
+ // Pan on the lower child.
+ Pan(child2, 45, 5);
+
+ // Pan on the upper child.
+ Pan(child1, 95, 55);
+
+ // Check that child1 and child2 are in a FLING state.
+ child1->AssertStateIsFling();
+ child2->AssertStateIsFling();
+
+ // Advance the animations on child1 and child2 until their end.
+ child1->AdvanceAnimationsUntilEnd();
+ child2->AdvanceAnimationsUntilEnd();
+
+ // Check that the flings have been handed off to the parents.
+ child1->AssertStateIsReset();
+ parent1->AssertStateIsFling();
+ child2->AssertStateIsReset();
+ parent2->AssertStateIsFling();
+}
+
+TEST_F(APZScrollHandoffTester, Scrollgrab) {
+ // Set up the layer tree
+ CreateScrollgrabLayerTree();
+
+ RefPtr<TestAsyncPanZoomController> childApzc = ApzcOf(layers[1]);
+
+ // Pan on the child, enough to fully scroll the scrollgrab parent (20 px)
+ // and leave some more (another 15 px) for the child.
+ Pan(childApzc, 80, 45);
+
+ // Check that the parent and child have scrolled as much as we expect.
+ EXPECT_EQ(20, rootApzc->GetFrameMetrics().GetScrollOffset().y);
+ EXPECT_EQ(15, childApzc->GetFrameMetrics().GetScrollOffset().y);
+}
+
+TEST_F(APZScrollHandoffTester, ScrollgrabFling) {
+ SCOPED_GFX_PREF(APZFlingMinVelocityThreshold, float, 0.0f);
+ // Set up the layer tree
+ CreateScrollgrabLayerTree();
+
+ RefPtr<TestAsyncPanZoomController> childApzc = ApzcOf(layers[1]);
+
+ // Pan on the child, not enough to fully scroll the scrollgrab parent.
+ Pan(childApzc, 80, 70);
+
+ // Check that it is the scrollgrab parent that's in a fling, not the child.
+ rootApzc->AssertStateIsFling();
+ childApzc->AssertStateIsReset();
+}
+
+TEST_F(APZScrollHandoffTester, ScrollgrabFlingAcceleration1) {
+ SCOPED_GFX_PREF(APZFlingMinVelocityThreshold, float, 0.0f);
+ CreateScrollgrabLayerTree(true /* make parent scrollable */);
+ TestFlingAcceleration();
+}
+
+TEST_F(APZScrollHandoffTester, ScrollgrabFlingAcceleration2) {
+ SCOPED_GFX_PREF(APZFlingMinVelocityThreshold, float, 0.0f);
+ CreateScrollgrabLayerTree(false /* do not make parent scrollable */);
+ TestFlingAcceleration();
+}
+
+TEST_F(APZScrollHandoffTester, ImmediateHandoffDisallowed_Pan) {
+ SCOPED_GFX_PREF(APZAllowImmediateHandoff, bool, false);
+
+ CreateScrollHandoffLayerTree1();
+
+ RefPtr<TestAsyncPanZoomController> parentApzc = ApzcOf(root);
+ RefPtr<TestAsyncPanZoomController> childApzc = ApzcOf(layers[1]);
+
+ // Pan on the child, enough to scroll it to its end and have scroll
+ // left to hand off. Since immediate handoff is disallowed, we expect
+ // the leftover scroll not to be handed off.
+ Pan(childApzc, 60, 5);
+
+ // Verify that the parent has not scrolled.
+ EXPECT_EQ(50, childApzc->GetFrameMetrics().GetScrollOffset().y);
+ EXPECT_EQ(0, parentApzc->GetFrameMetrics().GetScrollOffset().y);
+
+ // Pan again on the child. This time, since the child was scrolled to
+ // its end when the gesture began, we expect the scroll to be handed off.
+ Pan(childApzc, 60, 50);
+
+ // Verify that the parent scrolled.
+ EXPECT_EQ(10, parentApzc->GetFrameMetrics().GetScrollOffset().y);
+}
+
+TEST_F(APZScrollHandoffTester, ImmediateHandoffDisallowed_Fling) {
+ SCOPED_GFX_PREF(APZAllowImmediateHandoff, bool, false);
+ SCOPED_GFX_PREF(APZFlingMinVelocityThreshold, float, 0.0f);
+
+ CreateScrollHandoffLayerTree1();
+
+ RefPtr<TestAsyncPanZoomController> parentApzc = ApzcOf(root);
+ RefPtr<TestAsyncPanZoomController> childApzc = ApzcOf(layers[1]);
+
+ // Pan on the child, enough to get very close to the end, so that the
+ // subsequent fling reaches the end and has leftover velocity to hand off.
+ Pan(childApzc, 60, 12);
+
+ // Allow the fling to run its course.
+ childApzc->AdvanceAnimationsUntilEnd();
+ parentApzc->AdvanceAnimationsUntilEnd();
+
+ // Verify that the parent has not scrolled.
+ // The first comparison needs to be an ASSERT_NEAR because the fling
+ // computations are such that the final scroll position can be within
+ // COORDINATE_EPSILON of the end rather than right at the end.
+ ASSERT_NEAR(50, childApzc->GetFrameMetrics().GetScrollOffset().y, COORDINATE_EPSILON);
+ EXPECT_EQ(0, parentApzc->GetFrameMetrics().GetScrollOffset().y);
+
+ // Pan again on the child. This time, since the child was scrolled to
+ // its end when the gesture began, we expect the scroll to be handed off.
+ Pan(childApzc, 60, 50);
+
+ // Allow the fling to run its course. The fling should also be handed off.
+ childApzc->AdvanceAnimationsUntilEnd();
+ parentApzc->AdvanceAnimationsUntilEnd();
+
+ // Verify that the parent scrolled from the fling.
+ EXPECT_GT(parentApzc->GetFrameMetrics().GetScrollOffset().y, 10);
+}
diff --git a/gfx/layers/apz/test/gtest/TestSnapping.cpp b/gfx/layers/apz/test/gtest/TestSnapping.cpp
new file mode 100644
index 000000000..95c21ca44
--- /dev/null
+++ b/gfx/layers/apz/test/gtest/TestSnapping.cpp
@@ -0,0 +1,64 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set sw=2 ts=8 et tw=80 : */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "APZCTreeManagerTester.h"
+#include "APZTestCommon.h"
+#include "InputUtils.h"
+
+class APZCSnappingTester : public APZCTreeManagerTester
+{
+};
+
+TEST_F(APZCSnappingTester, Bug1265510)
+{
+ const char* layerTreeSyntax = "c(t)";
+ nsIntRegion layerVisibleRegion[] = {
+ nsIntRegion(IntRect(0, 0, 100, 100)),
+ nsIntRegion(IntRect(0, 100, 100, 100))
+ };
+ root = CreateLayerTree(layerTreeSyntax, layerVisibleRegion, nullptr, lm, layers);
+ SetScrollableFrameMetrics(root, FrameMetrics::START_SCROLL_ID, CSSRect(0, 0, 100, 200));
+ SetScrollableFrameMetrics(layers[1], FrameMetrics::START_SCROLL_ID + 1, CSSRect(0, 0, 100, 200));
+ SetScrollHandoff(layers[1], root);
+
+ ScrollSnapInfo snap;
+ snap.mScrollSnapTypeY = NS_STYLE_SCROLL_SNAP_TYPE_MANDATORY;
+ snap.mScrollSnapIntervalY = Some(100 * AppUnitsPerCSSPixel());
+
+ ScrollMetadata metadata = root->GetScrollMetadata(0);
+ metadata.SetSnapInfo(ScrollSnapInfo(snap));
+ root->SetScrollMetadata(metadata);
+
+ UniquePtr<ScopedLayerTreeRegistration> registration = MakeUnique<ScopedLayerTreeRegistration>(manager, 0, root, mcc);
+ manager->UpdateHitTestingTree(0, root, false, 0, 0);
+
+ TestAsyncPanZoomController* outer = ApzcOf(layers[0]);
+ TestAsyncPanZoomController* inner = ApzcOf(layers[1]);
+
+ // Position the mouse near the bottom of the outer frame and scroll by 60px.
+ // (6 lines of 10px each). APZC will actually scroll to y=100 because of the
+ // mandatory snap coordinate there.
+ TimeStamp now = mcc->Time();
+ SmoothWheel(manager, ScreenIntPoint(50, 80), ScreenPoint(0, 6), now);
+ // Advance in 5ms increments until we've scrolled by 70px. At this point, the
+ // closest snap point is y=100, and the inner frame should be under the mouse
+ // cursor.
+ while (outer->GetCurrentAsyncScrollOffset(AsyncPanZoomController::AsyncMode::NORMAL).y < 70) {
+ mcc->AdvanceByMillis(5);
+ outer->AdvanceAnimations(mcc->Time());
+ }
+ // Now do another wheel in a new transaction. This should start scrolling the
+ // inner frame; we verify that it does by checking the inner scroll position.
+ TimeStamp newTransactionTime = now + TimeDuration::FromMilliseconds(gfxPrefs::MouseWheelTransactionTimeoutMs() + 100);
+ SmoothWheel(manager, ScreenIntPoint(50, 80), ScreenPoint(0, 6), newTransactionTime);
+ inner->AdvanceAnimationsUntilEnd();
+ EXPECT_LT(0.0f, inner->GetCurrentAsyncScrollOffset(AsyncPanZoomController::AsyncMode::NORMAL).y);
+
+ // However, the outer frame should also continue to the snap point, otherwise
+ // it is demonstrating incorrect behaviour by violating the mandatory snapping.
+ outer->AdvanceAnimationsUntilEnd();
+ EXPECT_EQ(100.0f, outer->GetCurrentAsyncScrollOffset(AsyncPanZoomController::AsyncMode::NORMAL).y);
+}
diff --git a/gfx/layers/apz/test/gtest/TestTreeManager.cpp b/gfx/layers/apz/test/gtest/TestTreeManager.cpp
new file mode 100644
index 000000000..80a7d0579
--- /dev/null
+++ b/gfx/layers/apz/test/gtest/TestTreeManager.cpp
@@ -0,0 +1,112 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set sw=2 ts=8 et tw=80 : */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "APZCTreeManagerTester.h"
+#include "APZTestCommon.h"
+#include "InputUtils.h"
+
+TEST_F(APZCTreeManagerTester, ScrollablePaintedLayers) {
+ CreateSimpleMultiLayerTree();
+ ScopedLayerTreeRegistration registration(manager, 0, root, mcc);
+
+ // both layers have the same scrollId
+ SetScrollableFrameMetrics(layers[1], FrameMetrics::START_SCROLL_ID);
+ SetScrollableFrameMetrics(layers[2], FrameMetrics::START_SCROLL_ID);
+ manager->UpdateHitTestingTree(0, root, false, 0, 0);
+
+ TestAsyncPanZoomController* nullAPZC = nullptr;
+ // so they should have the same APZC
+ EXPECT_FALSE(layers[0]->HasScrollableFrameMetrics());
+ EXPECT_NE(nullAPZC, ApzcOf(layers[1]));
+ EXPECT_NE(nullAPZC, ApzcOf(layers[2]));
+ EXPECT_EQ(ApzcOf(layers[1]), ApzcOf(layers[2]));
+
+ // Change the scrollId of layers[1], and verify the APZC changes
+ SetScrollableFrameMetrics(layers[1], FrameMetrics::START_SCROLL_ID + 1);
+ manager->UpdateHitTestingTree(0, root, false, 0, 0);
+ EXPECT_NE(ApzcOf(layers[1]), ApzcOf(layers[2]));
+
+ // Change the scrollId of layers[2] to match that of layers[1], ensure we get the same
+ // APZC for both again
+ SetScrollableFrameMetrics(layers[2], FrameMetrics::START_SCROLL_ID + 1);
+ manager->UpdateHitTestingTree(0, root, false, 0, 0);
+ EXPECT_EQ(ApzcOf(layers[1]), ApzcOf(layers[2]));
+}
+
+TEST_F(APZCTreeManagerTester, Bug1068268) {
+ CreatePotentiallyLeakingTree();
+ ScopedLayerTreeRegistration registration(manager, 0, root, mcc);
+
+ manager->UpdateHitTestingTree(0, root, false, 0, 0);
+ RefPtr<HitTestingTreeNode> root = manager->GetRootNode();
+ RefPtr<HitTestingTreeNode> node2 = root->GetFirstChild()->GetFirstChild();
+ RefPtr<HitTestingTreeNode> node5 = root->GetLastChild()->GetLastChild();
+
+ EXPECT_EQ(ApzcOf(layers[2]), node5->GetApzc());
+ EXPECT_EQ(ApzcOf(layers[2]), node2->GetApzc());
+ EXPECT_EQ(ApzcOf(layers[0]), ApzcOf(layers[2])->GetParent());
+ EXPECT_EQ(ApzcOf(layers[2]), ApzcOf(layers[5]));
+
+ EXPECT_EQ(node2->GetFirstChild(), node2->GetLastChild());
+ EXPECT_EQ(ApzcOf(layers[3]), node2->GetLastChild()->GetApzc());
+ EXPECT_EQ(node5->GetFirstChild(), node5->GetLastChild());
+ EXPECT_EQ(ApzcOf(layers[6]), node5->GetLastChild()->GetApzc());
+ EXPECT_EQ(ApzcOf(layers[2]), ApzcOf(layers[3])->GetParent());
+ EXPECT_EQ(ApzcOf(layers[5]), ApzcOf(layers[6])->GetParent());
+}
+
+TEST_F(APZCTreeManagerTester, Bug1194876) {
+ CreateBug1194876Tree();
+ ScopedLayerTreeRegistration registration(manager, 0, root, mcc);
+ manager->UpdateHitTestingTree(0, root, false, 0, 0);
+
+ uint64_t blockId;
+ nsTArray<ScrollableLayerGuid> targets;
+
+ // First touch goes down, APZCTM will hit layers[1] because it is on top of
+ // layers[0], but we tell it the real target APZC is layers[0].
+ MultiTouchInput mti;
+ mti = CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_START, mcc->Time());
+ mti.mTouches.AppendElement(SingleTouchData(0, ParentLayerPoint(25, 50), ScreenSize(0, 0), 0, 0));
+ manager->ReceiveInputEvent(mti, nullptr, &blockId);
+ manager->ContentReceivedInputBlock(blockId, false);
+ targets.AppendElement(ApzcOf(layers[0])->GetGuid());
+ manager->SetTargetAPZC(blockId, targets);
+
+ // Around here, the above touch will get processed by ApzcOf(layers[0])
+
+ // Second touch goes down (first touch remains down), APZCTM will again hit
+ // layers[1]. Again we tell it both touches landed on layers[0], but because
+ // layers[1] is the RCD layer, it will end up being the multitouch target.
+ mti.mTouches.AppendElement(SingleTouchData(1, ParentLayerPoint(75, 50), ScreenSize(0, 0), 0, 0));
+ manager->ReceiveInputEvent(mti, nullptr, &blockId);
+ manager->ContentReceivedInputBlock(blockId, false);
+ targets.AppendElement(ApzcOf(layers[0])->GetGuid());
+ manager->SetTargetAPZC(blockId, targets);
+
+ // Around here, the above multi-touch will get processed by ApzcOf(layers[1]).
+ // We want to ensure that ApzcOf(layers[0]) has had its state cleared, because
+ // otherwise it will do things like dispatch spurious long-tap events.
+
+ EXPECT_CALL(*mcc, HandleTap(TapType::eLongTap, _, _, _, _)).Times(0);
+}
+
+TEST_F(APZCTreeManagerTester, Bug1198900) {
+ // This is just a test that cancels a wheel event to make sure it doesn't
+ // crash.
+ CreateSimpleDTCScrollingLayer();
+ ScopedLayerTreeRegistration registration(manager, 0, root, mcc);
+ manager->UpdateHitTestingTree(0, root, false, 0, 0);
+
+ ScreenPoint origin(100, 50);
+ ScrollWheelInput swi(MillisecondsSinceStartup(mcc->Time()), mcc->Time(), 0,
+ ScrollWheelInput::SCROLLMODE_INSTANT, ScrollWheelInput::SCROLLDELTA_PIXEL,
+ origin, 0, 10, false);
+ uint64_t blockId;
+ manager->ReceiveInputEvent(swi, nullptr, &blockId);
+ manager->ContentReceivedInputBlock(blockId, /* preventDefault= */ true);
+}
+
diff --git a/gfx/layers/apz/test/gtest/moz.build b/gfx/layers/apz/test/gtest/moz.build
new file mode 100644
index 000000000..f3dc8c3dc
--- /dev/null
+++ b/gfx/layers/apz/test/gtest/moz.build
@@ -0,0 +1,33 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+UNIFIED_SOURCES += [
+ 'TestBasic.cpp',
+ 'TestEventRegions.cpp',
+ 'TestGestureDetector.cpp',
+ 'TestHitTesting.cpp',
+ 'TestInputQueue.cpp',
+ 'TestPanning.cpp',
+ 'TestPinching.cpp',
+ 'TestScrollHandoff.cpp',
+ 'TestSnapping.cpp',
+ 'TestTreeManager.cpp',
+]
+
+include('/ipc/chromium/chromium-config.mozbuild')
+
+LOCAL_INCLUDES += [
+ '/gfx/2d',
+ '/gfx/layers',
+ '/gfx/tests/gtest' # for TestLayers.h, which is shared with the gfx gtests
+]
+
+FINAL_LIBRARY = 'xul-gtest'
+
+CXXFLAGS += CONFIG['MOZ_CAIRO_CFLAGS']
+
+if CONFIG['GNU_CXX']:
+ CXXFLAGS += ['-Wno-error=shadow']
diff --git a/gfx/layers/apz/test/mochitest/apz_test_native_event_utils.js b/gfx/layers/apz/test/mochitest/apz_test_native_event_utils.js
new file mode 100644
index 000000000..7f820a936
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/apz_test_native_event_utils.js
@@ -0,0 +1,261 @@
+// Utilities for synthesizing of native events.
+
+function getPlatform() {
+ if (navigator.platform.indexOf("Win") == 0) {
+ return "windows";
+ }
+ if (navigator.platform.indexOf("Mac") == 0) {
+ return "mac";
+ }
+ // Check for Android before Linux
+ if (navigator.appVersion.indexOf("Android") >= 0) {
+ return "android"
+ }
+ if (navigator.platform.indexOf("Linux") == 0) {
+ return "linux";
+ }
+ return "unknown";
+}
+
+function nativeVerticalWheelEventMsg() {
+ switch (getPlatform()) {
+ case "windows": return 0x020A; // WM_MOUSEWHEEL
+ case "mac": return 0; // value is unused, can be anything
+ case "linux": return 4; // value is unused, pass GDK_SCROLL_SMOOTH anyway
+ }
+ throw "Native wheel events not supported on platform " + getPlatform();
+}
+
+function nativeHorizontalWheelEventMsg() {
+ switch (getPlatform()) {
+ case "windows": return 0x020E; // WM_MOUSEHWHEEL
+ case "mac": return 0; // value is unused, can be anything
+ case "linux": return 4; // value is unused, pass GDK_SCROLL_SMOOTH anyway
+ }
+ throw "Native wheel events not supported on platform " + getPlatform();
+}
+
+// Given a pixel scrolling delta, converts it to the platform's native units.
+function nativeScrollUnits(aElement, aDimen) {
+ switch (getPlatform()) {
+ case "linux": {
+ // GTK deltas are treated as line height divided by 3 by gecko.
+ var targetWindow = aElement.ownerDocument.defaultView;
+ var lineHeight = targetWindow.getComputedStyle(aElement)["font-size"];
+ return aDimen / (parseInt(lineHeight) * 3);
+ }
+ }
+ return aDimen;
+}
+
+function nativeMouseDownEventMsg() {
+ switch (getPlatform()) {
+ case "windows": return 2; // MOUSEEVENTF_LEFTDOWN
+ case "mac": return 1; // NSLeftMouseDown
+ case "linux": return 4; // GDK_BUTTON_PRESS
+ case "android": return 5; // ACTION_POINTER_DOWN
+ }
+ throw "Native mouse-down events not supported on platform " + getPlatform();
+}
+
+function nativeMouseMoveEventMsg() {
+ switch (getPlatform()) {
+ case "windows": return 1; // MOUSEEVENTF_MOVE
+ case "mac": return 5; // NSMouseMoved
+ case "linux": return 3; // GDK_MOTION_NOTIFY
+ case "android": return 7; // ACTION_HOVER_MOVE
+ }
+ throw "Native mouse-move events not supported on platform " + getPlatform();
+}
+
+function nativeMouseUpEventMsg() {
+ switch (getPlatform()) {
+ case "windows": return 4; // MOUSEEVENTF_LEFTUP
+ case "mac": return 2; // NSLeftMouseUp
+ case "linux": return 7; // GDK_BUTTON_RELEASE
+ case "android": return 6; // ACTION_POINTER_UP
+ }
+ throw "Native mouse-up events not supported on platform " + getPlatform();
+}
+
+// Convert (aX, aY), in CSS pixels relative to aElement's bounding rect,
+// to device pixels relative to the screen.
+function coordinatesRelativeToScreen(aX, aY, aElement) {
+ var targetWindow = aElement.ownerDocument.defaultView;
+ var scale = targetWindow.devicePixelRatio;
+ var rect = aElement.getBoundingClientRect();
+ return {
+ x: (targetWindow.mozInnerScreenX + rect.left + aX) * scale,
+ y: (targetWindow.mozInnerScreenY + rect.top + aY) * scale
+ };
+}
+
+// Get the bounding box of aElement, and return it in device pixels
+// relative to the screen.
+function rectRelativeToScreen(aElement) {
+ var targetWindow = aElement.ownerDocument.defaultView;
+ var scale = targetWindow.devicePixelRatio;
+ var rect = aElement.getBoundingClientRect();
+ return {
+ x: (targetWindow.mozInnerScreenX + rect.left) * scale,
+ y: (targetWindow.mozInnerScreenY + rect.top) * scale,
+ w: (rect.width * scale),
+ h: (rect.height * scale)
+ };
+}
+
+// Synthesizes a native mousewheel event and returns immediately. This does not
+// guarantee anything; you probably want to use one of the other functions below
+// which actually wait for results.
+// aX and aY are relative to the top-left of |aElement|'s containing window.
+// aDeltaX and aDeltaY are pixel deltas, and aObserver can be left undefined
+// if not needed.
+function synthesizeNativeWheel(aElement, aX, aY, aDeltaX, aDeltaY, aObserver) {
+ var pt = coordinatesRelativeToScreen(aX, aY, aElement);
+ if (aDeltaX && aDeltaY) {
+ throw "Simultaneous wheeling of horizontal and vertical is not supported on all platforms.";
+ }
+ aDeltaX = nativeScrollUnits(aElement, aDeltaX);
+ aDeltaY = nativeScrollUnits(aElement, aDeltaY);
+ var msg = aDeltaX ? nativeHorizontalWheelEventMsg() : nativeVerticalWheelEventMsg();
+ var utils = SpecialPowers.getDOMWindowUtils(aElement.ownerDocument.defaultView);
+ utils.sendNativeMouseScrollEvent(pt.x, pt.y, msg, aDeltaX, aDeltaY, 0, 0, 0, aElement, aObserver);
+ return true;
+}
+
+// Synthesizes a native mousewheel event and invokes the callback once the
+// request has been successfully made to the OS. This does not necessarily
+// guarantee that the OS generates the event we requested. See
+// synthesizeNativeWheel for details on the parameters.
+function synthesizeNativeWheelAndWaitForObserver(aElement, aX, aY, aDeltaX, aDeltaY, aCallback) {
+ var observer = {
+ observe: function(aSubject, aTopic, aData) {
+ if (aCallback && aTopic == "mousescrollevent") {
+ setTimeout(aCallback, 0);
+ }
+ }
+ };
+ return synthesizeNativeWheel(aElement, aX, aY, aDeltaX, aDeltaY, observer);
+}
+
+// Synthesizes a native mousewheel event and invokes the callback once the
+// wheel event is dispatched to |aElement|'s containing window. If the event
+// targets content in a subdocument, |aElement| should be inside the
+// subdocument. See synthesizeNativeWheel for details on the other parameters.
+function synthesizeNativeWheelAndWaitForWheelEvent(aElement, aX, aY, aDeltaX, aDeltaY, aCallback) {
+ var targetWindow = aElement.ownerDocument.defaultView;
+ targetWindow.addEventListener("wheel", function wheelWaiter(e) {
+ targetWindow.removeEventListener("wheel", wheelWaiter);
+ setTimeout(aCallback, 0);
+ });
+ return synthesizeNativeWheel(aElement, aX, aY, aDeltaX, aDeltaY);
+}
+
+// Synthesizes a native mousewheel event and invokes the callback once the
+// first resulting scroll event is dispatched to |aElement|'s containing window.
+// If the event targets content in a subdocument, |aElement| should be inside
+// the subdocument. See synthesizeNativeWheel for details on the other
+// parameters.
+function synthesizeNativeWheelAndWaitForScrollEvent(aElement, aX, aY, aDeltaX, aDeltaY, aCallback) {
+ var targetWindow = aElement.ownerDocument.defaultView;
+ var useCapture = true; // scroll events don't always bubble
+ targetWindow.addEventListener("scroll", function scrollWaiter(e) {
+ targetWindow.removeEventListener("scroll", scrollWaiter, useCapture);
+ setTimeout(aCallback, 0);
+ }, useCapture);
+ return synthesizeNativeWheel(aElement, aX, aY, aDeltaX, aDeltaY);
+}
+
+// Synthesizes a native mouse move event and returns immediately.
+// aX and aY are relative to the top-left of |aElement|'s containing window.
+function synthesizeNativeMouseMove(aElement, aX, aY) {
+ var pt = coordinatesRelativeToScreen(aX, aY, aElement);
+ var utils = SpecialPowers.getDOMWindowUtils(aElement.ownerDocument.defaultView);
+ utils.sendNativeMouseEvent(pt.x, pt.y, nativeMouseMoveEventMsg(), 0, aElement);
+ return true;
+}
+
+// Synthesizes a native mouse move event and invokes the callback once the
+// mouse move event is dispatched to |aElement|'s containing window. If the event
+// targets content in a subdocument, |aElement| should be inside the
+// subdocument. See synthesizeNativeMouseMove for details on the other
+// parameters.
+function synthesizeNativeMouseMoveAndWaitForMoveEvent(aElement, aX, aY, aCallback) {
+ var targetWindow = aElement.ownerDocument.defaultView;
+ targetWindow.addEventListener("mousemove", function mousemoveWaiter(e) {
+ targetWindow.removeEventListener("mousemove", mousemoveWaiter);
+ setTimeout(aCallback, 0);
+ });
+ return synthesizeNativeMouseMove(aElement, aX, aY);
+}
+
+// Synthesizes a native touch event and dispatches it. aX and aY in CSS pixels
+// relative to the top-left of |aElement|'s bounding rect.
+function synthesizeNativeTouch(aElement, aX, aY, aType, aObserver = null, aTouchId = 0) {
+ var pt = coordinatesRelativeToScreen(aX, aY, aElement);
+ var utils = SpecialPowers.getDOMWindowUtils(aElement.ownerDocument.defaultView);
+ utils.sendNativeTouchPoint(aTouchId, aType, pt.x, pt.y, 1, 90, aObserver);
+ return true;
+}
+
+// A handy constant when synthesizing native touch drag events with the pref
+// "apz.touch_start_tolerance" set to 0. In this case, the first touchmove with
+// a nonzero pixel movement is consumed by the APZ to transition from the
+// "touching" state to the "panning" state, so calls to synthesizeNativeTouchDrag
+// should add an extra pixel pixel for this purpose. The TOUCH_SLOP provides
+// a constant that can be used for this purpose. Note that if the touch start
+// tolerance is set to something higher, the touch slop amount used must be
+// correspondingly increased so as to be higher than the tolerance.
+const TOUCH_SLOP = 1;
+function synthesizeNativeTouchDrag(aElement, aX, aY, aDeltaX, aDeltaY, aObserver = null, aTouchId = 0) {
+ synthesizeNativeTouch(aElement, aX, aY, SpecialPowers.DOMWindowUtils.TOUCH_CONTACT, null, aTouchId);
+ var steps = Math.max(Math.abs(aDeltaX), Math.abs(aDeltaY));
+ for (var i = 1; i < steps; i++) {
+ var dx = i * (aDeltaX / steps);
+ var dy = i * (aDeltaY / steps);
+ synthesizeNativeTouch(aElement, aX + dx, aY + dy, SpecialPowers.DOMWindowUtils.TOUCH_CONTACT, null, aTouchId);
+ }
+ synthesizeNativeTouch(aElement, aX + aDeltaX, aY + aDeltaY, SpecialPowers.DOMWindowUtils.TOUCH_CONTACT, null, aTouchId);
+ return synthesizeNativeTouch(aElement, aX + aDeltaX, aY + aDeltaY, SpecialPowers.DOMWindowUtils.TOUCH_REMOVE, aObserver, aTouchId);
+}
+
+function synthesizeNativeTap(aElement, aX, aY, aObserver = null) {
+ var pt = coordinatesRelativeToScreen(aX, aY, aElement);
+ var utils = SpecialPowers.getDOMWindowUtils(aElement.ownerDocument.defaultView);
+ utils.sendNativeTouchTap(pt.x, pt.y, false, aObserver);
+ return true;
+}
+
+function synthesizeNativeMouseEvent(aElement, aX, aY, aType, aObserver = null) {
+ var pt = coordinatesRelativeToScreen(aX, aY, aElement);
+ var utils = SpecialPowers.getDOMWindowUtils(aElement.ownerDocument.defaultView);
+ utils.sendNativeMouseEvent(pt.x, pt.y, aType, 0, aElement, aObserver);
+ return true;
+}
+
+function synthesizeNativeClick(aElement, aX, aY, aObserver = null) {
+ var pt = coordinatesRelativeToScreen(aX, aY, aElement);
+ var utils = SpecialPowers.getDOMWindowUtils(aElement.ownerDocument.defaultView);
+ utils.sendNativeMouseEvent(pt.x, pt.y, nativeMouseDownEventMsg(), 0, aElement, function() {
+ utils.sendNativeMouseEvent(pt.x, pt.y, nativeMouseUpEventMsg(), 0, aElement, aObserver);
+ });
+ return true;
+}
+
+// Move the mouse to (dx, dy) relative to |element|, and scroll the wheel
+// at that location.
+// Moving the mouse is necessary to avoid wheel events from two consecutive
+// moveMouseAndScrollWheelOver() calls on different elements being incorrectly
+// considered as part of the same wheel transaction.
+// We also wait for the mouse move event to be processed before sending the
+// wheel event, otherwise there is a chance they might get reordered, and
+// we have the transaction problem again.
+function moveMouseAndScrollWheelOver(element, dx, dy, testDriver, waitForScroll = true) {
+ return synthesizeNativeMouseMoveAndWaitForMoveEvent(element, dx, dy, function() {
+ if (waitForScroll) {
+ synthesizeNativeWheelAndWaitForScrollEvent(element, dx, dy, 0, -10, testDriver);
+ } else {
+ synthesizeNativeWheelAndWaitForWheelEvent(element, dx, dy, 0, -10, testDriver);
+ }
+ });
+}
diff --git a/gfx/layers/apz/test/mochitest/apz_test_utils.js b/gfx/layers/apz/test/mochitest/apz_test_utils.js
new file mode 100644
index 000000000..c97738434
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/apz_test_utils.js
@@ -0,0 +1,403 @@
+// Utilities for writing APZ tests using the framework added in bug 961289
+
+// ----------------------------------------------------------------------
+// Functions that convert the APZ test data into a more usable form.
+// Every place we have a WebIDL sequence whose elements are dictionaries
+// with two elements, a key, and a value, we convert this into a JS
+// object with a property for each key/value pair. (This is the structure
+// we really want, but we can't express in directly in WebIDL.)
+// ----------------------------------------------------------------------
+
+function convertEntries(entries) {
+ var result = {};
+ for (var i = 0; i < entries.length; ++i) {
+ result[entries[i].key] = entries[i].value;
+ }
+ return result;
+}
+
+function getPropertyAsRect(scrollFrames, scrollId, prop) {
+ SimpleTest.ok(scrollId in scrollFrames,
+ 'expected scroll frame data for scroll id ' + scrollId);
+ var scrollFrameData = scrollFrames[scrollId];
+ SimpleTest.ok('displayport' in scrollFrameData,
+ 'expected a ' + prop + ' for scroll id ' + scrollId);
+ var value = scrollFrameData[prop];
+ var pieces = value.replace(/[()\s]+/g, '').split(',');
+ SimpleTest.is(pieces.length, 4, "expected string of form (x,y,w,h)");
+ return { x: parseInt(pieces[0]),
+ y: parseInt(pieces[1]),
+ w: parseInt(pieces[2]),
+ h: parseInt(pieces[3]) };
+}
+
+function convertScrollFrameData(scrollFrames) {
+ var result = {};
+ for (var i = 0; i < scrollFrames.length; ++i) {
+ result[scrollFrames[i].scrollId] = convertEntries(scrollFrames[i].entries);
+ }
+ return result;
+}
+
+function convertBuckets(buckets) {
+ var result = {};
+ for (var i = 0; i < buckets.length; ++i) {
+ result[buckets[i].sequenceNumber] = convertScrollFrameData(buckets[i].scrollFrames);
+ }
+ return result;
+}
+
+function convertTestData(testData) {
+ var result = {};
+ result.paints = convertBuckets(testData.paints);
+ result.repaintRequests = convertBuckets(testData.repaintRequests);
+ return result;
+}
+
+// Given APZ test data for a single paint on the compositor side,
+// reconstruct the APZC tree structure from the 'parentScrollId'
+// entries that were logged. More specifically, the subset of the
+// APZC tree structure corresponding to the layer subtree for the
+// content process that triggered the paint, is reconstructed (as
+// the APZ test data only contains information abot this subtree).
+function buildApzcTree(paint) {
+ // The APZC tree can potentially have multiple root nodes,
+ // so we invent a node that is the parent of all roots.
+ // This 'root' does not correspond to an APZC.
+ var root = {scrollId: -1, children: []};
+ for (var scrollId in paint) {
+ paint[scrollId].children = [];
+ paint[scrollId].scrollId = scrollId;
+ }
+ for (var scrollId in paint) {
+ var parentNode = null;
+ if ("hasNoParentWithSameLayersId" in paint[scrollId]) {
+ parentNode = root;
+ } else if ("parentScrollId" in paint[scrollId]) {
+ parentNode = paint[paint[scrollId].parentScrollId];
+ }
+ parentNode.children.push(paint[scrollId]);
+ }
+ return root;
+}
+
+// Given an APZC tree produced by buildApzcTree, return the RCD node in
+// the tree, or null if there was none.
+function findRcdNode(apzcTree) {
+ if (!!apzcTree.isRootContent) { // isRootContent will be undefined or "1"
+ return apzcTree;
+ }
+ for (var i = 0; i < apzcTree.children.length; i++) {
+ var rcd = findRcdNode(apzcTree.children[i]);
+ if (rcd != null) {
+ return rcd;
+ }
+ }
+ return null;
+}
+
+// Return whether an element whose id includes |elementId| has been layerized.
+// Assumes |elementId| will be present in the content description for the
+// element, and not in the content descriptions of other elements.
+function isLayerized(elementId) {
+ var contentTestData = SpecialPowers.getDOMWindowUtils(window).getContentAPZTestData();
+ ok(contentTestData.paints.length > 0, "expected at least one paint");
+ var seqno = contentTestData.paints[contentTestData.paints.length - 1].sequenceNumber;
+ contentTestData = convertTestData(contentTestData);
+ var paint = contentTestData.paints[seqno];
+ for (var scrollId in paint) {
+ if ("contentDescription" in paint[scrollId]) {
+ if (paint[scrollId]["contentDescription"].includes(elementId)) {
+ return true;
+ }
+ }
+ }
+ return false;
+}
+
+function flushApzRepaints(aCallback, aWindow = window) {
+ if (!aCallback) {
+ throw "A callback must be provided!";
+ }
+ var repaintDone = function() {
+ SpecialPowers.Services.obs.removeObserver(repaintDone, "apz-repaints-flushed", false);
+ setTimeout(aCallback, 0);
+ };
+ SpecialPowers.Services.obs.addObserver(repaintDone, "apz-repaints-flushed", false);
+ if (SpecialPowers.getDOMWindowUtils(aWindow).flushApzRepaints()) {
+ dump("Flushed APZ repaints, waiting for callback...\n");
+ } else {
+ dump("Flushing APZ repaints was a no-op, triggering callback directly...\n");
+ repaintDone();
+ }
+}
+
+// Flush repaints, APZ pending repaints, and any repaints resulting from that
+// flush. This is particularly useful if the test needs to reach some sort of
+// "idle" state in terms of repaints. Usually just doing waitForAllPaints
+// followed by flushApzRepaints is sufficient to flush all APZ state back to
+// the main thread, but it can leave a paint scheduled which will get triggered
+// at some later time. For tests that specifically test for painting at
+// specific times, this method is the way to go. Even if in doubt, this is the
+// preferred method as the extra step is "safe" and shouldn't interfere with
+// most tests.
+function waitForApzFlushedRepaints(aCallback) {
+ // First flush the main-thread paints and send transactions to the APZ
+ waitForAllPaints(function() {
+ // Then flush the APZ to make sure any repaint requests have been sent
+ // back to the main thread
+ flushApzRepaints(function() {
+ // Then flush the main-thread again to process the repaint requests.
+ // Once this is done, we should be in a stable state with nothing
+ // pending, so we can trigger the callback.
+ waitForAllPaints(aCallback);
+ });
+ });
+}
+
+// This function takes a set of subtests to run one at a time in new top-level
+// windows, and returns a Promise that is resolved once all the subtests are
+// done running.
+//
+// The aSubtests array is an array of objects with the following keys:
+// file: required, the filename of the subtest.
+// prefs: optional, an array of arrays containing key-value prefs to set.
+// dp_suppression: optional, a boolean on whether or not to respect displayport
+// suppression during the test.
+// onload: optional, a function that will be registered as a load event listener
+// for the child window that will hold the subtest. the function will be
+// passed exactly one argument, which will be the child window.
+// An example of an array is:
+// aSubtests = [
+// { 'file': 'test_file_name.html' },
+// { 'file': 'test_file_2.html', 'prefs': [['pref.name', true], ['other.pref', 1000]], 'dp_suppression': false }
+// { 'file': 'file_3.html', 'onload': function(w) { w.subtestDone(); } }
+// ];
+//
+// Each subtest should call the subtestDone() function when it is done, to
+// indicate that the window should be torn down and the next text should run.
+// The subtestDone() function is injected into the subtest's window by this
+// function prior to loading the subtest. For convenience, the |is| and |ok|
+// functions provided by SimpleTest are also mapped into the subtest's window.
+// For other things from the parent, the subtest can use window.opener.<whatever>
+// to access objects.
+function runSubtestsSeriallyInFreshWindows(aSubtests) {
+ return new Promise(function(resolve, reject) {
+ var testIndex = -1;
+ var w = null;
+
+ function advanceSubtestExecution() {
+ var test = aSubtests[testIndex];
+ if (w) {
+ if (typeof test.dp_suppression != 'undefined') {
+ // We modified the suppression when starting the test, so now undo that.
+ SpecialPowers.getDOMWindowUtils(window).respectDisplayPortSuppression(!test.dp_suppression);
+ }
+ if (test.prefs) {
+ // We pushed some prefs for this test, pop them, and re-invoke
+ // advanceSubtestExecution() after that's been processed
+ SpecialPowers.popPrefEnv(function() {
+ w.close();
+ w = null;
+ advanceSubtestExecution();
+ });
+ return;
+ }
+
+ w.close();
+ }
+
+ testIndex++;
+ if (testIndex >= aSubtests.length) {
+ resolve();
+ return;
+ }
+
+ test = aSubtests[testIndex];
+ if (typeof test.dp_suppression != 'undefined') {
+ // Normally during a test, the displayport will get suppressed during page
+ // load, and unsuppressed at a non-deterministic time during the test. The
+ // unsuppression can trigger a repaint which interferes with the test, so
+ // to avoid that we can force the displayport to be unsuppressed for the
+ // entire test which is more deterministic.
+ SpecialPowers.getDOMWindowUtils(window).respectDisplayPortSuppression(test.dp_suppression);
+ }
+
+ function spawnTest(aFile) {
+ w = window.open('', "_blank");
+ w.subtestDone = advanceSubtestExecution;
+ w.SimpleTest = SimpleTest;
+ w.is = function(a, b, msg) { return is(a, b, aFile + " | " + msg); };
+ w.ok = function(cond, name, diag) { return ok(cond, aFile + " | " + name, diag); };
+ if (test.onload) {
+ w.addEventListener('load', function(e) { test.onload(w); }, { once: true });
+ }
+ w.location = location.href.substring(0, location.href.lastIndexOf('/') + 1) + aFile;
+ return w;
+ }
+
+ if (test.prefs) {
+ // Got some prefs for this subtest, push them
+ SpecialPowers.pushPrefEnv({"set": test.prefs}, function() {
+ w = spawnTest(test.file);
+ });
+ } else {
+ w = spawnTest(test.file);
+ }
+ }
+
+ advanceSubtestExecution();
+ });
+}
+
+function pushPrefs(prefs) {
+ return SpecialPowers.pushPrefEnv({'set': prefs});
+}
+
+function waitUntilApzStable() {
+ return new Promise(function(resolve, reject) {
+ SimpleTest.waitForFocus(function() {
+ waitForAllPaints(function() {
+ flushApzRepaints(resolve);
+ });
+ }, window);
+ });
+}
+
+function isApzEnabled() {
+ var enabled = SpecialPowers.getDOMWindowUtils(window).asyncPanZoomEnabled;
+ if (!enabled) {
+ // All tests are required to have at least one assertion. Since APZ is
+ // disabled, and the main test is presumably not going to run, we stick in
+ // a dummy assertion here to keep the test passing.
+ SimpleTest.ok(true, "APZ is not enabled; this test will be skipped");
+ }
+ return enabled;
+}
+
+// Despite what this function name says, this does not *directly* run the
+// provided continuation testFunction. Instead, it returns a function that
+// can be used to run the continuation. The extra level of indirection allows
+// it to be more easily added to a promise chain, like so:
+// waitUntilApzStable().then(runContinuation(myTest));
+//
+// If you want to run the continuation directly, outside of a promise chain,
+// you can invoke the return value of this function, like so:
+// runContinuation(myTest)();
+function runContinuation(testFunction) {
+ // We need to wrap this in an extra function, so that the call site can
+ // be more readable without running the promise too early. In other words,
+ // if we didn't have this extra function, the promise would start running
+ // during construction of the promise chain, concurrently with the first
+ // promise in the chain.
+ return function() {
+ return new Promise(function(resolve, reject) {
+ var testContinuation = null;
+
+ function driveTest() {
+ if (!testContinuation) {
+ testContinuation = testFunction(driveTest);
+ }
+ var ret = testContinuation.next();
+ if (ret.done) {
+ resolve();
+ }
+ }
+
+ driveTest();
+ });
+ };
+}
+
+// Take a snapshot of the given rect, *including compositor transforms* (i.e.
+// includes async scroll transforms applied by APZ). If you don't need the
+// compositor transforms, you can probably get away with using
+// SpecialPowers.snapshotWindowWithOptions or one of the friendlier wrappers.
+// The rect provided is expected to be relative to the screen, for example as
+// returned by rectRelativeToScreen in apz_test_native_event_utils.js.
+// Example usage:
+// var snapshot = getSnapshot(rectRelativeToScreen(myDiv));
+// which will take a snapshot of the 'myDiv' element. Note that if part of the
+// element is obscured by other things on top, the snapshot will include those
+// things. If it is clipped by a scroll container, the snapshot will include
+// that area anyway, so you will probably get parts of the scroll container in
+// the snapshot. If the rect extends outside the browser window then the
+// results are undefined.
+// The snapshot is returned in the form of a data URL.
+function getSnapshot(rect) {
+ function parentProcessSnapshot() {
+ addMessageListener('snapshot', function(rect) {
+ Components.utils.import('resource://gre/modules/Services.jsm');
+ var topWin = Services.wm.getMostRecentWindow('navigator:browser');
+
+ // reposition the rect relative to the top-level browser window
+ rect = JSON.parse(rect);
+ rect.x -= topWin.mozInnerScreenX;
+ rect.y -= topWin.mozInnerScreenY;
+
+ // take the snapshot
+ var canvas = topWin.document.createElementNS("http://www.w3.org/1999/xhtml", "canvas");
+ canvas.width = rect.w;
+ canvas.height = rect.h;
+ var ctx = canvas.getContext("2d");
+ ctx.drawWindow(topWin, rect.x, rect.y, rect.w, rect.h, 'rgb(255,255,255)', ctx.DRAWWINDOW_DRAW_VIEW | ctx.DRAWWINDOW_USE_WIDGET_LAYERS | ctx.DRAWWINDOW_DRAW_CARET);
+ return canvas.toDataURL();
+ });
+ }
+
+ if (typeof getSnapshot.chromeHelper == 'undefined') {
+ // This is the first time getSnapshot is being called; do initialization
+ getSnapshot.chromeHelper = SpecialPowers.loadChromeScript(parentProcessSnapshot);
+ SimpleTest.registerCleanupFunction(function() { getSnapshot.chromeHelper.destroy() });
+ }
+
+ return getSnapshot.chromeHelper.sendSyncMessage('snapshot', JSON.stringify(rect)).toString();
+}
+
+// Takes the document's query string and parses it, assuming the query string
+// is composed of key-value pairs where the value is in JSON format. The object
+// returned contains the various values indexed by their respective keys. In
+// case of duplicate keys, the last value be used.
+// Examples:
+// ?key="value"&key2=false&key3=500
+// produces { "key": "value", "key2": false, "key3": 500 }
+// ?key={"x":0,"y":50}&key2=[1,2,true]
+// produces { "key": { "x": 0, "y": 0 }, "key2": [1, 2, true] }
+function getQueryArgs() {
+ var args = {};
+ if (location.search.length > 0) {
+ var params = location.search.substr(1).split('&');
+ for (var p of params) {
+ var [k, v] = p.split('=');
+ args[k] = JSON.parse(v);
+ }
+ }
+ return args;
+}
+
+// Return a function that returns a promise to create a script element with the
+// given URI and append it to the head of the document in the given window.
+// As with runContinuation(), the extra function wrapper is for convenience
+// at the call site, so that this can be chained with other promises:
+// waitUntilApzStable().then(injectScript('foo'))
+// .then(injectScript('bar'));
+// If you want to do the injection right away, run the function returned by
+// this function:
+// injectScript('foo')();
+function injectScript(aScript, aWindow = window) {
+ return function() {
+ return new Promise(function(resolve, reject) {
+ var e = aWindow.document.createElement('script');
+ e.type = 'text/javascript';
+ e.onload = function() {
+ resolve();
+ };
+ e.onerror = function() {
+ dump('Script [' + aScript + '] errored out\n');
+ reject();
+ };
+ e.src = aScript;
+ aWindow.document.getElementsByTagName('head')[0].appendChild(e);
+ });
+ };
+}
diff --git a/gfx/layers/apz/test/mochitest/chrome.ini b/gfx/layers/apz/test/mochitest/chrome.ini
new file mode 100644
index 000000000..d52da5928
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/chrome.ini
@@ -0,0 +1,9 @@
+[DEFAULT]
+support-files =
+ apz_test_native_event_utils.js
+tags = apz-chrome
+
+[test_smoothness.html]
+# hardware vsync only on win/mac
+# e10s only since APZ is only enabled on e10s
+skip-if = debug || (os != 'mac' && os != 'win') || !e10s
diff --git a/gfx/layers/apz/test/mochitest/helper_basic_pan.html b/gfx/layers/apz/test/mochitest/helper_basic_pan.html
new file mode 100644
index 000000000..c33258da8
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_basic_pan.html
@@ -0,0 +1,38 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width; initial-scale=1.0">
+ <title>Sanity panning test</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript">
+
+function scrollPage() {
+ var transformEnd = function() {
+ SpecialPowers.Services.obs.removeObserver(transformEnd, "APZ:TransformEnd", false);
+ dump("Transform complete; flushing repaints...\n");
+ flushApzRepaints(checkScroll);
+ };
+ SpecialPowers.Services.obs.addObserver(transformEnd, "APZ:TransformEnd", false);
+
+ synthesizeNativeTouchDrag(document.body, 10, 100, 0, -(50 + TOUCH_SLOP));
+ dump("Finished native drag, waiting for transform-end observer...\n");
+}
+
+function checkScroll() {
+ is(window.scrollY, 50, "check that the window scrolled");
+ subtestDone();
+}
+
+waitUntilApzStable().then(scrollPage);
+
+ </script>
+</head>
+<body>
+ <div style="height: 5000px; background-color: lightgreen;">
+ This div makes the page scrollable.
+ </div>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_bug1151663.html b/gfx/layers/apz/test/mochitest/helper_bug1151663.html
new file mode 100644
index 000000000..ef2fde9a9
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_bug1151663.html
@@ -0,0 +1,83 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1151663
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1151663, helper page</title>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript">
+
+ // -------------------------------------------------------------------
+ // Infrastructure to get the test assertions to run at the right time.
+ // -------------------------------------------------------------------
+ var SimpleTest = window.opener.SimpleTest;
+
+ window.onload = function() {
+ window.addEventListener("MozAfterPaint", afterPaint, false);
+ };
+ var utils = SpecialPowers.getDOMWindowUtils(window);
+ function afterPaint(e) {
+ // If there is another paint pending, wait for it.
+ if (utils.isMozAfterPaintPending) {
+ return;
+ }
+
+ // Once there are no more paints pending, remove the
+ // MozAfterPaint listener and run the test logic.
+ window.removeEventListener("MozAfterPaint", afterPaint, false);
+ testBug1151663();
+ }
+
+ // --------------------------------------------------------------------
+ // The actual logic for testing bug 1151663.
+ //
+ // In this test we have a simple page which is scrollable, with a
+ // scrollable <div> which is also scrollable. We test that the
+ // <div> does not get an initial APZC, since primary scrollable
+ // frame is the page's root scroll frame.
+ // --------------------------------------------------------------------
+
+ function testBug1151663() {
+ // Get the content- and compositor-side test data from nsIDOMWindowUtils.
+ var contentTestData = utils.getContentAPZTestData();
+ var compositorTestData = utils.getCompositorAPZTestData();
+
+ // Get the sequence number of the last paint on the compositor side.
+ // We do this before converting the APZ test data because the conversion
+ // loses the order of the paints.
+ SimpleTest.ok(compositorTestData.paints.length > 0,
+ "expected at least one paint in compositor test data");
+ var lastCompositorPaint = compositorTestData.paints[compositorTestData.paints.length - 1];
+ var lastCompositorPaintSeqNo = lastCompositorPaint.sequenceNumber;
+
+ // Convert the test data into a representation that's easier to navigate.
+ contentTestData = convertTestData(contentTestData);
+ compositorTestData = convertTestData(compositorTestData);
+ var paint = compositorTestData.paints[lastCompositorPaintSeqNo];
+
+ // Reconstruct the APZC tree structure in the last paint.
+ var apzcTree = buildApzcTree(paint);
+
+ // The apzc tree for this page should consist of a single root APZC,
+ // which either is the RCD with no child APZCs (e10s/B2G case) or has a
+ // single child APZC which is for the RCD (fennec case).
+ var rcd = findRcdNode(apzcTree);
+ SimpleTest.ok(rcd != null, "found the RCD node");
+ SimpleTest.is(rcd.children.length, 0, "expected no children on the RCD");
+
+ window.opener.finishTest();
+ }
+ </script>
+</head>
+<body style="height: 500px; overflow: scroll">
+ <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1151663">Mozilla Bug 1151663</a>
+ <div style="height: 50px; width: 50px; overflow: scroll">
+ <!-- Put enough content into the subframe to make it have a nonzero scroll range -->
+ <div style="height: 100px; width: 50px"></div>
+ </div>
+ <!-- Put enough content into the page to make it have a nonzero scroll range -->
+ <div style="height: 1000px"></div>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_bug1162771.html b/gfx/layers/apz/test/mochitest/helper_bug1162771.html
new file mode 100644
index 000000000..18e4a2f05
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_bug1162771.html
@@ -0,0 +1,104 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width; initial-scale=1.0">
+ <title>Test for touchend on media elements</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript">
+
+function* test(testDriver) {
+ var v = document.getElementById('video');
+ var a = document.getElementById('audio');
+ var d = document.getElementById('div');
+
+ document.body.ontouchstart = function(e) {
+ if (e.target === v || e.target === a || e.target === d) {
+ e.target.style.display = 'none';
+ ok(true, 'Set display to none on #' + e.target.id);
+ } else {
+ ok(false, 'Got unexpected touchstart on ' + e.target);
+ }
+ waitForAllPaints(testDriver);
+ };
+
+ document.body.ontouchend = function(e) {
+ if (e.target === v || e.target === a || e.target === d) {
+ e.target._gotTouchend = true;
+ ok(true, 'Got touchend event on #' + e.target.id);
+ }
+ testDriver();
+ };
+
+ var utils = SpecialPowers.getDOMWindowUtils(window);
+
+ var pt = coordinatesRelativeToScreen(25, 5, v);
+ yield utils.sendNativeTouchPoint(0, SpecialPowers.DOMWindowUtils.TOUCH_CONTACT, pt.x, pt.y, 1, 90, null);
+ yield utils.sendNativeTouchPoint(0, SpecialPowers.DOMWindowUtils.TOUCH_REMOVE, pt.x, pt.y, 1, 90, null);
+ ok(v._gotTouchend, 'Touchend was received on video element');
+
+ pt = coordinatesRelativeToScreen(25, 5, a);
+ yield utils.sendNativeTouchPoint(0, SpecialPowers.DOMWindowUtils.TOUCH_CONTACT, pt.x, pt.y, 1, 90, null);
+ yield utils.sendNativeTouchPoint(0, SpecialPowers.DOMWindowUtils.TOUCH_REMOVE, pt.x, pt.y, 1, 90, null);
+ ok(a._gotTouchend, 'Touchend was received on audio element');
+
+ pt = coordinatesRelativeToScreen(25, 5, d);
+ yield utils.sendNativeTouchPoint(0, SpecialPowers.DOMWindowUtils.TOUCH_CONTACT, pt.x, pt.y, 1, 90, null);
+ yield utils.sendNativeTouchPoint(0, SpecialPowers.DOMWindowUtils.TOUCH_REMOVE, pt.x, pt.y, 1, 90, null);
+ ok(d._gotTouchend, 'Touchend was received on div element');
+}
+
+waitUntilApzStable()
+.then(runContinuation(test))
+.then(subtestDone);
+
+ </script>
+ <style>
+ * {
+ font-size: 24px;
+ box-sizing: border-box;
+ }
+
+ #video {
+ display:block;
+ position:absolute;
+ top: 100px;
+ left:0;
+ width: 33%;
+ height: 100px;
+ border:solid black 1px;
+ background-color: #8a8;
+ }
+
+ #audio {
+ display:block;
+ position:absolute;
+ top: 100px;
+ left:33%;
+ width: 33%;
+ height: 100px;
+ border:solid black 1px;
+ background-color: #a88;
+ }
+
+ #div {
+ display:block;
+ position:absolute;
+ top: 100px;
+ left: 66%;
+ width: 34%;
+ height: 100px;
+ border:solid black 1px;
+ background-color: #88a;
+ }
+ </style>
+</head>
+<body>
+ <p>Tap on the colored boxes to hide them.</p>
+ <video id="video"></video>
+ <audio id="audio" controls></audio>
+ <div id="div"></div>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_bug1271432.html b/gfx/layers/apz/test/mochitest/helper_bug1271432.html
new file mode 100644
index 000000000..8234b8232
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_bug1271432.html
@@ -0,0 +1,574 @@
+<head>
+ <title>Ensure that the hit region doesn't get unexpectedly expanded</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="/tests/SimpleTest/paint_listener.js"></script>
+<script type="application/javascript">
+function* test(testDriver) {
+ var scroller = document.getElementById('scroller');
+ var scrollerPos = scroller.scrollTop;
+ var dx = 100, dy = 50;
+
+ is(window.scrollY, 0, "Initial page scroll position should be 0");
+ is(scrollerPos, 0, "Initial scroller position should be 0");
+
+ yield moveMouseAndScrollWheelOver(scroller, dx, dy, testDriver);
+
+ is(window.scrollY, 0, "Page scroll position should still be 0");
+ ok(scroller.scrollTop > scrollerPos, "Scroller should have scrolled");
+
+ // wait for it to layerize fully and then try again
+ yield waitForAllPaints(function() {
+ flushApzRepaints(testDriver);
+ });
+ scrollerPos = scroller.scrollTop;
+
+ yield moveMouseAndScrollWheelOver(scroller, dx, dy, testDriver);
+ is(window.scrollY, 0, "Page scroll position should still be 0 after layerization");
+ ok(scroller.scrollTop > scrollerPos, "Scroller should have continued scrolling");
+}
+
+waitUntilApzStable()
+.then(runContinuation(test))
+.then(subtestDone);
+
+</script>
+<style>
+a#with_after_content {
+ background-color: #F16725;
+ opacity: 0.8;
+ display: inline-block;
+ margin-top: 40px;
+ margin-left: 40px;
+}
+a#with_after_content::after {
+ content: " ";
+ position: absolute;
+ width: 0px;
+ height: 0px;
+ bottom: 40px;
+ z-index: -1;
+ right: 40px;
+ background-color: transparent;
+ border-style: solid;
+ border-width: 15px 15px 15px 0;
+ border-color: #d54e0e transparent transparent transparent;
+ box-shadow: none;
+ box-sizing: border-box;
+}
+div#scroller {
+ overflow-y: scroll;
+ width: 50%;
+ height: 50%;
+}
+</style>
+</head>
+<body>
+<a id="with_after_content">Some text</a>
+
+<div id="scroller">
+Scrolling on the very left edge of this div will work.
+Scrolling on the right side of this div (starting with the left edge of the orange box above) should work, but doesn't.<br/>
+0<br>
+1<br>
+2<br>
+3<br>
+4<br>
+5<br>
+6<br>
+7<br>
+8<br>
+9<br>
+10<br>
+11<br>
+12<br>
+13<br>
+14<br>
+15<br>
+16<br>
+17<br>
+18<br>
+19<br>
+20<br>
+21<br>
+22<br>
+23<br>
+24<br>
+25<br>
+26<br>
+27<br>
+28<br>
+29<br>
+30<br>
+31<br>
+32<br>
+33<br>
+34<br>
+35<br>
+36<br>
+37<br>
+38<br>
+39<br>
+40<br>
+41<br>
+42<br>
+43<br>
+44<br>
+45<br>
+46<br>
+47<br>
+48<br>
+49<br>
+50<br>
+51<br>
+52<br>
+53<br>
+54<br>
+55<br>
+56<br>
+57<br>
+58<br>
+59<br>
+60<br>
+61<br>
+62<br>
+63<br>
+64<br>
+65<br>
+66<br>
+67<br>
+68<br>
+69<br>
+70<br>
+71<br>
+72<br>
+73<br>
+74<br>
+75<br>
+76<br>
+77<br>
+78<br>
+79<br>
+80<br>
+81<br>
+82<br>
+83<br>
+84<br>
+85<br>
+86<br>
+87<br>
+88<br>
+89<br>
+90<br>
+91<br>
+92<br>
+93<br>
+94<br>
+95<br>
+96<br>
+97<br>
+98<br>
+99<br>
+100<br>
+101<br>
+102<br>
+103<br>
+104<br>
+105<br>
+106<br>
+107<br>
+108<br>
+109<br>
+110<br>
+111<br>
+112<br>
+113<br>
+114<br>
+115<br>
+116<br>
+117<br>
+118<br>
+119<br>
+120<br>
+121<br>
+122<br>
+123<br>
+124<br>
+125<br>
+126<br>
+127<br>
+128<br>
+129<br>
+130<br>
+131<br>
+132<br>
+133<br>
+134<br>
+135<br>
+136<br>
+137<br>
+138<br>
+139<br>
+140<br>
+141<br>
+142<br>
+143<br>
+144<br>
+145<br>
+146<br>
+147<br>
+148<br>
+149<br>
+150<br>
+151<br>
+152<br>
+153<br>
+154<br>
+155<br>
+156<br>
+157<br>
+158<br>
+159<br>
+160<br>
+161<br>
+162<br>
+163<br>
+164<br>
+165<br>
+166<br>
+167<br>
+168<br>
+169<br>
+170<br>
+171<br>
+172<br>
+173<br>
+174<br>
+175<br>
+176<br>
+177<br>
+178<br>
+179<br>
+180<br>
+181<br>
+182<br>
+183<br>
+184<br>
+185<br>
+186<br>
+187<br>
+188<br>
+189<br>
+190<br>
+191<br>
+192<br>
+193<br>
+194<br>
+195<br>
+196<br>
+197<br>
+198<br>
+199<br>
+200<br>
+201<br>
+202<br>
+203<br>
+204<br>
+205<br>
+206<br>
+207<br>
+208<br>
+209<br>
+210<br>
+211<br>
+212<br>
+213<br>
+214<br>
+215<br>
+216<br>
+217<br>
+218<br>
+219<br>
+220<br>
+221<br>
+222<br>
+223<br>
+224<br>
+225<br>
+226<br>
+227<br>
+228<br>
+229<br>
+230<br>
+231<br>
+232<br>
+233<br>
+234<br>
+235<br>
+236<br>
+237<br>
+238<br>
+239<br>
+240<br>
+241<br>
+242<br>
+243<br>
+244<br>
+245<br>
+246<br>
+247<br>
+248<br>
+249<br>
+250<br>
+251<br>
+252<br>
+253<br>
+254<br>
+255<br>
+256<br>
+257<br>
+258<br>
+259<br>
+260<br>
+261<br>
+262<br>
+263<br>
+264<br>
+265<br>
+266<br>
+267<br>
+268<br>
+269<br>
+270<br>
+271<br>
+272<br>
+273<br>
+274<br>
+275<br>
+276<br>
+277<br>
+278<br>
+279<br>
+280<br>
+281<br>
+282<br>
+283<br>
+284<br>
+285<br>
+286<br>
+287<br>
+288<br>
+289<br>
+290<br>
+291<br>
+292<br>
+293<br>
+294<br>
+295<br>
+296<br>
+297<br>
+298<br>
+299<br>
+300<br>
+301<br>
+302<br>
+303<br>
+304<br>
+305<br>
+306<br>
+307<br>
+308<br>
+309<br>
+310<br>
+311<br>
+312<br>
+313<br>
+314<br>
+315<br>
+316<br>
+317<br>
+318<br>
+319<br>
+320<br>
+321<br>
+322<br>
+323<br>
+324<br>
+325<br>
+326<br>
+327<br>
+328<br>
+329<br>
+330<br>
+331<br>
+332<br>
+333<br>
+334<br>
+335<br>
+336<br>
+337<br>
+338<br>
+339<br>
+340<br>
+341<br>
+342<br>
+343<br>
+344<br>
+345<br>
+346<br>
+347<br>
+348<br>
+349<br>
+350<br>
+351<br>
+352<br>
+353<br>
+354<br>
+355<br>
+356<br>
+357<br>
+358<br>
+359<br>
+360<br>
+361<br>
+362<br>
+363<br>
+364<br>
+365<br>
+366<br>
+367<br>
+368<br>
+369<br>
+370<br>
+371<br>
+372<br>
+373<br>
+374<br>
+375<br>
+376<br>
+377<br>
+378<br>
+379<br>
+380<br>
+381<br>
+382<br>
+383<br>
+384<br>
+385<br>
+386<br>
+387<br>
+388<br>
+389<br>
+390<br>
+391<br>
+392<br>
+393<br>
+394<br>
+395<br>
+396<br>
+397<br>
+398<br>
+399<br>
+400<br>
+401<br>
+402<br>
+403<br>
+404<br>
+405<br>
+406<br>
+407<br>
+408<br>
+409<br>
+410<br>
+411<br>
+412<br>
+413<br>
+414<br>
+415<br>
+416<br>
+417<br>
+418<br>
+419<br>
+420<br>
+421<br>
+422<br>
+423<br>
+424<br>
+425<br>
+426<br>
+427<br>
+428<br>
+429<br>
+430<br>
+431<br>
+432<br>
+433<br>
+434<br>
+435<br>
+436<br>
+437<br>
+438<br>
+439<br>
+440<br>
+441<br>
+442<br>
+443<br>
+444<br>
+445<br>
+446<br>
+447<br>
+448<br>
+449<br>
+450<br>
+451<br>
+452<br>
+453<br>
+454<br>
+455<br>
+456<br>
+457<br>
+458<br>
+459<br>
+460<br>
+461<br>
+462<br>
+463<br>
+464<br>
+465<br>
+466<br>
+467<br>
+468<br>
+469<br>
+470<br>
+471<br>
+472<br>
+473<br>
+474<br>
+475<br>
+476<br>
+477<br>
+478<br>
+479<br>
+480<br>
+481<br>
+482<br>
+483<br>
+484<br>
+485<br>
+486<br>
+487<br>
+488<br>
+489<br>
+490<br>
+491<br>
+492<br>
+493<br>
+494<br>
+495<br>
+496<br>
+497<br>
+498<br>
+499<br>
+</div>
+<div style="height: 1000px">this div makes the page scrollable</div>
+</body>
diff --git a/gfx/layers/apz/test/mochitest/helper_bug1280013.html b/gfx/layers/apz/test/mochitest/helper_bug1280013.html
new file mode 100644
index 000000000..0c602901a
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_bug1280013.html
@@ -0,0 +1,74 @@
+<!DOCTYPE HTML>
+<html style="overflow:hidden">
+<head>
+ <meta charset="utf-8">
+ <!-- The viewport tag will result in APZ being in a "zoomed-in" state, assuming
+ the device width is less than 980px. -->
+ <meta name="viewport" content="width=980; initial-scale=1.0">
+ <title>Test for bug 1280013</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript">
+function* test(testDriver) {
+ SimpleTest.ok(screen.height > 500, "Screen height must be at least 500 pixels for this test to work");
+
+ // This listener will trigger the test to continue once APZ is done with
+ // processing the scroll.
+ SpecialPowers.Services.obs.addObserver(testDriver, "APZ:TransformEnd", false);
+
+ // Scroll down to the iframe. Do it in two drags instead of one in case the
+ // device screen is short
+ yield synthesizeNativeTouchDrag(document.body, 10, 400, 0, -(350 + TOUCH_SLOP));
+ yield synthesizeNativeTouchDrag(document.body, 10, 400, 0, -(350 + TOUCH_SLOP));
+ // Now the top of the visible area should be at y=700 of the top-level page,
+ // so if the screen is >= 500px tall, the entire iframe should be visible, at
+ // least vertically.
+
+ // However, because of the overflow:hidden on the root elements, all this
+ // scrolling is happening in APZ and is not reflected in the main-thread
+ // scroll position (it is stored in the callback transform instead). We check
+ // this by checking the scroll offset.
+ yield flushApzRepaints(testDriver);
+ SimpleTest.is(window.scrollY, 0, "Main-thread scroll position is still at 0");
+
+ // Scroll the iframe by 300px. Note that since the main-thread scroll position
+ // is still 0, the subframe's getBoundingClientRect is going to be off by
+ // 700 pixels, so we compensate for that here.
+ var subframe = document.getElementById('subframe');
+ yield synthesizeNativeTouchDrag(subframe, 10, 200 - 700, 0, -(300 + TOUCH_SLOP));
+
+ // Remove the observer, we don't need it any more.
+ SpecialPowers.Services.obs.removeObserver(testDriver, "APZ:TransformEnd", false);
+
+ // Flush any pending paints
+ yield flushApzRepaints(testDriver);
+
+ // get the displayport for the subframe
+ var utils = SpecialPowers.getDOMWindowUtils(window);
+ var contentPaints = utils.getContentAPZTestData().paints;
+ var lastPaint = convertScrollFrameData(contentPaints[contentPaints.length - 1].scrollFrames);
+ var foundIt = 0;
+ for (var scrollId in lastPaint) {
+ if (('contentDescription' in lastPaint[scrollId]) &&
+ (lastPaint[scrollId]['contentDescription'].includes('tall_html'))) {
+ var dp = getPropertyAsRect(lastPaint, scrollId, 'criticalDisplayport');
+ SimpleTest.ok(dp.y <= 0, 'The critical displayport top should be less than or equal to zero to cover the visible part of the subframe; it is ' + dp.y);
+ SimpleTest.ok(dp.y + dp.h >= subframe.clientHeight, 'The critical displayport bottom should be greater than the clientHeight; it is ' + (dp.y + dp.h));
+ foundIt++;
+ }
+ }
+ SimpleTest.is(foundIt, 1, "Found exactly one critical displayport for the subframe we were interested in.");
+}
+
+waitUntilApzStable()
+.then(runContinuation(test))
+.then(subtestDone);
+
+ </script>
+</head>
+<body style="overflow:hidden">
+ The iframe below is at (0, 800). Scroll it into view, and then scroll the contents. The content should be fully rendered in high-resolution.
+ <iframe id="subframe" style="position:absolute; left: 0px; top: 800px; width: 600px; height: 350px" src="helper_tall.html"></iframe>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_bug1285070.html b/gfx/layers/apz/test/mochitest/helper_bug1285070.html
new file mode 100644
index 000000000..3a4879034
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_bug1285070.html
@@ -0,0 +1,44 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width; initial-scale=1.0">
+ <title>Test pointer events are dispatched once for touch tap</title>
+ <script type="application/javascript" src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript">
+ function test() {
+ let pointerEventsList = ["pointerover", "pointerenter", "pointerdown",
+ "pointerup", "pointerleave", "pointerout"];
+ let pointerEventsCount = {};
+
+ pointerEventsList.forEach((eventName) => {
+ pointerEventsCount[eventName] = 0;
+ document.getElementById('div1').addEventListener(eventName, (event) => {
+ dump("Received event " + event.type + "\n");
+ ++pointerEventsCount[event.type];
+ }, false);
+ });
+
+ document.addEventListener("click", (event) => {
+ is(event.target, document.getElementById('div1'), "Clicked on div (at " + event.clientX + "," + event.clientY + ")");
+ for (var key in pointerEventsCount) {
+ is(pointerEventsCount[key], 1, "Event " + key + " should be generated once");
+ }
+ subtestDone();
+ }, false);
+
+ synthesizeNativeTap(document.getElementById('div1'), 100, 100, () => {
+ dump("Finished synthesizing tap, waiting for div to be clicked...\n");
+ });
+ }
+
+ waitUntilApzStable().then(test);
+
+ </script>
+</head>
+<body>
+ <div id="div1" style="width: 200px; height: 200px; background: black"></div>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_bug1299195.html b/gfx/layers/apz/test/mochitest/helper_bug1299195.html
new file mode 100644
index 000000000..8e746749c
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_bug1299195.html
@@ -0,0 +1,47 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width; initial-scale=1.0">
+ <title>Test pointer events are dispatched once for touch tap</title>
+ <script type="application/javascript" src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript">
+ /** Test for Bug 1299195 **/
+ function runTests() {
+ let target0 = document.getElementById("target0");
+ let mouseup_count = 0;
+ let mousedown_count = 0;
+ let pointerup_count = 0;
+ let pointerdown_count = 0;
+
+ target0.addEventListener("mouseup", () => {
+ ++mouseup_count;
+ if (mouseup_count == 2) {
+ is(mousedown_count, 2, "Double tap with touch should fire 2 mousedown events");
+ is(mouseup_count, 2, "Double tap with touch should fire 2 mouseup events");
+ is(pointerdown_count, 2, "Double tap with touch should fire 2 pointerdown events");
+ is(pointerup_count, 2, "Double tap with touch should fire 2 pointerup events");
+ subtestDone();
+ }
+ });
+ target0.addEventListener("mousedown", () => {
+ ++mousedown_count;
+ });
+ target0.addEventListener("pointerup", () => {
+ ++pointerup_count;
+ });
+ target0.addEventListener("pointerdown", () => {
+ ++pointerdown_count;
+ });
+ synthesizeNativeTap(document.getElementById('target0'), 100, 100);
+ synthesizeNativeTap(document.getElementById('target0'), 100, 100);
+ }
+ waitUntilApzStable().then(runTests);
+ </script>
+</head>
+<body>
+ <div id="target0" style="width: 200px; height: 200px; background: green"></div>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_bug982141.html b/gfx/layers/apz/test/mochitest/helper_bug982141.html
new file mode 100644
index 000000000..5d2f15397
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_bug982141.html
@@ -0,0 +1,149 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=982141
+-->
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="user-scalable=no">
+ <title>Test for Bug 982141, helper page</title>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript">
+
+ // -------------------------------------------------------------------
+ // Infrastructure to get the test assertions to run at the right time.
+ // -------------------------------------------------------------------
+ var SimpleTest = window.opener.SimpleTest;
+
+ window.onload = function() {
+ window.addEventListener("MozAfterPaint", afterPaint, false);
+ };
+ var utils = SpecialPowers.getDOMWindowUtils(window);
+ function afterPaint(e) {
+ // If there is another paint pending, wait for it.
+ if (utils.isMozAfterPaintPending) {
+ return;
+ }
+
+ // Once there are no more paints pending, remove the
+ // MozAfterPaint listener and run the test logic.
+ window.removeEventListener("MozAfterPaint", afterPaint, false);
+ testBug982141();
+ }
+
+ // --------------------------------------------------------------------
+ // The actual logic for testing bug 982141.
+ //
+ // In this test we have a simple page with a scrollable <div> which has
+ // enough content to make it scrollable. We test that this <div> got
+ // a displayport.
+ // --------------------------------------------------------------------
+
+ function testBug982141() {
+ // Get the content- and compositor-side test data from nsIDOMWindowUtils.
+ var contentTestData = utils.getContentAPZTestData();
+ var compositorTestData = utils.getCompositorAPZTestData();
+
+ // Get the sequence number of the last paint on the compositor side.
+ // We do this before converting the APZ test data because the conversion
+ // loses the order of the paints.
+ SimpleTest.ok(compositorTestData.paints.length > 0,
+ "expected at least one paint in compositor test data");
+ var lastCompositorPaint = compositorTestData.paints[compositorTestData.paints.length - 1];
+ var lastCompositorPaintSeqNo = lastCompositorPaint.sequenceNumber;
+
+ // Convert the test data into a representation that's easier to navigate.
+ contentTestData = convertTestData(contentTestData);
+ compositorTestData = convertTestData(compositorTestData);
+
+ // Reconstruct the APZC tree structure in the last paint.
+ var apzcTree = buildApzcTree(compositorTestData.paints[lastCompositorPaintSeqNo]);
+
+ // The apzc tree for this page should consist of a single child APZC on
+ // the RCD node (the child is for scrollable <div>). Note that in e10s/B2G
+ // cases the RCD will be the root of the tree but on Fennec it will not.
+ var rcd = findRcdNode(apzcTree);
+ SimpleTest.ok(rcd != null, "found the RCD node");
+ SimpleTest.is(rcd.children.length, 1, "expected a single child APZC");
+ var childScrollId = rcd.children[0].scrollId;
+
+ // We should have content-side data for the same paint.
+ SimpleTest.ok(lastCompositorPaintSeqNo in contentTestData.paints,
+ "expected a content paint with sequence number" + lastCompositorPaintSeqNo);
+ var correspondingContentPaint = contentTestData.paints[lastCompositorPaintSeqNo];
+
+ var dp = getPropertyAsRect(correspondingContentPaint, childScrollId, 'displayport');
+ var subframe = document.getElementById('subframe');
+ // The clientWidth and clientHeight may be less than 50 if there are scrollbars showing.
+ // In general they will be (50 - <scrollbarwidth>, 50 - <scrollbarheight>).
+ SimpleTest.ok(subframe.clientWidth > 0, "Expected a non-zero clientWidth, got: " + subframe.clientWidth);
+ SimpleTest.ok(subframe.clientHeight > 0, "Expected a non-zero clientHeight, got: " + subframe.clientHeight);
+ SimpleTest.ok(dp.w >= subframe.clientWidth && dp.h >= subframe.clientHeight,
+ "expected a displayport at least as large as the scrollable element, got " + JSON.stringify(dp));
+
+ window.opener.finishTest();
+ }
+ </script>
+</head>
+<body style="overflow: hidden;"><!-- This combined with the user-scalable=no ensures the root frame is not scrollable -->
+ <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=982141">Mozilla Bug 982141</a>
+ <!-- A scrollable subframe, with enough content to make it have a nonzero scroll range -->
+ <div id="subframe" style="height: 50px; width: 50px; overflow: scroll">
+ <div style="width: 100px">
+ Wide content so that the vertical scrollbar for the parent div
+ doesn't eat into the 50px width and reduce the width of the
+ displayport.
+ </div>
+ Line 1<br>
+ Line 2<br>
+ Line 3<br>
+ Line 4<br>
+ Line 5<br>
+ Line 6<br>
+ Line 7<br>
+ Line 8<br>
+ Line 9<br>
+ Line 10<br>
+ Line 11<br>
+ Line 12<br>
+ Line 13<br>
+ Line 14<br>
+ Line 15<br>
+ Line 16<br>
+ Line 17<br>
+ Line 18<br>
+ Line 19<br>
+ Line 20<br>
+ Line 21<br>
+ Line 22<br>
+ Line 23<br>
+ Line 24<br>
+ Line 25<br>
+ Line 26<br>
+ Line 27<br>
+ Line 28<br>
+ Line 29<br>
+ Line 30<br>
+ Line 31<br>
+ Line 32<br>
+ Line 33<br>
+ Line 34<br>
+ Line 35<br>
+ Line 36<br>
+ Line 37<br>
+ Line 38<br>
+ Line 39<br>
+ Line 40<br>
+ Line 41<br>
+ Line 42<br>
+ Line 43<br>
+ Line 44<br>
+ Line 45<br>
+ Line 46<br>
+ Line 40<br>
+ Line 48<br>
+ Line 49<br>
+ Line 50<br>
+ </div>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_click.html b/gfx/layers/apz/test/mochitest/helper_click.html
new file mode 100644
index 000000000..b74f175fe
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_click.html
@@ -0,0 +1,41 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width; initial-scale=1.0">
+ <title>Sanity mouse-clicking test</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript">
+
+function* clickButton(testDriver) {
+ document.addEventListener('click', clicked, false);
+
+ if (getQueryArgs()['dtc']) {
+ // force a dispatch-to-content region on the document
+ document.addEventListener('wheel', function() { /* no-op */ }, { passive: false });
+ yield waitForAllPaints(function() {
+ flushApzRepaints(testDriver);
+ });
+ }
+
+ synthesizeNativeClick(document.getElementById('b'), 5, 5, function() {
+ dump("Finished synthesizing click, waiting for button to be clicked...\n");
+ });
+}
+
+function clicked(e) {
+ is(e.target, document.getElementById('b'), "Clicked on button, yay! (at " + e.clientX + "," + e.clientY + ")");
+ subtestDone();
+}
+
+waitUntilApzStable()
+.then(runContinuation(clickButton));
+
+ </script>
+</head>
+<body>
+ <button id="b" style="width: 10px; height: 10px"></button>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_div_pan.html b/gfx/layers/apz/test/mochitest/helper_div_pan.html
new file mode 100644
index 000000000..f37be8ba6
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_div_pan.html
@@ -0,0 +1,44 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width; initial-scale=1.0">
+ <title>Sanity panning test for scrollable div</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript">
+
+function scrollOuter() {
+ var transformEnd = function() {
+ SpecialPowers.Services.obs.removeObserver(transformEnd, "APZ:TransformEnd", false);
+ dump("Transform complete; flushing repaints...\n");
+ flushApzRepaints(checkScroll);
+ };
+ SpecialPowers.Services.obs.addObserver(transformEnd, "APZ:TransformEnd", false);
+
+ synthesizeNativeTouchDrag(document.getElementById('outer'), 10, 100, 0, -(50 + TOUCH_SLOP));
+ dump("Finished native drag, waiting for transform-end observer...\n");
+}
+
+function checkScroll() {
+ var outerScroll = document.getElementById('outer').scrollTop;
+ is(outerScroll, 50, "check that the div scrolled");
+ subtestDone();
+}
+
+waitUntilApzStable().then(scrollOuter);
+
+ </script>
+</head>
+<body>
+ <div id="outer" style="height: 250px; border: solid 1px black; overflow:scroll">
+ <div style="height: 5000px; background-color: lightblue">
+ This div makes the |outer| div scrollable.
+ </div>
+ </div>
+ <div style="height: 5000px; background-color: lightgreen;">
+ This div makes the top-level page scrollable.
+ </div>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_drag_click.html b/gfx/layers/apz/test/mochitest/helper_drag_click.html
new file mode 100644
index 000000000..cf7117339
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_drag_click.html
@@ -0,0 +1,43 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width; initial-scale=1.0">
+ <title>Sanity mouse-drag click test</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript">
+
+function* test(testDriver) {
+ document.addEventListener('click', clicked, false);
+
+ // Ensure the pointer is inside the window
+ yield synthesizeNativeMouseEvent(document.getElementById('b'), 5, 5, nativeMouseMoveEventMsg(), testDriver);
+ // mouse down, move it around, and release it near where it went down. this
+ // should generate a click at the release point
+ yield synthesizeNativeMouseEvent(document.getElementById('b'), 5, 5, nativeMouseDownEventMsg(), testDriver);
+ yield synthesizeNativeMouseEvent(document.getElementById('b'), 100, 100, nativeMouseMoveEventMsg(), testDriver);
+ yield synthesizeNativeMouseEvent(document.getElementById('b'), 10, 10, nativeMouseMoveEventMsg(), testDriver);
+ yield synthesizeNativeMouseEvent(document.getElementById('b'), 8, 8, nativeMouseUpEventMsg(), testDriver);
+ dump("Finished synthesizing click with a drag in the middle\n");
+}
+
+function clicked(e) {
+ // The mouse down at (5, 5) should not have generated a click, but the up
+ // at (8, 8) should have.
+ is(e.target, document.getElementById('b'), "Clicked on button, yay! (at " + e.clientX + "," + e.clientY + ")");
+ is(e.clientX, 8 + Math.floor(document.getElementById('b').getBoundingClientRect().left), 'x-coord of click event looks sane');
+ is(e.clientY, 8 + Math.floor(document.getElementById('b').getBoundingClientRect().top), 'y-coord of click event looks sane');
+ subtestDone();
+}
+
+waitUntilApzStable()
+.then(runContinuation(test));
+
+ </script>
+</head>
+<body>
+ <button id="b" style="width: 10px; height: 10px"></button>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_drag_scroll.html b/gfx/layers/apz/test/mochitest/helper_drag_scroll.html
new file mode 100644
index 000000000..3c06a5b7e
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_drag_scroll.html
@@ -0,0 +1,603 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width; initial-scale=1.0">
+ <title>Dragging the mouse on a content-implemented scrollbar</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="/tests/SimpleTest/paint_listener.js"></script>
+ <style>
+ body {
+ background: linear-gradient(135deg, red, blue);
+ }
+ #scrollbar {
+ position:fixed;
+ top: 0;
+ right: 10px;
+ height: 100%;
+ width: 150px;
+ background-color: gray;
+ }
+ </style>
+ <script type="text/javascript">
+var bar = null;
+var mouseDown = false;
+
+function moveTo(mouseY, testDriver) {
+ var fraction = (mouseY - bar.getBoundingClientRect().top) / bar.getBoundingClientRect().height;
+ fraction = Math.max(0, fraction);
+ fraction = Math.min(1, fraction);
+ var oldScrollPos = document.scrollingElement.scrollTop;
+ var newScrollPos = fraction * window.scrollMaxY;
+ SimpleTest.ok(newScrollPos > oldScrollPos, "Scroll position strictly increased");
+ // split the scroll in two with a paint in between, just to increase the
+ // complexity of the simulated web content, and to ensure this works as well.
+ document.scrollingElement.scrollTop = (oldScrollPos + newScrollPos) / 2;
+ waitForAllPaints(function() {
+ document.scrollingElement.scrollTop = newScrollPos;
+ testDriver();
+ });
+}
+
+function setupDragging(testDriver) {
+ bar = document.getElementById('scrollbar');
+ mouseDown = false;
+
+ bar.addEventListener('mousedown', function(e) {
+ mouseDown = true;
+ moveTo(e.clientY, testDriver);
+ }, true);
+
+ bar.addEventListener('mousemove', function(e) {
+ if (mouseDown) {
+ dump("Got mousemove clientY " + e.clientY + "\n");
+ moveTo(e.clientY, testDriver);
+ e.stopPropagation();
+ }
+ }, true);
+
+ bar.addEventListener('mouseup', function(e) {
+ mouseDown = false;
+ }, true);
+
+ window.addEventListener('mousemove', function(e) {
+ if (mouseDown) {
+ SimpleTest.ok(false, "The mousemove at " + e.clientY + " was not stopped by the bar listener, and is a glitchy event!");
+ setTimeout(testDriver, 0);
+ }
+ }, false);
+}
+
+function* test(testDriver) {
+ setupDragging(testDriver);
+
+ // Move the mouse to the "scrollbar" (the div upon which dragging changes scroll position)
+ yield synthesizeNativeMouseEvent(bar, 10, 10, nativeMouseMoveEventMsg(), testDriver);
+ // mouse down
+ yield synthesizeNativeMouseEvent(bar, 10, 10, nativeMouseDownEventMsg());
+ // drag vertically by 400px, in 50px increments
+ yield synthesizeNativeMouseEvent(bar, 10, 60, nativeMouseMoveEventMsg());
+ yield synthesizeNativeMouseEvent(bar, 10, 110, nativeMouseMoveEventMsg());
+ yield synthesizeNativeMouseEvent(bar, 10, 160, nativeMouseMoveEventMsg());
+ yield synthesizeNativeMouseEvent(bar, 10, 210, nativeMouseMoveEventMsg());
+ yield synthesizeNativeMouseEvent(bar, 10, 260, nativeMouseMoveEventMsg());
+ yield synthesizeNativeMouseEvent(bar, 10, 310, nativeMouseMoveEventMsg());
+ yield synthesizeNativeMouseEvent(bar, 10, 360, nativeMouseMoveEventMsg());
+ yield synthesizeNativeMouseEvent(bar, 10, 410, nativeMouseMoveEventMsg());
+ // and release
+ yield synthesizeNativeMouseEvent(bar, 10, 410, nativeMouseUpEventMsg(), testDriver);
+}
+
+waitUntilApzStable()
+.then(runContinuation(test))
+.then(subtestDone);
+
+ </script>
+</head>
+<body>
+
+<div id="scrollbar">Drag up and down on this bar. The background/scrollbar shouldn't glitch</div>
+This is a tall page<br/>
+1<br/>
+2<br/>
+3<br/>
+4<br/>
+5<br/>
+6<br/>
+7<br/>
+8<br/>
+9<br/>
+10<br/>
+11<br/>
+12<br/>
+13<br/>
+14<br/>
+15<br/>
+16<br/>
+17<br/>
+18<br/>
+19<br/>
+20<br/>
+21<br/>
+22<br/>
+23<br/>
+24<br/>
+25<br/>
+26<br/>
+27<br/>
+28<br/>
+29<br/>
+30<br/>
+31<br/>
+32<br/>
+33<br/>
+34<br/>
+35<br/>
+36<br/>
+37<br/>
+38<br/>
+39<br/>
+40<br/>
+41<br/>
+42<br/>
+43<br/>
+44<br/>
+45<br/>
+46<br/>
+47<br/>
+48<br/>
+49<br/>
+50<br/>
+51<br/>
+52<br/>
+53<br/>
+54<br/>
+55<br/>
+56<br/>
+57<br/>
+58<br/>
+59<br/>
+60<br/>
+61<br/>
+62<br/>
+63<br/>
+64<br/>
+65<br/>
+66<br/>
+67<br/>
+68<br/>
+69<br/>
+70<br/>
+71<br/>
+72<br/>
+73<br/>
+74<br/>
+75<br/>
+76<br/>
+77<br/>
+78<br/>
+79<br/>
+80<br/>
+81<br/>
+82<br/>
+83<br/>
+84<br/>
+85<br/>
+86<br/>
+87<br/>
+88<br/>
+89<br/>
+90<br/>
+91<br/>
+92<br/>
+93<br/>
+94<br/>
+95<br/>
+96<br/>
+97<br/>
+98<br/>
+99<br/>
+100<br/>
+101<br/>
+102<br/>
+103<br/>
+104<br/>
+105<br/>
+106<br/>
+107<br/>
+108<br/>
+109<br/>
+110<br/>
+111<br/>
+112<br/>
+113<br/>
+114<br/>
+115<br/>
+116<br/>
+117<br/>
+118<br/>
+119<br/>
+120<br/>
+121<br/>
+122<br/>
+123<br/>
+124<br/>
+125<br/>
+126<br/>
+127<br/>
+128<br/>
+129<br/>
+130<br/>
+131<br/>
+132<br/>
+133<br/>
+134<br/>
+135<br/>
+136<br/>
+137<br/>
+138<br/>
+139<br/>
+140<br/>
+141<br/>
+142<br/>
+143<br/>
+144<br/>
+145<br/>
+146<br/>
+147<br/>
+148<br/>
+149<br/>
+150<br/>
+151<br/>
+152<br/>
+153<br/>
+154<br/>
+155<br/>
+156<br/>
+157<br/>
+158<br/>
+159<br/>
+160<br/>
+161<br/>
+162<br/>
+163<br/>
+164<br/>
+165<br/>
+166<br/>
+167<br/>
+168<br/>
+169<br/>
+170<br/>
+171<br/>
+172<br/>
+173<br/>
+174<br/>
+175<br/>
+176<br/>
+177<br/>
+178<br/>
+179<br/>
+180<br/>
+181<br/>
+182<br/>
+183<br/>
+184<br/>
+185<br/>
+186<br/>
+187<br/>
+188<br/>
+189<br/>
+190<br/>
+191<br/>
+192<br/>
+193<br/>
+194<br/>
+195<br/>
+196<br/>
+197<br/>
+198<br/>
+199<br/>
+200<br/>
+201<br/>
+202<br/>
+203<br/>
+204<br/>
+205<br/>
+206<br/>
+207<br/>
+208<br/>
+209<br/>
+210<br/>
+211<br/>
+212<br/>
+213<br/>
+214<br/>
+215<br/>
+216<br/>
+217<br/>
+218<br/>
+219<br/>
+220<br/>
+221<br/>
+222<br/>
+223<br/>
+224<br/>
+225<br/>
+226<br/>
+227<br/>
+228<br/>
+229<br/>
+230<br/>
+231<br/>
+232<br/>
+233<br/>
+234<br/>
+235<br/>
+236<br/>
+237<br/>
+238<br/>
+239<br/>
+240<br/>
+241<br/>
+242<br/>
+243<br/>
+244<br/>
+245<br/>
+246<br/>
+247<br/>
+248<br/>
+249<br/>
+250<br/>
+251<br/>
+252<br/>
+253<br/>
+254<br/>
+255<br/>
+256<br/>
+257<br/>
+258<br/>
+259<br/>
+260<br/>
+261<br/>
+262<br/>
+263<br/>
+264<br/>
+265<br/>
+266<br/>
+267<br/>
+268<br/>
+269<br/>
+270<br/>
+271<br/>
+272<br/>
+273<br/>
+274<br/>
+275<br/>
+276<br/>
+277<br/>
+278<br/>
+279<br/>
+280<br/>
+281<br/>
+282<br/>
+283<br/>
+284<br/>
+285<br/>
+286<br/>
+287<br/>
+288<br/>
+289<br/>
+290<br/>
+291<br/>
+292<br/>
+293<br/>
+294<br/>
+295<br/>
+296<br/>
+297<br/>
+298<br/>
+299<br/>
+300<br/>
+301<br/>
+302<br/>
+303<br/>
+304<br/>
+305<br/>
+306<br/>
+307<br/>
+308<br/>
+309<br/>
+310<br/>
+311<br/>
+312<br/>
+313<br/>
+314<br/>
+315<br/>
+316<br/>
+317<br/>
+318<br/>
+319<br/>
+320<br/>
+321<br/>
+322<br/>
+323<br/>
+324<br/>
+325<br/>
+326<br/>
+327<br/>
+328<br/>
+329<br/>
+330<br/>
+331<br/>
+332<br/>
+333<br/>
+334<br/>
+335<br/>
+336<br/>
+337<br/>
+338<br/>
+339<br/>
+340<br/>
+341<br/>
+342<br/>
+343<br/>
+344<br/>
+345<br/>
+346<br/>
+347<br/>
+348<br/>
+349<br/>
+350<br/>
+351<br/>
+352<br/>
+353<br/>
+354<br/>
+355<br/>
+356<br/>
+357<br/>
+358<br/>
+359<br/>
+360<br/>
+361<br/>
+362<br/>
+363<br/>
+364<br/>
+365<br/>
+366<br/>
+367<br/>
+368<br/>
+369<br/>
+370<br/>
+371<br/>
+372<br/>
+373<br/>
+374<br/>
+375<br/>
+376<br/>
+377<br/>
+378<br/>
+379<br/>
+380<br/>
+381<br/>
+382<br/>
+383<br/>
+384<br/>
+385<br/>
+386<br/>
+387<br/>
+388<br/>
+389<br/>
+390<br/>
+391<br/>
+392<br/>
+393<br/>
+394<br/>
+395<br/>
+396<br/>
+397<br/>
+398<br/>
+399<br/>
+400<br/>
+401<br/>
+402<br/>
+403<br/>
+404<br/>
+405<br/>
+406<br/>
+407<br/>
+408<br/>
+409<br/>
+410<br/>
+411<br/>
+412<br/>
+413<br/>
+414<br/>
+415<br/>
+416<br/>
+417<br/>
+418<br/>
+419<br/>
+420<br/>
+421<br/>
+422<br/>
+423<br/>
+424<br/>
+425<br/>
+426<br/>
+427<br/>
+428<br/>
+429<br/>
+430<br/>
+431<br/>
+432<br/>
+433<br/>
+434<br/>
+435<br/>
+436<br/>
+437<br/>
+438<br/>
+439<br/>
+440<br/>
+441<br/>
+442<br/>
+443<br/>
+444<br/>
+445<br/>
+446<br/>
+447<br/>
+448<br/>
+449<br/>
+450<br/>
+451<br/>
+452<br/>
+453<br/>
+454<br/>
+455<br/>
+456<br/>
+457<br/>
+458<br/>
+459<br/>
+460<br/>
+461<br/>
+462<br/>
+463<br/>
+464<br/>
+465<br/>
+466<br/>
+467<br/>
+468<br/>
+469<br/>
+470<br/>
+471<br/>
+472<br/>
+473<br/>
+474<br/>
+475<br/>
+476<br/>
+477<br/>
+478<br/>
+479<br/>
+480<br/>
+481<br/>
+482<br/>
+483<br/>
+484<br/>
+485<br/>
+486<br/>
+487<br/>
+488<br/>
+489<br/>
+490<br/>
+491<br/>
+492<br/>
+493<br/>
+494<br/>
+495<br/>
+496<br/>
+497<br/>
+498<br/>
+499<br/>
+
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_iframe1.html b/gfx/layers/apz/test/mochitest/helper_iframe1.html
new file mode 100644
index 000000000..047da96bd
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_iframe1.html
@@ -0,0 +1,14 @@
+<!DOCTYPE HTML>
+<!-- The purpose of the 'id' on the HTML element is to get something
+ identifiable to show up in the root scroll frame's content description,
+ so we can check it for layerization. -->
+<html id="outer3">
+ <head>
+ <link rel="stylesheet" type="text/css" href="helper_subframe_style.css"/>
+ </head>
+ <body>
+ <div id="inner3" class="inner-frame">
+ <div class="inner-content"></div>
+ </div>
+ </body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_iframe2.html b/gfx/layers/apz/test/mochitest/helper_iframe2.html
new file mode 100644
index 000000000..fee3883e9
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_iframe2.html
@@ -0,0 +1,14 @@
+<!DOCTYPE HTML>
+<!-- The purpose of the 'id' on the HTML element is to get something
+ identifiable to show up in the root scroll frame's content description,
+ so we can check it for layerization. -->
+<html id="outer4">
+ <head>
+ <link rel="stylesheet" type="text/css" href="helper_subframe_style.css"/>
+ </head>
+ <body>
+ <div id="inner4" class="inner-frame">
+ <div class="inner-content"></div>
+ </div>
+ </body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_iframe_pan.html b/gfx/layers/apz/test/mochitest/helper_iframe_pan.html
new file mode 100644
index 000000000..47213f33a
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_iframe_pan.html
@@ -0,0 +1,41 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width; initial-scale=1.0">
+ <title>Sanity panning test for scrollable div</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript">
+
+function scrollOuter() {
+ var outer = document.getElementById('outer');
+ var transformEnd = function() {
+ SpecialPowers.Services.obs.removeObserver(transformEnd, "APZ:TransformEnd", false);
+ dump("Transform complete; flushing repaints...\n");
+ flushApzRepaints(checkScroll, outer.contentWindow);
+ };
+ SpecialPowers.Services.obs.addObserver(transformEnd, "APZ:TransformEnd", false);
+
+ synthesizeNativeTouchDrag(outer.contentDocument.body, 10, 100, 0, -(50 + TOUCH_SLOP));
+ dump("Finished native drag, waiting for transform-end observer...\n");
+}
+
+function checkScroll() {
+ var outerScroll = document.getElementById('outer').contentWindow.scrollY;
+ is(outerScroll, 50, "check that the iframe scrolled");
+ subtestDone();
+}
+
+waitUntilApzStable().then(scrollOuter);
+
+ </script>
+</head>
+<body>
+ <iframe id="outer" style="height: 250px; border: solid 1px black" src="data:text/html,<body style='height:5000px'>"></iframe>
+ <div style="height: 5000px; background-color: lightgreen;">
+ This div makes the top-level page scrollable.
+ </div>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_long_tap.html b/gfx/layers/apz/test/mochitest/helper_long_tap.html
new file mode 100644
index 000000000..604d03d64
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_long_tap.html
@@ -0,0 +1,81 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width; initial-scale=1.0">
+ <title>Ensure we get a touch-cancel after a contextmenu comes up</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript">
+
+function longPressLink() {
+ synthesizeNativeTouch(document.getElementById('b'), 5, 5, SpecialPowers.DOMWindowUtils.TOUCH_CONTACT, function() {
+ dump("Finished synthesizing touch-start, waiting for events...\n");
+ });
+}
+
+var eventsFired = 0;
+function recordEvent(e) {
+ if (getPlatform() == "windows") {
+ // On Windows we get a mouselongtap event once the long-tap has been detected
+ // by APZ, and that's what we use as the trigger to lift the finger. That then
+ // triggers the contextmenu. This matches the platform convention.
+ switch (eventsFired) {
+ case 0: is(e.type, 'touchstart', 'Got a touchstart'); break;
+ case 1:
+ is(e.type, 'mouselongtap', 'Got a mouselongtap');
+ synthesizeNativeTouch(document.getElementById('b'), 5, 5, SpecialPowers.DOMWindowUtils.TOUCH_REMOVE);
+ break;
+ case 2: is(e.type, 'touchend', 'Got a touchend'); break;
+ case 3: is(e.type, 'contextmenu', 'Got a contextmenu'); e.preventDefault(); break;
+ default: ok(false, 'Got an unexpected event of type ' + e.type); break;
+ }
+ eventsFired++;
+
+ if (eventsFired == 4) {
+ dump("Finished waiting for events, doing an APZ flush to see if any more unexpected events come through...\n");
+ flushApzRepaints(function() {
+ dump("Done APZ flush, ending test...\n");
+ subtestDone();
+ });
+ }
+ } else {
+ // On non-Windows platforms we get a contextmenu event once the long-tap has
+ // been detected. Since we prevent-default that, we don't get a mouselongtap
+ // event at all, and instead get a touchcancel.
+ switch (eventsFired) {
+ case 0: is(e.type, 'touchstart', 'Got a touchstart'); break;
+ case 1: is(e.type, 'contextmenu', 'Got a contextmenu'); e.preventDefault(); break;
+ case 2: is(e.type, 'touchcancel', 'Got a touchcancel'); break;
+ default: ok(false, 'Got an unexpected event of type ' + e.type); break;
+ }
+ eventsFired++;
+
+ if (eventsFired == 3) {
+ synthesizeNativeTouch(document.getElementById('b'), 5, 5, SpecialPowers.DOMWindowUtils.TOUCH_REMOVE, function() {
+ dump("Finished synthesizing touch-end, doing an APZ flush to see if any more unexpected events come through...\n");
+ flushApzRepaints(function() {
+ dump("Done APZ flush, ending test...\n");
+ subtestDone();
+ });
+ });
+ }
+ }
+}
+
+window.addEventListener('touchstart', recordEvent, { passive: true, capture: true });
+window.addEventListener('touchend', recordEvent, { passive: true, capture: true });
+window.addEventListener('touchcancel', recordEvent, true);
+window.addEventListener('contextmenu', recordEvent, true);
+SpecialPowers.addChromeEventListener('mouselongtap', recordEvent, true);
+
+waitUntilApzStable()
+.then(longPressLink);
+
+ </script>
+</head>
+<body>
+ <a id="b" href="#">Link to nowhere</a>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_scroll_inactive_perspective.html b/gfx/layers/apz/test/mochitest/helper_scroll_inactive_perspective.html
new file mode 100644
index 000000000..da866c1ce
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_scroll_inactive_perspective.html
@@ -0,0 +1,46 @@
+<head>
+ <meta name="viewport" content="width=device-width; initial-scale=1.0">
+ <title>Wheel-scrolling over inactive subframe with perspective</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript">
+
+function* test(testDriver) {
+ var subframe = document.getElementById('scroll');
+
+ // scroll over the middle of the subframe, to make sure it scrolls,
+ // not the page
+ var scrollPos = subframe.scrollTop;
+ yield moveMouseAndScrollWheelOver(subframe, 100, 100, testDriver);
+ dump("after scroll, subframe.scrollTop = " + subframe.scrollTop + "\n");
+ ok(subframe.scrollTop > scrollPos, "subframe scrolled after wheeling over it");
+}
+
+waitUntilApzStable()
+.then(runContinuation(test))
+.then(subtestDone);
+
+ </script>
+ <style>
+ #scroll {
+ width: 200px;
+ height: 200px;
+ overflow: scroll;
+ perspective: 400px;
+ }
+ #scrolled {
+ width: 200px;
+ height: 1000px; /* so the subframe has room to scroll */
+ background: linear-gradient(red, blue); /* so you can see it scroll */
+ transform: translateZ(0px); /* so the perspective makes it to the display list */
+ }
+ </style>
+</head>
+<body>
+ <div id="scroll">
+ <div id="scrolled"></div>
+ </div>
+ <div style="height: 5000px;"></div><!-- So the page is scrollable as well -->
+</body>
+</head>
diff --git a/gfx/layers/apz/test/mochitest/helper_scroll_inactive_zindex.html b/gfx/layers/apz/test/mochitest/helper_scroll_inactive_zindex.html
new file mode 100644
index 000000000..763aaf92b
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_scroll_inactive_zindex.html
@@ -0,0 +1,47 @@
+<head>
+ <meta name="viewport" content="width=device-width; initial-scale=1.0">
+ <title>Wheel-scrolling over inactive subframe with z-index</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript">
+
+function* test(testDriver) {
+ var subframe = document.getElementById('scroll');
+
+ // scroll over the middle of the subframe, and make sure that it scrolls,
+ // not the page
+ var scrollPos = subframe.scrollTop;
+ yield moveMouseAndScrollWheelOver(subframe, 100, 100, testDriver);
+ dump("after scroll, subframe.scrollTop = " + subframe.scrollTop + "\n");
+ ok(subframe.scrollTop > scrollPos, "subframe scrolled after wheeling over it");
+}
+
+waitUntilApzStable()
+.then(runContinuation(test))
+.then(subtestDone);
+
+ </script>
+ <style>
+ #scroll {
+ width: 200px;
+ height: 200px;
+ overflow: scroll;
+ }
+ #scrolled {
+ width: 200px;
+ height: 1000px; /* so the subframe has room to scroll */
+ z-index: 2;
+ background: linear-gradient(red, blue); /* so you can see it scroll */
+ transform: translateZ(0px); /* to force active layers */
+ will-change: transform; /* to force active layers */
+ }
+ </style>
+</head>
+<body>
+ <div id="scroll">
+ <div id="scrolled"></div>
+ </div>
+ <div style="height: 5000px;"></div><!-- So the page is scrollable as well -->
+</body>
+</head>
diff --git a/gfx/layers/apz/test/mochitest/helper_scroll_on_position_fixed.html b/gfx/layers/apz/test/mochitest/helper_scroll_on_position_fixed.html
new file mode 100644
index 000000000..b9d187faf
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_scroll_on_position_fixed.html
@@ -0,0 +1,62 @@
+<head>
+ <meta name="viewport" content="width=device-width; initial-scale=1.0">
+ <title>Wheel-scrolling over position:fixed and position:sticky elements, in the top-level document as well as iframes</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript">
+
+function* test(testDriver) {
+ var iframeWin = document.getElementById('iframe').contentWindow;
+
+ // scroll over the middle of the iframe's position:sticky element, check
+ // that it scrolls the iframe
+ var scrollPos = iframeWin.scrollY;
+ yield moveMouseAndScrollWheelOver(iframeWin.document.body, 50, 150, testDriver);
+ ok(iframeWin.scrollY > scrollPos, "iframe scrolled after wheeling over the position:sticky element");
+
+ // same, but using the iframe's position:fixed element
+ scrollPos = iframeWin.scrollY;
+ yield moveMouseAndScrollWheelOver(iframeWin.document.body, 250, 150, testDriver);
+ ok(iframeWin.scrollY > scrollPos, "iframe scrolled after wheeling over the position:fixed element");
+
+ // same, but scrolling the scrollable frame *inside* the position:fixed item
+ var fpos = document.getElementById('fpos_scrollable');
+ scrollPos = fpos.scrollTop;
+ yield moveMouseAndScrollWheelOver(fpos, 50, 150, testDriver);
+ ok(fpos.scrollTop > scrollPos, "scrollable item inside fixed-pos element scrolled");
+ // wait for it to layerize fully and then try again
+ yield waitForAllPaints(function() {
+ flushApzRepaints(testDriver);
+ });
+ scrollPos = fpos.scrollTop;
+ yield moveMouseAndScrollWheelOver(fpos, 50, 150, testDriver);
+ ok(fpos.scrollTop > scrollPos, "scrollable item inside fixed-pos element scrolled after layerization");
+
+ // same, but using the top-level window's position:sticky element
+ scrollPos = window.scrollY;
+ yield moveMouseAndScrollWheelOver(document.body, 50, 150, testDriver);
+ ok(window.scrollY > scrollPos, "top-level document scrolled after wheeling over the position:sticky element");
+
+ // same, but using the top-level window's position:fixed element
+ scrollPos = window.scrollY;
+ yield moveMouseAndScrollWheelOver(document.body, 250, 150, testDriver);
+ ok(window.scrollY > scrollPos, "top-level document scrolled after wheeling over the position:fixed element");
+}
+
+waitUntilApzStable()
+.then(runContinuation(test))
+.then(subtestDone);
+
+ </script>
+</head>
+<body style="height:5000px; margin:0">
+ <div style="position:sticky; width: 100px; height: 300px; top: 0; background-color:red">sticky</div>
+ <div style="position:fixed; width: 100px; height: 300px; top: 0; left: 200px; background-color: green">fixed</div>
+ <iframe id='iframe' width="300" height="400" src="data:text/html,<body style='height:5000px; margin:0'><div style='position:sticky; width:100px; height:300px; top: 0; background-color:red'>sticky</div><div style='position:fixed; right:0; top: 0; width:100px; height:300px; background-color:green'>fixed</div>"></iframe>
+
+ <div id="fpos_scrollable" style="position:fixed; width: 100px; height: 300px; top: 0; left: 400px; background-color: red; overflow:scroll">
+ <div style="background-color: blue; height: 1000px; margin: 3px">scrollable content inside a fixed-pos item</div>
+ </div>
+</body>
+</head>
diff --git a/gfx/layers/apz/test/mochitest/helper_scrollto_tap.html b/gfx/layers/apz/test/mochitest/helper_scrollto_tap.html
new file mode 100644
index 000000000..fc444f2b7
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_scrollto_tap.html
@@ -0,0 +1,61 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width; initial-scale=1.0">
+ <title>Sanity touch-tapping test</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript">
+
+function startTest() {
+ if (window.scrollY == 0) {
+ // the scrollframe is not yet marked as APZ-scrollable. Mark it so and
+ // start over.
+ window.scrollTo(0, 1);
+ waitForApzFlushedRepaints(startTest);
+ return;
+ }
+
+ // This is a scroll by 20px that should use paint-skipping if possible.
+ // If paint-skipping is enabled, this should not trigger a paint, but go
+ // directly to the compositor using an empty transaction. We check for this
+ // by ensuring the document element did not get painted.
+ var utils = window.opener.SpecialPowers.getDOMWindowUtils(window);
+ var elem = document.documentElement;
+ var skipping = location.search == '?true';
+ utils.checkAndClearPaintedState(elem);
+ window.scrollTo(0, 20);
+ waitForAllPaints(function() {
+ if (skipping) {
+ is(utils.checkAndClearPaintedState(elem), false, "Document element didn't get painted");
+ }
+ // After that's done, we click on the button to make sure the
+ // skipped-paint codepath still has working APZ event transformations.
+ clickButton();
+ });
+}
+
+function clickButton() {
+ document.addEventListener('click', clicked, false);
+
+ synthesizeNativeTap(document.getElementById('b'), 5, 5, function() {
+ dump("Finished synthesizing tap, waiting for button to be clicked...\n");
+ });
+}
+
+function clicked(e) {
+ is(e.target, document.getElementById('b'), "Clicked on button, yay! (at " + e.clientX + "," + e.clientY + ")");
+ subtestDone();
+}
+
+waitUntilApzStable().then(startTest);
+
+ </script>
+</head>
+<body style="height: 5000px">
+ <div style="height: 50px">spacer</div>
+ <button id="b" style="width: 10px; height: 10px"></button>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_subframe_style.css b/gfx/layers/apz/test/mochitest/helper_subframe_style.css
new file mode 100644
index 000000000..5af964080
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_subframe_style.css
@@ -0,0 +1,15 @@
+body {
+ height: 500px;
+}
+
+.inner-frame {
+ margin-top: 50px; /* this should be at least 30px */
+ height: 200%;
+ width: 75%;
+ overflow: scroll;
+}
+.inner-content {
+ height: 200%;
+ width: 200%;
+ background: repeating-linear-gradient(#EEE, #EEE 100px, #DDD 100px, #DDD 200px);
+}
diff --git a/gfx/layers/apz/test/mochitest/helper_tall.html b/gfx/layers/apz/test/mochitest/helper_tall.html
new file mode 100644
index 000000000..7fde795fd
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_tall.html
@@ -0,0 +1,504 @@
+<html id="tall_html">
+<body>
+This is a tall page<br/>
+1<br/>
+2<br/>
+3<br/>
+4<br/>
+5<br/>
+6<br/>
+7<br/>
+8<br/>
+9<br/>
+10<br/>
+11<br/>
+12<br/>
+13<br/>
+14<br/>
+15<br/>
+16<br/>
+17<br/>
+18<br/>
+19<br/>
+20<br/>
+21<br/>
+22<br/>
+23<br/>
+24<br/>
+25<br/>
+26<br/>
+27<br/>
+28<br/>
+29<br/>
+30<br/>
+31<br/>
+32<br/>
+33<br/>
+34<br/>
+35<br/>
+36<br/>
+37<br/>
+38<br/>
+39<br/>
+40<br/>
+41<br/>
+42<br/>
+43<br/>
+44<br/>
+45<br/>
+46<br/>
+47<br/>
+48<br/>
+49<br/>
+50<br/>
+51<br/>
+52<br/>
+53<br/>
+54<br/>
+55<br/>
+56<br/>
+57<br/>
+58<br/>
+59<br/>
+60<br/>
+61<br/>
+62<br/>
+63<br/>
+64<br/>
+65<br/>
+66<br/>
+67<br/>
+68<br/>
+69<br/>
+70<br/>
+71<br/>
+72<br/>
+73<br/>
+74<br/>
+75<br/>
+76<br/>
+77<br/>
+78<br/>
+79<br/>
+80<br/>
+81<br/>
+82<br/>
+83<br/>
+84<br/>
+85<br/>
+86<br/>
+87<br/>
+88<br/>
+89<br/>
+90<br/>
+91<br/>
+92<br/>
+93<br/>
+94<br/>
+95<br/>
+96<br/>
+97<br/>
+98<br/>
+99<br/>
+100<br/>
+101<br/>
+102<br/>
+103<br/>
+104<br/>
+105<br/>
+106<br/>
+107<br/>
+108<br/>
+109<br/>
+110<br/>
+111<br/>
+112<br/>
+113<br/>
+114<br/>
+115<br/>
+116<br/>
+117<br/>
+118<br/>
+119<br/>
+120<br/>
+121<br/>
+122<br/>
+123<br/>
+124<br/>
+125<br/>
+126<br/>
+127<br/>
+128<br/>
+129<br/>
+130<br/>
+131<br/>
+132<br/>
+133<br/>
+134<br/>
+135<br/>
+136<br/>
+137<br/>
+138<br/>
+139<br/>
+140<br/>
+141<br/>
+142<br/>
+143<br/>
+144<br/>
+145<br/>
+146<br/>
+147<br/>
+148<br/>
+149<br/>
+150<br/>
+151<br/>
+152<br/>
+153<br/>
+154<br/>
+155<br/>
+156<br/>
+157<br/>
+158<br/>
+159<br/>
+160<br/>
+161<br/>
+162<br/>
+163<br/>
+164<br/>
+165<br/>
+166<br/>
+167<br/>
+168<br/>
+169<br/>
+170<br/>
+171<br/>
+172<br/>
+173<br/>
+174<br/>
+175<br/>
+176<br/>
+177<br/>
+178<br/>
+179<br/>
+180<br/>
+181<br/>
+182<br/>
+183<br/>
+184<br/>
+185<br/>
+186<br/>
+187<br/>
+188<br/>
+189<br/>
+190<br/>
+191<br/>
+192<br/>
+193<br/>
+194<br/>
+195<br/>
+196<br/>
+197<br/>
+198<br/>
+199<br/>
+200<br/>
+201<br/>
+202<br/>
+203<br/>
+204<br/>
+205<br/>
+206<br/>
+207<br/>
+208<br/>
+209<br/>
+210<br/>
+211<br/>
+212<br/>
+213<br/>
+214<br/>
+215<br/>
+216<br/>
+217<br/>
+218<br/>
+219<br/>
+220<br/>
+221<br/>
+222<br/>
+223<br/>
+224<br/>
+225<br/>
+226<br/>
+227<br/>
+228<br/>
+229<br/>
+230<br/>
+231<br/>
+232<br/>
+233<br/>
+234<br/>
+235<br/>
+236<br/>
+237<br/>
+238<br/>
+239<br/>
+240<br/>
+241<br/>
+242<br/>
+243<br/>
+244<br/>
+245<br/>
+246<br/>
+247<br/>
+248<br/>
+249<br/>
+250<br/>
+251<br/>
+252<br/>
+253<br/>
+254<br/>
+255<br/>
+256<br/>
+257<br/>
+258<br/>
+259<br/>
+260<br/>
+261<br/>
+262<br/>
+263<br/>
+264<br/>
+265<br/>
+266<br/>
+267<br/>
+268<br/>
+269<br/>
+270<br/>
+271<br/>
+272<br/>
+273<br/>
+274<br/>
+275<br/>
+276<br/>
+277<br/>
+278<br/>
+279<br/>
+280<br/>
+281<br/>
+282<br/>
+283<br/>
+284<br/>
+285<br/>
+286<br/>
+287<br/>
+288<br/>
+289<br/>
+290<br/>
+291<br/>
+292<br/>
+293<br/>
+294<br/>
+295<br/>
+296<br/>
+297<br/>
+298<br/>
+299<br/>
+300<br/>
+301<br/>
+302<br/>
+303<br/>
+304<br/>
+305<br/>
+306<br/>
+307<br/>
+308<br/>
+309<br/>
+310<br/>
+311<br/>
+312<br/>
+313<br/>
+314<br/>
+315<br/>
+316<br/>
+317<br/>
+318<br/>
+319<br/>
+320<br/>
+321<br/>
+322<br/>
+323<br/>
+324<br/>
+325<br/>
+326<br/>
+327<br/>
+328<br/>
+329<br/>
+330<br/>
+331<br/>
+332<br/>
+333<br/>
+334<br/>
+335<br/>
+336<br/>
+337<br/>
+338<br/>
+339<br/>
+340<br/>
+341<br/>
+342<br/>
+343<br/>
+344<br/>
+345<br/>
+346<br/>
+347<br/>
+348<br/>
+349<br/>
+350<br/>
+351<br/>
+352<br/>
+353<br/>
+354<br/>
+355<br/>
+356<br/>
+357<br/>
+358<br/>
+359<br/>
+360<br/>
+361<br/>
+362<br/>
+363<br/>
+364<br/>
+365<br/>
+366<br/>
+367<br/>
+368<br/>
+369<br/>
+370<br/>
+371<br/>
+372<br/>
+373<br/>
+374<br/>
+375<br/>
+376<br/>
+377<br/>
+378<br/>
+379<br/>
+380<br/>
+381<br/>
+382<br/>
+383<br/>
+384<br/>
+385<br/>
+386<br/>
+387<br/>
+388<br/>
+389<br/>
+390<br/>
+391<br/>
+392<br/>
+393<br/>
+394<br/>
+395<br/>
+396<br/>
+397<br/>
+398<br/>
+399<br/>
+400<br/>
+401<br/>
+402<br/>
+403<br/>
+404<br/>
+405<br/>
+406<br/>
+407<br/>
+408<br/>
+409<br/>
+410<br/>
+411<br/>
+412<br/>
+413<br/>
+414<br/>
+415<br/>
+416<br/>
+417<br/>
+418<br/>
+419<br/>
+420<br/>
+421<br/>
+422<br/>
+423<br/>
+424<br/>
+425<br/>
+426<br/>
+427<br/>
+428<br/>
+429<br/>
+430<br/>
+431<br/>
+432<br/>
+433<br/>
+434<br/>
+435<br/>
+436<br/>
+437<br/>
+438<br/>
+439<br/>
+440<br/>
+441<br/>
+442<br/>
+443<br/>
+444<br/>
+445<br/>
+446<br/>
+447<br/>
+448<br/>
+449<br/>
+450<br/>
+451<br/>
+452<br/>
+453<br/>
+454<br/>
+455<br/>
+456<br/>
+457<br/>
+458<br/>
+459<br/>
+460<br/>
+461<br/>
+462<br/>
+463<br/>
+464<br/>
+465<br/>
+466<br/>
+467<br/>
+468<br/>
+469<br/>
+470<br/>
+471<br/>
+472<br/>
+473<br/>
+474<br/>
+475<br/>
+476<br/>
+477<br/>
+478<br/>
+479<br/>
+480<br/>
+481<br/>
+482<br/>
+483<br/>
+484<br/>
+485<br/>
+486<br/>
+487<br/>
+488<br/>
+489<br/>
+490<br/>
+491<br/>
+492<br/>
+493<br/>
+494<br/>
+495<br/>
+496<br/>
+497<br/>
+498<br/>
+499<br/>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_tap.html b/gfx/layers/apz/test/mochitest/helper_tap.html
new file mode 100644
index 000000000..6fde9387d
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_tap.html
@@ -0,0 +1,32 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width; initial-scale=1.0">
+ <title>Sanity touch-tapping test</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript">
+
+function clickButton() {
+ document.addEventListener('click', clicked, false);
+
+ synthesizeNativeTap(document.getElementById('b'), 5, 5, function() {
+ dump("Finished synthesizing tap, waiting for button to be clicked...\n");
+ });
+}
+
+function clicked(e) {
+ is(e.target, document.getElementById('b'), "Clicked on button, yay! (at " + e.clientX + "," + e.clientY + ")");
+ subtestDone();
+}
+
+waitUntilApzStable().then(clickButton);
+
+ </script>
+</head>
+<body>
+ <button id="b" style="width: 10px; height: 10px"></button>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_tap_fullzoom.html b/gfx/layers/apz/test/mochitest/helper_tap_fullzoom.html
new file mode 100644
index 000000000..494363b9c
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_tap_fullzoom.html
@@ -0,0 +1,33 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width; initial-scale=1.0">
+ <title>Sanity touch-tapping test with fullzoom</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript">
+
+function clickButton() {
+ document.addEventListener('click', clicked, false);
+
+ synthesizeNativeTap(document.getElementById('b'), 5, 5, function() {
+ dump("Finished synthesizing tap, waiting for button to be clicked...\n");
+ });
+}
+
+function clicked(e) {
+ is(e.target, document.getElementById('b'), "Clicked on button, yay! (at " + e.clientX + "," + e.clientY + ")");
+ subtestDone();
+}
+
+SpecialPowers.setFullZoom(window, 2.0);
+waitUntilApzStable().then(clickButton);
+
+ </script>
+</head>
+<body>
+ <button id="b" style="width: 10px; height: 10px; position: relative; top: 100px"></button>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_tap_passive.html b/gfx/layers/apz/test/mochitest/helper_tap_passive.html
new file mode 100644
index 000000000..dc3d85ed2
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_tap_passive.html
@@ -0,0 +1,64 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width; initial-scale=1.0">
+ <title>Ensure APZ doesn't wait for passive listeners</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript">
+
+var touchdownTime;
+
+function longPressLink() {
+ synthesizeNativeTouch(document.getElementById('b'), 5, 5, SpecialPowers.DOMWindowUtils.TOUCH_CONTACT, function() {
+ dump("Finished synthesizing touch-start, waiting for events...\n");
+ });
+}
+
+var touchstartReceived = false;
+function recordEvent(e) {
+ if (!touchstartReceived) {
+ touchstartReceived = true;
+ is(e.type, 'touchstart', 'Got a touchstart');
+ e.preventDefault(); // should be a no-op because it's a passive listener
+ return;
+ }
+
+ // If APZ decides to wait for the content response on a particular input block,
+ // it needs to wait until both the touchstart and touchmove event are handled
+ // by the main thread. In this case there is no touchmove at all, so APZ would
+ // end up waiting indefinitely and time out the test. The fact that we get this
+ // contextmenu event (mouselongtap on Windows) at all means that APZ decided
+ // not to wait for the content response, which is the desired behaviour, since
+ // the touchstart listener was registered as a passive listener.
+ if (getPlatform() == "windows") {
+ is(e.type, 'mouselongtap', 'Got a mouselongtap');
+ } else {
+ is(e.type, 'contextmenu', 'Got a contextmenu');
+ }
+ e.preventDefault();
+
+ synthesizeNativeTouch(document.getElementById('b'), 5, 5, SpecialPowers.DOMWindowUtils.TOUCH_REMOVE, function() {
+ dump("Finished synthesizing touch-end to clear state; finishing test...\n");
+ subtestDone();
+ });
+}
+
+window.addEventListener('touchstart', recordEvent, { passive: true, capture: true });
+if (getPlatform() == "windows") {
+ SpecialPowers.addChromeEventListener('mouselongtap', recordEvent, true);
+} else {
+ window.addEventListener('contextmenu', recordEvent, true);
+}
+
+waitUntilApzStable()
+.then(longPressLink);
+
+ </script>
+</head>
+<body>
+ <a id="b" href="#">Link to nowhere</a>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_touch_action.html b/gfx/layers/apz/test/mochitest/helper_touch_action.html
new file mode 100644
index 000000000..4495dc76e
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_touch_action.html
@@ -0,0 +1,115 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width; initial-scale=1.0">
+ <title>Sanity touch-action test</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript">
+
+function checkScroll(x, y, desc) {
+ is(window.scrollX, x, desc + " - x axis");
+ is(window.scrollY, y, desc + " - y axis");
+}
+
+function* test(testDriver) {
+ var target = document.getElementById('target');
+
+ document.body.addEventListener('touchend', testDriver, { passive: true });
+
+ // drag the page up to scroll down by 50px
+ yield ok(synthesizeNativeTouchDrag(target, 10, 100, 0, -(50 + TOUCH_SLOP)),
+ "Synthesized native vertical drag (1), waiting for touch-end event...");
+ yield flushApzRepaints(testDriver);
+ checkScroll(0, 50, "After first vertical drag, with pan-y" );
+
+ // switch style to pan-x
+ document.body.style.touchAction = 'pan-x';
+ ok(true, "Waiting for pan-x to propagate...");
+ yield waitForAllPaintsFlushed(function() {
+ flushApzRepaints(testDriver);
+ });
+
+ // drag the page up to scroll down by 50px, but it won't happen because pan-x
+ yield ok(synthesizeNativeTouchDrag(target, 10, 100, 0, -(50 + TOUCH_SLOP)),
+ "Synthesized native vertical drag (2), waiting for touch-end event...");
+ yield flushApzRepaints(testDriver);
+ checkScroll(0, 50, "After second vertical drag, with pan-x");
+
+ // drag the page left to scroll right by 50px
+ yield ok(synthesizeNativeTouchDrag(target, 100, 10, -(50 + TOUCH_SLOP), 0),
+ "Synthesized horizontal drag (1), waiting for touch-end event...");
+ yield flushApzRepaints(testDriver);
+ checkScroll(50, 50, "After first horizontal drag, with pan-x");
+
+ // drag the page diagonally right/down to scroll up/left by 40px each axis;
+ // only the x-axis will actually scroll because pan-x
+ yield ok(synthesizeNativeTouchDrag(target, 10, 10, (40 + TOUCH_SLOP), (40 + TOUCH_SLOP)),
+ "Synthesized diagonal drag (1), waiting for touch-end event...");
+ yield flushApzRepaints(testDriver);
+ checkScroll(10, 50, "After first diagonal drag, with pan-x");
+
+ // switch style back to pan-y
+ document.body.style.touchAction = 'pan-y';
+ ok(true, "Waiting for pan-y to propagate...");
+ yield waitForAllPaintsFlushed(function() {
+ flushApzRepaints(testDriver);
+ });
+
+ // drag the page diagonally right/down to scroll up/left by 40px each axis;
+ // only the y-axis will actually scroll because pan-y
+ yield ok(synthesizeNativeTouchDrag(target, 10, 10, (40 + TOUCH_SLOP), (40 + TOUCH_SLOP)),
+ "Synthesized diagonal drag (2), waiting for touch-end event...");
+ yield flushApzRepaints(testDriver);
+ checkScroll(10, 10, "After second diagonal drag, with pan-y");
+
+ // switch style to none
+ document.body.style.touchAction = 'none';
+ ok(true, "Waiting for none to propagate...");
+ yield waitForAllPaintsFlushed(function() {
+ flushApzRepaints(testDriver);
+ });
+
+ // drag the page diagonally up/left to scroll down/right by 40px each axis;
+ // neither will scroll because of touch-action
+ yield ok(synthesizeNativeTouchDrag(target, 100, 100, -(40 + TOUCH_SLOP), -(40 + TOUCH_SLOP)),
+ "Synthesized diagonal drag (3), waiting for touch-end event...");
+ yield flushApzRepaints(testDriver);
+ checkScroll(10, 10, "After third diagonal drag, with none");
+
+ document.body.style.touchAction = 'manipulation';
+ ok(true, "Waiting for manipulation to propagate...");
+ yield waitForAllPaintsFlushed(function() {
+ flushApzRepaints(testDriver);
+ });
+
+ // drag the page diagonally up/left to scroll down/right by 40px each axis;
+ // both will scroll because of touch-action
+ yield ok(synthesizeNativeTouchDrag(target, 100, 100, -(40 + TOUCH_SLOP), -(40 + TOUCH_SLOP)),
+ "Synthesized diagonal drag (4), waiting for touch-end event...");
+ yield flushApzRepaints(testDriver);
+ checkScroll(50, 50, "After fourth diagonal drag, with manipulation");
+}
+
+waitUntilApzStable()
+.then(runContinuation(test))
+.then(subtestDone);
+
+ </script>
+</head>
+<body style="touch-action: pan-y">
+ <div style="width: 5000px; height: 5000px; background-color: lightgreen;">
+ This div makes the page scrollable on both axes.<br>
+ This is the second line of text.<br>
+ This is the third line of text.<br>
+ This is the fourth line of text.
+ </div>
+ <!-- This fixed-position div remains in the same place relative to the browser chrome, so we
+ can use it as a targeting device for synthetic touch events. The body will move around
+ as we scroll, so we'd have to be constantly adjusting the synthetic drag coordinates
+ if we used that as the target element. -->
+ <div style="position:fixed; left: 10px; top: 10px; width: 1px; height: 1px" id="target"></div>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_touch_action_complex.html b/gfx/layers/apz/test/mochitest/helper_touch_action_complex.html
new file mode 100644
index 000000000..11d6e66e1
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_touch_action_complex.html
@@ -0,0 +1,143 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width; initial-scale=1.0">
+ <title>Complex touch-action test</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript">
+
+function checkScroll(target, x, y, desc) {
+ is(target.scrollLeft, x, desc + " - x axis");
+ is(target.scrollTop, y, desc + " - y axis");
+}
+
+function resetConfiguration(config, testDriver) {
+ // Cycle through all the configuration_X elements, setting them to display:none
+ // except for when X == config, in which case set it to display:block
+ var i = 0;
+ while (true) {
+ i++;
+ var element = document.getElementById('configuration_' + i);
+ if (element == null) {
+ if (i <= config) {
+ ok(false, "The configuration requested was not encountered!");
+ }
+ break;
+ }
+
+ if (i == config) {
+ element.style.display = 'block';
+ } else {
+ element.style.display = 'none';
+ }
+ }
+
+ // Also reset the scroll position on the scrollframe
+ var s = document.getElementById('scrollframe');
+ s.scrollLeft = 0;
+ s.scrollTop = 0;
+
+ return waitForAllPaints(function() {
+ flushApzRepaints(testDriver);
+ });
+}
+
+function* test(testDriver) {
+ var scrollframe = document.getElementById('scrollframe');
+
+ document.body.addEventListener('touchend', testDriver, { passive: true });
+
+ // Helper function for the tests below.
+ // Touch-pan configuration |configuration| towards scroll offset (dx, dy) with
+ // the pan touching down at (x, y). Check that the final scroll offset is
+ // (ex, ey). |desc| is some description string.
+ function* scrollAndCheck(configuration, x, y, dx, dy, ex, ey, desc) {
+ // Start with a clean slate
+ yield resetConfiguration(configuration, testDriver);
+ // Figure out the panning deltas
+ if (dx != 0) {
+ dx = -(dx + TOUCH_SLOP);
+ }
+ if (dy != 0) {
+ dy = -(dy + TOUCH_SLOP);
+ }
+ // Do the pan
+ yield ok(synthesizeNativeTouchDrag(scrollframe, x, y, dx, dy),
+ "Synthesized drag of (" + dx + ", " + dy + ") on configuration " + configuration);
+ yield waitForAllPaints(function() {
+ flushApzRepaints(testDriver);
+ });
+ // Check for expected scroll position
+ checkScroll(scrollframe, ex, ey, 'configuration ' + configuration + ' ' + desc);
+ }
+
+ // Test configuration_1, which contains two sibling elements that are
+ // overlapping. The touch-action from the second sibling (which is on top)
+ // should be used for the overlapping area.
+ yield* scrollAndCheck(1, 25, 75, 20, 0, 20, 0, "first element horizontal scroll");
+ yield* scrollAndCheck(1, 25, 75, 0, 50, 0, 0, "first element vertical scroll");
+ yield* scrollAndCheck(1, 75, 75, 50, 0, 0, 0, "overlap horizontal scroll");
+ yield* scrollAndCheck(1, 75, 75, 0, 50, 0, 50, "overlap vertical scroll");
+ yield* scrollAndCheck(1, 125, 75, 20, 0, 0, 0, "second element horizontal scroll");
+ yield* scrollAndCheck(1, 125, 75, 0, 50, 0, 50, "second element vertical scroll");
+
+ // Test configuration_2, which contains two overlapping elements with a
+ // parent/child relationship. The parent has pan-x and the child has pan-y,
+ // which means that panning on the parent should work horizontally only, and
+ // on the child no panning should occur at all.
+ yield* scrollAndCheck(2, 125, 125, 50, 50, 0, 0, "child scroll");
+ yield* scrollAndCheck(2, 75, 75, 50, 50, 0, 0, "overlap scroll");
+ yield* scrollAndCheck(2, 25, 75, 0, 50, 0, 0, "parent vertical scroll");
+ yield* scrollAndCheck(2, 75, 25, 50, 0, 50, 0, "parent horizontal scroll");
+
+ // Test configuration_3, which is the same as configuration_2, except the child
+ // has a rotation transform applied. This forces the event regions on the two
+ // elements to be built separately and then get merged.
+ yield* scrollAndCheck(3, 125, 125, 50, 50, 0, 0, "child scroll");
+ yield* scrollAndCheck(3, 75, 75, 50, 50, 0, 0, "overlap scroll");
+ yield* scrollAndCheck(3, 25, 75, 0, 50, 0, 0, "parent vertical scroll");
+ yield* scrollAndCheck(3, 75, 25, 50, 0, 50, 0, "parent horizontal scroll");
+
+ // Test configuration_4 has two elements, one above the other, not overlapping,
+ // and the second element is a child of the first. The parent has pan-x, the
+ // child has pan-y, but that means panning horizontally on the parent should
+ // work and panning in any direction on the child should not do anything.
+ yield* scrollAndCheck(4, 75, 75, 50, 50, 50, 0, "parent diagonal scroll");
+ yield* scrollAndCheck(4, 75, 150, 50, 50, 0, 0, "child diagonal scroll");
+}
+
+waitUntilApzStable()
+.then(runContinuation(test))
+.then(subtestDone);
+
+ </script>
+</head>
+<body>
+ <div id="scrollframe" style="width: 300px; height: 300px; overflow:scroll">
+ <div id="scrolled_content" style="width: 1000px; height: 1000px; background-color: green">
+ </div>
+ <div id="configuration_1" style="display:none; position: relative; top: -1000px">
+ <div style="touch-action: pan-x; width: 100px; height: 100px; background-color: blue"></div>
+ <div style="touch-action: pan-y; width: 100px; height: 100px; position: relative; top: -100px; left: 50px; background-color: yellow"></div>
+ </div>
+ <div id="configuration_2" style="display:none; position: relative; top: -1000px">
+ <div style="touch-action: pan-x; width: 100px; height: 100px; background-color: blue">
+ <div style="touch-action: pan-y; width: 100px; height: 100px; position: relative; top: 50px; left: 50px; background-color: yellow"></div>
+ </div>
+ </div>
+ <div id="configuration_3" style="display:none; position: relative; top: -1000px">
+ <div style="touch-action: pan-x; width: 100px; height: 100px; background-color: blue">
+ <div style="touch-action: pan-y; width: 100px; height: 100px; position: relative; top: 50px; left: 50px; background-color: yellow; transform: rotate(90deg)"></div>
+ </div>
+ </div>
+ <div id="configuration_4" style="display:none; position: relative; top: -1000px">
+ <div style="touch-action: pan-x; width: 100px; height: 100px; background-color: blue">
+ <div style="touch-action: pan-y; width: 100px; height: 100px; position: relative; top: 125px; background-color: yellow"></div>
+ </div>
+ </div>
+ </div>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/helper_touch_action_regions.html b/gfx/layers/apz/test/mochitest/helper_touch_action_regions.html
new file mode 100644
index 000000000..cbd4cd61d
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_touch_action_regions.html
@@ -0,0 +1,246 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width; initial-scale=1.0">
+ <title>Test to ensure APZ doesn't always wait for touch-action</title>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript">
+
+function failure(e) {
+ ok(false, "This event listener should not have triggered: " + e.type);
+}
+
+function success(e) {
+ success.triggered = true;
+}
+
+// This helper function provides a way for the child process to synchronously
+// check how many touch events the chrome process main-thread has processed. This
+// function can be called with three values: 'start', 'report', and 'end'.
+// The 'start' invocation sets up the listeners, and should be invoked before
+// the touch events of interest are generated. This should only be called once.
+// This returns true on success, and false on failure.
+// The 'report' invocation can be invoked multiple times, and returns an object
+// (in JSON string format) containing the counters.
+// The 'end' invocation tears down the listeners, and should be invoked once
+// at the end to clean up. Returns true on success, false on failure.
+function chromeTouchEventCounter(operation) {
+ function chromeProcessCounter() {
+ addMessageListener('start', function() {
+ Components.utils.import('resource://gre/modules/Services.jsm');
+ var topWin = Services.wm.getMostRecentWindow('navigator:browser');
+ if (typeof topWin.eventCounts != 'undefined') {
+ dump('Found pre-existing eventCounts object on the top window!\n');
+ return false;
+ }
+ topWin.eventCounts = { 'touchstart': 0, 'touchmove': 0, 'touchend': 0 };
+ topWin.counter = function(e) {
+ topWin.eventCounts[e.type]++;
+ }
+
+ topWin.addEventListener('touchstart', topWin.counter, { passive: true });
+ topWin.addEventListener('touchmove', topWin.counter, { passive: true });
+ topWin.addEventListener('touchend', topWin.counter, { passive: true });
+
+ return true;
+ });
+
+ addMessageListener('report', function() {
+ Components.utils.import('resource://gre/modules/Services.jsm');
+ var topWin = Services.wm.getMostRecentWindow('navigator:browser');
+ return JSON.stringify(topWin.eventCounts);
+ });
+
+ addMessageListener('end', function() {
+ Components.utils.import('resource://gre/modules/Services.jsm');
+ var topWin = Services.wm.getMostRecentWindow('navigator:browser');
+ if (typeof topWin.eventCounts == 'undefined') {
+ dump('The eventCounts object was not found on the top window!\n');
+ return false;
+ }
+ topWin.removeEventListener('touchstart', topWin.counter);
+ topWin.removeEventListener('touchmove', topWin.counter);
+ topWin.removeEventListener('touchend', topWin.counter);
+ delete topWin.counter;
+ delete topWin.eventCounts;
+ return true;
+ });
+ }
+
+ if (typeof chromeTouchEventCounter.chromeHelper == 'undefined') {
+ // This is the first time getSnapshot is being called; do initialization
+ chromeTouchEventCounter.chromeHelper = SpecialPowers.loadChromeScript(chromeProcessCounter);
+ SimpleTest.registerCleanupFunction(function() { chromeTouchEventCounter.chromeHelper.destroy() });
+ }
+
+ return chromeTouchEventCounter.chromeHelper.sendSyncMessage(operation, "");
+}
+
+// Simple wrapper that waits until the chrome process has seen |count| instances
+// of the |eventType| event. Returns true on success, and false if 10 seconds
+// go by without the condition being satisfied.
+function waitFor(eventType, count) {
+ var start = Date.now();
+ while (JSON.parse(chromeTouchEventCounter('report'))[eventType] != count) {
+ if (Date.now() - start > 10000) {
+ // It's taking too long, let's abort
+ return false;
+ }
+ }
+ return true;
+}
+
+function* test(testDriver) {
+ // The main part of this test should run completely before the child process'
+ // main-thread deals with the touch event, so check to make sure that happens.
+ document.body.addEventListener('touchstart', failure, { passive: true });
+
+ // What we want here is to synthesize all of the touch events (from this code in
+ // the child process), and have the chrome process generate and process them,
+ // but not allow the events to be dispatched back into the child process until
+ // later. This allows us to ensure that the APZ in the chrome process is not
+ // waiting for the child process to send notifications upon processing the
+ // events. If it were doing so, the APZ would block and this test would fail.
+
+ // In order to actually implement this, we call the synthesize functions with
+ // a async callback in between. The synthesize functions just queue up a
+ // runnable on the child process main thread and return immediately, so with
+ // the async callbacks, the child process main thread queue looks like
+ // this after we're done setting it up:
+ // synthesizeTouchStart
+ // callback testDriver
+ // synthesizeTouchMove
+ // callback testDriver
+ // ...
+ // synthesizeTouchEnd
+ // callback testDriver
+ //
+ // If, after setting up this queue, we yield once, the first synthesization and
+ // callback will run - this will send a synthesization message to the chrome
+ // process, and return control back to us right away. When the chrome process
+ // processes with the synthesized event, it will dispatch the DOM touch event
+ // back to the child process over IPC, which will go into the end of the child
+ // process main thread queue, like so:
+ // synthesizeTouchStart (done)
+ // invoke testDriver (done)
+ // synthesizeTouchMove
+ // invoke testDriver
+ // ...
+ // synthesizeTouchEnd
+ // invoke testDriver
+ // handle DOM touchstart <-- touchstart goes at end of queue
+ //
+ // As we continue yielding one at a time, the synthesizations run, and the
+ // touch events get added to the end of the queue. As we yield, we take
+ // snapshots in the chrome process, to make sure that the APZ has started
+ // scrolling even though we know we haven't yet processed the DOM touch events
+ // in the child process yet.
+ //
+ // Note that the "async callback" we use here is SpecialPowers.executeSoon,
+ // because nothing else does exactly what we want:
+ // - setTimeout(..., 0) does not maintain ordering, because it respects the
+ // time delta provided (i.e. the callback can jump the queue to meet its
+ // deadline).
+ // - SpecialPowers.spinEventLoop and SpecialPowers.executeAfterFlushingMessageQueue
+ // are not e10s friendly, and can get arbitrarily delayed due to IPC
+ // round-trip time.
+ // - SimpleTest.executeSoon has a codepath that delegates to setTimeout, so
+ // is less reliable if it ever decides to switch to that codepath.
+
+ // The other problem we need to deal with is the asynchronicity in the chrome
+ // process. That is, we might request a snapshot before the chrome process has
+ // actually synthesized the event and processed it. To guard against this, we
+ // register a thing in the chrome process that counts the touch events that
+ // have been dispatched, and poll that thing synchronously in order to make
+ // sure we only snapshot after the event in question has been processed.
+ // That's what the chromeTouchEventCounter business is all about. The sync
+ // polling looks bad but in practice only ends up needing to poll once or
+ // twice before the condition is satisfied, and as an extra precaution we add
+ // a time guard so it fails after 10s of polling.
+
+ // So, here we go...
+
+ // Set up the chrome process touch listener
+ ok(chromeTouchEventCounter('start'), "Chrome touch counter registered");
+
+ // Set up the child process events and callbacks
+ var scroller = document.getElementById('scroller');
+ synthesizeNativeTouch(scroller, 10, 110, SpecialPowers.DOMWindowUtils.TOUCH_CONTACT, null, 0);
+ SpecialPowers.executeSoon(testDriver);
+ for (var i = 1; i < 10; i++) {
+ synthesizeNativeTouch(scroller, 10, 110 - (i * 10), SpecialPowers.DOMWindowUtils.TOUCH_CONTACT, null, 0);
+ SpecialPowers.executeSoon(testDriver);
+ }
+ synthesizeNativeTouch(scroller, 10, 10, SpecialPowers.DOMWindowUtils.TOUCH_REMOVE, null, 0);
+ SpecialPowers.executeSoon(testDriver);
+ ok(true, "Finished setting up event queue");
+
+ // Get our baseline snapshot
+ var rect = rectRelativeToScreen(scroller);
+ var lastSnapshot = getSnapshot(rect);
+ ok(true, "Got baseline snapshot");
+
+ yield; // this will tell the chrome process to synthesize the touchstart event
+ // and then we wait to make sure it got processed:
+ ok(waitFor('touchstart', 1), "Touchstart processed in chrome process");
+
+ // Loop through the touchmove events
+ for (var i = 1; i < 10; i++) {
+ yield;
+ ok(waitFor('touchmove', i), "Touchmove processed in chrome process");
+
+ var snapshot = getSnapshot(rect);
+ if (i == 1) {
+ // The first touchmove is consumed to get us into the panning state, so
+ // no actual panning occurs
+ ok(lastSnapshot == snapshot, "Snapshot 1 was the same as baseline");
+ } else {
+ ok(lastSnapshot != snapshot, "Snapshot " + i + " was different from the previous one");
+ }
+ lastSnapshot = snapshot;
+ }
+
+ // Wait for the touchend as well, just for good form
+ yield;
+ ok(waitFor('touchend', 1), "Touchend processed in chrome process");
+
+ // Clean up the chrome process hooks
+ chromeTouchEventCounter('end');
+
+ // Now we are going to release our grip on the child process main thread,
+ // so that all the DOM events that were queued up can be processed. We
+ // register a touchstart listener to make sure this happens.
+ document.body.removeEventListener('touchstart', failure);
+ document.body.addEventListener('touchstart', success, { passive: true });
+ yield flushApzRepaints(testDriver);
+ ok(success.triggered, "The touchstart event handler was triggered after snapshotting completed");
+ document.body.removeEventListener('touchstart', success);
+}
+
+if (SpecialPowers.isMainProcess()) {
+ // This is probably android, where everything is single-process. The
+ // test structure depends on e10s, so the test won't run properly on
+ // this platform. Skip it
+ ok(true, "Skipping test because it is designed to run from the content process");
+ subtestDone();
+} else {
+ waitUntilApzStable()
+ .then(runContinuation(test))
+ .then(subtestDone);
+}
+
+ </script>
+</head>
+<body>
+ <div id="scroller" style="width: 400px; height: 400px; overflow: scroll; touch-action: pan-y">
+ <div style="width: 200px; height: 200px; background-color: lightgreen;">
+ This is a colored div that will move on the screen as the scroller scrolls.
+ </div>
+ <div style="width: 1000px; height: 1000px; background-color: lightblue">
+ This is a large div to make the scroller scrollable.
+ </div>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/mochitest.ini b/gfx/layers/apz/test/mochitest/mochitest.ini
new file mode 100644
index 000000000..09e62428c
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/mochitest.ini
@@ -0,0 +1,67 @@
+[DEFAULT]
+ support-files =
+ apz_test_native_event_utils.js
+ apz_test_utils.js
+ helper_basic_pan.html
+ helper_bug982141.html
+ helper_bug1151663.html
+ helper_bug1162771.html
+ helper_bug1271432.html
+ helper_bug1280013.html
+ helper_bug1285070.html
+ helper_bug1299195.html
+ helper_click.html
+ helper_div_pan.html
+ helper_drag_click.html
+ helper_drag_scroll.html
+ helper_iframe_pan.html
+ helper_iframe1.html
+ helper_iframe2.html
+ helper_long_tap.html
+ helper_scroll_inactive_perspective.html
+ helper_scroll_inactive_zindex.html
+ helper_scroll_on_position_fixed.html
+ helper_scrollto_tap.html
+ helper_subframe_style.css
+ helper_tall.html
+ helper_tap.html
+ helper_tap_fullzoom.html
+ helper_tap_passive.html
+ helper_touch_action.html
+ helper_touch_action_regions.html
+ helper_touch_action_complex.html
+ tags = apz
+[test_bug982141.html]
+[test_bug1151663.html]
+[test_bug1151667.html]
+ skip-if = (os == 'android') # wheel events not supported on mobile
+[test_bug1253683.html]
+ skip-if = (os == 'android') # wheel events not supported on mobile
+[test_bug1277814.html]
+ skip-if = (os == 'android') # wheel events not supported on mobile
+[test_bug1304689.html]
+[test_bug1304689-2.html]
+[test_frame_reconstruction.html]
+[test_group_mouseevents.html]
+ skip-if = (toolkit == 'android') # mouse events not supported on mobile
+[test_group_pointerevents.html]
+[test_group_touchevents.html]
+[test_group_wheelevents.html]
+ skip-if = (toolkit == 'android') # wheel events not supported on mobile
+[test_group_zoom.html]
+ skip-if = (toolkit != 'android') # only android supports zoom
+[test_interrupted_reflow.html]
+[test_layerization.html]
+ skip-if = (os == 'android') # wheel events not supported on mobile
+[test_scroll_inactive_bug1190112.html]
+ skip-if = (os == 'android') # wheel events not supported on mobile
+[test_scroll_inactive_flattened_frame.html]
+ skip-if = (os == 'android') # wheel events not supported on mobile
+[test_scroll_subframe_scrollbar.html]
+ skip-if = (os == 'android') # wheel events not supported on mobile
+[test_touch_listeners_impacting_wheel.html]
+ skip-if = (toolkit == 'android') || (toolkit == 'cocoa') # wheel events not supported on mobile, and synthesized wheel smooth-scrolling not supported on OS X
+[test_wheel_scroll.html]
+ skip-if = (os == 'android') # wheel events not supported on mobile
+[test_wheel_transactions.html]
+ skip-if = (os == 'android') # wheel events not supported on mobile
diff --git a/gfx/layers/apz/test/mochitest/test_bug1151663.html b/gfx/layers/apz/test/mochitest/test_bug1151663.html
new file mode 100644
index 000000000..10810c6ca
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/test_bug1151663.html
@@ -0,0 +1,38 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1151663
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1151663</title>
+ <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="application/javascript">
+
+ if (isApzEnabled()) {
+ SimpleTest.waitForExplicitFinish();
+
+ // Run the actual test in its own window, because it requires that the
+ // root APZC be scrollable. Mochitest pages themselves often run
+ // inside an iframe which means we have no control over the root APZC.
+ var w = null;
+ window.onload = function() {
+ pushPrefs([["apz.test.logging_enabled", true]]).then(function() {
+ w = window.open("helper_bug1151663.html", "_blank");
+ });
+ };
+ }
+
+ function finishTest() {
+ w.close();
+ SimpleTest.finish();
+ };
+
+ </script>
+</head>
+<body>
+ <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1151663">Mozilla Bug 1151663</a>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/test_bug1151667.html b/gfx/layers/apz/test/mochitest/test_bug1151667.html
new file mode 100644
index 000000000..88facf6e9
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/test_bug1151667.html
@@ -0,0 +1,65 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1151667
+-->
+<head>
+ <title>Test for Bug 1151667</title>
+ <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="application/javascript" src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <style>
+ #subframe {
+ margin-top: 100px;
+ height: 500px;
+ width: 500px;
+ overflow: scroll;
+ }
+ #subframe-content {
+ height: 1000px;
+ width: 500px;
+ /* the background is so that we can see it scroll*/
+ background: repeating-linear-gradient(#EEE, #EEE 100px, #DDD 100px, #DDD 200px);
+ }
+ #page-content {
+ height: 5000px;
+ width: 500px;
+ }
+ </style>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1151667">Mozilla Bug 1151667</a>
+<p id="display"></p>
+<div id="subframe">
+ <!-- This makes sure the subframe is scrollable -->
+ <div id="subframe-content"></div>
+</div>
+<!-- This makes sure the page is also scrollable, so it (rather than the subframe)
+ is considered the primary async-scrollable frame, and so the subframe isn't
+ layerized upon page load. -->
+<div id="page-content"></div>
+<pre id="test">
+<script type="application/javascript;version=1.7">
+
+function startTest() {
+ var subframe = document.getElementById('subframe');
+ synthesizeNativeWheelAndWaitForScrollEvent(subframe, 100, 150, 0, -10, continueTest);
+}
+
+function continueTest() {
+ var subframe = document.getElementById('subframe');
+ is(subframe.scrollTop > 0, true, "We should have scrolled the subframe down");
+ is(document.documentElement.scrollTop, 0, "We should not have scrolled the page");
+ SimpleTest.finish();
+}
+
+SimpleTest.waitForExplicitFinish();
+waitUntilApzStable().then(startTest);
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/test_bug1253683.html b/gfx/layers/apz/test/mochitest/test_bug1253683.html
new file mode 100644
index 000000000..52c8e4a96
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/test_bug1253683.html
@@ -0,0 +1,59 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1253683
+-->
+<head>
+ <title>Test to ensure non-scrollable frames don't get layerized</title>
+ <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="application/javascript" src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+ <p id="display"></p>
+ <div id="container" style="height: 500px; overflow:scroll">
+ <pre id="no_layer" style="background-color: #f5f5f5; margin: 15px; padding: 15px; margin-top: 100px; border: 1px solid #eee; overflow:scroll">sample code here</pre>
+ <div style="height: 5000px">spacer to make the 'container' div the root scrollable element</div>
+ </div>
+<pre id="test">
+<script type="application/javascript;version=1.7">
+
+function* test(testDriver) {
+ var container = document.getElementById('container');
+ var no_layer = document.getElementById('no_layer');
+
+ // Check initial state
+ is(container.scrollTop, 0, "Initial scrollY should be 0");
+ ok(!isLayerized('no_layer'), "initially 'no_layer' should not be layerized");
+
+ // Scrolling over outer1 should layerize outer1, but not inner1.
+ yield moveMouseAndScrollWheelOver(no_layer, 10, 10, testDriver, true);
+ yield waitForAllPaints(function() {
+ flushApzRepaints(testDriver);
+ });
+
+ ok(container.scrollTop > 0, "We should have scrolled the body");
+ ok(!isLayerized('no_layer'), "no_layer should still not be layerized");
+}
+
+if (isApzEnabled()) {
+ SimpleTest.waitForExplicitFinish();
+
+ // Turn off displayport expiry so that we don't miss failures where the
+ // displayport is set and expired before we check for layerization.
+ // Also enable APZ test logging, since we use that data to determine whether
+ // a scroll frame was layerized.
+ pushPrefs([["apz.displayport_expiry_ms", 0],
+ ["apz.test.logging_enabled", true]])
+ .then(waitUntilApzStable)
+ .then(runContinuation(test))
+ .then(SimpleTest.finish);
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/test_bug1277814.html b/gfx/layers/apz/test/mochitest/test_bug1277814.html
new file mode 100644
index 000000000..877286468
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/test_bug1277814.html
@@ -0,0 +1,106 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1277814
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1277814</title>
+ <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="application/javascript">
+ function* test(testDriver) {
+ // Trigger the buggy scenario
+ var subframe = document.getElementById('bug1277814-div');
+ subframe.classList.add('a');
+
+ // The transform change is animated, so let's step through 1s of animation
+ var utils = SpecialPowers.getDOMWindowUtils(window);
+ for (var i = 0; i < 60; i++) {
+ utils.advanceTimeAndRefresh(16);
+ }
+ utils.restoreNormalRefresh();
+
+ // Wait for the layer tree with any updated dispatch-to-content region to
+ // get pushed over to the APZ
+ yield waitForAllPaints(function() {
+ flushApzRepaints(testDriver);
+ });
+
+ // Trigger layerization of the subframe by scrolling the wheel over it
+ yield moveMouseAndScrollWheelOver(subframe, 10, 10, testDriver);
+
+ // Give APZ the chance to compute a displayport, and content
+ // to render based on it.
+ yield waitForApzFlushedRepaints(testDriver);
+
+ // Examine the content-side APZ test data
+ var contentTestData = utils.getContentAPZTestData();
+
+ // Test that the scroll frame for the div 'bug1277814-div' appears in
+ // the APZ test data. The bug this test is for causes the displayport
+ // calculation for this scroll frame to go wrong, causing it not to
+ // become layerized.
+ contentTestData = convertTestData(contentTestData);
+ var foundIt = false;
+ for (var seqNo in contentTestData.paints) {
+ var paint = contentTestData.paints[seqNo];
+ for (var scrollId in paint) {
+ var scrollFrame = paint[scrollId];
+ if ('contentDescription' in scrollFrame &&
+ scrollFrame['contentDescription'].includes('bug1277814-div')) {
+ foundIt = true;
+ }
+ }
+ }
+ SimpleTest.ok(foundIt, "expected to find APZ test data for bug1277814-div");
+ }
+
+ if (isApzEnabled()) {
+ SimpleTest.waitForExplicitFinish();
+
+ pushPrefs([["apz.test.logging_enabled", true]])
+ .then(waitUntilApzStable)
+ .then(runContinuation(test))
+ .then(SimpleTest.finish);
+ }
+ </script>
+ <style>
+ #bug1277814-div
+ {
+ position: absolute;
+ left: 0;
+ top: 0;
+ padding: .5em;
+ overflow: auto;
+ color: white;
+ background: green;
+ max-width: 30em;
+ max-height: 6em;
+ visibility: hidden;
+ transform: scaleY(0);
+ transition: transform .15s ease-out, visibility 0s ease .15s;
+ }
+ #bug1277814-div.a
+ {
+ visibility: visible;
+ transform: scaleY(1);
+ transition: transform .15s ease-out;
+ }
+ </style>
+</head>
+<body>
+ <!-- Use a unique id because we'll be checking for it in the content
+ description logged in the APZ test data -->
+ <div id="bug1277814-div">
+ CoolCmd<br>CoolCmd<br>CoolCmd<br>CoolCmd<br>
+ CoolCmd<br>CoolCmd<br>CoolCmd<br>CoolCmd<br>
+ CoolCmd<br>CoolCmd<br>CoolCmd<br>CoolCmd<br>
+ CoolCmd<br>CoolCmd<br>CoolCmd<br>CoolCmd<br>
+ CoolCmd<br>CoolCmd<br>CoolCmd<br>CoolCmd<br>
+ <button>click me</button>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/test_bug1304689-2.html b/gfx/layers/apz/test/mochitest/test_bug1304689-2.html
new file mode 100644
index 000000000..356d7bcb3
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/test_bug1304689-2.html
@@ -0,0 +1,131 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1304689
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1285070</title>
+ <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <style type="text/css">
+ #outer {
+ height: 400px;
+ width: 415px;
+ overflow: scroll;
+ position: relative;
+ scroll-behavior: smooth;
+ }
+ #outer.contentBefore::before {
+ top: 0;
+ content: '';
+ display: block;
+ height: 2px;
+ position: absolute;
+ width: 100%;
+ z-index: 99;
+ }
+ </style>
+ <script type="application/javascript">
+
+function* test(testDriver) {
+ var utils = SpecialPowers.DOMWindowUtils;
+ var elm = document.getElementById('outer');
+
+ // Set margins on the element, to ensure it is layerized
+ utils.setDisplayPortMarginsForElement(0, 0, 0, 0, elm, /*priority*/ 1);
+ yield waitForAllPaints(function() {
+ flushApzRepaints(testDriver);
+ });
+
+ // Take control of the refresh driver
+ utils.advanceTimeAndRefresh(0);
+
+ // Start a smooth-scroll animation in the compositor and let it go a few
+ // frames, so that there is some "user scrolling" going on (per the comment
+ // in AsyncPanZoomController::NotifyLayersUpdated)
+ elm.scrollTop = 10;
+ utils.advanceTimeAndRefresh(16);
+ utils.advanceTimeAndRefresh(16);
+ utils.advanceTimeAndRefresh(16);
+ utils.advanceTimeAndRefresh(16);
+
+ // Do another scroll update but also do a frame reconstruction within the same
+ // tick of the refresh driver.
+ elm.scrollTop = 100;
+ elm.classList.add('contentBefore');
+
+ // Now let everything settle and all the animations run out
+ for (var i = 0; i < 60; i++) {
+ utils.advanceTimeAndRefresh(16);
+ }
+ utils.restoreNormalRefresh();
+
+ yield flushApzRepaints(testDriver);
+ is(elm.scrollTop, 100, "The scrollTop now should be y=100");
+}
+
+if (isApzEnabled()) {
+ SimpleTest.waitForExplicitFinish();
+ pushPrefs([["apz.displayport_expiry_ms", 0]])
+ .then(waitUntilApzStable)
+ .then(runContinuation(test))
+ .then(SimpleTest.finish);
+}
+
+ </script>
+</head>
+<body>
+ <div id="outer">
+ <div id="inner">
+ this is some scrollable text.<br>
+ this is a second line to make the scrolling more obvious.<br>
+ and a third for good measure.<br>
+ this is some scrollable text.<br>
+ this is a second line to make the scrolling more obvious.<br>
+ and a third for good measure.<br>
+ this is some scrollable text.<br>
+ this is a second line to make the scrolling more obvious.<br>
+ and a third for good measure.<br>
+ this is some scrollable text.<br>
+ this is a second line to make the scrolling more obvious.<br>
+ and a third for good measure.<br>
+ this is some scrollable text.<br>
+ this is a second line to make the scrolling more obvious.<br>
+ and a third for good measure.<br>
+ this is some scrollable text.<br>
+ this is a second line to make the scrolling more obvious.<br>
+ and a third for good measure.<br>
+ this is some scrollable text.<br>
+ this is a second line to make the scrolling more obvious.<br>
+ and a third for good measure.<br>
+ this is some scrollable text.<br>
+ this is a second line to make the scrolling more obvious.<br>
+ and a third for good measure.<br>
+ this is some scrollable text.<br>
+ this is a second line to make the scrolling more obvious.<br>
+ and a third for good measure.<br>
+ this is some scrollable text.<br>
+ this is a second line to make the scrolling more obvious.<br>
+ and a third for good measure.<br>
+ this is some scrollable text.<br>
+ this is a second line to make the scrolling more obvious.<br>
+ and a third for good measure.<br>
+ this is some scrollable text.<br>
+ this is a second line to make the scrolling more obvious.<br>
+ and a third for good measure.<br>
+ this is some scrollable text.<br>
+ this is a second line to make the scrolling more obvious.<br>
+ and a third for good measure.<br>
+ this is some scrollable text.<br>
+ this is a second line to make the scrolling more obvious.<br>
+ and a third for good measure.<br>
+ this is some scrollable text.<br>
+ this is a second line to make the scrolling more obvious.<br>
+ and a third for good measure.<br>
+ </div>
+ </div>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/test_bug1304689.html b/gfx/layers/apz/test/mochitest/test_bug1304689.html
new file mode 100644
index 000000000..a64f8a34e
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/test_bug1304689.html
@@ -0,0 +1,135 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1304689
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1285070</title>
+ <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <style type="text/css">
+ #outer {
+ height: 400px;
+ width: 415px;
+ overflow: scroll;
+ position: relative;
+ scroll-behavior: smooth;
+ }
+ #outer.instant {
+ scroll-behavior: auto;
+ }
+ #outer.contentBefore::before {
+ top: 0;
+ content: '';
+ display: block;
+ height: 2px;
+ position: absolute;
+ width: 100%;
+ z-index: 99;
+ }
+ </style>
+ <script type="application/javascript">
+
+function* test(testDriver) {
+ var utils = SpecialPowers.DOMWindowUtils;
+ var elm = document.getElementById('outer');
+
+ // Set margins on the element, to ensure it is layerized
+ utils.setDisplayPortMarginsForElement(0, 0, 0, 0, elm, /*priority*/ 1);
+ yield waitForAllPaints(function() {
+ flushApzRepaints(testDriver);
+ });
+
+ // Take control of the refresh driver
+ utils.advanceTimeAndRefresh(0);
+
+ // Start a smooth-scroll animation in the compositor and let it go a few
+ // frames, so that there is some "user scrolling" going on (per the comment
+ // in AsyncPanZoomController::NotifyLayersUpdated)
+ elm.scrollTop = 10;
+ utils.advanceTimeAndRefresh(16);
+ utils.advanceTimeAndRefresh(16);
+ utils.advanceTimeAndRefresh(16);
+ utils.advanceTimeAndRefresh(16);
+
+ // Do another scroll update but also do a frame reconstruction within the same
+ // tick of the refresh driver.
+ elm.classList.add('instant');
+ elm.scrollTop = 100;
+ elm.classList.add('contentBefore');
+
+ // Now let everything settle and all the animations run out
+ for (var i = 0; i < 60; i++) {
+ utils.advanceTimeAndRefresh(16);
+ }
+ utils.restoreNormalRefresh();
+
+ yield flushApzRepaints(testDriver);
+ is(elm.scrollTop, 100, "The scrollTop now should be y=100");
+}
+
+if (isApzEnabled()) {
+ SimpleTest.waitForExplicitFinish();
+ pushPrefs([["apz.displayport_expiry_ms", 0]])
+ .then(waitUntilApzStable)
+ .then(runContinuation(test))
+ .then(SimpleTest.finish);
+}
+
+ </script>
+</head>
+<body>
+ <div id="outer">
+ <div id="inner">
+ this is some scrollable text.<br>
+ this is a second line to make the scrolling more obvious.<br>
+ and a third for good measure.<br>
+ this is some scrollable text.<br>
+ this is a second line to make the scrolling more obvious.<br>
+ and a third for good measure.<br>
+ this is some scrollable text.<br>
+ this is a second line to make the scrolling more obvious.<br>
+ and a third for good measure.<br>
+ this is some scrollable text.<br>
+ this is a second line to make the scrolling more obvious.<br>
+ and a third for good measure.<br>
+ this is some scrollable text.<br>
+ this is a second line to make the scrolling more obvious.<br>
+ and a third for good measure.<br>
+ this is some scrollable text.<br>
+ this is a second line to make the scrolling more obvious.<br>
+ and a third for good measure.<br>
+ this is some scrollable text.<br>
+ this is a second line to make the scrolling more obvious.<br>
+ and a third for good measure.<br>
+ this is some scrollable text.<br>
+ this is a second line to make the scrolling more obvious.<br>
+ and a third for good measure.<br>
+ this is some scrollable text.<br>
+ this is a second line to make the scrolling more obvious.<br>
+ and a third for good measure.<br>
+ this is some scrollable text.<br>
+ this is a second line to make the scrolling more obvious.<br>
+ and a third for good measure.<br>
+ this is some scrollable text.<br>
+ this is a second line to make the scrolling more obvious.<br>
+ and a third for good measure.<br>
+ this is some scrollable text.<br>
+ this is a second line to make the scrolling more obvious.<br>
+ and a third for good measure.<br>
+ this is some scrollable text.<br>
+ this is a second line to make the scrolling more obvious.<br>
+ and a third for good measure.<br>
+ this is some scrollable text.<br>
+ this is a second line to make the scrolling more obvious.<br>
+ and a third for good measure.<br>
+ this is some scrollable text.<br>
+ this is a second line to make the scrolling more obvious.<br>
+ and a third for good measure.<br>
+ </div>
+ </div>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/test_bug982141.html b/gfx/layers/apz/test/mochitest/test_bug982141.html
new file mode 100644
index 000000000..9984b79ff
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/test_bug982141.html
@@ -0,0 +1,38 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=982141
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 982141</title>
+ <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="application/javascript">
+
+ if (isApzEnabled()) {
+ SimpleTest.waitForExplicitFinish();
+
+ // Run the actual test in its own window, because it requires that the
+ // root APZC not be scrollable. Mochitest pages themselves often run
+ // inside an iframe which means we have no control over the root APZC.
+ var w = null;
+ window.onload = function() {
+ pushPrefs([["apz.test.logging_enabled", true]]).then(function() {
+ w = window.open("helper_bug982141.html", "_blank");
+ });
+ };
+ }
+
+ function finishTest() {
+ w.close();
+ SimpleTest.finish();
+ };
+
+ </script>
+</head>
+<body>
+ <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=982141">Mozilla Bug 982141</a>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/test_frame_reconstruction.html b/gfx/layers/apz/test/mochitest/test_frame_reconstruction.html
new file mode 100644
index 000000000..589fb2843
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/test_frame_reconstruction.html
@@ -0,0 +1,218 @@
+<!DOCTYPE html>
+<html>
+ <!--
+ https://bugzilla.mozilla.org/show_bug.cgi?id=1235899
+ -->
+ <head>
+ <title>Test for bug 1235899</title>
+ <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="application/javascript" src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <style>
+ .outer {
+ height: 400px;
+ width: 415px;
+ overflow: hidden;
+ position: relative;
+ }
+ .inner {
+ height: 100%;
+ outline: none;
+ overflow-x: hidden;
+ overflow-y: scroll;
+ position: relative;
+ scroll-behavior: smooth;
+ }
+ .outer.contentBefore::before {
+ top: 0;
+ content: '';
+ display: block;
+ height: 2px;
+ position: absolute;
+ width: 100%;
+ z-index: 99;
+ }
+ </style>
+ </head>
+ <body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1235899">Mozilla Bug 1235899</a>
+<p id="display"></p>
+<div id="content">
+ <p>You should be able to fling this list without it stopping abruptly</p>
+ <div class="outer">
+ <div class="inner">
+ <ol>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ <li>Some text</li>
+ </ol>
+ </div>
+ </div>
+</div>
+
+<pre id="test">
+<script type="application/javascript;version=1.7">
+function* test(testDriver) {
+ var elm = document.getElementsByClassName('inner')[0];
+ elm.scrollTop = 0;
+ yield flushApzRepaints(testDriver);
+
+ // Take over control of the refresh driver and compositor
+ var utils = SpecialPowers.DOMWindowUtils;
+ utils.advanceTimeAndRefresh(0);
+
+ // Kick off an APZ smooth-scroll to 0,200
+ elm.scrollTo(0, 200);
+ yield waitForAllPaints(function() { setTimeout(testDriver, 0); });
+
+ // Let's do a couple of frames of the animation, and make sure it's going
+ utils.advanceTimeAndRefresh(16);
+ utils.advanceTimeAndRefresh(16);
+ yield flushApzRepaints(testDriver);
+ ok(elm.scrollTop > 0, "APZ animation in progress", "scrollTop is now " + elm.scrollTop);
+ ok(elm.scrollTop < 200, "APZ animation not yet completed", "scrollTop is now " + elm.scrollTop);
+
+ var frameReconstructionTriggered = 0;
+ // Register the listener that triggers the frame reconstruction
+ elm.onscroll = function() {
+ // Do the reconstruction
+ elm.parentNode.classList.add('contentBefore');
+ frameReconstructionTriggered++;
+ // schedule a thing to undo the changes above
+ setTimeout(function() {
+ elm.parentNode.classList.remove('contentBefore');
+ }, 0);
+ }
+
+ // and do a few more frames of the animation, this should trigger the listener
+ // and the frame reconstruction
+ utils.advanceTimeAndRefresh(16);
+ utils.advanceTimeAndRefresh(16);
+ yield flushApzRepaints(testDriver);
+ ok(elm.scrollTop < 200, "APZ animation not yet completed", "scrollTop is now " + elm.scrollTop);
+ ok(frameReconstructionTriggered > 0, "Frame reconstruction triggered", "reconstruction triggered " + frameReconstructionTriggered + " times");
+
+ // and now run to completion
+ for (var i = 0; i < 100; i++) {
+ utils.advanceTimeAndRefresh(16);
+ }
+ utils.restoreNormalRefresh();
+ yield waitForAllPaints(function() { setTimeout(testDriver, 0); });
+ yield flushApzRepaints(testDriver);
+
+ is(elm.scrollTop, 200, "Element should have scrolled by 200px");
+}
+
+if (isApzEnabled()) {
+ SimpleTest.waitForExplicitFinish();
+ SimpleTest.expectAssertions(0, 1); // this test triggers an assertion, see bug 1247050
+ waitUntilApzStable()
+ .then(runContinuation(test))
+ .then(SimpleTest.finish);
+}
+
+</script>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/test_group_mouseevents.html b/gfx/layers/apz/test/mochitest/test_group_mouseevents.html
new file mode 100644
index 000000000..dcf71f0cc
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/test_group_mouseevents.html
@@ -0,0 +1,35 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Various mouse tests that spawn in new windows</title>
+ <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="application/javascript">
+
+var subtests = [
+ // Sanity test to synthesize a mouse click
+ {'file': 'helper_click.html?dtc=false'},
+ // Same as above, but with a dispatch-to-content region that exercises the
+ // main-thread notification codepaths for mouse events
+ {'file': 'helper_click.html?dtc=true'},
+ // Sanity test for click but with some mouse movement between the down and up
+ {'file': 'helper_drag_click.html'},
+ // Test for dragging on a fake-scrollbar element that scrolls the page
+ {'file': 'helper_drag_scroll.html'}
+];
+
+if (isApzEnabled()) {
+ SimpleTest.waitForExplicitFinish();
+ window.onload = function() {
+ runSubtestsSeriallyInFreshWindows(subtests)
+ .then(SimpleTest.finish);
+ };
+}
+
+ </script>
+</head>
+<body>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/test_group_pointerevents.html b/gfx/layers/apz/test/mochitest/test_group_pointerevents.html
new file mode 100644
index 000000000..2e8d7c240
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/test_group_pointerevents.html
@@ -0,0 +1,31 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1285070
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1285070</title>
+ <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="application/javascript">
+
+ var subtests = [
+ {'file': 'helper_bug1285070.html', 'prefs': [["dom.w3c_pointer_events.enabled", true]]},
+ {'file': 'helper_bug1299195.html', 'prefs': [["dom.w3c_pointer_events.enabled", true]]}
+ ];
+
+ if (isApzEnabled()) {
+ SimpleTest.waitForExplicitFinish();
+ window.onload = function() {
+ runSubtestsSeriallyInFreshWindows(subtests)
+ .then(SimpleTest.finish);
+ };
+ }
+
+ </script>
+</head>
+<body>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/test_group_touchevents.html b/gfx/layers/apz/test/mochitest/test_group_touchevents.html
new file mode 100644
index 000000000..bc0261d46
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/test_group_touchevents.html
@@ -0,0 +1,104 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Various touch tests that spawn in new windows</title>
+ <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="application/javascript">
+
+var basic_pan_prefs = [
+ // Dropping the touch slop to 0 makes the tests easier to write because
+ // we can just do a one-pixel drag to get over the pan threshold rather
+ // than having to hard-code some larger value.
+ ["apz.touch_start_tolerance", "0.0"],
+ // The touchstart from the drag can turn into a long-tap if the touch-move
+ // events get held up. Try to prevent that by making long-taps require
+ // a 10 second hold. Note that we also cannot enable chaos mode on this
+ // test for this reason, since chaos mode can cause the long-press timer
+ // to fire sooner than the pref dictates.
+ ["ui.click_hold_context_menus.delay", 10000],
+ // The subtests in this test do touch-drags to pan the page, but we don't
+ // want those pans to turn into fling animations, so we increase the
+ // fling min velocity requirement absurdly high.
+ ["apz.fling_min_velocity_threshold", "10000"],
+ // The helper_div_pan's div gets a displayport on scroll, but if the
+ // test takes too long the displayport can expire before the new scroll
+ // position is synced back to the main thread. So we disable displayport
+ // expiry for these tests.
+ ["apz.displayport_expiry_ms", 0],
+];
+
+var touch_action_prefs = basic_pan_prefs.slice(); // make a copy
+touch_action_prefs.push(["layout.css.touch_action.enabled", true]);
+
+var isWindows = (getPlatform() == "windows");
+
+var subtests = [
+ // Simple tests to exercise basic panning behaviour
+ {'file': 'helper_basic_pan.html', 'prefs': basic_pan_prefs},
+ {'file': 'helper_div_pan.html', 'prefs': basic_pan_prefs},
+ {'file': 'helper_iframe_pan.html', 'prefs': basic_pan_prefs},
+
+ // Simple test to exercise touch-tapping behaviour
+ {'file': 'helper_tap.html'},
+ // Tapping, but with a full-zoom applied
+ {'file': 'helper_tap_fullzoom.html'},
+
+ // For the following two tests, disable displayport suppression to make sure it
+ // doesn't interfere with the test by scheduling paints non-deterministically.
+ {'file': 'helper_scrollto_tap.html?true', 'prefs': [["apz.paint_skipping.enabled", true]], 'dp_suppression': false},
+ {'file': 'helper_scrollto_tap.html?false', 'prefs': [["apz.paint_skipping.enabled", false]], 'dp_suppression': false},
+
+ // Taps on media elements to make sure the touchend event is delivered
+ // properly. We increase the long-tap timeout to ensure it doesn't get trip
+ // during the tap.
+ // Also this test (on Windows) cannot satisfy the OS requirement of providing
+ // an injected touch event every 100ms, because it waits for a paint between
+ // the touchstart and the touchend, so we have to use the "fake injection"
+ // code instead.
+ {'file': 'helper_bug1162771.html', 'prefs': [["ui.click_hold_context_menus.delay", 10000],
+ ["apz.test.fails_with_native_injection", isWindows]]},
+
+ // As with the previous test, this test cannot inject touch events every 100ms
+ // because it waits for a long-tap, so we have to use the "fake injection" code
+ // instead.
+ {'file': 'helper_long_tap.html', 'prefs': [["apz.test.fails_with_native_injection", isWindows]]},
+
+ // For the following test, we want to make sure APZ doesn't wait for a content
+ // response that is never going to arrive. To detect this we set the content response
+ // timeout to a day, so that the entire test times out and fails if APZ does
+ // end up waiting.
+ {'file': 'helper_tap_passive.html', 'prefs': [["apz.content_response_timeout", 24 * 60 * 60 * 1000],
+ ["apz.test.fails_with_native_injection", isWindows]]},
+
+ // Simple test to exercise touch-action CSS property
+ {'file': 'helper_touch_action.html', 'prefs': touch_action_prefs},
+ // More complex touch-action tests, with overlapping regions and such
+ {'file': 'helper_touch_action_complex.html', 'prefs': touch_action_prefs},
+ // Tests that touch-action CSS properties are handled in APZ without waiting
+ // on the main-thread, when possible
+ {'file': 'helper_touch_action_regions.html', 'prefs': touch_action_prefs},
+];
+
+if (isApzEnabled()) {
+ ok(window.TouchEvent, "Check if TouchEvent is supported (it should be, the test harness forces it on everywhere)");
+ if (getPlatform() == "android") {
+ // This has a lot of subtests, and Android emulators are slow.
+ SimpleTest.requestLongerTimeout(2);
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ window.onload = function() {
+ runSubtestsSeriallyInFreshWindows(subtests)
+ .then(SimpleTest.finish);
+ };
+}
+
+ </script>
+</head>
+<body>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/test_group_wheelevents.html b/gfx/layers/apz/test/mochitest/test_group_wheelevents.html
new file mode 100644
index 000000000..98c36f320
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/test_group_wheelevents.html
@@ -0,0 +1,41 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Various wheel-scrolling tests that spawn in new windows</title>
+ <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="application/javascript">
+
+var prefs = [
+ // turn off smooth scrolling so that we don't have to wait for
+ // APZ animations to finish before sampling the scroll offset
+ ['general.smoothScroll', false],
+ // ensure that any mouse movement will trigger a new wheel transaction,
+ // because in this test we move the mouse a bunch and want to recalculate
+ // the target APZC after each such movement.
+ ['mousewheel.transaction.ignoremovedelay', 0],
+ ['mousewheel.transaction.timeout', 0]
+]
+
+var subtests = [
+ {'file': 'helper_scroll_on_position_fixed.html', 'prefs': prefs},
+ {'file': 'helper_bug1271432.html', 'prefs': prefs},
+ {'file': 'helper_scroll_inactive_perspective.html', 'prefs': prefs},
+ {'file': 'helper_scroll_inactive_zindex.html', 'prefs': prefs}
+];
+
+if (isApzEnabled()) {
+ SimpleTest.waitForExplicitFinish();
+ window.onload = function() {
+ runSubtestsSeriallyInFreshWindows(subtests)
+ .then(SimpleTest.finish);
+ };
+}
+
+ </script>
+</head>
+<body>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/test_group_zoom.html b/gfx/layers/apz/test/mochitest/test_group_zoom.html
new file mode 100644
index 000000000..4bf9c0bed
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/test_group_zoom.html
@@ -0,0 +1,44 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Various zoom-related tests that spawn in new windows</title>
+ <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="application/javascript">
+
+var prefs = [
+ // We need the APZ paint logging information
+ ["apz.test.logging_enabled", true],
+ // Dropping the touch slop to 0 makes the tests easier to write because
+ // we can just do a one-pixel drag to get over the pan threshold rather
+ // than having to hard-code some larger value.
+ ["apz.touch_start_tolerance", "0.0"],
+ // The subtests in this test do touch-drags to pan the page, but we don't
+ // want those pans to turn into fling animations, so we increase the
+ // fling-stop threshold velocity to absurdly high.
+ ["apz.fling_stopped_threshold", "10000"],
+ // The helper_bug1280013's div gets a displayport on scroll, but if the
+ // test takes too long the displayport can expire before we read the value
+ // out of the test. So we disable displayport expiry for these tests.
+ ["apz.displayport_expiry_ms", 0],
+];
+
+var subtests = [
+ {'file': 'helper_bug1280013.html', 'prefs': prefs},
+];
+
+if (isApzEnabled()) {
+ SimpleTest.waitForExplicitFinish();
+ window.onload = function() {
+ runSubtestsSeriallyInFreshWindows(subtests)
+ .then(SimpleTest.finish);
+ };
+}
+
+ </script>
+</head>
+<body>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/test_interrupted_reflow.html b/gfx/layers/apz/test/mochitest/test_interrupted_reflow.html
new file mode 100644
index 000000000..05c5e5478
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/test_interrupted_reflow.html
@@ -0,0 +1,719 @@
+<!DOCTYPE html>
+<html>
+ <!--
+ https://bugzilla.mozilla.org/show_bug.cgi?id=1292781
+ -->
+ <head>
+ <title>Test for bug 1292781</title>
+ <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="application/javascript" src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <style>
+ .outer {
+ height: 400px;
+ width: 415px;
+ overflow: hidden;
+ position: relative;
+ }
+ .inner {
+ height: 100%;
+ outline: none;
+ overflow-x: hidden;
+ overflow-y: scroll;
+ position: relative;
+ }
+ .inner div:nth-child(even) {
+ background-color: lightblue;
+ }
+ .inner div:nth-child(odd) {
+ background-color: lightgreen;
+ }
+ .outer.contentBefore::before {
+ top: 0;
+ content: '';
+ display: block;
+ height: 2px;
+ position: absolute;
+ width: 100%;
+ z-index: 99;
+ }
+ </style>
+ </head>
+ <body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1292781">Mozilla Bug 1292781</a>
+<p id="display"></p>
+<div id="content">
+ <p>The frame reconstruction should not leave this scrollframe in a bad state</p>
+ <div class="outer">
+ <div class="inner">
+ this is the top of the scrollframe.
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ this is near the top of the scrollframe.
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ this is near the bottom of the scrollframe.
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ <div>this is a box</div>
+ this is the bottom of the scrollframe.
+ </div>
+ </div>
+</div>
+
+<pre id="test">
+<script type="text/javascript">
+
+// Returns a list of async scroll offsets that the |inner| element had, one for
+// each paint.
+function getAsyncScrollOffsets(aPaintsToIgnore) {
+ var offsets = [];
+ var compositorTestData = SpecialPowers.getDOMWindowUtils(window).getCompositorAPZTestData();
+ var buckets = compositorTestData.paints.slice(aPaintsToIgnore);
+ ok(buckets.length >= 3, "Expected at least three paints in the compositor test data");
+ var childIsLayerized = false;
+ for (var i = 0; i < buckets.length; ++i) {
+ var apzcTree = buildApzcTree(convertScrollFrameData(buckets[i].scrollFrames));
+ var rcd = findRcdNode(apzcTree);
+ if (rcd == null) {
+ continue;
+ }
+ if (rcd.children.length > 0) {
+ // The child may not be layerized in the first few paints, but once it is
+ // layerized, it should stay layerized.
+ childIsLayerized = true;
+ }
+ if (!childIsLayerized) {
+ continue;
+ }
+
+ ok(rcd.children.length == 1, "Root content APZC has exactly one child");
+ var scroll = rcd.children[0].asyncScrollOffset;
+ var pieces = scroll.replace(/[()\s]+/g, '').split(',');
+ is(pieces.length, 2, "expected string of form (x,y)");
+ offsets.push({ x: parseInt(pieces[0]),
+ y: parseInt(pieces[1]) });
+ }
+ return offsets;
+}
+
+function* test(testDriver) {
+ var utils = SpecialPowers.DOMWindowUtils;
+
+ // The APZ test data accumulates whenever a test turns it on. We just want
+ // the data for this test, so we check how many frames are already recorded
+ // and discard those later.
+ var framesToSkip = SpecialPowers.getDOMWindowUtils(window).getCompositorAPZTestData().paints.length;
+
+ var elm = document.getElementsByClassName('inner')[0];
+ // Set a zero-margin displayport to ensure that the element is async-scrollable
+ // otherwise on Fennec it is not
+ utils.setDisplayPortMarginsForElement(0, 0, 0, 0, elm, 0);
+
+ var maxScroll = elm.scrollTopMax;
+ elm.scrollTop = maxScroll;
+ yield waitForAllPaints(function() {
+ flushApzRepaints(testDriver);
+ });
+
+ // Take control of the refresh driver
+ utils.advanceTimeAndRefresh(0);
+
+ // Force the next reflow to get interrupted
+ utils.forceReflowInterrupt();
+
+ // Make a change that triggers frame reconstruction, and then tick the refresh
+ // driver so that layout processes the pending restyles and then runs an
+ // interruptible reflow. That reflow *will* be interrupted (because of the flag
+ // we set above), and we should end up with a transient 0,0 scroll offset
+ // being sent to the compositor.
+ elm.parentNode.classList.add('contentBefore');
+ utils.advanceTimeAndRefresh(0);
+ // On android, and maybe non-e10s platforms generally, we need to manually
+ // kick the paint to send the layer transaction to the compositor.
+ yield waitForAllPaints(function() { setTimeout(testDriver, 0) });
+
+ // Read the main-thread scroll offset; although this is temporarily 0,0 that
+ // temporary value is never exposed to content - instead reading this value
+ // will finish doing the interrupted reflow from above and then report the
+ // correct scroll offset.
+ is(elm.scrollTop, maxScroll, "Main-thread scroll position was restored");
+
+ // .. and now flush everything to make sure the state gets pushed over to the
+ // compositor and APZ as well.
+ utils.restoreNormalRefresh();
+ yield waitForApzFlushedRepaints(testDriver);
+
+ // Now we pull the compositor data and check it. What we expect to see is that
+ // the scroll position goes to maxScroll, then drops to 0 and then goes back
+ // to maxScroll. This test is specifically testing that last bit - that it
+ // properly gets restored from 0 to maxScroll.
+ // The one hitch is that on Android this page is loaded with some amount of
+ // zoom, and the async scroll is in ParentLayerPixel coordinates, so it will
+ // not match maxScroll exactly. Since we can't reliably compute what that
+ // ParentLayer scroll will be, we just make sure the async scroll is nonzero
+ // and use the first value we encounter to verify that it got restored properly.
+ // The other alternative is to spawn this test into a new window with 1.0 zoom
+ // but I'm tired of doing that for pretty much every test.
+ var state = 0;
+ var asyncScrollOffsets = getAsyncScrollOffsets(framesToSkip);
+ dump("Got scroll offsets: " + JSON.stringify(asyncScrollOffsets) + "\n");
+ var maxScrollParentLayerPixels = maxScroll;
+ while (asyncScrollOffsets.length > 0) {
+ let offset = asyncScrollOffsets.shift();
+ switch (state) {
+ // 0 is the initial state, the scroll offset might be zero but should
+ // become non-zero from when we set scrollTop to scrollTopMax
+ case 0:
+ if (offset.y == 0) {
+ break;
+ }
+ if (getPlatform() == "android") {
+ ok(offset.y > 0, "Async scroll y of scrollframe is " + offset.y);
+ maxScrollParentLayerPixels = offset.y;
+ } else {
+ is(offset.y, maxScrollParentLayerPixels, "Async scroll y of scrollframe is " + offset.y);
+ }
+ state = 1;
+ break;
+
+ // state 1 starts out at maxScrollParentLayerPixels, should drop to 0
+ // because of the interrupted reflow putting the scroll into a transient
+ // zero state
+ case 1:
+ if (offset.y == maxScrollParentLayerPixels) {
+ break;
+ }
+ is(offset.y, 0, "Async scroll position was temporarily 0");
+ state = 2;
+ break;
+
+ // state 2 starts out the transient 0 scroll offset, and we expect the
+ // scroll position to get restored back to maxScrollParentLayerPixels
+ case 2:
+ if (offset.y == 0) {
+ break;
+ }
+ is(offset.y, maxScrollParentLayerPixels, "Async scroll y of scrollframe restored to " + offset.y);
+ state = 3;
+ break;
+
+ // Terminal state. The scroll position should stay at maxScrollParentLayerPixels
+ case 3:
+ is(offset.y, maxScrollParentLayerPixels, "Scroll position maintained");
+ break;
+ }
+ }
+ is(state, 3, "The scroll position did drop to 0 and then get restored properly");
+}
+
+if (isApzEnabled()) {
+ SimpleTest.waitForExplicitFinish();
+
+ pushPrefs([["apz.test.logging_enabled", true],
+ ["apz.displayport_expiry_ms", 0]])
+ .then(waitUntilApzStable)
+ .then(runContinuation(test))
+ .then(SimpleTest.finish);
+}
+
+</script>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/test_layerization.html b/gfx/layers/apz/test/mochitest/test_layerization.html
new file mode 100644
index 000000000..c74b181bd
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/test_layerization.html
@@ -0,0 +1,214 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1173580
+-->
+<head>
+ <title>Test for layerization</title>
+ <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="application/javascript" src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <link rel="stylesheet" type="text/css" href="helper_subframe_style.css"/>
+ <style>
+ #container {
+ display: flex;
+ overflow: scroll;
+ height: 500px;
+ }
+ .outer-frame {
+ height: 500px;
+ overflow: scroll;
+ flex-basis: 100%;
+ background: repeating-linear-gradient(#CCC, #CCC 100px, #BBB 100px, #BBB 200px);
+ }
+ #container-content {
+ height: 200%;
+ }
+ </style>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1173580">APZ layerization tests</a>
+<p id="display"></p>
+<div id="container">
+ <div id="outer1" class="outer-frame">
+ <div id="inner1" class="inner-frame">
+ <div class="inner-content"></div>
+ </div>
+ </div>
+ <div id="outer2" class="outer-frame">
+ <div id="inner2" class="inner-frame">
+ <div class="inner-content"></div>
+ </div>
+ </div>
+ <iframe id="outer3" class="outer-frame" src="helper_iframe1.html"></iframe>
+ <iframe id="outer4" class="outer-frame" src="helper_iframe2.html"></iframe>
+<!-- The container-content div ensures 'container' is scrollable, so the
+ optimization that layerizes the primary async-scrollable frame on page
+ load layerizes it rather than its child subframes. -->
+ <div id="container-content"></div>
+</div>
+<pre id="test">
+<script type="application/javascript;version=1.7">
+
+// Scroll the mouse wheel over |element|.
+function scrollWheelOver(element, waitForScroll, testDriver) {
+ moveMouseAndScrollWheelOver(element, 10, 10, testDriver, waitForScroll);
+}
+
+const DISPLAYPORT_EXPIRY = 100;
+
+// This helper function produces another helper function, which, when invoked,
+// invokes the provided testDriver argument in a setTimeout 0. This is really
+// just useful in cases when there are no paints pending, because then
+// waitForAllPaints will invoke its callback synchronously. If we did
+// waitForAllPaints(testDriver) that might cause reentrancy into the testDriver
+// which is bad. This function works around that.
+function asyncWrapper(testDriver) {
+ return function() {
+ setTimeout(testDriver, 0);
+ };
+}
+
+function* test(testDriver) {
+ // Initially, nothing should be layerized.
+ ok(!isLayerized('outer1'), "initially 'outer1' should not be layerized");
+ ok(!isLayerized('inner1'), "initially 'inner1' should not be layerized");
+ ok(!isLayerized('outer2'), "initially 'outer2' should not be layerized");
+ ok(!isLayerized('inner2'), "initially 'inner2' should not be layerized");
+ ok(!isLayerized('outer3'), "initially 'outer3' should not be layerized");
+ ok(!isLayerized('inner3'), "initially 'inner3' should not be layerized");
+ ok(!isLayerized('outer4'), "initially 'outer4' should not be layerized");
+ ok(!isLayerized('inner4'), "initially 'inner4' should not be layerized");
+
+ // Scrolling over outer1 should layerize outer1, but not inner1.
+ yield scrollWheelOver(document.getElementById('outer1'), true, testDriver);
+ ok(isLayerized('outer1'), "scrolling 'outer1' should cause it to be layerized");
+ ok(!isLayerized('inner1'), "scrolling 'outer1' should not cause 'inner1' to be layerized");
+
+ // Scrolling over inner2 should layerize both outer2 and inner2.
+ yield scrollWheelOver(document.getElementById('inner2'), true, testDriver);
+ ok(isLayerized('inner2'), "scrolling 'inner2' should cause it to be layerized");
+ ok(isLayerized('outer2'), "scrolling 'inner2' should also cause 'outer2' to be layerized");
+
+ // The second half of the test repeats the same checks as the first half,
+ // but with an iframe as the outer scrollable frame.
+
+ // Scrolling over outer3 should layerize outer3, but not inner3.
+ yield scrollWheelOver(document.getElementById('outer3').contentDocument.documentElement, true, testDriver);
+ ok(isLayerized('outer3'), "scrolling 'outer3' should cause it to be layerized");
+ ok(!isLayerized('inner3'), "scrolling 'outer3' should not cause 'inner3' to be layerized");
+
+ // Scrolling over outer4 should layerize both outer4 and inner4.
+ yield scrollWheelOver(document.getElementById('outer4').contentDocument.getElementById('inner4'), true, testDriver);
+ ok(isLayerized('inner4'), "scrolling 'inner4' should cause it to be layerized");
+ ok(isLayerized('outer4'), "scrolling 'inner4' should also cause 'outer4' to be layerized");
+
+ // Now we enable displayport expiry, and verify that things are still
+ // layerized as they were before.
+ yield SpecialPowers.pushPrefEnv({"set": [["apz.displayport_expiry_ms", DISPLAYPORT_EXPIRY]]}, testDriver);
+ ok(isLayerized('outer1'), "outer1 is still layerized after enabling expiry");
+ ok(!isLayerized('inner1'), "inner1 is still not layerized after enabling expiry");
+ ok(isLayerized('outer2'), "outer2 is still layerized after enabling expiry");
+ ok(isLayerized('inner2'), "inner2 is still layerized after enabling expiry");
+ ok(isLayerized('outer3'), "outer3 is still layerized after enabling expiry");
+ ok(!isLayerized('inner3'), "inner3 is still not layerized after enabling expiry");
+ ok(isLayerized('outer4'), "outer4 is still layerized after enabling expiry");
+ ok(isLayerized('inner4'), "inner4 is still layerized after enabling expiry");
+
+ // Now we trigger a scroll on some of the things still layerized, so that
+ // the displayport expiry gets triggered.
+
+ // Expire displayport with scrolling on outer1
+ yield scrollWheelOver(document.getElementById('outer1'), true, testDriver);
+ yield waitForAllPaints(function() {
+ flushApzRepaints(testDriver);
+ });
+ yield setTimeout(testDriver, DISPLAYPORT_EXPIRY);
+ yield waitForAllPaints(asyncWrapper(testDriver));
+ ok(!isLayerized('outer1'), "outer1 is no longer layerized after displayport expiry");
+ ok(!isLayerized('inner1'), "inner1 is still not layerized after displayport expiry");
+
+ // Expire displayport with scrolling on inner2
+ yield scrollWheelOver(document.getElementById('inner2'), true, testDriver);
+ yield waitForAllPaints(function() {
+ flushApzRepaints(testDriver);
+ });
+ // Once the expiry elapses, it will trigger expiry on outer2, so we check
+ // both, one at a time.
+ yield setTimeout(testDriver, DISPLAYPORT_EXPIRY);
+ yield waitForAllPaints(asyncWrapper(testDriver));
+ ok(!isLayerized('inner2'), "inner2 is no longer layerized after displayport expiry");
+ yield setTimeout(testDriver, DISPLAYPORT_EXPIRY);
+ yield waitForAllPaints(asyncWrapper(testDriver));
+ ok(!isLayerized('outer2'), "outer2 got de-layerized with inner2");
+
+ // Scroll on inner3. inner3 isn't layerized, and this will cause it to
+ // get layerized, but it will also trigger displayport expiration for inner3
+ // which will eventually trigger displayport expiration on inner3 and outer3.
+ // Note that the displayport expiration might actually happen before the wheel
+ // input is processed in the compositor (see bug 1246480 comment 3), and so
+ // we make sure not to wait for a scroll event here, since it may never fire.
+ // However, if we do get a scroll event while waiting for the expiry, we need
+ // to restart the expiry timer because the displayport expiry got reset. There's
+ // no good way that I can think of to deterministically avoid doing this.
+ let inner3 = document.getElementById('outer3').contentDocument.getElementById('inner3');
+ yield scrollWheelOver(inner3, false, testDriver);
+ yield waitForAllPaints(function() {
+ flushApzRepaints(testDriver);
+ });
+ var timerId = setTimeout(testDriver, DISPLAYPORT_EXPIRY);
+ var timeoutResetter = function() {
+ ok(true, "Got a scroll event; resetting timer...");
+ clearTimeout(timerId);
+ setTimeout(testDriver, DISPLAYPORT_EXPIRY);
+ // by not updating timerId we ensure that this listener resets the timeout
+ // at most once.
+ };
+ inner3.addEventListener('scroll', timeoutResetter, false);
+ yield; // wait for the setTimeout to elapse
+ inner3.removeEventListener('scroll', timeoutResetter, false);
+
+ yield waitForAllPaints(asyncWrapper(testDriver));
+ ok(!isLayerized('inner3'), "inner3 becomes unlayerized after expiry");
+ yield setTimeout(testDriver, DISPLAYPORT_EXPIRY);
+ yield waitForAllPaints(asyncWrapper(testDriver));
+ ok(!isLayerized('outer3'), "outer3 is no longer layerized after inner3 triggered expiry");
+
+ // Scroll outer4 and wait for the expiry. It should NOT get expired because
+ // inner4 is still layerized
+ yield scrollWheelOver(document.getElementById('outer4').contentDocument.documentElement, true, testDriver);
+ yield waitForAllPaints(function() {
+ flushApzRepaints(testDriver);
+ });
+ // Wait for the expiry to elapse
+ yield setTimeout(testDriver, DISPLAYPORT_EXPIRY);
+ yield waitForAllPaints(asyncWrapper(testDriver));
+ ok(isLayerized('inner4'), "inner4 is still layerized because it never expired");
+ ok(isLayerized('outer4'), "outer4 is still layerized because inner4 is still layerized");
+}
+
+if (isApzEnabled()) {
+ SimpleTest.waitForExplicitFinish();
+ SimpleTest.requestFlakyTimeout("we are testing code that measures an actual timeout");
+ SimpleTest.expectAssertions(0, 8); // we get a bunch of "ASSERTION: Bounds computation mismatch" sometimes (bug 1232856)
+
+ // Disable smooth scrolling, because it results in long-running scroll
+ // animations that can result in a 'scroll' event triggered by an earlier
+ // wheel event as corresponding to a later wheel event.
+ // Also enable APZ test logging, since we use that data to determine whether
+ // a scroll frame was layerized.
+ pushPrefs([["general.smoothScroll", false],
+ ["apz.displayport_expiry_ms", 0],
+ ["apz.test.logging_enabled", true]])
+ .then(waitUntilApzStable)
+ .then(runContinuation(test))
+ .then(SimpleTest.finish);
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/test_scroll_inactive_bug1190112.html b/gfx/layers/apz/test/mochitest/test_scroll_inactive_bug1190112.html
new file mode 100644
index 000000000..3349ef1ab
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/test_scroll_inactive_bug1190112.html
@@ -0,0 +1,541 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test scrolling flattened inactive frames</title>
+ <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="application/javascript" src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+<style>
+p {
+ width:200px;
+ height:200px;
+ border:solid 1px black;
+ overflow:auto;
+}
+</style>
+</head>
+<body>
+<div id="iframe-body" style="overflow: auto; height: 1000px">
+<hr>
+<hr>
+<hr>
+<p>
+1 <br>
+2 <br>
+3 <br>
+4 <br>
+5 <br>
+6 <br>
+7 <br>
+8 <br>
+9 <br>
+10 <br>
+11 <br>
+12 <br>
+13 <br>
+14 <br>
+15 <br>
+16 <br>
+17 <br>
+18 <br>
+19 <br>
+20 <br>
+21 <br>
+22 <br>
+23 <br>
+24 <br>
+25 <br>
+26 <br>
+27 <br>
+28 <br>
+29 <br>
+30 <br>
+31 <br>
+32 <br>
+33 <br>
+34 <br>
+35 <br>
+36 <br>
+37 <br>
+38 <br>
+39 <br>
+40 <br>
+
+</p><p id="subframe">
+1 <br>
+2 <br>
+3 <br>
+4 <br>
+5 <br>
+6 <br>
+7 <br>
+8 <br>
+9 <br>
+10 <br>
+11 <br>
+12 <br>
+13 <br>
+14 <br>
+15 <br>
+16 <br>
+17 <br>
+18 <br>
+19 <br>
+20 <br>
+21 <br>
+22 <br>
+23 <br>
+24 <br>
+25 <br>
+26 <br>
+27 <br>
+28 <br>
+29 <br>
+30 <br>
+31 <br>
+32 <br>
+33 <br>
+34 <br>
+35 <br>
+36 <br>
+37 <br>
+38 <br>
+39 <br>
+40 <br>
+
+</p><p>
+1 <br>
+2 <br>
+3 <br>
+4 <br>
+5 <br>
+6 <br>
+7 <br>
+8 <br>
+9 <br>
+10 <br>
+11 <br>
+12 <br>
+13 <br>
+14 <br>
+15 <br>
+16 <br>
+17 <br>
+18 <br>
+19 <br>
+20 <br>
+21 <br>
+22 <br>
+23 <br>
+24 <br>
+25 <br>
+26 <br>
+27 <br>
+28 <br>
+29 <br>
+30 <br>
+31 <br>
+32 <br>
+33 <br>
+34 <br>
+35 <br>
+36 <br>
+37 <br>
+38 <br>
+39 <br>
+40 <br>
+
+</p><p>
+1 <br>
+2 <br>
+3 <br>
+4 <br>
+5 <br>
+6 <br>
+7 <br>
+8 <br>
+9 <br>
+10 <br>
+11 <br>
+12 <br>
+13 <br>
+14 <br>
+15 <br>
+16 <br>
+17 <br>
+18 <br>
+19 <br>
+20 <br>
+21 <br>
+22 <br>
+23 <br>
+24 <br>
+25 <br>
+26 <br>
+27 <br>
+28 <br>
+29 <br>
+30 <br>
+31 <br>
+32 <br>
+33 <br>
+34 <br>
+35 <br>
+36 <br>
+37 <br>
+38 <br>
+39 <br>
+40 <br>
+
+</p><p>
+1 <br>
+2 <br>
+3 <br>
+4 <br>
+5 <br>
+6 <br>
+7 <br>
+8 <br>
+9 <br>
+10 <br>
+11 <br>
+12 <br>
+13 <br>
+14 <br>
+15 <br>
+16 <br>
+17 <br>
+18 <br>
+19 <br>
+20 <br>
+21 <br>
+22 <br>
+23 <br>
+24 <br>
+25 <br>
+26 <br>
+27 <br>
+28 <br>
+29 <br>
+30 <br>
+31 <br>
+32 <br>
+33 <br>
+34 <br>
+35 <br>
+36 <br>
+37 <br>
+38 <br>
+39 <br>
+40 <br>
+
+</p><p>
+1 <br>
+2 <br>
+3 <br>
+4 <br>
+5 <br>
+6 <br>
+7 <br>
+8 <br>
+9 <br>
+10 <br>
+11 <br>
+12 <br>
+13 <br>
+14 <br>
+15 <br>
+16 <br>
+17 <br>
+18 <br>
+19 <br>
+20 <br>
+21 <br>
+22 <br>
+23 <br>
+24 <br>
+25 <br>
+26 <br>
+27 <br>
+28 <br>
+29 <br>
+30 <br>
+31 <br>
+32 <br>
+33 <br>
+34 <br>
+35 <br>
+36 <br>
+37 <br>
+38 <br>
+39 <br>
+40 <br>
+
+</p><p>
+1 <br>
+2 <br>
+3 <br>
+4 <br>
+5 <br>
+6 <br>
+7 <br>
+8 <br>
+9 <br>
+10 <br>
+11 <br>
+12 <br>
+13 <br>
+14 <br>
+15 <br>
+16 <br>
+17 <br>
+18 <br>
+19 <br>
+20 <br>
+21 <br>
+22 <br>
+23 <br>
+24 <br>
+25 <br>
+26 <br>
+27 <br>
+28 <br>
+29 <br>
+30 <br>
+31 <br>
+32 <br>
+33 <br>
+34 <br>
+35 <br>
+36 <br>
+37 <br>
+38 <br>
+39 <br>
+40 <br>
+
+</p><p>
+1 <br>
+2 <br>
+3 <br>
+4 <br>
+5 <br>
+6 <br>
+7 <br>
+8 <br>
+9 <br>
+10 <br>
+11 <br>
+12 <br>
+13 <br>
+14 <br>
+15 <br>
+16 <br>
+17 <br>
+18 <br>
+19 <br>
+20 <br>
+21 <br>
+22 <br>
+23 <br>
+24 <br>
+25 <br>
+26 <br>
+27 <br>
+28 <br>
+29 <br>
+30 <br>
+31 <br>
+32 <br>
+33 <br>
+34 <br>
+35 <br>
+36 <br>
+37 <br>
+38 <br>
+39 <br>
+40 <br>
+
+</p><p>
+1 <br>
+2 <br>
+3 <br>
+4 <br>
+5 <br>
+6 <br>
+7 <br>
+8 <br>
+9 <br>
+10 <br>
+11 <br>
+12 <br>
+13 <br>
+14 <br>
+15 <br>
+16 <br>
+17 <br>
+18 <br>
+19 <br>
+20 <br>
+21 <br>
+22 <br>
+23 <br>
+24 <br>
+25 <br>
+26 <br>
+27 <br>
+28 <br>
+29 <br>
+30 <br>
+31 <br>
+32 <br>
+33 <br>
+34 <br>
+35 <br>
+36 <br>
+37 <br>
+38 <br>
+39 <br>
+40 <br>
+
+</p><p>
+1 <br>
+2 <br>
+3 <br>
+4 <br>
+5 <br>
+6 <br>
+7 <br>
+8 <br>
+9 <br>
+10 <br>
+11 <br>
+12 <br>
+13 <br>
+14 <br>
+15 <br>
+16 <br>
+17 <br>
+18 <br>
+19 <br>
+20 <br>
+21 <br>
+22 <br>
+23 <br>
+24 <br>
+25 <br>
+26 <br>
+27 <br>
+28 <br>
+29 <br>
+30 <br>
+31 <br>
+32 <br>
+33 <br>
+34 <br>
+35 <br>
+36 <br>
+37 <br>
+38 <br>
+39 <br>
+40 <br>
+
+</p><p>
+1 <br>
+2 <br>
+3 <br>
+4 <br>
+5 <br>
+6 <br>
+7 <br>
+8 <br>
+9 <br>
+10 <br>
+11 <br>
+12 <br>
+13 <br>
+14 <br>
+15 <br>
+16 <br>
+17 <br>
+18 <br>
+19 <br>
+20 <br>
+21 <br>
+22 <br>
+23 <br>
+24 <br>
+25 <br>
+26 <br>
+27 <br>
+28 <br>
+29 <br>
+30 <br>
+31 <br>
+32 <br>
+33 <br>
+34 <br>
+35 <br>
+36 <br>
+37 <br>
+38 <br>
+39 <br>
+40 <br>
+
+</p>
+</div>
+<script clss="testbody" type="text/javascript;version=1.7">
+function ScrollTops() {
+ this.outerScrollTop = document.getElementById('iframe-body').scrollTop;
+ this.innerScrollTop = document.getElementById('subframe').scrollTop;
+}
+
+var DefaultEvent = {
+ deltaMode: WheelEvent.DOM_DELTA_LINE,
+ deltaX: 0, deltaY: 1,
+ lineOrPageDeltaX: 0, lineOrPageDeltaY: 1,
+};
+
+function test() {
+ var subframe = document.getElementById('subframe');
+ var oldpos = new ScrollTops();
+ sendWheelAndPaint(subframe, 10, 10, DefaultEvent, function () {
+ var newpos = new ScrollTops();
+ ok(oldpos.outerScrollTop == newpos.outerScrollTop, "viewport should not have scrolled");
+ ok(oldpos.innerScrollTop != newpos.innerScrollTop, "subframe should have scrolled");
+ doOuterScroll(subframe, newpos);
+ });
+}
+
+function doOuterScroll(subframe, oldpos) {
+ var outer = document.getElementById('iframe-body');
+ sendWheelAndPaint(outer, 20, 5, DefaultEvent, function () {
+ var newpos = new ScrollTops();
+ ok(oldpos.outerScrollTop != newpos.outerScrollTop, "viewport should have scrolled");
+ ok(oldpos.innerScrollTop == newpos.innerScrollTop, "subframe should not have scrolled");
+ doInnerScrollAgain(subframe, newpos);
+ });
+}
+
+function doInnerScrollAgain(subframe, oldpos) {
+ sendWheelAndPaint(subframe, 10, 10, DefaultEvent, function () {
+ var newpos = new ScrollTops();
+ ok(oldpos.outerScrollTop == newpos.outerScrollTop, "viewport should not have scrolled");
+ ok(oldpos.innerScrollTop != newpos.innerScrollTop, "subframe should have scrolled");
+ SimpleTest.finish();
+ });
+}
+
+SimpleTest.testInChaosMode();
+SimpleTest.waitForExplicitFinish();
+
+pushPrefs([['general.smoothScroll', false],
+ ['mousewheel.transaction.timeout', 0],
+ ['mousewheel.transaction.ignoremovedelay', 0]])
+.then(waitUntilApzStable)
+.then(test);
+
+</script>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/test_scroll_inactive_flattened_frame.html b/gfx/layers/apz/test/mochitest/test_scroll_inactive_flattened_frame.html
new file mode 100644
index 000000000..51e16aab9
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/test_scroll_inactive_flattened_frame.html
@@ -0,0 +1,49 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test scrolling flattened inactive frames</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="application/javascript" src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<div id="container" style="height: 300px; width: 600px; overflow: auto; background: yellow">
+ <div id="outer" style="height: 400px; width: 500px; overflow: auto; background: black">
+ <div id="inner" style="mix-blend-mode: screen; height: 800px; overflow: auto; background: purple">
+ </div>
+ </div>
+</div>
+<script class="testbody" type="text/javascript;version=1.7">
+function test() {
+ var container = document.getElementById('container');
+ var outer = document.getElementById('outer');
+ var inner = document.getElementById('inner');
+ var outerScrollTop = outer.scrollTop;
+ var containerScrollTop = container.scrollTop;
+ var event = {
+ deltaMode: WheelEvent.DOM_DELTA_LINE,
+ deltaX: 0,
+ deltaY: 10,
+ lineOrPageDeltaX: 0,
+ lineOrPageDeltaY: 10,
+ };
+ sendWheelAndPaint(inner, 20, 30, event, function () {
+ ok(container.scrollTop == containerScrollTop, "container scrollframe should not have scrolled");
+ ok(outer.scrollTop > outerScrollTop, "nested scrollframe should have scrolled");
+ SimpleTest.finish();
+ });
+}
+
+SimpleTest.testInChaosMode();
+SimpleTest.waitForExplicitFinish();
+
+pushPrefs([['general.smoothScroll', false],
+ ['mousewheel.transaction.timeout', 1000000]])
+.then(waitUntilApzStable)
+.then(test);
+
+</script>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/test_scroll_subframe_scrollbar.html b/gfx/layers/apz/test/mochitest/test_scroll_subframe_scrollbar.html
new file mode 100644
index 000000000..4d9da8c2c
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/test_scroll_subframe_scrollbar.html
@@ -0,0 +1,117 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test scrolling subframe scrollbars</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="application/javascript" src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+<style>
+p {
+ width:200px;
+ height:200px;
+ border:solid 1px black;
+}
+</style>
+</head>
+<body>
+<p id="subframe">
+1 <br>
+2 <br>
+3 <br>
+4 <br>
+5 <br>
+6 <br>
+7 <br>
+8 <br>
+9 <br>
+10 <br>
+11 <br>
+12 <br>
+13 <br>
+14 <br>
+15 <br>
+16 <br>
+17 <br>
+18 <br>
+19 <br>
+20 <br>
+21 <br>
+22 <br>
+23 <br>
+24 <br>
+25 <br>
+26 <br>
+27 <br>
+28 <br>
+29 <br>
+30 <br>
+31 <br>
+32 <br>
+33 <br>
+34 <br>
+35 <br>
+36 <br>
+37 <br>
+38 <br>
+39 <br>
+40 <br>
+</p>
+<script clss="testbody" type="text/javascript;version=1.7">
+
+var DefaultEvent = {
+ deltaMode: WheelEvent.DOM_DELTA_LINE,
+ deltaX: 0, deltaY: 1,
+ lineOrPageDeltaX: 0, lineOrPageDeltaY: 1,
+};
+
+var ScrollbarWidth = 0;
+
+function test() {
+ var subframe = document.getElementById('subframe');
+ var oldClientWidth = subframe.clientWidth;
+
+ subframe.style.overflow = 'auto';
+ subframe.getBoundingClientRect();
+
+ waitForAllPaintsFlushed(function () {
+ ScrollbarWidth = oldClientWidth - subframe.clientWidth;
+ if (!ScrollbarWidth) {
+ // Probably we have overlay scrollbars - abort the test.
+ ok(true, "overlay scrollbars - skipping test");
+ SimpleTest.finish();
+ return;
+ }
+
+ ok(subframe.scrollHeight > subframe.clientHeight, "subframe should have scrollable content");
+ testScrolling(subframe);
+ });
+}
+
+function testScrolling(subframe) {
+ // Send a wheel event roughly to where we think the trackbar is. We pick a
+ // point at the bottom, in the middle of the trackbar, where the slider is
+ // unlikely to be (since it starts at the top).
+ var posX = subframe.clientWidth + (ScrollbarWidth / 2);
+ var posY = subframe.clientHeight - 20;
+
+ var oldScrollTop = subframe.scrollTop;
+
+ sendWheelAndPaint(subframe, posX, posY, DefaultEvent, function () {
+ ok(subframe.scrollTop > oldScrollTop, "subframe should have scrolled");
+ SimpleTest.finish();
+ });
+}
+
+SimpleTest.waitForExplicitFinish();
+
+pushPrefs([['general.smoothScroll', false],
+ ['mousewheel.transaction.timeout', 0],
+ ['mousewheel.transaction.ignoremovedelay', 0]])
+.then(waitUntilApzStable)
+.then(test);
+
+</script>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/test_smoothness.html b/gfx/layers/apz/test/mochitest/test_smoothness.html
new file mode 100644
index 000000000..88373957a
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/test_smoothness.html
@@ -0,0 +1,77 @@
+<html>
+<head>
+ <title>Test Frame Uniformity While Scrolling</title>
+ <script type="text/javascript"
+ src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
+ <script type="application/javascript"
+ src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+
+ <style>
+ #content {
+ height: 5000px;
+ background: repeating-linear-gradient(#EEE, #EEE 100px, #DDD 100px, #DDD 200px);
+ }
+ </style>
+ <script type="text/javascript">
+ var scrollEvents = 100;
+ var i = 0;
+ var testPref = "gfx.vsync.collect-scroll-transforms";
+ // Scroll points
+ var x = 100;
+ var y = 150;
+
+ SimpleTest.waitForExplicitFinish();
+ var utils = _getDOMWindowUtils(window);
+
+ function sendScrollEvent(aRafTimestamp) {
+ var scrollDiv = document.getElementById("content");
+
+ if (i < scrollEvents) {
+ i++;
+ // Scroll diff
+ var dx = 0;
+ var dy = -10; // Negative to scroll down
+ synthesizeNativeWheelAndWaitForWheelEvent(scrollDiv, x, y, dx, dy);
+ window.requestAnimationFrame(sendScrollEvent);
+ } else {
+ // Locally, with silk and apz + e10s, retina 15" mbp usually get ~1.0 - 1.5
+ // w/o silk + e10s + apz, I get up to 7. Lower is better.
+ // Windows, I get ~3. Values are not valid w/o hardware vsync
+ var uniformities = _getDOMWindowUtils().getFrameUniformityTestData();
+ for (var j = 0; j < uniformities.layerUniformities.length; j++) {
+ var layerResult = uniformities.layerUniformities[j];
+ var layerAddr = layerResult.layerAddress;
+ var uniformity = layerResult.frameUniformity;
+ var msg = "Layer: " + layerAddr.toString(16) + " Uniformity: " + uniformity;
+ SimpleTest.ok((uniformity >= 0) && (uniformity < 4.0), msg);
+ }
+ SimpleTest.finish();
+ }
+ }
+
+ function startTest() {
+ window.requestAnimationFrame(sendScrollEvent);
+ }
+
+ window.onload = function() {
+ var apzEnabled = SpecialPowers.getBoolPref("layers.async-pan-zoom.enabled");
+ if (!apzEnabled) {
+ SimpleTest.ok(true, "APZ not enabled, skipping test");
+ SimpleTest.finish();
+ }
+
+ SpecialPowers.pushPrefEnv({
+ "set" : [
+ [testPref, true]
+ ]
+ }, startTest);
+ }
+ </script>
+</head>
+
+<body>
+ <div id="content">
+ </div>
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/test_touch_listeners_impacting_wheel.html b/gfx/layers/apz/test/mochitest/test_touch_listeners_impacting_wheel.html
new file mode 100644
index 000000000..913269a67
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/test_touch_listeners_impacting_wheel.html
@@ -0,0 +1,114 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1203140
+-->
+<head>
+ <title>Test for Bug 1203140</title>
+ <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="application/javascript" src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <style>
+ </style>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1203140">Mozilla Bug 1203140</a>
+<p id="display"></p>
+<div id="content" style="overflow-y:scroll; height: 400px">
+ <p>The box below has a touch listener and a passive wheel listener. With touch events disabled, APZ shouldn't wait for any listeners.</p>
+ <div id="box" style="width: 200px; height: 200px; background-color: blue"></div>
+ <div style="height: 1000px; width: 10px">Div to make 'content' scrollable</div>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+const kResponseTimeoutMs = 2 * 60 * 1000; // 2 minutes
+
+function takeSnapshots(e) {
+ // Grab some snapshots, and make sure some of them are different (i.e. check
+ // the page is scrolling in the compositor, concurrently with this wheel
+ // listener running).
+ // Note that we want this function to take less time than the content response
+ // timeout, otherwise the scrolling will start even if we haven't returned,
+ // and that would invalidate purpose of the test.
+ var start = Date.now();
+ var lastSnapshot = null;
+ var success = false;
+
+ // Get the position of the 'content' div relative to the screen
+ var rect = rectRelativeToScreen(document.getElementById('content'));
+
+ for (var i = 0; i < 10; i++) {
+ SpecialPowers.DOMWindowUtils.advanceTimeAndRefresh(16);
+ var snapshot = getSnapshot(rect);
+ //dump("Took snapshot " + snapshot + "\n"); // this might help with debugging
+
+ if (lastSnapshot && lastSnapshot != snapshot) {
+ ok(true, "Found some different pixels in snapshot " + i + " compared to previous");
+ success = true;
+ }
+ lastSnapshot = snapshot;
+ }
+ ok(success, "Found some snapshots that were different");
+ ok((Date.now() - start) < kResponseTimeoutMs, "Snapshotting ran quickly enough");
+
+ // Until now, no scroll events will have been dispatched to content. That's
+ // because scroll events are dispatched on the main thread, which we've been
+ // hogging with the code above. At this point we restore the normal refresh
+ // behaviour and let the main thread go back to C++ code, so the scroll events
+ // fire and we unwind from the main test continuation.
+ SpecialPowers.DOMWindowUtils.restoreNormalRefresh();
+}
+
+function* test(testDriver) {
+ var box = document.getElementById('box');
+
+ // Ensure the div is layerized by scrolling it
+ yield moveMouseAndScrollWheelOver(box, 10, 10, testDriver);
+
+ box.addEventListener('touchstart', function(e) {
+ ok(false, "This should never be run");
+ }, false);
+ box.addEventListener('wheel', takeSnapshots, { capture: false, passive: true });
+
+ // Let the event regions and layerization propagate to the APZ
+ yield waitForAllPaints(function() {
+ flushApzRepaints(testDriver);
+ });
+
+ // Take over control of the refresh driver and compositor
+ var utils = SpecialPowers.DOMWindowUtils;
+ utils.advanceTimeAndRefresh(0);
+
+ // Trigger an APZ scroll using a wheel event. If APZ is waiting for a
+ // content response, it will wait for takeSnapshots to finish running before
+ // it starts scrolling, which will cause the checks in takeSnapshots to fail.
+ yield synthesizeNativeMouseMoveAndWaitForMoveEvent(box, 10, 10, testDriver);
+ yield synthesizeNativeWheelAndWaitForScrollEvent(box, 10, 10, 0, -50, testDriver);
+}
+
+if (isApzEnabled()) {
+ SimpleTest.waitForExplicitFinish();
+ // Disable touch events, so that APZ knows not to wait for touch listeners.
+ // Also explicitly set the content response timeout, so we know how long it
+ // is (see comment in takeSnapshots).
+ // Finally, enable smooth scrolling, so that the wheel-scroll we do as part
+ // of the test triggers an APZ animation rather than doing an instant scroll.
+ // Note that this pref doesn't work for the synthesized wheel events on OS X,
+ // those are hard-coded to be instant scrolls.
+ pushPrefs([["dom.w3c_touch_events.enabled", 0],
+ ["apz.content_response_timeout", kResponseTimeoutMs],
+ ["general.smoothscroll", true]])
+ .then(waitUntilApzStable)
+ .then(runContinuation(test))
+ .then(SimpleTest.finish);
+}
+
+</script>
+</pre>
+
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/test_wheel_scroll.html b/gfx/layers/apz/test/mochitest/test_wheel_scroll.html
new file mode 100644
index 000000000..479478d42
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/test_wheel_scroll.html
@@ -0,0 +1,106 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1013412
+-->
+<head>
+ <title>Test for Bug 1013412</title>
+ <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="application/javascript" src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <style>
+ #content {
+ height: 800px;
+ overflow: scroll;
+ }
+
+ #scroller {
+ height: 2000px;
+ background: repeating-linear-gradient(#EEE, #EEE 100px, #DDD 100px, #DDD 200px);
+ }
+
+ #scrollbox {
+ margin-top: 200px;
+ width: 500px;
+ height: 500px;
+ border-radius: 250px;
+ box-shadow: inset 0 0 0 60px #555;
+ background: #777;
+ }
+
+ #circle {
+ position: relative;
+ left: 240px;
+ top: 20px;
+ border: 10px solid white;
+ border-radius: 10px;
+ width: 0px;
+ height: 0px;
+ transform-origin: 10px 230px;
+ will-change: transform;
+ }
+ </style>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1161206">Mozilla Bug 1161206</a>
+<p id="display"></p>
+<div id="content">
+ <p>Scrolling the page should be async, but scrolling over the dark circle should not scroll the page and instead rotate the white ball.</p>
+ <div id="scroller">
+ <div id="scrollbox">
+ <div id="circle"></div>
+ </div>
+ </div>
+</div>
+<pre id="test">
+<script type="application/javascript;version=1.7">
+
+var rotation = 0;
+var rotationAdjusted = false;
+
+var incrementForMode = function (mode) {
+ switch (mode) {
+ case WheelEvent.DOM_DELTA_PIXEL: return 1;
+ case WheelEvent.DOM_DELTA_LINE: return 15;
+ case WheelEvent.DOM_DELTA_PAGE: return 400;
+ }
+ return 0;
+};
+
+document.getElementById("scrollbox").addEventListener("wheel", function (e) {
+ rotation += e.deltaY * incrementForMode(e.deltaMode) * 0.2;
+ document.getElementById("circle").style.transform = "rotate(" + rotation + "deg)";
+ rotationAdjusted = true;
+ e.preventDefault();
+});
+
+function* test(testDriver) {
+ var content = document.getElementById('content');
+ for (i = 0; i < 300; i++) { // enough iterations that we would scroll to the bottom of 'content'
+ yield synthesizeNativeWheelAndWaitForWheelEvent(content, 100, 150, 0, -5, testDriver);
+ }
+ var scrollbox = document.getElementById('scrollbox');
+ is(content.scrollTop > 0, true, "We should have scrolled down somewhat");
+ is(content.scrollTop < content.scrollTopMax, true, "We should not have scrolled to the bottom of the scrollframe");
+ is(rotationAdjusted, true, "The rotation should have been adjusted");
+}
+
+SimpleTest.testInChaosMode();
+SimpleTest.waitForExplicitFinish();
+
+// If we allow smooth scrolling the "smooth" scrolling may cause the page to
+// glide past the scrollbox (which is supposed to stop the scrolling) and so
+// we might end up at the bottom of the page.
+pushPrefs([["general.smoothScroll", false]])
+.then(waitUntilApzStable)
+.then(runContinuation(test))
+.then(SimpleTest.finish);
+
+</script>
+</pre>
+
+</body>
+</html>
diff --git a/gfx/layers/apz/test/mochitest/test_wheel_transactions.html b/gfx/layers/apz/test/mochitest/test_wheel_transactions.html
new file mode 100644
index 000000000..e00e992cd
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/test_wheel_transactions.html
@@ -0,0 +1,137 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1175585
+-->
+<head>
+ <title>Test for Bug 1175585</title>
+ <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="application/javascript" src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+ <script type="application/javascript" src="apz_test_utils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <style>
+ #outer-frame {
+ height: 500px;
+ overflow: scroll;
+ background: repeating-linear-gradient(#CCC, #CCC 100px, #BBB 100px, #BBB 200px);
+ }
+ #inner-frame {
+ margin-top: 25%;
+ height: 200%;
+ width: 75%;
+ overflow: scroll;
+ }
+ #inner-content {
+ height: 200%;
+ width: 200%;
+ background: repeating-linear-gradient(#EEE, #EEE 100px, #DDD 100px, #DDD 200px);
+ }
+ </style>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1175585">APZ wheel transactions test</a>
+<p id="display"></p>
+<div id="outer-frame">
+ <div id="inner-frame">
+ <div id="inner-content"></div>
+ </div>
+</div>
+<pre id="test">
+<script type="application/javascript;version=1.7">
+
+function scrollWheelOver(element, deltaY, testDriver) {
+ synthesizeNativeWheelAndWaitForScrollEvent(element, 10, 10, 0, deltaY, testDriver);
+}
+
+function* test(testDriver) {
+ var outer = document.getElementById('outer-frame');
+ var inner = document.getElementById('inner-frame');
+ var innerContent = document.getElementById('inner-content');
+
+ // Register a wheel event listener that records the target of
+ // the last wheel event, so that we can make assertions about it.
+ var lastWheelTarget;
+ var wheelTargetRecorder = function(e) { lastWheelTarget = e.target; };
+ window.addEventListener("wheel", wheelTargetRecorder);
+
+ // Scroll |outer| to the bottom.
+ while (outer.scrollTop < outer.scrollTopMax) {
+ yield scrollWheelOver(outer, -10, testDriver);
+ }
+
+ // Verify that this has brought |inner| under the wheel.
+ is(lastWheelTarget, innerContent, "'inner-content' should have been brought under the wheel");
+ window.removeEventListener("wheel", wheelTargetRecorder);
+
+ // Immediately after, scroll it back up a bit.
+ yield scrollWheelOver(outer, 10, testDriver);
+
+ // Check that it was |outer| that scrolled back, and |inner| didn't
+ // scroll at all, as all the above scrolls should be in the same
+ // transaction.
+ ok(outer.scrollTop < outer.scrollTopMax, "'outer' should have scrolled back a bit");
+ is(inner.scrollTop, 0, "'inner' should not have scrolled");
+
+ // The next part of the test is related to the transaction timeout.
+ // Turn it down a bit so waiting for the timeout to elapse doesn't
+ // slow down the test harness too much.
+ var timeout = 5;
+ yield SpecialPowers.pushPrefEnv({"set": [["mousewheel.transaction.timeout", timeout]]}, testDriver);
+ SimpleTest.requestFlakyTimeout("we are testing code that measures actual elapsed time between two events");
+
+ // Scroll up a bit more. It's still |outer| scrolling because
+ // |inner| is still scrolled all the way to the top.
+ yield scrollWheelOver(outer, 10, testDriver);
+
+ // Wait for the transaction timeout to elapse.
+ // timeout * 5 is used to make it less likely that the timeout is less than
+ // the system timestamp resolution
+ yield window.setTimeout(testDriver, timeout * 5);
+
+ // Now scroll down. The transaction having timed out, the event
+ // should pick up a new target, and that should be |inner|.
+ yield scrollWheelOver(outer, -10, testDriver);
+ ok(inner.scrollTop > 0, "'inner' should have been scrolled");
+
+ // Finally, test scroll handoff after a timeout.
+
+ // Continue scrolling |inner| down to the bottom.
+ var prevScrollTop = inner.scrollTop;
+ while (inner.scrollTop < inner.scrollTopMax) {
+ yield scrollWheelOver(outer, -10, testDriver);
+ // Avoid a failure getting us into an infinite loop.
+ ok(inner.scrollTop > prevScrollTop, "scrolling down should increase scrollTop");
+ prevScrollTop = inner.scrollTop;
+ }
+
+ // Wait for the transaction timeout to elapse.
+ // timeout * 5 is used to make it less likely that the timeout is less than
+ // the system timestamp resolution
+ yield window.setTimeout(testDriver, timeout * 5);
+
+ // Continued downward scrolling should scroll |outer| to the bottom.
+ prevScrollTop = outer.scrollTop;
+ while (outer.scrollTop < outer.scrollTopMax) {
+ yield scrollWheelOver(outer, -10, testDriver);
+ // Avoid a failure getting us into an infinite loop.
+ ok(outer.scrollTop > prevScrollTop, "scrolling down should increase scrollTop");
+ prevScrollTop = outer.scrollTop;
+ }
+}
+
+SimpleTest.waitForExplicitFinish();
+
+// Disable smooth scrolling because it makes the test flaky (we don't have a good
+// way of detecting when the scrolling is finished).
+pushPrefs([["general.smoothScroll", false]])
+.then(waitUntilApzStable)
+.then(runContinuation(test))
+.then(SimpleTest.finish);
+
+</script>
+</pre>
+
+</body>
+</html>
diff --git a/gfx/layers/apz/test/reftest/async-scrollbar-1-h-ref.html b/gfx/layers/apz/test/reftest/async-scrollbar-1-h-ref.html
new file mode 100644
index 000000000..0e2698b86
--- /dev/null
+++ b/gfx/layers/apz/test/reftest/async-scrollbar-1-h-ref.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html class="reftest-wait"><head>
+<meta name="viewport" content="width=device-width">
+</head>
+<body onload="scrollTo(450,0); document.documentElement.classList.remove('reftest-wait')">
+<div style="width: 9000px; height: 10px; background: white;"></div>
+</body>
+</html>
+
diff --git a/gfx/layers/apz/test/reftest/async-scrollbar-1-h-rtl-ref.html b/gfx/layers/apz/test/reftest/async-scrollbar-1-h-rtl-ref.html
new file mode 100644
index 000000000..ee2524a16
--- /dev/null
+++ b/gfx/layers/apz/test/reftest/async-scrollbar-1-h-rtl-ref.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<html class="reftest-wait"><head>
+<meta name="viewport" content="width=device-width">
+<style> html { direction: rtl; } </style>
+</head>
+<body onload="scrollTo(-450,0); document.documentElement.classList.remove('reftest-wait')">
+<div style="width: 9000px; height: 10px; background: white;"></div>
+</body>
+</html>
+
diff --git a/gfx/layers/apz/test/reftest/async-scrollbar-1-h-rtl.html b/gfx/layers/apz/test/reftest/async-scrollbar-1-h-rtl.html
new file mode 100644
index 000000000..2f3d94639
--- /dev/null
+++ b/gfx/layers/apz/test/reftest/async-scrollbar-1-h-rtl.html
@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<html class="reftest-wait"
+ reftest-async-scroll
+ reftest-async-scroll-x="-449" reftest-async-scroll-y="0"><head>
+<meta name="viewport" content="width=device-width">
+<style> html { direction: rtl; } </style>
+</head>
+<!-- Doing scrollTo(-1,0) is to activate the right arrow in the scrollbar
+ for non-overlay scrollbar environments -->
+<body onload="scrollTo(-1,0); document.documentElement.classList.remove('reftest-wait')">
+<div style="width: 9000px; height: 10px; background: white"></div>
+</body>
+</html>
+
diff --git a/gfx/layers/apz/test/reftest/async-scrollbar-1-h.html b/gfx/layers/apz/test/reftest/async-scrollbar-1-h.html
new file mode 100644
index 000000000..1eca19246
--- /dev/null
+++ b/gfx/layers/apz/test/reftest/async-scrollbar-1-h.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html class="reftest-wait"
+ reftest-async-scroll
+ reftest-async-scroll-x="449" reftest-async-scroll-y="0"><head>
+<meta name="viewport" content="width=device-width">
+</head>
+<!-- Doing scrollTo(1,0) is to activate the left arrow in the scrollbar
+ for non-overlay scrollbar environments -->
+<body onload="scrollTo(1,0); document.documentElement.classList.remove('reftest-wait')">
+<div style="width: 9000px; height: 10px; background: white"></div>
+</body>
+</html>
+
diff --git a/gfx/layers/apz/test/reftest/async-scrollbar-1-v-ref.html b/gfx/layers/apz/test/reftest/async-scrollbar-1-v-ref.html
new file mode 100644
index 000000000..9ac5485bc
--- /dev/null
+++ b/gfx/layers/apz/test/reftest/async-scrollbar-1-v-ref.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html class="reftest-wait"><head>
+<meta name="viewport" content="width=device-width">
+</head>
+<body onload="scrollTo(0,10000); document.documentElement.classList.remove('reftest-wait')">
+<div style="width: 10px; height: 20000px; background: white;"></div>
+</body>
+</html>
+
diff --git a/gfx/layers/apz/test/reftest/async-scrollbar-1-v-rtl-ref.html b/gfx/layers/apz/test/reftest/async-scrollbar-1-v-rtl-ref.html
new file mode 100644
index 000000000..94fb501ba
--- /dev/null
+++ b/gfx/layers/apz/test/reftest/async-scrollbar-1-v-rtl-ref.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<html class="reftest-wait"><head>
+<meta name="viewport" content="width=device-width">
+<style> html { direction: rtl; } </style>
+</head>
+<body onload="scrollTo(0,10000); document.documentElement.classList.remove('reftest-wait')">
+<div style="width: 10px; height: 20000px; background: white;"></div>
+</body>
+</html>
+
diff --git a/gfx/layers/apz/test/reftest/async-scrollbar-1-v-rtl.html b/gfx/layers/apz/test/reftest/async-scrollbar-1-v-rtl.html
new file mode 100644
index 000000000..9a2eb8818
--- /dev/null
+++ b/gfx/layers/apz/test/reftest/async-scrollbar-1-v-rtl.html
@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<html class="reftest-wait"
+ reftest-async-scroll
+ reftest-async-scroll-x="0" reftest-async-scroll-y="9999"><head>
+<meta name="viewport" content="width=device-width">
+<style> html { direction: rtl; } </style>
+</head>
+<!-- Doing scrollTo(0,1) is to activate the up arrow in the scrollbar
+ for non-overlay scrollbar environments -->
+<body onload="scrollTo(0,1); document.documentElement.classList.remove('reftest-wait')">
+<div style="width: 10px; height: 20000px; background: white;"></div>
+</body>
+</html>
+
diff --git a/gfx/layers/apz/test/reftest/async-scrollbar-1-v.html b/gfx/layers/apz/test/reftest/async-scrollbar-1-v.html
new file mode 100644
index 000000000..56fe23c28
--- /dev/null
+++ b/gfx/layers/apz/test/reftest/async-scrollbar-1-v.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html class="reftest-wait"
+ reftest-async-scroll
+ reftest-async-scroll-x="0" reftest-async-scroll-y="9999"><head>
+<meta name="viewport" content="width=device-width">
+</head>
+<!-- Doing scrollTo(0,1) is to activate the up arrow in the scrollbar
+ for non-overlay scrollbar environments -->
+<body onload="scrollTo(0,1); document.documentElement.classList.remove('reftest-wait')">
+<div style="width: 10px; height: 20000px; background: white;"></div>
+</body>
+</html>
+
diff --git a/gfx/layers/apz/test/reftest/async-scrollbar-1-vh-ref.html b/gfx/layers/apz/test/reftest/async-scrollbar-1-vh-ref.html
new file mode 100644
index 000000000..564697b37
--- /dev/null
+++ b/gfx/layers/apz/test/reftest/async-scrollbar-1-vh-ref.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html class="reftest-wait"><head>
+<meta name="viewport" content="width=device-width">
+</head>
+<body onload="scrollTo(450,8000); document.documentElement.classList.remove('reftest-wait')">
+<div style="width: 9000px; height: 20000px; background: white;"></div>
+</body>
+</html>
+
diff --git a/gfx/layers/apz/test/reftest/async-scrollbar-1-vh-rtl-ref.html b/gfx/layers/apz/test/reftest/async-scrollbar-1-vh-rtl-ref.html
new file mode 100644
index 000000000..78cb0332c
--- /dev/null
+++ b/gfx/layers/apz/test/reftest/async-scrollbar-1-vh-rtl-ref.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<html class="reftest-wait"><head>
+<meta name="viewport" content="width=device-width">
+<style> html { direction: rtl; } </style>
+</head>
+<body onload="scrollTo(-450,8000); document.documentElement.classList.remove('reftest-wait')">
+<div style="width: 9000px; height: 20000px; background: white;"></div>
+</body>
+</html>
+
diff --git a/gfx/layers/apz/test/reftest/async-scrollbar-1-vh-rtl.html b/gfx/layers/apz/test/reftest/async-scrollbar-1-vh-rtl.html
new file mode 100644
index 000000000..397d1cf9b
--- /dev/null
+++ b/gfx/layers/apz/test/reftest/async-scrollbar-1-vh-rtl.html
@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<html class="reftest-wait"
+ reftest-async-scroll
+ reftest-async-scroll-x="-440" reftest-async-scroll-y="7999"><head>
+<meta name="viewport" content="width=device-width">
+<style> html { direction: rtl; } </style>
+</head>
+<!-- Doing scrollTo(-10,1) is to activate the right/up arrows in the scrollbars
+ for non-overlay scrollbar environments -->
+<body onload="scrollTo(-10,1); document.documentElement.classList.remove('reftest-wait')">
+<div style="width: 9000px; height: 20000px; background: white;"></div>
+</body>
+</html>
+
diff --git a/gfx/layers/apz/test/reftest/async-scrollbar-1-vh.html b/gfx/layers/apz/test/reftest/async-scrollbar-1-vh.html
new file mode 100644
index 000000000..a1d1527dd
--- /dev/null
+++ b/gfx/layers/apz/test/reftest/async-scrollbar-1-vh.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html class="reftest-wait"
+ reftest-async-scroll
+ reftest-async-scroll-x="449" reftest-async-scroll-y="7999"><head>
+<meta name="viewport" content="width=device-width">
+</head>
+<!-- Doing scrollTo(1,1) is to activate the left/up arrows in the scrollbars
+ for non-overlay scrollbar environments -->
+<body onload="scrollTo(1,1); document.documentElement.classList.remove('reftest-wait')">
+<div style="width: 9000px; height: 20000px; background: white;"></div>
+</body>
+</html>
+
diff --git a/gfx/layers/apz/test/reftest/async-scrollbar-zoom-1-ref.html b/gfx/layers/apz/test/reftest/async-scrollbar-zoom-1-ref.html
new file mode 100644
index 000000000..5ed970f76
--- /dev/null
+++ b/gfx/layers/apz/test/reftest/async-scrollbar-zoom-1-ref.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html class="reftest-wait"><head>
+<meta name="viewport" content="width=device-width">
+</head>
+<body onload="scrollTo(450,10000); document.documentElement.classList.remove('reftest-wait')">
+<div style="width: 9000px; height: 20000px; background: white;"></div>
+</body>
+</html>
+
diff --git a/gfx/layers/apz/test/reftest/async-scrollbar-zoom-1.html b/gfx/layers/apz/test/reftest/async-scrollbar-zoom-1.html
new file mode 100644
index 000000000..09be51a79
--- /dev/null
+++ b/gfx/layers/apz/test/reftest/async-scrollbar-zoom-1.html
@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<html class="reftest-wait"
+ reftest-async-scroll
+ reftest-async-scroll-x="224" reftest-async-scroll-y="4999"
+ reftest-async-zoom="2.0"><head>
+<meta name="viewport" content="width=device-width">
+</head>
+<!-- Doing scrollTo(1,1) is to activate the left/up arrows in the scrollbars
+ for non-overlay scrollbar environments -->
+<body onload="scrollTo(1,1); document.documentElement.classList.remove('reftest-wait')">
+<div style="width: 4500px; height: 10000px; background: white;"></div>
+</body>
+</html>
+
diff --git a/gfx/layers/apz/test/reftest/async-scrollbar-zoom-2-ref.html b/gfx/layers/apz/test/reftest/async-scrollbar-zoom-2-ref.html
new file mode 100644
index 000000000..5ed970f76
--- /dev/null
+++ b/gfx/layers/apz/test/reftest/async-scrollbar-zoom-2-ref.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html class="reftest-wait"><head>
+<meta name="viewport" content="width=device-width">
+</head>
+<body onload="scrollTo(450,10000); document.documentElement.classList.remove('reftest-wait')">
+<div style="width: 9000px; height: 20000px; background: white;"></div>
+</body>
+</html>
+
diff --git a/gfx/layers/apz/test/reftest/async-scrollbar-zoom-2.html b/gfx/layers/apz/test/reftest/async-scrollbar-zoom-2.html
new file mode 100644
index 000000000..abe822c21
--- /dev/null
+++ b/gfx/layers/apz/test/reftest/async-scrollbar-zoom-2.html
@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<html class="reftest-wait"
+ reftest-async-scroll
+ reftest-async-scroll-x="899" reftest-async-scroll-y="19999"
+ reftest-async-zoom="0.5"><head>
+<meta name="viewport" content="width=device-width">
+</head>
+<!-- Doing scrollTo(1,1) is to activate the left/up arrows in the scrollbars
+ for non-overlay scrollbar environments -->
+<body onload="scrollTo(1,1); document.documentElement.classList.remove('reftest-wait')">
+<div style="width: 18000px; height: 40000px; background: white;"></div>
+</body>
+</html>
+
diff --git a/gfx/layers/apz/test/reftest/frame-reconstruction-scroll-clamping-ref.html b/gfx/layers/apz/test/reftest/frame-reconstruction-scroll-clamping-ref.html
new file mode 100644
index 000000000..3db9f2969
--- /dev/null
+++ b/gfx/layers/apz/test/reftest/frame-reconstruction-scroll-clamping-ref.html
@@ -0,0 +1,27 @@
+<html>
+<script>
+ function run() {
+ document.body.classList.toggle('noscroll');
+ document.getElementById('spacer').style.height = '100%';
+ // Scroll to the very end, including any fractional pixels
+ document.body.scrollTop = document.body.scrollTopMax + 1;
+ }
+</script>
+<style>
+ html, body {
+ margin: 0;
+ padding: 0;
+ background-color: green;
+ }
+
+ .noscroll {
+ overflow: hidden;
+ height: 100%;
+ }
+</style>
+<body onload="run()">
+ <div id="spacer" style="height: 5000px">
+ This is the top of the page.
+ </div>
+ This is the bottom of the page.
+</body>
diff --git a/gfx/layers/apz/test/reftest/frame-reconstruction-scroll-clamping.html b/gfx/layers/apz/test/reftest/frame-reconstruction-scroll-clamping.html
new file mode 100644
index 000000000..479363f3f
--- /dev/null
+++ b/gfx/layers/apz/test/reftest/frame-reconstruction-scroll-clamping.html
@@ -0,0 +1,53 @@
+<html class="reftest-wait">
+<!--
+For bug 1266833; syncing the scroll offset to APZ properly when the scroll
+position is clamped to a smaller value during a frame reconstruction.
+-->
+<script>
+ function run() {
+ document.body.scrollTop = document.body.scrollTopMax;
+
+ // Let the scroll position propagate to APZ before we do the frame
+ // reconstruction. Ideally we would wait for flushApzRepaints here but
+ // we don't have access to DOMWindowUtils in a reftest, so we just wait
+ // 100ms to approximate it. With bug 1266833 fixed, this test should
+ // never fail regardless of what this timeout value is.
+ setTimeout(frameReconstruction, 100);
+ }
+
+ function frameReconstruction() {
+ document.body.classList.toggle('noscroll');
+ document.documentElement.classList.toggle('reconstruct-body');
+ document.getElementById('spacer').style.height = '100%';
+ document.documentElement.classList.remove('reftest-wait');
+ }
+</script>
+<style>
+ html, body {
+ margin: 0;
+ padding: 0;
+ background-color: green;
+ }
+
+ .noscroll {
+ overflow: hidden;
+ height: 100%;
+ }
+
+ /* Toggling this on and off triggers a frame reconstruction on the <body> */
+ html.reconstruct-body::before {
+ top: 0;
+ content: '';
+ display: block;
+ height: 2px;
+ position: absolute;
+ width: 100%;
+ z-index: 99;
+ }
+</style>
+<body onload="setTimeout(run, 0)">
+ <div id="spacer" style="height: 5000px">
+ This is the top of the page.
+ </div>
+ This is the bottom of the page.
+</body>
diff --git a/gfx/layers/apz/test/reftest/initial-scale-1-ref.html b/gfx/layers/apz/test/reftest/initial-scale-1-ref.html
new file mode 100644
index 000000000..dc99712d3
--- /dev/null
+++ b/gfx/layers/apz/test/reftest/initial-scale-1-ref.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<html><head>
+<meta name="viewport" content="width=device-width">
+</head>
+<body>
+This tests that an initial-scale of 0 (i.e. garbage) is overridden<br/>
+with something a little more sane.
+</body>
+</html>
+
diff --git a/gfx/layers/apz/test/reftest/initial-scale-1.html b/gfx/layers/apz/test/reftest/initial-scale-1.html
new file mode 100644
index 000000000..45bed0809
--- /dev/null
+++ b/gfx/layers/apz/test/reftest/initial-scale-1.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<html><head>
+<meta name="viewport" content="initial-scale=0; width=device-width">
+</head>
+<body>
+This tests that an initial-scale of 0 (i.e. garbage) is overridden<br/>
+with something a little more sane.
+</body>
+</html>
+
diff --git a/gfx/layers/apz/test/reftest/reftest-stylo.list b/gfx/layers/apz/test/reftest/reftest-stylo.list
new file mode 100644
index 000000000..cc2c76827
--- /dev/null
+++ b/gfx/layers/apz/test/reftest/reftest-stylo.list
@@ -0,0 +1,20 @@
+# DO NOT EDIT! This is a auto-generated temporary list for Stylo testing
+# The following tests test the async positioning of the scrollbars.
+# Basic root-frame scrollbar with async scrolling
+skip-if(!asyncPan) fuzzy-if(Android,6,8) == async-scrollbar-1-v.html async-scrollbar-1-v.html
+skip-if(!asyncPan) fuzzy-if(Android,6,8) == async-scrollbar-1-h.html async-scrollbar-1-h.html
+skip-if(!asyncPan) fuzzy-if(Android,6,8) == async-scrollbar-1-vh.html async-scrollbar-1-vh.html
+skip-if(!asyncPan) fuzzy-if(Android,6,8) == async-scrollbar-1-v-rtl.html async-scrollbar-1-v-rtl.html
+skip-if(!asyncPan) fuzzy-if(Android,13,8) == async-scrollbar-1-h-rtl.html async-scrollbar-1-h-rtl.html
+skip-if(!asyncPan) fuzzy-if(Android,8,10) == async-scrollbar-1-vh-rtl.html async-scrollbar-1-vh-rtl.html
+
+# Different async zoom levels. Since the scrollthumb gets async-scaled in the
+# compositor, the border-radius ends of the scrollthumb are going to be a little
+# off, hence the fuzzy-if clauses.
+skip-if(!asyncZoom) fuzzy-if(B2G,98,82) == async-scrollbar-zoom-1.html async-scrollbar-zoom-1.html
+skip-if(!asyncZoom) fuzzy-if(B2G,94,146) == async-scrollbar-zoom-2.html async-scrollbar-zoom-2.html
+
+# Meta-viewport tag support
+skip-if(!asyncZoom) == initial-scale-1.html initial-scale-1.html
+
+skip-if(!asyncPan) == frame-reconstruction-scroll-clamping.html frame-reconstruction-scroll-clamping.html
diff --git a/gfx/layers/apz/test/reftest/reftest.list b/gfx/layers/apz/test/reftest/reftest.list
new file mode 100644
index 000000000..4ab29420c
--- /dev/null
+++ b/gfx/layers/apz/test/reftest/reftest.list
@@ -0,0 +1,19 @@
+# The following tests test the async positioning of the scrollbars.
+# Basic root-frame scrollbar with async scrolling
+fuzzy-if(Android,1,2) skip-if(!Android) pref(apz.allow_zooming,true) == async-scrollbar-1-v.html async-scrollbar-1-v-ref.html
+fuzzy-if(Android,4,5) skip-if(!Android) pref(apz.allow_zooming,true) == async-scrollbar-1-h.html async-scrollbar-1-h-ref.html
+fuzzy-if(Android,3,5) skip-if(!Android) pref(apz.allow_zooming,true) == async-scrollbar-1-vh.html async-scrollbar-1-vh-ref.html
+fuzzy-if(Android,1,2) skip-if(!Android) pref(apz.allow_zooming,true) == async-scrollbar-1-v-rtl.html async-scrollbar-1-v-rtl-ref.html
+fuzzy-if(Android,4,5) skip-if(!Android) pref(apz.allow_zooming,true) == async-scrollbar-1-h-rtl.html async-scrollbar-1-h-rtl-ref.html
+fuzzy-if(Android,3,7) skip-if(!Android) pref(apz.allow_zooming,true) == async-scrollbar-1-vh-rtl.html async-scrollbar-1-vh-rtl-ref.html
+
+# Different async zoom levels. Since the scrollthumb gets async-scaled in the
+# compositor, the border-radius ends of the scrollthumb are going to be a little
+# off, hence the fuzzy-if clauses.
+fuzzy-if(Android,54,18) skip-if(!Android) pref(apz.allow_zooming,true) == async-scrollbar-zoom-1.html async-scrollbar-zoom-1-ref.html
+fuzzy-if(Android,45,21) skip-if(!Android) pref(apz.allow_zooming,true) == async-scrollbar-zoom-2.html async-scrollbar-zoom-2-ref.html
+
+# Meta-viewport tag support
+skip-if(!Android) pref(apz.allow_zooming,true) == initial-scale-1.html initial-scale-1-ref.html
+
+skip-if(!asyncPan) == frame-reconstruction-scroll-clamping.html frame-reconstruction-scroll-clamping-ref.html
diff --git a/gfx/layers/apz/testutil/APZTestData.cpp b/gfx/layers/apz/testutil/APZTestData.cpp
new file mode 100644
index 000000000..3c9440b64
--- /dev/null
+++ b/gfx/layers/apz/testutil/APZTestData.cpp
@@ -0,0 +1,66 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "APZTestData.h"
+#include "mozilla/dom/APZTestDataBinding.h"
+#include "mozilla/dom/ToJSValue.h"
+#include "nsString.h"
+
+namespace mozilla {
+namespace layers {
+
+struct APZTestDataToJSConverter {
+ template <typename Key, typename Value, typename KeyValuePair>
+ static void ConvertMap(const std::map<Key, Value>& aFrom,
+ dom::Sequence<KeyValuePair>& aOutTo,
+ void (*aElementConverter)(const Key&, const Value&, KeyValuePair&)) {
+ for (auto it = aFrom.begin(); it != aFrom.end(); ++it) {
+ aOutTo.AppendElement(fallible);
+ aElementConverter(it->first, it->second, aOutTo.LastElement());
+ }
+ }
+
+ static void ConvertAPZTestData(const APZTestData& aFrom,
+ dom::APZTestData& aOutTo) {
+ ConvertMap(aFrom.mPaints, aOutTo.mPaints.Construct(), ConvertBucket);
+ ConvertMap(aFrom.mRepaintRequests, aOutTo.mRepaintRequests.Construct(), ConvertBucket);
+ }
+
+ static void ConvertBucket(const SequenceNumber& aKey,
+ const APZTestData::Bucket& aValue,
+ dom::APZBucket& aOutKeyValuePair) {
+ aOutKeyValuePair.mSequenceNumber.Construct() = aKey;
+ ConvertMap(aValue, aOutKeyValuePair.mScrollFrames.Construct(), ConvertScrollFrameData);
+ }
+
+ static void ConvertScrollFrameData(const APZTestData::ViewID& aKey,
+ const APZTestData::ScrollFrameData& aValue,
+ dom::ScrollFrameData& aOutKeyValuePair) {
+ aOutKeyValuePair.mScrollId.Construct() = aKey;
+ ConvertMap(aValue, aOutKeyValuePair.mEntries.Construct(), ConvertEntry);
+ }
+
+ static void ConvertEntry(const std::string& aKey,
+ const std::string& aValue,
+ dom::ScrollFrameDataEntry& aOutKeyValuePair) {
+ ConvertString(aKey, aOutKeyValuePair.mKey.Construct());
+ ConvertString(aValue, aOutKeyValuePair.mValue.Construct());
+ }
+
+ static void ConvertString(const std::string& aFrom, nsString& aOutTo) {
+ aOutTo = NS_ConvertUTF8toUTF16(aFrom.c_str(), aFrom.size());
+ }
+};
+
+bool
+APZTestData::ToJS(JS::MutableHandleValue aOutValue, JSContext* aContext) const
+{
+ dom::APZTestData result;
+ APZTestDataToJSConverter::ConvertAPZTestData(*this, result);
+ return dom::ToJSValue(aContext, result, aOutValue);
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/apz/testutil/APZTestData.h b/gfx/layers/apz/testutil/APZTestData.h
new file mode 100644
index 000000000..c0cc35b5a
--- /dev/null
+++ b/gfx/layers/apz/testutil/APZTestData.h
@@ -0,0 +1,168 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_layers_APZTestData_h
+#define mozilla_layers_APZTestData_h
+
+#include <map>
+
+#include "FrameMetrics.h"
+#include "nsDebug.h" // for NS_WARNING
+#include "mozilla/Assertions.h" // for MOZ_ASSERT
+#include "mozilla/DebugOnly.h" // for DebugOnly
+#include "mozilla/ToString.h" // for ToString
+#include "ipc/IPCMessageUtils.h"
+#include "js/TypeDecls.h"
+
+namespace mozilla {
+namespace layers {
+
+typedef uint32_t SequenceNumber;
+
+/**
+ * This structure is used to store information logged by various gecko
+ * components for later examination by test code.
+ * It consists of a bucket for every paint (initiated on the client side),
+ * and every repaint request (initiated on the compositor side by
+ * AsyncPanZoomController::RequestContentRepait), which are identified by
+ * sequence numbers, and within that, a set of arbitrary string key/value
+ * pairs for every scrollable frame, identified by a scroll id.
+ * There are two instances of this data structure for every content thread:
+ * one on the client side and one of the compositor side.
+ */
+// TODO(botond):
+// - Improve warnings/asserts.
+// - Add ability to associate a repaint request triggered during a layers update
+// with the sequence number of the paint that caused the layers update.
+class APZTestData {
+ typedef FrameMetrics::ViewID ViewID;
+ friend struct IPC::ParamTraits<APZTestData>;
+ friend struct APZTestDataToJSConverter;
+public:
+ void StartNewPaint(SequenceNumber aSequenceNumber) {
+ // We should never get more than one paint with the same sequence number.
+ MOZ_ASSERT(mPaints.find(aSequenceNumber) == mPaints.end());
+ mPaints.insert(DataStore::value_type(aSequenceNumber, Bucket()));
+ }
+ void LogTestDataForPaint(SequenceNumber aSequenceNumber,
+ ViewID aScrollId,
+ const std::string& aKey,
+ const std::string& aValue) {
+ LogTestDataImpl(mPaints, aSequenceNumber, aScrollId, aKey, aValue);
+ }
+
+ void StartNewRepaintRequest(SequenceNumber aSequenceNumber) {
+ typedef std::pair<DataStore::iterator, bool> InsertResultT;
+ DebugOnly<InsertResultT> insertResult = mRepaintRequests.insert(DataStore::value_type(aSequenceNumber, Bucket()));
+ MOZ_ASSERT(((InsertResultT&)insertResult).second, "Already have a repaint request with this sequence number");
+ }
+ void LogTestDataForRepaintRequest(SequenceNumber aSequenceNumber,
+ ViewID aScrollId,
+ const std::string& aKey,
+ const std::string& aValue) {
+ LogTestDataImpl(mRepaintRequests, aSequenceNumber, aScrollId, aKey, aValue);
+ }
+
+ // Convert this object to a JS representation.
+ bool ToJS(JS::MutableHandleValue aOutValue, JSContext* aContext) const;
+
+ // Use dummy derived structures wrapping the tyepdefs to work around a type
+ // name length limit in MSVC.
+ typedef std::map<std::string, std::string> ScrollFrameDataBase;
+ struct ScrollFrameData : ScrollFrameDataBase {};
+ typedef std::map<ViewID, ScrollFrameData> BucketBase;
+ struct Bucket : BucketBase {};
+ typedef std::map<SequenceNumber, Bucket> DataStoreBase;
+ struct DataStore : DataStoreBase {};
+private:
+ DataStore mPaints;
+ DataStore mRepaintRequests;
+
+ void LogTestDataImpl(DataStore& aDataStore,
+ SequenceNumber aSequenceNumber,
+ ViewID aScrollId,
+ const std::string& aKey,
+ const std::string& aValue) {
+ auto bucketIterator = aDataStore.find(aSequenceNumber);
+ if (bucketIterator == aDataStore.end()) {
+ MOZ_ASSERT(false, "LogTestDataImpl called with nonexistent sequence number");
+ return;
+ }
+ Bucket& bucket = bucketIterator->second;
+ ScrollFrameData& scrollFrameData = bucket[aScrollId]; // create if doesn't exist
+ MOZ_ASSERT(scrollFrameData.find(aKey) == scrollFrameData.end()
+ || scrollFrameData[aKey] == aValue);
+ scrollFrameData.insert(ScrollFrameData::value_type(aKey, aValue));
+ }
+};
+
+// A helper class for logging data for a paint.
+class APZPaintLogHelper {
+public:
+ APZPaintLogHelper(APZTestData* aTestData, SequenceNumber aPaintSequenceNumber)
+ : mTestData(aTestData),
+ mPaintSequenceNumber(aPaintSequenceNumber)
+ {}
+
+ template <typename Value>
+ void LogTestData(FrameMetrics::ViewID aScrollId,
+ const std::string& aKey,
+ const Value& aValue) const {
+ if (mTestData) { // avoid stringifying if mTestData == nullptr
+ LogTestData(aScrollId, aKey, ToString(aValue));
+ }
+ }
+
+ void LogTestData(FrameMetrics::ViewID aScrollId,
+ const std::string& aKey,
+ const std::string& aValue) const {
+ if (mTestData) {
+ mTestData->LogTestDataForPaint(mPaintSequenceNumber, aScrollId, aKey, aValue);
+ }
+ }
+private:
+ APZTestData* mTestData;
+ SequenceNumber mPaintSequenceNumber;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+namespace IPC {
+
+template <>
+struct ParamTraits<mozilla::layers::APZTestData>
+{
+ typedef mozilla::layers::APZTestData paramType;
+
+ static void Write(Message* aMsg, const paramType& aParam)
+ {
+ WriteParam(aMsg, aParam.mPaints);
+ WriteParam(aMsg, aParam.mRepaintRequests);
+ }
+
+ static bool Read(const Message* aMsg, PickleIterator* aIter, paramType* aResult)
+ {
+ return (ReadParam(aMsg, aIter, &aResult->mPaints) &&
+ ReadParam(aMsg, aIter, &aResult->mRepaintRequests));
+ }
+};
+
+template <>
+struct ParamTraits<mozilla::layers::APZTestData::ScrollFrameData>
+ : ParamTraits<mozilla::layers::APZTestData::ScrollFrameDataBase> {};
+
+template <>
+struct ParamTraits<mozilla::layers::APZTestData::Bucket>
+ : ParamTraits<mozilla::layers::APZTestData::BucketBase> {};
+
+template <>
+struct ParamTraits<mozilla::layers::APZTestData::DataStore>
+ : ParamTraits<mozilla::layers::APZTestData::DataStoreBase> {};
+
+} // namespace IPC
+
+
+#endif /* mozilla_layers_APZTestData_h */
diff --git a/gfx/layers/apz/util/APZCCallbackHelper.cpp b/gfx/layers/apz/util/APZCCallbackHelper.cpp
new file mode 100644
index 000000000..3f33a59e4
--- /dev/null
+++ b/gfx/layers/apz/util/APZCCallbackHelper.cpp
@@ -0,0 +1,942 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "APZCCallbackHelper.h"
+
+#include "TouchActionHelper.h"
+#include "gfxPlatform.h" // For gfxPlatform::UseTiling
+#include "gfxPrefs.h"
+#include "LayersLogging.h" // For Stringify
+#include "mozilla/dom/Element.h"
+#include "mozilla/dom/TabParent.h"
+#include "mozilla/IntegerPrintfMacros.h"
+#include "mozilla/layers/LayerTransactionChild.h"
+#include "mozilla/layers/ShadowLayers.h"
+#include "mozilla/TouchEvents.h"
+#include "nsContentUtils.h"
+#include "nsContainerFrame.h"
+#include "nsIScrollableFrame.h"
+#include "nsLayoutUtils.h"
+#include "nsIInterfaceRequestorUtils.h"
+#include "nsIContent.h"
+#include "nsIDocument.h"
+#include "nsIDOMWindow.h"
+#include "nsIDOMWindowUtils.h"
+#include "nsRefreshDriver.h"
+#include "nsString.h"
+#include "nsView.h"
+#include "Layers.h"
+
+#define APZCCH_LOG(...)
+// #define APZCCH_LOG(...) printf_stderr("APZCCH: " __VA_ARGS__)
+
+namespace mozilla {
+namespace layers {
+
+using dom::TabParent;
+
+uint64_t APZCCallbackHelper::sLastTargetAPZCNotificationInputBlock = uint64_t(-1);
+
+void
+APZCCallbackHelper::AdjustDisplayPortForScrollDelta(
+ mozilla::layers::FrameMetrics& aFrameMetrics,
+ const CSSPoint& aActualScrollOffset)
+{
+ // Correct the display-port by the difference between the requested scroll
+ // offset and the resulting scroll offset after setting the requested value.
+ ScreenPoint shift =
+ (aFrameMetrics.GetScrollOffset() - aActualScrollOffset) *
+ aFrameMetrics.DisplayportPixelsPerCSSPixel();
+ ScreenMargin margins = aFrameMetrics.GetDisplayPortMargins();
+ margins.left -= shift.x;
+ margins.right += shift.x;
+ margins.top -= shift.y;
+ margins.bottom += shift.y;
+ aFrameMetrics.SetDisplayPortMargins(margins);
+}
+
+static void
+RecenterDisplayPort(mozilla::layers::FrameMetrics& aFrameMetrics)
+{
+ ScreenMargin margins = aFrameMetrics.GetDisplayPortMargins();
+ margins.right = margins.left = margins.LeftRight() / 2;
+ margins.top = margins.bottom = margins.TopBottom() / 2;
+ aFrameMetrics.SetDisplayPortMargins(margins);
+}
+
+static CSSPoint
+ScrollFrameTo(nsIScrollableFrame* aFrame, const FrameMetrics& aMetrics, bool& aSuccessOut)
+{
+ aSuccessOut = false;
+ CSSPoint targetScrollPosition = aMetrics.GetScrollOffset();
+
+ if (!aFrame) {
+ return targetScrollPosition;
+ }
+
+ CSSPoint geckoScrollPosition = CSSPoint::FromAppUnits(aFrame->GetScrollPosition());
+
+ // If the repaint request was triggered due to a previous main-thread scroll
+ // offset update sent to the APZ, then we don't need to do another scroll here
+ // and we can just return.
+ if (!aMetrics.GetScrollOffsetUpdated()) {
+ return geckoScrollPosition;
+ }
+
+ // If the frame is overflow:hidden on a particular axis, we don't want to allow
+ // user-driven scroll on that axis. Simply set the scroll position on that axis
+ // to whatever it already is. Note that this will leave the APZ's async scroll
+ // position out of sync with the gecko scroll position, but APZ can deal with that
+ // (by design). Note also that when we run into this case, even if both axes
+ // have overflow:hidden, we want to set aSuccessOut to true, so that the displayport
+ // follows the async scroll position rather than the gecko scroll position.
+ if (aFrame->GetScrollbarStyles().mVertical == NS_STYLE_OVERFLOW_HIDDEN) {
+ targetScrollPosition.y = geckoScrollPosition.y;
+ }
+ if (aFrame->GetScrollbarStyles().mHorizontal == NS_STYLE_OVERFLOW_HIDDEN) {
+ targetScrollPosition.x = geckoScrollPosition.x;
+ }
+
+ // If the scrollable frame is currently in the middle of an async or smooth
+ // scroll then we don't want to interrupt it (see bug 961280).
+ // Also if the scrollable frame got a scroll request from a higher priority origin
+ // since the last layers update, then we don't want to push our scroll request
+ // because we'll clobber that one, which is bad.
+ bool scrollInProgress = APZCCallbackHelper::IsScrollInProgress(aFrame);
+ if (!scrollInProgress) {
+ aFrame->ScrollToCSSPixelsApproximate(targetScrollPosition, nsGkAtoms::apz);
+ geckoScrollPosition = CSSPoint::FromAppUnits(aFrame->GetScrollPosition());
+ aSuccessOut = true;
+ }
+ // Return the final scroll position after setting it so that anything that relies
+ // on it can have an accurate value. Note that even if we set it above re-querying it
+ // is a good idea because it may have gotten clamped or rounded.
+ return geckoScrollPosition;
+}
+
+/**
+ * Scroll the scroll frame associated with |aContent| to the scroll position
+ * requested in |aMetrics|.
+ * The scroll offset in |aMetrics| is updated to reflect the actual scroll
+ * position.
+ * The displayport stored in |aMetrics| and the callback-transform stored on
+ * the content are updated to reflect any difference between the requested
+ * and actual scroll positions.
+ */
+static void
+ScrollFrame(nsIContent* aContent,
+ FrameMetrics& aMetrics)
+{
+ // Scroll the window to the desired spot
+ nsIScrollableFrame* sf = nsLayoutUtils::FindScrollableFrameFor(aMetrics.GetScrollId());
+ if (sf) {
+ sf->ResetScrollInfoIfGeneration(aMetrics.GetScrollGeneration());
+ sf->SetScrollableByAPZ(!aMetrics.IsScrollInfoLayer());
+ }
+ bool scrollUpdated = false;
+ CSSPoint apzScrollOffset = aMetrics.GetScrollOffset();
+ CSSPoint actualScrollOffset = ScrollFrameTo(sf, aMetrics, scrollUpdated);
+
+ if (scrollUpdated) {
+ if (aMetrics.IsScrollInfoLayer()) {
+ // In cases where the APZ scroll offset is different from the content scroll
+ // offset, we want to interpret the margins as relative to the APZ scroll
+ // offset except when the frame is not scrollable by APZ. Therefore, if the
+ // layer is a scroll info layer, we leave the margins as-is and they will
+ // be interpreted as relative to the content scroll offset.
+ if (nsIFrame* frame = aContent->GetPrimaryFrame()) {
+ frame->SchedulePaint();
+ }
+ } else {
+ // Correct the display port due to the difference between mScrollOffset and the
+ // actual scroll offset.
+ APZCCallbackHelper::AdjustDisplayPortForScrollDelta(aMetrics, actualScrollOffset);
+ }
+ } else {
+ // For whatever reason we couldn't update the scroll offset on the scroll frame,
+ // which means the data APZ used for its displayport calculation is stale. Fall
+ // back to a sane default behaviour. Note that we don't tile-align the recentered
+ // displayport because tile-alignment depends on the scroll position, and the
+ // scroll position here is out of our control. See bug 966507 comment 21 for a
+ // more detailed explanation.
+ RecenterDisplayPort(aMetrics);
+ }
+
+ aMetrics.SetScrollOffset(actualScrollOffset);
+
+ // APZ transforms inputs assuming we applied the exact scroll offset it
+ // requested (|apzScrollOffset|). Since we may not have, record the difference
+ // between what APZ asked for and what we actually applied, and apply it to
+ // input events to compensate.
+ // Note that if the main-thread had a change in its scroll position, we don't
+ // want to record that difference here, because it can be large and throw off
+ // input events by a large amount. It is also going to be transient, because
+ // any main-thread scroll position change will be synced to APZ and we will
+ // get another repaint request when APZ confirms. In the interval while this
+ // is happening we can just leave the callback transform as it was.
+ bool mainThreadScrollChanged =
+ sf && sf->CurrentScrollGeneration() != aMetrics.GetScrollGeneration() && nsLayoutUtils::CanScrollOriginClobberApz(sf->LastScrollOrigin());
+ if (aContent && !mainThreadScrollChanged) {
+ CSSPoint scrollDelta = apzScrollOffset - actualScrollOffset;
+ aContent->SetProperty(nsGkAtoms::apzCallbackTransform, new CSSPoint(scrollDelta),
+ nsINode::DeleteProperty<CSSPoint>);
+ }
+}
+
+static void
+SetDisplayPortMargins(nsIPresShell* aPresShell,
+ nsIContent* aContent,
+ const FrameMetrics& aMetrics)
+{
+ if (!aContent) {
+ return;
+ }
+
+ bool hadDisplayPort = nsLayoutUtils::HasDisplayPort(aContent);
+ ScreenMargin margins = aMetrics.GetDisplayPortMargins();
+ nsLayoutUtils::SetDisplayPortMargins(aContent, aPresShell, margins, 0);
+ if (!hadDisplayPort) {
+ nsLayoutUtils::SetZeroMarginDisplayPortOnAsyncScrollableAncestors(
+ aContent->GetPrimaryFrame(), nsLayoutUtils::RepaintMode::Repaint);
+ }
+
+ CSSRect baseCSS = aMetrics.CalculateCompositedRectInCssPixels();
+ nsRect base(0, 0,
+ baseCSS.width * nsPresContext::AppUnitsPerCSSPixel(),
+ baseCSS.height * nsPresContext::AppUnitsPerCSSPixel());
+ nsLayoutUtils::SetDisplayPortBaseIfNotSet(aContent, base);
+}
+
+static already_AddRefed<nsIPresShell>
+GetPresShell(const nsIContent* aContent)
+{
+ nsCOMPtr<nsIPresShell> result;
+ if (nsIDocument* doc = aContent->GetComposedDoc()) {
+ result = doc->GetShell();
+ }
+ return result.forget();
+}
+
+static void
+SetPaintRequestTime(nsIContent* aContent, const TimeStamp& aPaintRequestTime)
+{
+ aContent->SetProperty(nsGkAtoms::paintRequestTime,
+ new TimeStamp(aPaintRequestTime),
+ nsINode::DeleteProperty<TimeStamp>);
+}
+
+void
+APZCCallbackHelper::UpdateRootFrame(FrameMetrics& aMetrics)
+{
+ if (aMetrics.GetScrollId() == FrameMetrics::NULL_SCROLL_ID) {
+ return;
+ }
+ nsIContent* content = nsLayoutUtils::FindContentFor(aMetrics.GetScrollId());
+ if (!content) {
+ return;
+ }
+
+ nsCOMPtr<nsIPresShell> shell = GetPresShell(content);
+ if (!shell || aMetrics.GetPresShellId() != shell->GetPresShellId()) {
+ return;
+ }
+
+ MOZ_ASSERT(aMetrics.GetUseDisplayPortMargins());
+
+ if (gfxPrefs::APZAllowZooming()) {
+ // If zooming is disabled then we don't really want to let APZ fiddle
+ // with these things. In theory setting the resolution here should be a
+ // no-op, but setting the SPCSPS is bad because it can cause a stale value
+ // to be returned by window.innerWidth/innerHeight (see bug 1187792).
+
+ float presShellResolution = shell->GetResolution();
+
+ // If the pres shell resolution has changed on the content side side
+ // the time this repaint request was fired, consider this request out of date
+ // and drop it; setting a zoom based on the out-of-date resolution can have
+ // the effect of getting us stuck with the stale resolution.
+ if (!FuzzyEqualsMultiplicative(presShellResolution, aMetrics.GetPresShellResolution())) {
+ return;
+ }
+
+ // The pres shell resolution is updated by the the async zoom since the
+ // last paint.
+ presShellResolution = aMetrics.GetPresShellResolution()
+ * aMetrics.GetAsyncZoom().scale;
+ shell->SetResolutionAndScaleTo(presShellResolution);
+ }
+
+ // Do this as late as possible since scrolling can flush layout. It also
+ // adjusts the display port margins, so do it before we set those.
+ ScrollFrame(content, aMetrics);
+
+ SetDisplayPortMargins(shell, content, aMetrics);
+ SetPaintRequestTime(content, aMetrics.GetPaintRequestTime());
+}
+
+void
+APZCCallbackHelper::UpdateSubFrame(FrameMetrics& aMetrics)
+{
+ if (aMetrics.GetScrollId() == FrameMetrics::NULL_SCROLL_ID) {
+ return;
+ }
+ nsIContent* content = nsLayoutUtils::FindContentFor(aMetrics.GetScrollId());
+ if (!content) {
+ return;
+ }
+
+ MOZ_ASSERT(aMetrics.GetUseDisplayPortMargins());
+
+ // We don't currently support zooming for subframes, so nothing extra
+ // needs to be done beyond the tasks common to this and UpdateRootFrame.
+ ScrollFrame(content, aMetrics);
+ if (nsCOMPtr<nsIPresShell> shell = GetPresShell(content)) {
+ SetDisplayPortMargins(shell, content, aMetrics);
+ }
+ SetPaintRequestTime(content, aMetrics.GetPaintRequestTime());
+}
+
+bool
+APZCCallbackHelper::GetOrCreateScrollIdentifiers(nsIContent* aContent,
+ uint32_t* aPresShellIdOut,
+ FrameMetrics::ViewID* aViewIdOut)
+{
+ if (!aContent) {
+ return false;
+ }
+ *aViewIdOut = nsLayoutUtils::FindOrCreateIDFor(aContent);
+ if (nsCOMPtr<nsIPresShell> shell = GetPresShell(aContent)) {
+ *aPresShellIdOut = shell->GetPresShellId();
+ return true;
+ }
+ return false;
+}
+
+void
+APZCCallbackHelper::InitializeRootDisplayport(nsIPresShell* aPresShell)
+{
+ // Create a view-id and set a zero-margin displayport for the root element
+ // of the root document in the chrome process. This ensures that the scroll
+ // frame for this element gets an APZC, which in turn ensures that all content
+ // in the chrome processes is covered by an APZC.
+ // The displayport is zero-margin because this element is generally not
+ // actually scrollable (if it is, APZC will set proper margins when it's
+ // scrolled).
+ if (!aPresShell) {
+ return;
+ }
+
+ MOZ_ASSERT(aPresShell->GetDocument());
+ nsIContent* content = aPresShell->GetDocument()->GetDocumentElement();
+ if (!content) {
+ return;
+ }
+
+ uint32_t presShellId;
+ FrameMetrics::ViewID viewId;
+ if (APZCCallbackHelper::GetOrCreateScrollIdentifiers(content, &presShellId, &viewId)) {
+ // Note that the base rect that goes with these margins is set in
+ // nsRootBoxFrame::BuildDisplayList.
+ nsLayoutUtils::SetDisplayPortMargins(content, aPresShell, ScreenMargin(), 0,
+ nsLayoutUtils::RepaintMode::DoNotRepaint);
+ nsLayoutUtils::SetZeroMarginDisplayPortOnAsyncScrollableAncestors(
+ content->GetPrimaryFrame(), nsLayoutUtils::RepaintMode::DoNotRepaint);
+ }
+}
+
+nsPresContext*
+APZCCallbackHelper::GetPresContextForContent(nsIContent* aContent)
+{
+ nsIDocument* doc = aContent->GetComposedDoc();
+ if (!doc) {
+ return nullptr;
+ }
+ nsIPresShell* shell = doc->GetShell();
+ if (!shell) {
+ return nullptr;
+ }
+ return shell->GetPresContext();
+}
+
+nsIPresShell*
+APZCCallbackHelper::GetRootContentDocumentPresShellForContent(nsIContent* aContent)
+{
+ nsPresContext* context = GetPresContextForContent(aContent);
+ if (!context) {
+ return nullptr;
+ }
+ context = context->GetToplevelContentDocumentPresContext();
+ if (!context) {
+ return nullptr;
+ }
+ return context->PresShell();
+}
+
+static nsIPresShell*
+GetRootDocumentPresShell(nsIContent* aContent)
+{
+ nsIDocument* doc = aContent->GetComposedDoc();
+ if (!doc) {
+ return nullptr;
+ }
+ nsIPresShell* shell = doc->GetShell();
+ if (!shell) {
+ return nullptr;
+ }
+ nsPresContext* context = shell->GetPresContext();
+ if (!context) {
+ return nullptr;
+ }
+ context = context->GetRootPresContext();
+ if (!context) {
+ return nullptr;
+ }
+ return context->PresShell();
+}
+
+CSSPoint
+APZCCallbackHelper::ApplyCallbackTransform(const CSSPoint& aInput,
+ const ScrollableLayerGuid& aGuid)
+{
+ CSSPoint input = aInput;
+ if (aGuid.mScrollId == FrameMetrics::NULL_SCROLL_ID) {
+ return input;
+ }
+ nsCOMPtr<nsIContent> content = nsLayoutUtils::FindContentFor(aGuid.mScrollId);
+ if (!content) {
+ return input;
+ }
+
+ // First, scale inversely by the root content document's pres shell
+ // resolution to cancel the scale-to-resolution transform that the
+ // compositor adds to the layer with the pres shell resolution. The points
+ // sent to Gecko by APZ don't have this transform unapplied (unlike other
+ // compositor-side transforms) because APZ doesn't know about it.
+ if (nsIPresShell* shell = GetRootDocumentPresShell(content)) {
+ input = input / shell->GetResolution();
+ }
+
+ // This represents any resolution on the Root Content Document (RCD)
+ // that's not on the Root Document (RD). That is, on platforms where
+ // RCD == RD, it's 1, and on platforms where RCD != RD, it's the RCD
+ // resolution. 'input' has this resolution applied, but the scroll
+ // delta retrieved below do not, so we need to apply them to the
+ // delta before adding the delta to 'input'. (Technically, deltas
+ // from scroll frames outside the RCD would already have this
+ // resolution applied, but we don't have such scroll frames in
+ // practice.)
+ float nonRootResolution = 1.0f;
+ if (nsIPresShell* shell = GetRootContentDocumentPresShellForContent(content)) {
+ nonRootResolution = shell->GetCumulativeNonRootScaleResolution();
+ }
+ // Now apply the callback-transform. This is only approximately correct,
+ // see the comment on GetCumulativeApzCallbackTransform for details.
+ CSSPoint transform = nsLayoutUtils::GetCumulativeApzCallbackTransform(content->GetPrimaryFrame());
+ return input + transform * nonRootResolution;
+}
+
+LayoutDeviceIntPoint
+APZCCallbackHelper::ApplyCallbackTransform(const LayoutDeviceIntPoint& aPoint,
+ const ScrollableLayerGuid& aGuid,
+ const CSSToLayoutDeviceScale& aScale)
+{
+ LayoutDevicePoint point = LayoutDevicePoint(aPoint.x, aPoint.y);
+ point = ApplyCallbackTransform(point / aScale, aGuid) * aScale;
+ return LayoutDeviceIntPoint::Round(point);
+}
+
+void
+APZCCallbackHelper::ApplyCallbackTransform(WidgetEvent& aEvent,
+ const ScrollableLayerGuid& aGuid,
+ const CSSToLayoutDeviceScale& aScale)
+{
+ if (aEvent.AsTouchEvent()) {
+ WidgetTouchEvent& event = *(aEvent.AsTouchEvent());
+ for (size_t i = 0; i < event.mTouches.Length(); i++) {
+ event.mTouches[i]->mRefPoint = ApplyCallbackTransform(
+ event.mTouches[i]->mRefPoint, aGuid, aScale);
+ }
+ } else {
+ aEvent.mRefPoint = ApplyCallbackTransform(aEvent.mRefPoint, aGuid, aScale);
+ }
+}
+
+nsEventStatus
+APZCCallbackHelper::DispatchWidgetEvent(WidgetGUIEvent& aEvent)
+{
+ nsEventStatus status = nsEventStatus_eConsumeNoDefault;
+ if (aEvent.mWidget) {
+ aEvent.mWidget->DispatchEvent(&aEvent, status);
+ }
+ return status;
+}
+
+nsEventStatus
+APZCCallbackHelper::DispatchSynthesizedMouseEvent(EventMessage aMsg,
+ uint64_t aTime,
+ const LayoutDevicePoint& aRefPoint,
+ Modifiers aModifiers,
+ int32_t aClickCount,
+ nsIWidget* aWidget)
+{
+ MOZ_ASSERT(aMsg == eMouseMove || aMsg == eMouseDown ||
+ aMsg == eMouseUp || aMsg == eMouseLongTap);
+
+ WidgetMouseEvent event(true, aMsg, aWidget,
+ WidgetMouseEvent::eReal, WidgetMouseEvent::eNormal);
+ event.mRefPoint = LayoutDeviceIntPoint::Truncate(aRefPoint.x, aRefPoint.y);
+ event.mTime = aTime;
+ event.button = WidgetMouseEvent::eLeftButton;
+ event.inputSource = nsIDOMMouseEvent::MOZ_SOURCE_TOUCH;
+ if (aMsg == eMouseLongTap) {
+ event.mFlags.mOnlyChromeDispatch = true;
+ }
+ event.mIgnoreRootScrollFrame = true;
+ if (aMsg != eMouseMove) {
+ event.mClickCount = aClickCount;
+ }
+ event.mModifiers = aModifiers;
+ // Real touch events will generate corresponding pointer events. We set
+ // convertToPointer to false to prevent the synthesized mouse events generate
+ // pointer events again.
+ event.convertToPointer = false;
+ return DispatchWidgetEvent(event);
+}
+
+bool
+APZCCallbackHelper::DispatchMouseEvent(const nsCOMPtr<nsIPresShell>& aPresShell,
+ const nsString& aType,
+ const CSSPoint& aPoint,
+ int32_t aButton,
+ int32_t aClickCount,
+ int32_t aModifiers,
+ bool aIgnoreRootScrollFrame,
+ unsigned short aInputSourceArg)
+{
+ NS_ENSURE_TRUE(aPresShell, true);
+
+ bool defaultPrevented = false;
+ nsContentUtils::SendMouseEvent(aPresShell, aType, aPoint.x, aPoint.y,
+ aButton, nsIDOMWindowUtils::MOUSE_BUTTONS_NOT_SPECIFIED, aClickCount,
+ aModifiers, aIgnoreRootScrollFrame, 0, aInputSourceArg, false,
+ &defaultPrevented, false, /* aIsWidgetEventSynthesized = */ false);
+ return defaultPrevented;
+}
+
+
+void
+APZCCallbackHelper::FireSingleTapEvent(const LayoutDevicePoint& aPoint,
+ Modifiers aModifiers,
+ int32_t aClickCount,
+ nsIWidget* aWidget)
+{
+ if (aWidget->Destroyed()) {
+ return;
+ }
+ APZCCH_LOG("Dispatching single-tap component events to %s\n",
+ Stringify(aPoint).c_str());
+ int time = 0;
+ DispatchSynthesizedMouseEvent(eMouseMove, time, aPoint, aModifiers, aClickCount, aWidget);
+ DispatchSynthesizedMouseEvent(eMouseDown, time, aPoint, aModifiers, aClickCount, aWidget);
+ DispatchSynthesizedMouseEvent(eMouseUp, time, aPoint, aModifiers, aClickCount, aWidget);
+}
+
+static dom::Element*
+GetDisplayportElementFor(nsIScrollableFrame* aScrollableFrame)
+{
+ if (!aScrollableFrame) {
+ return nullptr;
+ }
+ nsIFrame* scrolledFrame = aScrollableFrame->GetScrolledFrame();
+ if (!scrolledFrame) {
+ return nullptr;
+ }
+ // |scrolledFrame| should at this point be the root content frame of the
+ // nearest ancestor scrollable frame. The element corresponding to this
+ // frame should be the one with the displayport set on it, so find that
+ // element and return it.
+ nsIContent* content = scrolledFrame->GetContent();
+ MOZ_ASSERT(content->IsElement()); // roc says this must be true
+ return content->AsElement();
+}
+
+
+static dom::Element*
+GetRootDocumentElementFor(nsIWidget* aWidget)
+{
+ // This returns the root element that ChromeProcessController sets the
+ // displayport on during initialization.
+ if (nsView* view = nsView::GetViewFor(aWidget)) {
+ if (nsIPresShell* shell = view->GetPresShell()) {
+ MOZ_ASSERT(shell->GetDocument());
+ return shell->GetDocument()->GetDocumentElement();
+ }
+ }
+ return nullptr;
+}
+
+static nsIFrame*
+UpdateRootFrameForTouchTargetDocument(nsIFrame* aRootFrame)
+{
+#if defined(MOZ_WIDGET_ANDROID)
+ // Re-target so that the hit test is performed relative to the frame for the
+ // Root Content Document instead of the Root Document which are different in
+ // Android. See bug 1229752 comment 16 for an explanation of why this is necessary.
+ if (nsIDocument* doc = aRootFrame->PresContext()->PresShell()->GetTouchEventTargetDocument()) {
+ if (nsIPresShell* shell = doc->GetShell()) {
+ if (nsIFrame* frame = shell->GetRootFrame()) {
+ return frame;
+ }
+ }
+ }
+#endif
+ return aRootFrame;
+}
+
+// Determine the scrollable target frame for the given point and add it to
+// the target list. If the frame doesn't have a displayport, set one.
+// Return whether or not a displayport was set.
+static bool
+PrepareForSetTargetAPZCNotification(nsIWidget* aWidget,
+ const ScrollableLayerGuid& aGuid,
+ nsIFrame* aRootFrame,
+ const LayoutDeviceIntPoint& aRefPoint,
+ nsTArray<ScrollableLayerGuid>* aTargets)
+{
+ ScrollableLayerGuid guid(aGuid.mLayersId, 0, FrameMetrics::NULL_SCROLL_ID);
+ nsPoint point =
+ nsLayoutUtils::GetEventCoordinatesRelativeTo(aWidget, aRefPoint, aRootFrame);
+ nsIFrame* target =
+ nsLayoutUtils::GetFrameForPoint(aRootFrame, point, nsLayoutUtils::IGNORE_ROOT_SCROLL_FRAME);
+ nsIScrollableFrame* scrollAncestor = target
+ ? nsLayoutUtils::GetAsyncScrollableAncestorFrame(target)
+ : aRootFrame->PresContext()->PresShell()->GetRootScrollFrameAsScrollable();
+
+ // Assuming that if there's no scrollAncestor, there's already a displayPort.
+ nsCOMPtr<dom::Element> dpElement = scrollAncestor
+ ? GetDisplayportElementFor(scrollAncestor)
+ : GetRootDocumentElementFor(aWidget);
+
+ nsAutoString dpElementDesc;
+ if (dpElement) {
+ dpElement->Describe(dpElementDesc);
+ }
+ APZCCH_LOG("For event at %s found scrollable element %p (%s)\n",
+ Stringify(aRefPoint).c_str(), dpElement.get(),
+ NS_LossyConvertUTF16toASCII(dpElementDesc).get());
+
+ bool guidIsValid = APZCCallbackHelper::GetOrCreateScrollIdentifiers(
+ dpElement, &(guid.mPresShellId), &(guid.mScrollId));
+ aTargets->AppendElement(guid);
+
+ if (!guidIsValid || nsLayoutUtils::HasDisplayPort(dpElement)) {
+ return false;
+ }
+
+ if (!scrollAncestor) {
+ MOZ_ASSERT(false); // If you hit this, please file a bug with STR.
+
+ // Attempt some sort of graceful handling based on a theory as to why we
+ // reach this point...
+ // If we get here, the document element is non-null, valid, but doesn't have
+ // a displayport. It's possible that the init code in ChromeProcessController
+ // failed for some reason, or the document element got swapped out at some
+ // later time. In this case let's try to set a displayport on the document
+ // element again and bail out on this operation.
+ APZCCH_LOG("Widget %p's document element %p didn't have a displayport\n",
+ aWidget, dpElement.get());
+ APZCCallbackHelper::InitializeRootDisplayport(aRootFrame->PresContext()->PresShell());
+ return false;
+ }
+
+ APZCCH_LOG("%p didn't have a displayport, so setting one...\n", dpElement.get());
+ bool activated = nsLayoutUtils::CalculateAndSetDisplayPortMargins(
+ scrollAncestor, nsLayoutUtils::RepaintMode::Repaint);
+ if (!activated) {
+ return false;
+ }
+
+ nsIFrame* frame = do_QueryFrame(scrollAncestor);
+ nsLayoutUtils::SetZeroMarginDisplayPortOnAsyncScrollableAncestors(frame,
+ nsLayoutUtils::RepaintMode::Repaint);
+
+ return true;
+}
+
+static void
+SendLayersDependentApzcTargetConfirmation(nsIPresShell* aShell, uint64_t aInputBlockId,
+ const nsTArray<ScrollableLayerGuid>& aTargets)
+{
+ LayerManager* lm = aShell->GetLayerManager();
+ if (!lm) {
+ return;
+ }
+
+ LayerTransactionChild* shadow = lm->AsShadowForwarder()->GetShadowManager();
+ if (!shadow) {
+ return;
+ }
+
+ shadow->SendSetConfirmedTargetAPZC(aInputBlockId, aTargets);
+}
+
+class DisplayportSetListener : public nsAPostRefreshObserver {
+public:
+ DisplayportSetListener(nsIPresShell* aPresShell,
+ const uint64_t& aInputBlockId,
+ const nsTArray<ScrollableLayerGuid>& aTargets)
+ : mPresShell(aPresShell)
+ , mInputBlockId(aInputBlockId)
+ , mTargets(aTargets)
+ {
+ }
+
+ virtual ~DisplayportSetListener()
+ {
+ }
+
+ void DidRefresh() override {
+ if (!mPresShell) {
+ MOZ_ASSERT_UNREACHABLE("Post-refresh observer fired again after failed attempt at unregistering it");
+ return;
+ }
+
+ APZCCH_LOG("Got refresh, sending target APZCs for input block %" PRIu64 "\n", mInputBlockId);
+ SendLayersDependentApzcTargetConfirmation(mPresShell, mInputBlockId, Move(mTargets));
+
+ if (!mPresShell->RemovePostRefreshObserver(this)) {
+ MOZ_ASSERT_UNREACHABLE("Unable to unregister post-refresh observer! Leaking it instead of leaving garbage registered");
+ // Graceful handling, just in case...
+ mPresShell = nullptr;
+ return;
+ }
+
+ delete this;
+ }
+
+private:
+ RefPtr<nsIPresShell> mPresShell;
+ uint64_t mInputBlockId;
+ nsTArray<ScrollableLayerGuid> mTargets;
+};
+
+// Sends a SetTarget notification for APZC, given one or more previous
+// calls to PrepareForAPZCSetTargetNotification().
+static void
+SendSetTargetAPZCNotificationHelper(nsIWidget* aWidget,
+ nsIPresShell* aShell,
+ const uint64_t& aInputBlockId,
+ const nsTArray<ScrollableLayerGuid>& aTargets,
+ bool aWaitForRefresh)
+{
+ bool waitForRefresh = aWaitForRefresh;
+ if (waitForRefresh) {
+ APZCCH_LOG("At least one target got a new displayport, need to wait for refresh\n");
+ waitForRefresh = aShell->AddPostRefreshObserver(
+ new DisplayportSetListener(aShell, aInputBlockId, Move(aTargets)));
+ }
+ if (!waitForRefresh) {
+ APZCCH_LOG("Sending target APZCs for input block %" PRIu64 "\n", aInputBlockId);
+ aWidget->SetConfirmedTargetAPZC(aInputBlockId, aTargets);
+ } else {
+ APZCCH_LOG("Successfully registered post-refresh observer\n");
+ }
+}
+
+void
+APZCCallbackHelper::SendSetTargetAPZCNotification(nsIWidget* aWidget,
+ nsIDocument* aDocument,
+ const WidgetGUIEvent& aEvent,
+ const ScrollableLayerGuid& aGuid,
+ uint64_t aInputBlockId)
+{
+ if (!aWidget || !aDocument) {
+ return;
+ }
+ if (aInputBlockId == sLastTargetAPZCNotificationInputBlock) {
+ // We have already confirmed the target APZC for a previous event of this
+ // input block. If we activated a scroll frame for this input block,
+ // sending another target APZC confirmation would be harmful, as it might
+ // race the original confirmation (which needs to go through a layers
+ // transaction).
+ APZCCH_LOG("Not resending target APZC confirmation for input block %" PRIu64 "\n", aInputBlockId);
+ return;
+ }
+ sLastTargetAPZCNotificationInputBlock = aInputBlockId;
+ if (nsIPresShell* shell = aDocument->GetShell()) {
+ if (nsIFrame* rootFrame = shell->GetRootFrame()) {
+ rootFrame = UpdateRootFrameForTouchTargetDocument(rootFrame);
+
+ bool waitForRefresh = false;
+ nsTArray<ScrollableLayerGuid> targets;
+
+ if (const WidgetTouchEvent* touchEvent = aEvent.AsTouchEvent()) {
+ for (size_t i = 0; i < touchEvent->mTouches.Length(); i++) {
+ waitForRefresh |= PrepareForSetTargetAPZCNotification(aWidget, aGuid,
+ rootFrame, touchEvent->mTouches[i]->mRefPoint, &targets);
+ }
+ } else if (const WidgetWheelEvent* wheelEvent = aEvent.AsWheelEvent()) {
+ waitForRefresh = PrepareForSetTargetAPZCNotification(aWidget, aGuid,
+ rootFrame, wheelEvent->mRefPoint, &targets);
+ } else if (const WidgetMouseEvent* mouseEvent = aEvent.AsMouseEvent()) {
+ waitForRefresh = PrepareForSetTargetAPZCNotification(aWidget, aGuid,
+ rootFrame, mouseEvent->mRefPoint, &targets);
+ }
+ // TODO: Do other types of events need to be handled?
+
+ if (!targets.IsEmpty()) {
+ SendSetTargetAPZCNotificationHelper(
+ aWidget,
+ shell,
+ aInputBlockId,
+ Move(targets),
+ waitForRefresh);
+ }
+ }
+ }
+}
+
+void
+APZCCallbackHelper::SendSetAllowedTouchBehaviorNotification(
+ nsIWidget* aWidget,
+ nsIDocument* aDocument,
+ const WidgetTouchEvent& aEvent,
+ uint64_t aInputBlockId,
+ const SetAllowedTouchBehaviorCallback& aCallback)
+{
+ if (nsIPresShell* shell = aDocument->GetShell()) {
+ if (nsIFrame* rootFrame = shell->GetRootFrame()) {
+ rootFrame = UpdateRootFrameForTouchTargetDocument(rootFrame);
+
+ nsTArray<TouchBehaviorFlags> flags;
+ for (uint32_t i = 0; i < aEvent.mTouches.Length(); i++) {
+ flags.AppendElement(
+ TouchActionHelper::GetAllowedTouchBehavior(aWidget,
+ rootFrame, aEvent.mTouches[i]->mRefPoint));
+ }
+ aCallback(aInputBlockId, Move(flags));
+ }
+ }
+}
+
+void
+APZCCallbackHelper::NotifyMozMouseScrollEvent(const FrameMetrics::ViewID& aScrollId, const nsString& aEvent)
+{
+ nsCOMPtr<nsIContent> targetContent = nsLayoutUtils::FindContentFor(aScrollId);
+ if (!targetContent) {
+ return;
+ }
+ nsCOMPtr<nsIDocument> ownerDoc = targetContent->OwnerDoc();
+ if (!ownerDoc) {
+ return;
+ }
+
+ nsContentUtils::DispatchTrustedEvent(
+ ownerDoc, targetContent,
+ aEvent,
+ true, true);
+}
+
+void
+APZCCallbackHelper::NotifyFlushComplete(nsIPresShell* aShell)
+{
+ MOZ_ASSERT(NS_IsMainThread());
+ // In some cases, flushing the APZ state to the main thread doesn't actually
+ // trigger a flush and repaint (this is an intentional optimization - the stuff
+ // visible to the user is still correct). However, reftests update their
+ // snapshot based on invalidation events that are emitted during paints,
+ // so we ensure that we kick off a paint when an APZ flush is done. Note that
+ // only chrome/testing code can trigger this behaviour.
+ if (aShell && aShell->GetRootFrame()) {
+ aShell->GetRootFrame()->SchedulePaint();
+ }
+
+ nsCOMPtr<nsIObserverService> observerService = mozilla::services::GetObserverService();
+ MOZ_ASSERT(observerService);
+ observerService->NotifyObservers(nullptr, "apz-repaints-flushed", nullptr);
+}
+
+static int32_t sActiveSuppressDisplayport = 0;
+static bool sDisplayPortSuppressionRespected = true;
+
+void
+APZCCallbackHelper::SuppressDisplayport(const bool& aEnabled,
+ const nsCOMPtr<nsIPresShell>& aShell)
+{
+ if (aEnabled) {
+ sActiveSuppressDisplayport++;
+ } else {
+ bool isSuppressed = IsDisplayportSuppressed();
+ sActiveSuppressDisplayport--;
+ if (isSuppressed && !IsDisplayportSuppressed() &&
+ aShell && aShell->GetRootFrame()) {
+ // We unsuppressed the displayport, trigger a paint
+ aShell->GetRootFrame()->SchedulePaint();
+ }
+ }
+
+ MOZ_ASSERT(sActiveSuppressDisplayport >= 0);
+}
+
+void
+APZCCallbackHelper::RespectDisplayPortSuppression(bool aEnabled,
+ const nsCOMPtr<nsIPresShell>& aShell)
+{
+ bool isSuppressed = IsDisplayportSuppressed();
+ sDisplayPortSuppressionRespected = aEnabled;
+ if (isSuppressed && !IsDisplayportSuppressed() &&
+ aShell && aShell->GetRootFrame()) {
+ // We unsuppressed the displayport, trigger a paint
+ aShell->GetRootFrame()->SchedulePaint();
+ }
+}
+
+bool
+APZCCallbackHelper::IsDisplayportSuppressed()
+{
+ return sDisplayPortSuppressionRespected
+ && sActiveSuppressDisplayport > 0;
+}
+
+/* static */ bool
+APZCCallbackHelper::IsScrollInProgress(nsIScrollableFrame* aFrame)
+{
+ return aFrame->IsProcessingAsyncScroll()
+ || nsLayoutUtils::CanScrollOriginClobberApz(aFrame->LastScrollOrigin())
+ || aFrame->LastSmoothScrollOrigin();
+}
+
+/* static */ void
+APZCCallbackHelper::NotifyPinchGesture(PinchGestureInput::PinchGestureType aType,
+ LayoutDeviceCoord aSpanChange,
+ Modifiers aModifiers,
+ nsIWidget* aWidget)
+{
+ EventMessage msg;
+ switch (aType) {
+ case PinchGestureInput::PINCHGESTURE_START:
+ msg = eMagnifyGestureStart;
+ break;
+ case PinchGestureInput::PINCHGESTURE_SCALE:
+ msg = eMagnifyGestureUpdate;
+ break;
+ case PinchGestureInput::PINCHGESTURE_END:
+ msg = eMagnifyGesture;
+ break;
+ case PinchGestureInput::PINCHGESTURE_SENTINEL:
+ default:
+ MOZ_ASSERT_UNREACHABLE("Invalid gesture type");
+ return;
+ }
+
+ WidgetSimpleGestureEvent event(true, msg, aWidget);
+ event.mDelta = aSpanChange;
+ event.mModifiers = aModifiers;
+ DispatchWidgetEvent(event);
+}
+
+} // namespace layers
+} // namespace mozilla
+
diff --git a/gfx/layers/apz/util/APZCCallbackHelper.h b/gfx/layers/apz/util/APZCCallbackHelper.h
new file mode 100644
index 000000000..8e888805c
--- /dev/null
+++ b/gfx/layers/apz/util/APZCCallbackHelper.h
@@ -0,0 +1,209 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_layers_APZCCallbackHelper_h
+#define mozilla_layers_APZCCallbackHelper_h
+
+#include "FrameMetrics.h"
+#include "InputData.h"
+#include "mozilla/EventForwards.h"
+#include "mozilla/Function.h"
+#include "mozilla/layers/APZUtils.h"
+#include "nsIDOMWindowUtils.h"
+
+class nsIContent;
+class nsIDocument;
+class nsIPresShell;
+class nsIScrollableFrame;
+class nsIWidget;
+template<class T> struct already_AddRefed;
+template<class T> class nsCOMPtr;
+
+namespace mozilla {
+namespace layers {
+
+typedef function<void(uint64_t, const nsTArray<TouchBehaviorFlags>&)>
+ SetAllowedTouchBehaviorCallback;
+
+/* This class contains some helper methods that facilitate implementing the
+ GeckoContentController callback interface required by the AsyncPanZoomController.
+ Since different platforms need to implement this interface in similar-but-
+ not-quite-the-same ways, this utility class provides some helpful methods
+ to hold code that can be shared across the different platform implementations.
+ */
+class APZCCallbackHelper
+{
+ typedef mozilla::layers::FrameMetrics FrameMetrics;
+ typedef mozilla::layers::ScrollableLayerGuid ScrollableLayerGuid;
+
+public:
+ /* Applies the scroll and zoom parameters from the given FrameMetrics object
+ to the root frame for the given metrics' scrollId. If tiled thebes layers
+ are enabled, this will align the displayport to tile boundaries. Setting
+ the scroll position can cause some small adjustments to be made to the
+ actual scroll position. aMetrics' display port and scroll position will
+ be updated with any modifications made. */
+ static void UpdateRootFrame(FrameMetrics& aMetrics);
+
+ /* Applies the scroll parameters from the given FrameMetrics object to the
+ subframe corresponding to given metrics' scrollId. If tiled thebes
+ layers are enabled, this will align the displayport to tile boundaries.
+ Setting the scroll position can cause some small adjustments to be made
+ to the actual scroll position. aMetrics' display port and scroll position
+ will be updated with any modifications made. */
+ static void UpdateSubFrame(FrameMetrics& aMetrics);
+
+ /* Get the presShellId and view ID for the given content element.
+ * If the view ID does not exist, one is created.
+ * The pres shell ID should generally already exist; if it doesn't for some
+ * reason, false is returned. */
+ static bool GetOrCreateScrollIdentifiers(nsIContent* aContent,
+ uint32_t* aPresShellIdOut,
+ FrameMetrics::ViewID* aViewIdOut);
+
+ /* Initialize a zero-margin displayport on the root document element of the
+ given presShell. */
+ static void InitializeRootDisplayport(nsIPresShell* aPresShell);
+
+ /* Get the pres context associated with the document enclosing |aContent|. */
+ static nsPresContext* GetPresContextForContent(nsIContent* aContent);
+
+ /* Get the pres shell associated with the root content document enclosing |aContent|. */
+ static nsIPresShell* GetRootContentDocumentPresShellForContent(nsIContent* aContent);
+
+ /* Apply an "input transform" to the given |aInput| and return the transformed value.
+ The input transform applied is the one for the content element corresponding to
+ |aGuid|; this is populated in a previous call to UpdateCallbackTransform. See that
+ method's documentations for details.
+ This method additionally adjusts |aInput| by inversely scaling by the provided
+ pres shell resolution, to cancel out a compositor-side transform (added in
+ bug 1076241) that APZ doesn't unapply. */
+ static CSSPoint ApplyCallbackTransform(const CSSPoint& aInput,
+ const ScrollableLayerGuid& aGuid);
+
+ /* Same as above, but operates on LayoutDeviceIntPoint.
+ Requires an additonal |aScale| parameter to convert between CSS and
+ LayoutDevice space. */
+ static mozilla::LayoutDeviceIntPoint
+ ApplyCallbackTransform(const LayoutDeviceIntPoint& aPoint,
+ const ScrollableLayerGuid& aGuid,
+ const CSSToLayoutDeviceScale& aScale);
+
+ /* Convenience function for applying a callback transform to all refpoints
+ * in the input event. */
+ static void ApplyCallbackTransform(WidgetEvent& aEvent,
+ const ScrollableLayerGuid& aGuid,
+ const CSSToLayoutDeviceScale& aScale);
+
+ /* Dispatch a widget event via the widget stored in the event, if any.
+ * In a child process, allows the TabParent event-capture mechanism to
+ * intercept the event. */
+ static nsEventStatus DispatchWidgetEvent(WidgetGUIEvent& aEvent);
+
+ /* Synthesize a mouse event with the given parameters, and dispatch it
+ * via the given widget. */
+ static nsEventStatus DispatchSynthesizedMouseEvent(EventMessage aMsg,
+ uint64_t aTime,
+ const LayoutDevicePoint& aRefPoint,
+ Modifiers aModifiers,
+ int32_t aClickCount,
+ nsIWidget* aWidget);
+
+ /* Dispatch a mouse event with the given parameters.
+ * Return whether or not any listeners have called preventDefault on the event. */
+ static bool DispatchMouseEvent(const nsCOMPtr<nsIPresShell>& aPresShell,
+ const nsString& aType,
+ const CSSPoint& aPoint,
+ int32_t aButton,
+ int32_t aClickCount,
+ int32_t aModifiers,
+ bool aIgnoreRootScrollFrame,
+ unsigned short aInputSourceArg);
+
+ /* Fire a single-tap event at the given point. The event is dispatched
+ * via the given widget. */
+ static void FireSingleTapEvent(const LayoutDevicePoint& aPoint,
+ Modifiers aModifiers,
+ int32_t aClickCount,
+ nsIWidget* aWidget);
+
+ /* Perform hit-testing on the touch points of |aEvent| to determine
+ * which scrollable frames they target. If any of these frames don't have
+ * a displayport, set one.
+ *
+ * If any displayports need to be set, the actual notification to APZ is
+ * sent to the compositor, which will then post a message back to APZ's
+ * controller thread. Otherwise, the provided widget's SetConfirmedTargetAPZC
+ * method is invoked immediately.
+ */
+ static void SendSetTargetAPZCNotification(nsIWidget* aWidget,
+ nsIDocument* aDocument,
+ const WidgetGUIEvent& aEvent,
+ const ScrollableLayerGuid& aGuid,
+ uint64_t aInputBlockId);
+
+ /* Figure out the allowed touch behaviors of each touch point in |aEvent|
+ * and send that information to the provided callback. */
+ static void SendSetAllowedTouchBehaviorNotification(nsIWidget* aWidget,
+ nsIDocument* aDocument,
+ const WidgetTouchEvent& aEvent,
+ uint64_t aInputBlockId,
+ const SetAllowedTouchBehaviorCallback& aCallback);
+
+ /* Notify content of a mouse scroll testing event. */
+ static void NotifyMozMouseScrollEvent(const FrameMetrics::ViewID& aScrollId, const nsString& aEvent);
+
+ /* Notify content that the repaint flush is complete. */
+ static void NotifyFlushComplete(nsIPresShell* aShell);
+
+ /* Temporarily ignore the Displayport for better paint performance. If at
+ * all possible, pass in a presShell if you have one at the call site, we
+ * use it to trigger a repaint once suppression is disabled. Without that
+ * the displayport may get left at the suppressed size for an extended
+ * period of time and result in unnecessary checkerboarding (see bug
+ * 1255054). */
+ static void SuppressDisplayport(const bool& aEnabled,
+ const nsCOMPtr<nsIPresShell>& aShell);
+
+ /* Whether or not displayport suppression should be turned on. Note that
+ * this only affects the return value of |IsDisplayportSuppressed()|, and
+ * doesn't change the value of the internal counter. As with
+ * SuppressDisplayport, this function should be passed a presShell to trigger
+ * a repaint if suppression is being turned off.
+ */
+ static void RespectDisplayPortSuppression(bool aEnabled,
+ const nsCOMPtr<nsIPresShell>& aShell);
+
+ /* Whether or not the displayport is currently suppressed. */
+ static bool IsDisplayportSuppressed();
+
+ static void
+ AdjustDisplayPortForScrollDelta(mozilla::layers::FrameMetrics& aFrameMetrics,
+ const CSSPoint& aActualScrollOffset);
+
+ /*
+ * Check if the scrollable frame is currently in the middle of an async
+ * or smooth scroll. We want to discard certain scroll input if this is
+ * true to prevent clobbering higher priority origins.
+ */
+ static bool
+ IsScrollInProgress(nsIScrollableFrame* aFrame);
+
+ /* Notify content of the progress of a pinch gesture that APZ won't do
+ * zooming for (because the apz.allow_zooming pref is false). This function
+ * will dispatch appropriate WidgetSimpleGestureEvent events to gecko.
+ */
+ static void NotifyPinchGesture(PinchGestureInput::PinchGestureType aType,
+ LayoutDeviceCoord aSpanChange,
+ Modifiers aModifiers,
+ nsIWidget* aWidget);
+private:
+ static uint64_t sLastTargetAPZCNotificationInputBlock;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif /* mozilla_layers_APZCCallbackHelper_h */
diff --git a/gfx/layers/apz/util/APZEventState.cpp b/gfx/layers/apz/util/APZEventState.cpp
new file mode 100644
index 000000000..20a41eed5
--- /dev/null
+++ b/gfx/layers/apz/util/APZEventState.cpp
@@ -0,0 +1,512 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "APZEventState.h"
+
+#include "ActiveElementManager.h"
+#include "APZCCallbackHelper.h"
+#include "gfxPrefs.h"
+#include "LayersLogging.h"
+#include "mozilla/BasicEvents.h"
+#include "mozilla/IntegerPrintfMacros.h"
+#include "mozilla/Move.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/TouchEvents.h"
+#include "mozilla/layers/APZCCallbackHelper.h"
+#include "nsCOMPtr.h"
+#include "nsDocShell.h"
+#include "nsIDOMMouseEvent.h"
+#include "nsIDOMWindowUtils.h"
+#include "nsIScrollableFrame.h"
+#include "nsIScrollbarMediator.h"
+#include "nsITimer.h"
+#include "nsIWeakReferenceUtils.h"
+#include "nsIWidget.h"
+#include "nsLayoutUtils.h"
+#include "nsQueryFrame.h"
+#include "TouchManager.h"
+#include "nsIDOMMouseEvent.h"
+#include "nsLayoutUtils.h"
+#include "nsIScrollableFrame.h"
+#include "nsIScrollbarMediator.h"
+#include "mozilla/TouchEvents.h"
+
+#define APZES_LOG(...)
+// #define APZES_LOG(...) printf_stderr("APZES: " __VA_ARGS__)
+
+// Static helper functions
+namespace {
+
+int32_t
+WidgetModifiersToDOMModifiers(mozilla::Modifiers aModifiers)
+{
+ int32_t result = 0;
+ if (aModifiers & mozilla::MODIFIER_SHIFT) {
+ result |= nsIDOMWindowUtils::MODIFIER_SHIFT;
+ }
+ if (aModifiers & mozilla::MODIFIER_CONTROL) {
+ result |= nsIDOMWindowUtils::MODIFIER_CONTROL;
+ }
+ if (aModifiers & mozilla::MODIFIER_ALT) {
+ result |= nsIDOMWindowUtils::MODIFIER_ALT;
+ }
+ if (aModifiers & mozilla::MODIFIER_META) {
+ result |= nsIDOMWindowUtils::MODIFIER_META;
+ }
+ if (aModifiers & mozilla::MODIFIER_ALTGRAPH) {
+ result |= nsIDOMWindowUtils::MODIFIER_ALTGRAPH;
+ }
+ if (aModifiers & mozilla::MODIFIER_CAPSLOCK) {
+ result |= nsIDOMWindowUtils::MODIFIER_CAPSLOCK;
+ }
+ if (aModifiers & mozilla::MODIFIER_FN) {
+ result |= nsIDOMWindowUtils::MODIFIER_FN;
+ }
+ if (aModifiers & mozilla::MODIFIER_FNLOCK) {
+ result |= nsIDOMWindowUtils::MODIFIER_FNLOCK;
+ }
+ if (aModifiers & mozilla::MODIFIER_NUMLOCK) {
+ result |= nsIDOMWindowUtils::MODIFIER_NUMLOCK;
+ }
+ if (aModifiers & mozilla::MODIFIER_SCROLLLOCK) {
+ result |= nsIDOMWindowUtils::MODIFIER_SCROLLLOCK;
+ }
+ if (aModifiers & mozilla::MODIFIER_SYMBOL) {
+ result |= nsIDOMWindowUtils::MODIFIER_SYMBOL;
+ }
+ if (aModifiers & mozilla::MODIFIER_SYMBOLLOCK) {
+ result |= nsIDOMWindowUtils::MODIFIER_SYMBOLLOCK;
+ }
+ if (aModifiers & mozilla::MODIFIER_OS) {
+ result |= nsIDOMWindowUtils::MODIFIER_OS;
+ }
+ return result;
+}
+
+} // namespace
+
+namespace mozilla {
+namespace layers {
+
+static int32_t sActiveDurationMs = 10;
+static bool sActiveDurationMsSet = false;
+
+APZEventState::APZEventState(nsIWidget* aWidget,
+ ContentReceivedInputBlockCallback&& aCallback)
+ : mWidget(nullptr) // initialized in constructor body
+ , mActiveElementManager(new ActiveElementManager())
+ , mContentReceivedInputBlockCallback(Move(aCallback))
+ , mPendingTouchPreventedResponse(false)
+ , mPendingTouchPreventedBlockId(0)
+ , mEndTouchIsClick(false)
+ , mTouchEndCancelled(false)
+ , mLastTouchIdentifier(0)
+{
+ nsresult rv;
+ mWidget = do_GetWeakReference(aWidget, &rv);
+ MOZ_ASSERT(NS_SUCCEEDED(rv), "APZEventState constructed with a widget that"
+ " does not support weak references. APZ will NOT work!");
+
+ if (!sActiveDurationMsSet) {
+ Preferences::AddIntVarCache(&sActiveDurationMs,
+ "ui.touch_activation.duration_ms",
+ sActiveDurationMs);
+ sActiveDurationMsSet = true;
+ }
+}
+
+APZEventState::~APZEventState()
+{}
+
+class DelayedFireSingleTapEvent final : public nsITimerCallback
+{
+public:
+ NS_DECL_ISUPPORTS
+
+ DelayedFireSingleTapEvent(nsWeakPtr aWidget,
+ LayoutDevicePoint& aPoint,
+ Modifiers aModifiers,
+ int32_t aClickCount,
+ nsITimer* aTimer)
+ : mWidget(aWidget)
+ , mPoint(aPoint)
+ , mModifiers(aModifiers)
+ , mClickCount(aClickCount)
+ // Hold the reference count until we are called back.
+ , mTimer(aTimer)
+ {
+ }
+
+ NS_IMETHOD Notify(nsITimer*) override
+ {
+ if (nsCOMPtr<nsIWidget> widget = do_QueryReferent(mWidget)) {
+ APZCCallbackHelper::FireSingleTapEvent(mPoint, mModifiers, mClickCount, widget);
+ }
+ mTimer = nullptr;
+ return NS_OK;
+ }
+
+ void ClearTimer() {
+ mTimer = nullptr;
+ }
+
+private:
+ ~DelayedFireSingleTapEvent()
+ {
+ }
+
+ nsWeakPtr mWidget;
+ LayoutDevicePoint mPoint;
+ Modifiers mModifiers;
+ int32_t mClickCount;
+ nsCOMPtr<nsITimer> mTimer;
+};
+
+NS_IMPL_ISUPPORTS(DelayedFireSingleTapEvent, nsITimerCallback)
+
+void
+APZEventState::ProcessSingleTap(const CSSPoint& aPoint,
+ const CSSToLayoutDeviceScale& aScale,
+ Modifiers aModifiers,
+ const ScrollableLayerGuid& aGuid,
+ int32_t aClickCount)
+{
+ APZES_LOG("Handling single tap at %s on %s with %d\n",
+ Stringify(aPoint).c_str(), Stringify(aGuid).c_str(), mTouchEndCancelled);
+
+ nsCOMPtr<nsIWidget> widget = GetWidget();
+ if (!widget) {
+ return;
+ }
+
+ if (mTouchEndCancelled) {
+ return;
+ }
+
+ LayoutDevicePoint ldPoint = aPoint * aScale;
+ if (!mActiveElementManager->ActiveElementUsesStyle()) {
+ // If the active element isn't visually affected by the :active style, we
+ // have no need to wait the extra sActiveDurationMs to make the activation
+ // visually obvious to the user.
+ APZCCallbackHelper::FireSingleTapEvent(ldPoint, aModifiers, aClickCount, widget);
+ return;
+ }
+
+ APZES_LOG("Active element uses style, scheduling timer for click event\n");
+ nsCOMPtr<nsITimer> timer = do_CreateInstance(NS_TIMER_CONTRACTID);
+ RefPtr<DelayedFireSingleTapEvent> callback =
+ new DelayedFireSingleTapEvent(mWidget, ldPoint, aModifiers, aClickCount, timer);
+ nsresult rv = timer->InitWithCallback(callback,
+ sActiveDurationMs,
+ nsITimer::TYPE_ONE_SHOT);
+ if (NS_FAILED(rv)) {
+ // Make |callback| not hold the timer, so they will both be destructed when
+ // we leave the scope of this function.
+ callback->ClearTimer();
+ }
+}
+
+bool
+APZEventState::FireContextmenuEvents(const nsCOMPtr<nsIPresShell>& aPresShell,
+ const CSSPoint& aPoint,
+ const CSSToLayoutDeviceScale& aScale,
+ Modifiers aModifiers,
+ const nsCOMPtr<nsIWidget>& aWidget)
+{
+ // Converting the modifiers to DOM format for the DispatchMouseEvent call
+ // is the most useless thing ever because nsDOMWindowUtils::SendMouseEvent
+ // just converts them back to widget format, but that API has many callers,
+ // including in JS code, so it's not trivial to change.
+ bool eventHandled =
+ APZCCallbackHelper::DispatchMouseEvent(aPresShell, NS_LITERAL_STRING("contextmenu"),
+ aPoint, 2, 1, WidgetModifiersToDOMModifiers(aModifiers), true,
+ nsIDOMMouseEvent::MOZ_SOURCE_TOUCH);
+
+ APZES_LOG("Contextmenu event handled: %d\n", eventHandled);
+ if (eventHandled) {
+ // If the contextmenu event was handled then we're showing a contextmenu,
+ // and so we should remove any activation
+ mActiveElementManager->ClearActivation();
+#ifndef XP_WIN
+ } else {
+ // If the contextmenu wasn't consumed, fire the eMouseLongTap event.
+ nsEventStatus status = APZCCallbackHelper::DispatchSynthesizedMouseEvent(
+ eMouseLongTap, /*time*/ 0, aPoint * aScale, aModifiers,
+ /*clickCount*/ 1, aWidget);
+ eventHandled = (status == nsEventStatus_eConsumeNoDefault);
+ APZES_LOG("eMouseLongTap event handled: %d\n", eventHandled);
+#endif
+ }
+
+ return eventHandled;
+}
+
+void
+APZEventState::ProcessLongTap(const nsCOMPtr<nsIPresShell>& aPresShell,
+ const CSSPoint& aPoint,
+ const CSSToLayoutDeviceScale& aScale,
+ Modifiers aModifiers,
+ const ScrollableLayerGuid& aGuid,
+ uint64_t aInputBlockId)
+{
+ APZES_LOG("Handling long tap at %s\n", Stringify(aPoint).c_str());
+
+ nsCOMPtr<nsIWidget> widget = GetWidget();
+ if (!widget) {
+ return;
+ }
+
+ SendPendingTouchPreventedResponse(false);
+
+#ifdef XP_WIN
+ // On Windows, we fire the contextmenu events when the user lifts their
+ // finger, in keeping with the platform convention. This happens in the
+ // ProcessLongTapUp function. However, we still fire the eMouseLongTap event
+ // at this time, because things like text selection or dragging may want
+ // to know about it.
+ nsEventStatus status = APZCCallbackHelper::DispatchSynthesizedMouseEvent(
+ eMouseLongTap, /*time*/ 0, aPoint * aScale, aModifiers, /*clickCount*/ 1,
+ widget);
+
+ bool eventHandled = (status == nsEventStatus_eConsumeNoDefault);
+#else
+ bool eventHandled = FireContextmenuEvents(aPresShell, aPoint, aScale,
+ aModifiers, widget);
+#endif
+ mContentReceivedInputBlockCallback(aGuid, aInputBlockId, eventHandled);
+
+ if (eventHandled) {
+ // Also send a touchcancel to content, so that listeners that might be
+ // waiting for a touchend don't trigger.
+ WidgetTouchEvent cancelTouchEvent(true, eTouchCancel, widget.get());
+ cancelTouchEvent.mModifiers = aModifiers;
+ auto ldPoint = LayoutDeviceIntPoint::Round(aPoint * aScale);
+ cancelTouchEvent.mTouches.AppendElement(new mozilla::dom::Touch(mLastTouchIdentifier,
+ ldPoint, LayoutDeviceIntPoint(), 0, 0));
+ APZCCallbackHelper::DispatchWidgetEvent(cancelTouchEvent);
+ }
+}
+
+void
+APZEventState::ProcessLongTapUp(const nsCOMPtr<nsIPresShell>& aPresShell,
+ const CSSPoint& aPoint,
+ const CSSToLayoutDeviceScale& aScale,
+ Modifiers aModifiers)
+{
+#ifdef XP_WIN
+ nsCOMPtr<nsIWidget> widget = GetWidget();
+ if (widget) {
+ FireContextmenuEvents(aPresShell, aPoint, aScale, aModifiers, widget);
+ }
+#endif
+}
+
+void
+APZEventState::ProcessTouchEvent(const WidgetTouchEvent& aEvent,
+ const ScrollableLayerGuid& aGuid,
+ uint64_t aInputBlockId,
+ nsEventStatus aApzResponse,
+ nsEventStatus aContentResponse)
+{
+ if (aEvent.mMessage == eTouchStart && aEvent.mTouches.Length() > 0) {
+ mActiveElementManager->SetTargetElement(aEvent.mTouches[0]->GetTarget());
+ mLastTouchIdentifier = aEvent.mTouches[0]->Identifier();
+ }
+
+ bool isTouchPrevented = aContentResponse == nsEventStatus_eConsumeNoDefault;
+ bool sentContentResponse = false;
+ APZES_LOG("Handling event type %d\n", aEvent.mMessage);
+ switch (aEvent.mMessage) {
+ case eTouchStart: {
+ mTouchEndCancelled = false;
+ sentContentResponse = SendPendingTouchPreventedResponse(false);
+ // sentContentResponse can be true here if we get two TOUCH_STARTs in a row
+ // and just responded to the first one.
+
+ // We're about to send a response back to APZ, but we should only do it
+ // for events that went through APZ (which should be all of them).
+ MOZ_ASSERT(aEvent.mFlags.mHandledByAPZ);
+
+ if (isTouchPrevented) {
+ mContentReceivedInputBlockCallback(aGuid, aInputBlockId, isTouchPrevented);
+ sentContentResponse = true;
+ } else {
+ APZES_LOG("Event not prevented; pending response for %" PRIu64 " %s\n",
+ aInputBlockId, Stringify(aGuid).c_str());
+ mPendingTouchPreventedResponse = true;
+ mPendingTouchPreventedGuid = aGuid;
+ mPendingTouchPreventedBlockId = aInputBlockId;
+ }
+ break;
+ }
+
+ case eTouchEnd:
+ if (isTouchPrevented) {
+ mTouchEndCancelled = true;
+ mEndTouchIsClick = false;
+ }
+ MOZ_FALLTHROUGH;
+ case eTouchCancel:
+ mActiveElementManager->HandleTouchEndEvent(mEndTouchIsClick);
+ MOZ_FALLTHROUGH;
+ case eTouchMove: {
+ if (mPendingTouchPreventedResponse) {
+ MOZ_ASSERT(aGuid == mPendingTouchPreventedGuid);
+ }
+ sentContentResponse = SendPendingTouchPreventedResponse(isTouchPrevented);
+ break;
+ }
+
+ default:
+ NS_WARNING("Unknown touch event type");
+ }
+
+ if (sentContentResponse &&
+ aApzResponse == nsEventStatus_eConsumeDoDefault &&
+ gfxPrefs::PointerEventsEnabled()) {
+ WidgetTouchEvent cancelEvent(aEvent);
+ cancelEvent.mMessage = eTouchCancel;
+ cancelEvent.mFlags.mCancelable = false; // mMessage != eTouchCancel;
+ for (uint32_t i = 0; i < cancelEvent.mTouches.Length(); ++i) {
+ if (mozilla::dom::Touch* touch = cancelEvent.mTouches[i]) {
+ touch->convertToPointer = true;
+ }
+ }
+ nsEventStatus status;
+ cancelEvent.mWidget->DispatchEvent(&cancelEvent, status);
+ }
+}
+
+void
+APZEventState::ProcessWheelEvent(const WidgetWheelEvent& aEvent,
+ const ScrollableLayerGuid& aGuid,
+ uint64_t aInputBlockId)
+{
+ // If this event starts a swipe, indicate that it shouldn't result in a
+ // scroll by setting defaultPrevented to true.
+ bool defaultPrevented = aEvent.DefaultPrevented() || aEvent.TriggersSwipe();
+ mContentReceivedInputBlockCallback(aGuid, aInputBlockId, defaultPrevented);
+}
+
+void
+APZEventState::ProcessMouseEvent(const WidgetMouseEvent& aEvent,
+ const ScrollableLayerGuid& aGuid,
+ uint64_t aInputBlockId)
+{
+ // If we get here and the drag block has not been confirmed by the code in
+ // nsSliderFrame, then no scrollbar reacted to the event thus APZC will
+ // ignore this drag block. We can send defaultPrevented as either true or
+ // false, it doesn't matter, because APZ won't have the scrollbar metrics
+ // anyway, and will know to drop the block.
+ bool defaultPrevented = false;
+ mContentReceivedInputBlockCallback(aGuid, aInputBlockId, defaultPrevented);
+}
+
+void
+APZEventState::ProcessAPZStateChange(ViewID aViewId,
+ APZStateChange aChange,
+ int aArg)
+{
+ switch (aChange)
+ {
+ case APZStateChange::eTransformBegin:
+ {
+ nsIScrollableFrame* sf = nsLayoutUtils::FindScrollableFrameFor(aViewId);
+ if (sf) {
+ sf->SetTransformingByAPZ(true);
+ }
+ nsIScrollbarMediator* scrollbarMediator = do_QueryFrame(sf);
+ if (scrollbarMediator) {
+ scrollbarMediator->ScrollbarActivityStarted();
+ }
+
+ nsIContent* content = nsLayoutUtils::FindContentFor(aViewId);
+ nsIDocument* doc = content ? content->GetComposedDoc() : nullptr;
+ nsCOMPtr<nsIDocShell> docshell(doc ? doc->GetDocShell() : nullptr);
+ if (docshell && sf) {
+ nsDocShell* nsdocshell = static_cast<nsDocShell*>(docshell.get());
+ nsdocshell->NotifyAsyncPanZoomStarted();
+ }
+ break;
+ }
+ case APZStateChange::eTransformEnd:
+ {
+ nsIScrollableFrame* sf = nsLayoutUtils::FindScrollableFrameFor(aViewId);
+ if (sf) {
+ sf->SetTransformingByAPZ(false);
+ }
+ nsIScrollbarMediator* scrollbarMediator = do_QueryFrame(sf);
+ if (scrollbarMediator) {
+ scrollbarMediator->ScrollbarActivityStopped();
+ }
+
+ nsIContent* content = nsLayoutUtils::FindContentFor(aViewId);
+ nsIDocument* doc = content ? content->GetComposedDoc() : nullptr;
+ nsCOMPtr<nsIDocShell> docshell(doc ? doc->GetDocShell() : nullptr);
+ if (docshell && sf) {
+ nsDocShell* nsdocshell = static_cast<nsDocShell*>(docshell.get());
+ nsdocshell->NotifyAsyncPanZoomStopped();
+ }
+ break;
+ }
+ case APZStateChange::eStartTouch:
+ {
+ mActiveElementManager->HandleTouchStart(aArg);
+ break;
+ }
+ case APZStateChange::eStartPanning:
+ {
+ // The user started to pan, so we don't want anything to be :active.
+ mActiveElementManager->ClearActivation();
+ break;
+ }
+ case APZStateChange::eEndTouch:
+ {
+ mEndTouchIsClick = aArg;
+ mActiveElementManager->HandleTouchEnd();
+ break;
+ }
+ case APZStateChange::eSentinel:
+ // Should never happen, but we want this case branch to stop the compiler
+ // whining about unhandled values.
+ MOZ_ASSERT(false);
+ break;
+ }
+}
+
+void
+APZEventState::ProcessClusterHit()
+{
+ // If we hit a cluster of links then we shouldn't activate any of them,
+ // as we will be showing the zoomed view. (This is only called on Fennec).
+#ifndef MOZ_WIDGET_ANDROID
+ MOZ_ASSERT(false);
+#endif
+ mActiveElementManager->ClearActivation();
+}
+
+bool
+APZEventState::SendPendingTouchPreventedResponse(bool aPreventDefault)
+{
+ if (mPendingTouchPreventedResponse) {
+ APZES_LOG("Sending response %d for pending guid: %s\n", aPreventDefault,
+ Stringify(mPendingTouchPreventedGuid).c_str());
+ mContentReceivedInputBlockCallback(mPendingTouchPreventedGuid,
+ mPendingTouchPreventedBlockId, aPreventDefault);
+ mPendingTouchPreventedResponse = false;
+ return true;
+ }
+ return false;
+}
+
+already_AddRefed<nsIWidget>
+APZEventState::GetWidget() const
+{
+ nsCOMPtr<nsIWidget> result = do_QueryReferent(mWidget);
+ return result.forget();
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/apz/util/APZEventState.h b/gfx/layers/apz/util/APZEventState.h
new file mode 100644
index 000000000..44188eaa7
--- /dev/null
+++ b/gfx/layers/apz/util/APZEventState.h
@@ -0,0 +1,103 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_layers_APZEventState_h
+#define mozilla_layers_APZEventState_h
+
+#include <stdint.h>
+
+#include "FrameMetrics.h" // for ScrollableLayerGuid
+#include "Units.h"
+#include "mozilla/EventForwards.h"
+#include "mozilla/Function.h"
+#include "mozilla/layers/GeckoContentController.h" // for APZStateChange
+#include "mozilla/RefPtr.h"
+#include "nsCOMPtr.h"
+#include "nsISupportsImpl.h" // for NS_INLINE_DECL_REFCOUNTING
+#include "nsIWeakReferenceUtils.h" // for nsWeakPtr
+
+template <class> class nsCOMPtr;
+class nsIDocument;
+class nsIPresShell;
+class nsIWidget;
+
+namespace mozilla {
+namespace layers {
+
+class ActiveElementManager;
+
+typedef function<void(const ScrollableLayerGuid&,
+ uint64_t /* input block id */,
+ bool /* prevent default */)>
+ ContentReceivedInputBlockCallback;
+
+/**
+ * A content-side component that keeps track of state for handling APZ
+ * gestures and sending APZ notifications.
+ */
+class APZEventState {
+ typedef GeckoContentController::APZStateChange APZStateChange;
+ typedef FrameMetrics::ViewID ViewID;
+public:
+ APZEventState(nsIWidget* aWidget,
+ ContentReceivedInputBlockCallback&& aCallback);
+
+ NS_INLINE_DECL_REFCOUNTING(APZEventState);
+
+ void ProcessSingleTap(const CSSPoint& aPoint,
+ const CSSToLayoutDeviceScale& aScale,
+ Modifiers aModifiers,
+ const ScrollableLayerGuid& aGuid,
+ int32_t aClickCount);
+ void ProcessLongTap(const nsCOMPtr<nsIPresShell>& aUtils,
+ const CSSPoint& aPoint,
+ const CSSToLayoutDeviceScale& aScale,
+ Modifiers aModifiers,
+ const ScrollableLayerGuid& aGuid,
+ uint64_t aInputBlockId);
+ void ProcessLongTapUp(const nsCOMPtr<nsIPresShell>& aPresShell,
+ const CSSPoint& aPoint,
+ const CSSToLayoutDeviceScale& aScale,
+ Modifiers aModifiers);
+ void ProcessTouchEvent(const WidgetTouchEvent& aEvent,
+ const ScrollableLayerGuid& aGuid,
+ uint64_t aInputBlockId,
+ nsEventStatus aApzResponse,
+ nsEventStatus aContentResponse);
+ void ProcessWheelEvent(const WidgetWheelEvent& aEvent,
+ const ScrollableLayerGuid& aGuid,
+ uint64_t aInputBlockId);
+ void ProcessMouseEvent(const WidgetMouseEvent& aEvent,
+ const ScrollableLayerGuid& aGuid,
+ uint64_t aInputBlockId);
+ void ProcessAPZStateChange(ViewID aViewId,
+ APZStateChange aChange,
+ int aArg);
+ void ProcessClusterHit();
+private:
+ ~APZEventState();
+ bool SendPendingTouchPreventedResponse(bool aPreventDefault);
+ bool FireContextmenuEvents(const nsCOMPtr<nsIPresShell>& aPresShell,
+ const CSSPoint& aPoint,
+ const CSSToLayoutDeviceScale& aScale,
+ Modifiers aModifiers,
+ const nsCOMPtr<nsIWidget>& aWidget);
+ already_AddRefed<nsIWidget> GetWidget() const;
+private:
+ nsWeakPtr mWidget;
+ RefPtr<ActiveElementManager> mActiveElementManager;
+ ContentReceivedInputBlockCallback mContentReceivedInputBlockCallback;
+ bool mPendingTouchPreventedResponse;
+ ScrollableLayerGuid mPendingTouchPreventedGuid;
+ uint64_t mPendingTouchPreventedBlockId;
+ bool mEndTouchIsClick;
+ bool mTouchEndCancelled;
+ int32_t mLastTouchIdentifier;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif /* mozilla_layers_APZEventState_h */
diff --git a/gfx/layers/apz/util/APZThreadUtils.cpp b/gfx/layers/apz/util/APZThreadUtils.cpp
new file mode 100644
index 000000000..46f67d010
--- /dev/null
+++ b/gfx/layers/apz/util/APZThreadUtils.cpp
@@ -0,0 +1,96 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/layers/APZThreadUtils.h"
+
+#include "mozilla/layers/Compositor.h"
+#ifdef MOZ_WIDGET_ANDROID
+#include "AndroidBridge.h"
+#endif
+
+namespace mozilla {
+namespace layers {
+
+static bool sThreadAssertionsEnabled = true;
+static MessageLoop* sControllerThread;
+
+/*static*/ void
+APZThreadUtils::SetThreadAssertionsEnabled(bool aEnabled) {
+ sThreadAssertionsEnabled = aEnabled;
+}
+
+/*static*/ bool
+APZThreadUtils::GetThreadAssertionsEnabled() {
+ return sThreadAssertionsEnabled;
+}
+
+/*static*/ void
+APZThreadUtils::SetControllerThread(MessageLoop* aLoop)
+{
+ // We must either be setting the initial controller thread, or removing it,
+ // or re-using an existing controller thread.
+ MOZ_ASSERT(!sControllerThread || !aLoop || sControllerThread == aLoop);
+ sControllerThread = aLoop;
+}
+
+/*static*/ void
+APZThreadUtils::AssertOnControllerThread() {
+ if (!GetThreadAssertionsEnabled()) {
+ return;
+ }
+
+ MOZ_ASSERT(sControllerThread == MessageLoop::current());
+}
+
+/*static*/ void
+APZThreadUtils::AssertOnCompositorThread()
+{
+ if (GetThreadAssertionsEnabled()) {
+ Compositor::AssertOnCompositorThread();
+ }
+}
+
+/*static*/ void
+APZThreadUtils::RunOnControllerThread(already_AddRefed<Runnable> aTask)
+{
+ RefPtr<Runnable> task = aTask;
+
+#ifdef MOZ_WIDGET_ANDROID
+ // This is needed while nsWindow::ConfigureAPZControllerThread is not propper
+ // implemented.
+ if (AndroidBridge::IsJavaUiThread()) {
+ task->Run();
+ } else {
+ AndroidBridge::Bridge()->PostTaskToUiThread(task.forget(), 0);
+ }
+#else
+ if (!sControllerThread) {
+ // Could happen on startup
+ NS_WARNING("Dropping task posted to controller thread");
+ return;
+ }
+
+ if (sControllerThread == MessageLoop::current()) {
+ task->Run();
+ } else {
+ sControllerThread->PostTask(task.forget());
+ }
+#endif
+}
+
+/*static*/ bool
+APZThreadUtils::IsControllerThread()
+{
+#ifdef MOZ_WIDGET_ANDROID
+ return AndroidBridge::IsJavaUiThread();
+#else
+ return sControllerThread == MessageLoop::current();
+#endif
+}
+
+NS_IMPL_ISUPPORTS(GenericTimerCallbackBase, nsITimerCallback)
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/apz/util/APZThreadUtils.h b/gfx/layers/apz/util/APZThreadUtils.h
new file mode 100644
index 000000000..4b9b2c0d0
--- /dev/null
+++ b/gfx/layers/apz/util/APZThreadUtils.h
@@ -0,0 +1,104 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_layers_APZThreadUtils_h
+#define mozilla_layers_APZThreadUtils_h
+
+#include "base/message_loop.h"
+#include "nsITimer.h"
+
+namespace mozilla {
+
+class Runnable;
+
+namespace layers {
+
+class APZThreadUtils
+{
+public:
+ /**
+ * In the gtest environment everything runs on one thread, so we
+ * shouldn't assert that we're on a particular thread. This enables
+ * that behaviour.
+ */
+ static void SetThreadAssertionsEnabled(bool aEnabled);
+ static bool GetThreadAssertionsEnabled();
+
+ /**
+ * Set the controller thread.
+ */
+ static void SetControllerThread(MessageLoop* aLoop);
+
+ /**
+ * This can be used to assert that the current thread is the
+ * controller/UI thread (on which input events are received).
+ * This does nothing if thread assertions are disabled.
+ */
+ static void AssertOnControllerThread();
+
+ /**
+ * This can be used to assert that the current thread is the
+ * compositor thread (which applies the async transform).
+ * This does nothing if thread assertions are disabled.
+ */
+ static void AssertOnCompositorThread();
+
+ /**
+ * Run the given task on the APZ "controller thread" for this platform. If
+ * this function is called from the controller thread itself then the task is
+ * run immediately without getting queued.
+ */
+ static void RunOnControllerThread(already_AddRefed<Runnable> aTask);
+
+ /**
+ * Returns true if currently on APZ "controller thread".
+ */
+ static bool IsControllerThread();
+};
+
+// A base class for GenericTimerCallback<Function>.
+// This is necessary because NS_IMPL_ISUPPORTS doesn't work for a class
+// template.
+class GenericTimerCallbackBase : public nsITimerCallback
+{
+public:
+ NS_DECL_THREADSAFE_ISUPPORTS
+
+protected:
+ virtual ~GenericTimerCallbackBase() {}
+};
+
+// An nsITimerCallback implementation that can be used with any function
+// object that's callable with no arguments.
+template <typename Function>
+class GenericTimerCallback final : public GenericTimerCallbackBase
+{
+public:
+ explicit GenericTimerCallback(const Function& aFunction) : mFunction(aFunction) {}
+
+ NS_IMETHOD Notify(nsITimer*) override
+ {
+ mFunction();
+ return NS_OK;
+ }
+private:
+ Function mFunction;
+};
+
+// Convenience function for constructing a GenericTimerCallback.
+// Returns a raw pointer, suitable for passing directly as an argument to
+// nsITimer::InitWithCallback(). The intention is to enable the following
+// terse inline usage:
+// timer->InitWithCallback(NewTimerCallback([](){ ... }), delay);
+template <typename Function>
+GenericTimerCallback<Function>* NewTimerCallback(const Function& aFunction)
+{
+ return new GenericTimerCallback<Function>(aFunction);
+}
+
+} // namespace layers
+} // namespace mozilla
+
+#endif /* mozilla_layers_APZThreadUtils_h */
diff --git a/gfx/layers/apz/util/ActiveElementManager.cpp b/gfx/layers/apz/util/ActiveElementManager.cpp
new file mode 100644
index 000000000..20d34aa2b
--- /dev/null
+++ b/gfx/layers/apz/util/ActiveElementManager.cpp
@@ -0,0 +1,237 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "ActiveElementManager.h"
+#include "mozilla/EventStateManager.h"
+#include "mozilla/EventStates.h"
+#include "mozilla/StyleSetHandle.h"
+#include "mozilla/StyleSetHandleInlines.h"
+#include "mozilla/Preferences.h"
+#include "base/message_loop.h"
+#include "base/task.h"
+#include "mozilla/dom/Element.h"
+#include "nsIDocument.h"
+#include "nsStyleSet.h"
+
+#define AEM_LOG(...)
+// #define AEM_LOG(...) printf_stderr("AEM: " __VA_ARGS__)
+
+namespace mozilla {
+namespace layers {
+
+static int32_t sActivationDelayMs = 100;
+static bool sActivationDelayMsSet = false;
+
+ActiveElementManager::ActiveElementManager()
+ : mCanBePan(false),
+ mCanBePanSet(false),
+ mSetActiveTask(nullptr),
+ mActiveElementUsesStyle(false)
+{
+ if (!sActivationDelayMsSet) {
+ Preferences::AddIntVarCache(&sActivationDelayMs,
+ "ui.touch_activation.delay_ms",
+ sActivationDelayMs);
+ sActivationDelayMsSet = true;
+ }
+}
+
+ActiveElementManager::~ActiveElementManager() {}
+
+void
+ActiveElementManager::SetTargetElement(dom::EventTarget* aTarget)
+{
+ if (mTarget) {
+ // Multiple fingers on screen (since HandleTouchEnd clears mTarget).
+ AEM_LOG("Multiple fingers on-screen, clearing target element\n");
+ CancelTask();
+ ResetActive();
+ ResetTouchBlockState();
+ return;
+ }
+
+ mTarget = do_QueryInterface(aTarget);
+ AEM_LOG("Setting target element to %p\n", mTarget.get());
+ TriggerElementActivation();
+}
+
+void
+ActiveElementManager::HandleTouchStart(bool aCanBePan)
+{
+ AEM_LOG("Touch start, aCanBePan: %d\n", aCanBePan);
+ if (mCanBePanSet) {
+ // Multiple fingers on screen (since HandleTouchEnd clears mCanBePanSet).
+ AEM_LOG("Multiple fingers on-screen, clearing touch block state\n");
+ CancelTask();
+ ResetActive();
+ ResetTouchBlockState();
+ return;
+ }
+
+ mCanBePan = aCanBePan;
+ mCanBePanSet = true;
+ TriggerElementActivation();
+}
+
+void
+ActiveElementManager::TriggerElementActivation()
+{
+ // Both HandleTouchStart() and SetTargetElement() call this. They can be
+ // called in either order. One will set mCanBePanSet, and the other, mTarget.
+ // We want to actually trigger the activation once both are set.
+ if (!(mTarget && mCanBePanSet)) {
+ return;
+ }
+
+ // If the touch cannot be a pan, make mTarget :active right away.
+ // Otherwise, wait a bit to see if the user will pan or not.
+ if (!mCanBePan) {
+ SetActive(mTarget);
+ } else {
+ CancelTask(); // this is only needed because of bug 1169802. Fixing that
+ // bug properly should make this unnecessary.
+ MOZ_ASSERT(mSetActiveTask == nullptr);
+
+ RefPtr<CancelableRunnable> task =
+ NewCancelableRunnableMethod<nsCOMPtr<dom::Element>>(this,
+ &ActiveElementManager::SetActiveTask,
+ mTarget);
+ mSetActiveTask = task;
+ MessageLoop::current()->PostDelayedTask(task.forget(), sActivationDelayMs);
+ AEM_LOG("Scheduling mSetActiveTask %p\n", mSetActiveTask);
+ }
+}
+
+void
+ActiveElementManager::ClearActivation()
+{
+ AEM_LOG("Clearing element activation\n");
+ CancelTask();
+ ResetActive();
+}
+
+void
+ActiveElementManager::HandleTouchEndEvent(bool aWasClick)
+{
+ AEM_LOG("Touch end event, aWasClick: %d\n", aWasClick);
+
+ // If the touch was a click, make mTarget :active right away.
+ // nsEventStateManager will reset the active element when processing
+ // the mouse-down event generated by the click.
+ CancelTask();
+ if (aWasClick) {
+ SetActive(mTarget);
+ } else {
+ // We might reach here if mCanBePan was false on touch-start and
+ // so we set the element active right away. Now it turns out the
+ // action was not a click so we need to reset the active element.
+ ResetActive();
+ }
+
+ ResetTouchBlockState();
+}
+
+void
+ActiveElementManager::HandleTouchEnd()
+{
+ AEM_LOG("Touch end, clearing pan state\n");
+ mCanBePanSet = false;
+}
+
+bool
+ActiveElementManager::ActiveElementUsesStyle() const
+{
+ return mActiveElementUsesStyle;
+}
+
+static nsPresContext*
+GetPresContextFor(nsIContent* aContent)
+{
+ if (!aContent) {
+ return nullptr;
+ }
+ nsIPresShell* shell = aContent->OwnerDoc()->GetShell();
+ if (!shell) {
+ return nullptr;
+ }
+ return shell->GetPresContext();
+}
+
+static bool
+ElementHasActiveStyle(dom::Element* aElement)
+{
+ nsPresContext* pc = GetPresContextFor(aElement);
+ if (!pc) {
+ return false;
+ }
+ StyleSetHandle styleSet = pc->StyleSet();
+ for (dom::Element* e = aElement; e; e = e->GetParentElement()) {
+ if (styleSet->HasStateDependentStyle(e, NS_EVENT_STATE_ACTIVE)) {
+ AEM_LOG("Element %p's style is dependent on the active state\n", e);
+ return true;
+ }
+ }
+ AEM_LOG("Element %p doesn't use active styles\n", aElement);
+ return false;
+}
+
+void
+ActiveElementManager::SetActive(dom::Element* aTarget)
+{
+ AEM_LOG("Setting active %p\n", aTarget);
+
+ if (nsPresContext* pc = GetPresContextFor(aTarget)) {
+ pc->EventStateManager()->SetContentState(aTarget, NS_EVENT_STATE_ACTIVE);
+ mActiveElementUsesStyle = ElementHasActiveStyle(aTarget);
+ }
+}
+
+void
+ActiveElementManager::ResetActive()
+{
+ AEM_LOG("Resetting active from %p\n", mTarget.get());
+
+ // Clear the :active flag from mTarget by setting it on the document root.
+ if (mTarget) {
+ dom::Element* root = mTarget->OwnerDoc()->GetDocumentElement();
+ if (root) {
+ AEM_LOG("Found root %p, making active\n", root);
+ SetActive(root);
+ }
+ }
+}
+
+void
+ActiveElementManager::ResetTouchBlockState()
+{
+ mTarget = nullptr;
+ mCanBePanSet = false;
+}
+
+void
+ActiveElementManager::SetActiveTask(const nsCOMPtr<dom::Element>& aTarget)
+{
+ AEM_LOG("mSetActiveTask %p running\n", mSetActiveTask);
+
+ // This gets called from mSetActiveTask's Run() method. The message loop
+ // deletes the task right after running it, so we need to null out
+ // mSetActiveTask to make sure we're not left with a dangling pointer.
+ mSetActiveTask = nullptr;
+ SetActive(aTarget);
+}
+
+void
+ActiveElementManager::CancelTask()
+{
+ AEM_LOG("Cancelling task %p\n", mSetActiveTask);
+
+ if (mSetActiveTask) {
+ mSetActiveTask->Cancel();
+ mSetActiveTask = nullptr;
+ }
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/apz/util/ActiveElementManager.h b/gfx/layers/apz/util/ActiveElementManager.h
new file mode 100644
index 000000000..83d0cb29f
--- /dev/null
+++ b/gfx/layers/apz/util/ActiveElementManager.h
@@ -0,0 +1,104 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_layers_ActiveElementManager_h
+#define mozilla_layers_ActiveElementManager_h
+
+#include "nsCOMPtr.h"
+#include "nsISupportsImpl.h"
+
+namespace mozilla {
+
+class CancelableRunnable;
+
+namespace dom {
+class Element;
+class EventTarget;
+} // namespace dom
+
+namespace layers {
+
+/**
+ * Manages setting and clearing the ':active' CSS pseudostate in the presence
+ * of touch input.
+ */
+class ActiveElementManager {
+ ~ActiveElementManager();
+public:
+ NS_INLINE_DECL_REFCOUNTING(ActiveElementManager)
+
+ ActiveElementManager();
+
+ /**
+ * Specify the target of a touch. Typically this should be called right
+ * after HandleTouchStart(), but in cases where the APZ needs to wait for
+ * a content response the HandleTouchStart() may be delayed, in which case
+ * this function can be called first.
+ * |aTarget| may be nullptr.
+ */
+ void SetTargetElement(dom::EventTarget* aTarget);
+ /**
+ * Handle a touch-start state notification from APZ. This notification
+ * may be delayed until after touch listeners have responded to the APZ.
+ * @param aCanBePan whether the touch can be a pan
+ */
+ void HandleTouchStart(bool aCanBePan);
+ /**
+ * Clear the active element.
+ */
+ void ClearActivation();
+ /**
+ * Handle a touch-end or touch-cancel event.
+ * @param aWasClick whether the touch was a click
+ */
+ void HandleTouchEndEvent(bool aWasClick);
+ /**
+ * Handle a touch-end state notification from APZ. This notification may be
+ * delayed until after touch listeners have responded to the APZ.
+ */
+ void HandleTouchEnd();
+ /**
+ * @return true iff the currently active element (or one of its ancestors)
+ * actually had a style for the :active pseudo-class. The currently active
+ * element is the root element if no other elements are active.
+ */
+ bool ActiveElementUsesStyle() const;
+private:
+ /**
+ * The target of the first touch point in the current touch block.
+ */
+ nsCOMPtr<dom::Element> mTarget;
+ /**
+ * Whether the current touch block can be a pan. Set in HandleTouchStart().
+ */
+ bool mCanBePan;
+ /**
+ * Whether mCanBePan has been set for the current touch block.
+ * We need to keep track of this to allow HandleTouchStart() and
+ * SetTargetElement() to be called in either order.
+ */
+ bool mCanBePanSet;
+ /**
+ * A task for calling SetActive() after a timeout.
+ */
+ RefPtr<CancelableRunnable> mSetActiveTask;
+ /**
+ * See ActiveElementUsesStyle() documentation.
+ */
+ bool mActiveElementUsesStyle;
+
+ // Helpers
+ void TriggerElementActivation();
+ void SetActive(dom::Element* aTarget);
+ void ResetActive();
+ void ResetTouchBlockState();
+ void SetActiveTask(const nsCOMPtr<dom::Element>& aTarget);
+ void CancelTask();
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif /* mozilla_layers_ActiveElementManager_h */
diff --git a/gfx/layers/apz/util/CheckerboardReportService.cpp b/gfx/layers/apz/util/CheckerboardReportService.cpp
new file mode 100644
index 000000000..0924fe92d
--- /dev/null
+++ b/gfx/layers/apz/util/CheckerboardReportService.cpp
@@ -0,0 +1,228 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "CheckerboardReportService.h"
+
+#include "gfxPrefs.h" // for gfxPrefs
+#include "jsapi.h" // for JS_Now
+#include "MainThreadUtils.h" // for NS_IsMainThread
+#include "mozilla/Assertions.h" // for MOZ_ASSERT
+#include "mozilla/ClearOnShutdown.h" // for ClearOnShutdown
+#include "mozilla/Unused.h"
+#include "mozilla/dom/CheckerboardReportServiceBinding.h" // for dom::CheckerboardReports
+#include "mozilla/gfx/GPUParent.h"
+#include "mozilla/gfx/GPUProcessManager.h"
+#include "nsContentUtils.h" // for nsContentUtils
+#include "nsXULAppAPI.h"
+
+namespace mozilla {
+namespace layers {
+
+/*static*/ StaticRefPtr<CheckerboardEventStorage> CheckerboardEventStorage::sInstance;
+
+/*static*/ already_AddRefed<CheckerboardEventStorage>
+CheckerboardEventStorage::GetInstance()
+{
+ // The instance in the parent process does all the work, so if this is getting
+ // called in the child process something is likely wrong.
+ MOZ_ASSERT(XRE_IsParentProcess());
+
+ MOZ_ASSERT(NS_IsMainThread());
+ if (!sInstance) {
+ sInstance = new CheckerboardEventStorage();
+ ClearOnShutdown(&sInstance);
+ }
+ RefPtr<CheckerboardEventStorage> instance = sInstance.get();
+ return instance.forget();
+}
+
+void
+CheckerboardEventStorage::Report(uint32_t aSeverity, const std::string& aLog)
+{
+ if (!NS_IsMainThread()) {
+ RefPtr<Runnable> task = NS_NewRunnableFunction([aSeverity, aLog] () -> void {
+ CheckerboardEventStorage::Report(aSeverity, aLog);
+ });
+ NS_DispatchToMainThread(task.forget());
+ return;
+ }
+
+ if (XRE_IsGPUProcess()) {
+ if (gfx::GPUParent* gpu = gfx::GPUParent::GetSingleton()) {
+ nsCString log(aLog.c_str());
+ Unused << gpu->SendReportCheckerboard(aSeverity, log);
+ }
+ return;
+ }
+
+ RefPtr<CheckerboardEventStorage> storage = GetInstance();
+ storage->ReportCheckerboard(aSeverity, aLog);
+}
+
+void
+CheckerboardEventStorage::ReportCheckerboard(uint32_t aSeverity, const std::string& aLog)
+{
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (aSeverity == 0) {
+ // This code assumes all checkerboard reports have a nonzero severity.
+ return;
+ }
+
+ CheckerboardReport severe(aSeverity, JS_Now(), aLog);
+ CheckerboardReport recent;
+
+ // First look in the "severe" reports to see if the new one belongs in that
+ // list.
+ for (int i = 0; i < SEVERITY_MAX_INDEX; i++) {
+ if (mCheckerboardReports[i].mSeverity >= severe.mSeverity) {
+ continue;
+ }
+ // The new one deserves to be in the "severe" list. Take the one getting
+ // bumped off the list, and put it in |recent| for possible insertion into
+ // the recents list.
+ recent = mCheckerboardReports[SEVERITY_MAX_INDEX - 1];
+
+ // Shuffle the severe list down, insert the new one.
+ for (int j = SEVERITY_MAX_INDEX - 1; j > i; j--) {
+ mCheckerboardReports[j] = mCheckerboardReports[j - 1];
+ }
+ mCheckerboardReports[i] = severe;
+ severe.mSeverity = 0; // mark |severe| as inserted
+ break;
+ }
+
+ // If |severe.mSeverity| is nonzero, the incoming report didn't get inserted
+ // into the severe list; put it into |recent| for insertion into the recent
+ // list.
+ if (severe.mSeverity) {
+ MOZ_ASSERT(recent.mSeverity == 0, "recent should be empty here");
+ recent = severe;
+ } // else |recent| may hold a report that got knocked out of the severe list.
+
+ if (recent.mSeverity == 0) {
+ // Nothing to be inserted into the recent list.
+ return;
+ }
+
+ // If it wasn't in the "severe" list, add it to the "recent" list.
+ for (int i = SEVERITY_MAX_INDEX; i < RECENT_MAX_INDEX; i++) {
+ if (mCheckerboardReports[i].mTimestamp >= recent.mTimestamp) {
+ continue;
+ }
+ // |recent| needs to be inserted at |i|. Shuffle the remaining ones down
+ // and insert it.
+ for (int j = RECENT_MAX_INDEX - 1; j > i; j--) {
+ mCheckerboardReports[j] = mCheckerboardReports[j - 1];
+ }
+ mCheckerboardReports[i] = recent;
+ break;
+ }
+}
+
+void
+CheckerboardEventStorage::GetReports(nsTArray<dom::CheckerboardReport>& aOutReports)
+{
+ MOZ_ASSERT(NS_IsMainThread());
+
+ for (int i = 0; i < RECENT_MAX_INDEX; i++) {
+ CheckerboardReport& r = mCheckerboardReports[i];
+ if (r.mSeverity == 0) {
+ continue;
+ }
+ dom::CheckerboardReport report;
+ report.mSeverity.Construct() = r.mSeverity;
+ report.mTimestamp.Construct() = r.mTimestamp / 1000; // micros to millis
+ report.mLog.Construct() = NS_ConvertUTF8toUTF16(r.mLog.c_str(), r.mLog.size());
+ report.mReason.Construct() = (i < SEVERITY_MAX_INDEX)
+ ? dom::CheckerboardReason::Severe
+ : dom::CheckerboardReason::Recent;
+ aOutReports.AppendElement(report);
+ }
+}
+
+} // namespace layers
+
+namespace dom {
+
+NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(CheckerboardReportService, mParent)
+NS_IMPL_CYCLE_COLLECTION_ROOT_NATIVE(CheckerboardReportService, AddRef)
+NS_IMPL_CYCLE_COLLECTION_UNROOT_NATIVE(CheckerboardReportService, Release)
+
+/*static*/ bool
+CheckerboardReportService::IsEnabled(JSContext* aCtx, JSObject* aGlobal)
+{
+ // Only allow this in the parent process
+ if (!XRE_IsParentProcess()) {
+ return false;
+ }
+ // Allow privileged code or about:checkerboard (unprivileged) to access this.
+ return nsContentUtils::IsCallerChrome()
+ || nsContentUtils::IsSpecificAboutPage(aGlobal, "about:checkerboard");
+}
+
+/*static*/ already_AddRefed<CheckerboardReportService>
+CheckerboardReportService::Constructor(const dom::GlobalObject& aGlobal, ErrorResult& aRv)
+{
+ RefPtr<CheckerboardReportService> ces = new CheckerboardReportService(aGlobal.GetAsSupports());
+ return ces.forget();
+}
+
+CheckerboardReportService::CheckerboardReportService(nsISupports* aParent)
+ : mParent(aParent)
+{
+}
+
+JSObject*
+CheckerboardReportService::WrapObject(JSContext* aCtx, JS::Handle<JSObject*> aGivenProto)
+{
+ return CheckerboardReportServiceBinding::Wrap(aCtx, this, aGivenProto);
+}
+
+nsISupports*
+CheckerboardReportService::GetParentObject()
+{
+ return mParent;
+}
+
+void
+CheckerboardReportService::GetReports(nsTArray<dom::CheckerboardReport>& aOutReports)
+{
+ RefPtr<mozilla::layers::CheckerboardEventStorage> instance =
+ mozilla::layers::CheckerboardEventStorage::GetInstance();
+ MOZ_ASSERT(instance);
+ instance->GetReports(aOutReports);
+}
+
+bool
+CheckerboardReportService::IsRecordingEnabled() const
+{
+ return gfxPrefs::APZRecordCheckerboarding();
+}
+
+void
+CheckerboardReportService::SetRecordingEnabled(bool aEnabled)
+{
+ gfxPrefs::SetAPZRecordCheckerboarding(aEnabled);
+}
+
+void
+CheckerboardReportService::FlushActiveReports()
+{
+ MOZ_ASSERT(XRE_IsParentProcess());
+ gfx::GPUProcessManager* gpu = gfx::GPUProcessManager::Get();
+ if (gpu && gpu->NotifyGpuObservers("APZ:FlushActiveCheckerboard")) {
+ return;
+ }
+
+ nsCOMPtr<nsIObserverService> obsSvc = mozilla::services::GetObserverService();
+ MOZ_ASSERT(obsSvc);
+ if (obsSvc) {
+ obsSvc->NotifyObservers(nullptr, "APZ:FlushActiveCheckerboard", nullptr);
+ }
+}
+
+} // namespace dom
+} // namespace mozilla
diff --git a/gfx/layers/apz/util/CheckerboardReportService.h b/gfx/layers/apz/util/CheckerboardReportService.h
new file mode 100644
index 000000000..743b29825
--- /dev/null
+++ b/gfx/layers/apz/util/CheckerboardReportService.h
@@ -0,0 +1,144 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_CheckerboardReportService_h
+#define mozilla_dom_CheckerboardReportService_h
+
+#include <string>
+
+#include "js/TypeDecls.h" // for JSContext, JSObject
+#include "mozilla/ErrorResult.h" // for ErrorResult
+#include "mozilla/StaticPtr.h" // for StaticRefPtr
+#include "nsCOMPtr.h" // for nsCOMPtr
+#include "nsISupports.h" // for NS_INLINE_DECL_REFCOUNTING
+#include "nsWrapperCache.h" // for nsWrapperCache
+
+namespace mozilla {
+
+namespace dom {
+struct CheckerboardReport;
+}
+
+namespace layers {
+
+// CheckerboardEventStorage is a singleton that stores info on checkerboard
+// events, so that they can be accessed from about:checkerboard and visualized.
+// Note that this class is NOT threadsafe, and all methods must be called on
+// the main thread.
+class CheckerboardEventStorage
+{
+ NS_INLINE_DECL_REFCOUNTING(CheckerboardEventStorage)
+
+public:
+ /**
+ * Get the singleton instance.
+ */
+ static already_AddRefed<CheckerboardEventStorage> GetInstance();
+
+ /**
+ * Get the stored checkerboard reports.
+ */
+ void GetReports(nsTArray<dom::CheckerboardReport>& aOutReports);
+
+ /**
+ * Save a checkerboard event log, optionally dropping older ones that were
+ * less severe or less recent. Zero-severity reports may be ignored entirely.
+ */
+ static void Report(uint32_t aSeverity, const std::string& aLog);
+
+private:
+ /* Stuff for refcounted singleton */
+ CheckerboardEventStorage() {}
+ virtual ~CheckerboardEventStorage() {}
+
+ static StaticRefPtr<CheckerboardEventStorage> sInstance;
+
+ void ReportCheckerboard(uint32_t aSeverity, const std::string& aLog);
+
+private:
+ /**
+ * Struct that this class uses internally to store a checkerboard report.
+ */
+ struct CheckerboardReport {
+ uint32_t mSeverity; // if 0, this report is empty
+ int64_t mTimestamp; // microseconds since epoch, as from JS_Now()
+ std::string mLog;
+
+ CheckerboardReport()
+ : mSeverity(0)
+ , mTimestamp(0)
+ {}
+
+ CheckerboardReport(uint32_t aSeverity, int64_t aTimestamp,
+ const std::string& aLog)
+ : mSeverity(aSeverity)
+ , mTimestamp(aTimestamp)
+ , mLog(aLog)
+ {}
+ };
+
+ // The first 5 (indices 0-4) are the most severe ones in decreasing order
+ // of severity; the next 5 (indices 5-9) are the most recent ones that are
+ // not already in the "severe" list.
+ static const int SEVERITY_MAX_INDEX = 5;
+ static const int RECENT_MAX_INDEX = 10;
+ CheckerboardReport mCheckerboardReports[RECENT_MAX_INDEX];
+};
+
+} // namespace layers
+
+namespace dom {
+
+class GlobalObject;
+
+/**
+ * CheckerboardReportService is a wrapper object that allows access to the
+ * stuff in CheckerboardEventStorage (above). We need this wrapper for proper
+ * garbage/cycle collection, since this can be accessed from JS.
+ */
+class CheckerboardReportService : public nsWrapperCache
+{
+public:
+ /**
+ * Check if the given page is allowed to access this object via the WebIDL
+ * bindings. It only returns true if the page is about:checkerboard.
+ */
+ static bool IsEnabled(JSContext* aCtx, JSObject* aGlobal);
+
+ /*
+ * Other standard WebIDL binding glue.
+ */
+
+ static already_AddRefed<CheckerboardReportService>
+ Constructor(const dom::GlobalObject& aGlobal, ErrorResult& aRv);
+
+ explicit CheckerboardReportService(nsISupports* aSupports);
+
+ JSObject* WrapObject(JSContext* aCtx, JS::Handle<JSObject*> aGivenProto) override;
+
+ nsISupports* GetParentObject();
+
+ NS_INLINE_DECL_CYCLE_COLLECTING_NATIVE_REFCOUNTING(CheckerboardReportService)
+ NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_NATIVE_CLASS(CheckerboardReportService)
+
+public:
+ /*
+ * The methods exposed via the webidl.
+ */
+ void GetReports(nsTArray<dom::CheckerboardReport>& aOutReports);
+ bool IsRecordingEnabled() const;
+ void SetRecordingEnabled(bool aEnabled);
+ void FlushActiveReports();
+
+private:
+ virtual ~CheckerboardReportService() {}
+
+ nsCOMPtr<nsISupports> mParent;
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif /* mozilla_layers_CheckerboardReportService_h */
diff --git a/gfx/layers/apz/util/ChromeProcessController.cpp b/gfx/layers/apz/util/ChromeProcessController.cpp
new file mode 100644
index 000000000..ac8b3824f
--- /dev/null
+++ b/gfx/layers/apz/util/ChromeProcessController.cpp
@@ -0,0 +1,276 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "ChromeProcessController.h"
+
+#include "MainThreadUtils.h" // for NS_IsMainThread()
+#include "base/message_loop.h" // for MessageLoop
+#include "mozilla/dom/Element.h"
+#include "mozilla/layers/CompositorBridgeParent.h"
+#include "mozilla/layers/APZCCallbackHelper.h"
+#include "mozilla/layers/APZEventState.h"
+#include "mozilla/layers/IAPZCTreeManager.h"
+#include "mozilla/layers/DoubleTapToZoom.h"
+#include "nsIDocument.h"
+#include "nsIInterfaceRequestorUtils.h"
+#include "nsIPresShell.h"
+#include "nsLayoutUtils.h"
+#include "nsView.h"
+
+using namespace mozilla;
+using namespace mozilla::layers;
+using namespace mozilla::widget;
+
+ChromeProcessController::ChromeProcessController(nsIWidget* aWidget,
+ APZEventState* aAPZEventState,
+ IAPZCTreeManager* aAPZCTreeManager)
+ : mWidget(aWidget)
+ , mAPZEventState(aAPZEventState)
+ , mAPZCTreeManager(aAPZCTreeManager)
+ , mUILoop(MessageLoop::current())
+{
+ // Otherwise we're initializing mUILoop incorrectly.
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aAPZEventState);
+ MOZ_ASSERT(aAPZCTreeManager);
+
+ mUILoop->PostTask(NewRunnableMethod(this, &ChromeProcessController::InitializeRoot));
+}
+
+ChromeProcessController::~ChromeProcessController() {}
+
+void
+ChromeProcessController::InitializeRoot()
+{
+ APZCCallbackHelper::InitializeRootDisplayport(GetPresShell());
+}
+
+void
+ChromeProcessController::RequestContentRepaint(const FrameMetrics& aFrameMetrics)
+{
+ MOZ_ASSERT(IsRepaintThread());
+
+ FrameMetrics metrics = aFrameMetrics;
+ if (metrics.IsRootContent()) {
+ APZCCallbackHelper::UpdateRootFrame(metrics);
+ } else {
+ APZCCallbackHelper::UpdateSubFrame(metrics);
+ }
+}
+
+void
+ChromeProcessController::PostDelayedTask(already_AddRefed<Runnable> aTask, int aDelayMs)
+{
+ MessageLoop::current()->PostDelayedTask(Move(aTask), aDelayMs);
+}
+
+bool
+ChromeProcessController::IsRepaintThread()
+{
+ return NS_IsMainThread();
+}
+
+void
+ChromeProcessController::DispatchToRepaintThread(already_AddRefed<Runnable> aTask)
+{
+ NS_DispatchToMainThread(Move(aTask));
+}
+
+void
+ChromeProcessController::Destroy()
+{
+ if (MessageLoop::current() != mUILoop) {
+ mUILoop->PostTask(NewRunnableMethod(this, &ChromeProcessController::Destroy));
+ return;
+ }
+
+ MOZ_ASSERT(MessageLoop::current() == mUILoop);
+ mWidget = nullptr;
+ mAPZEventState = nullptr;
+}
+
+nsIPresShell*
+ChromeProcessController::GetPresShell() const
+{
+ if (!mWidget) {
+ return nullptr;
+ }
+ if (nsView* view = nsView::GetViewFor(mWidget)) {
+ return view->GetPresShell();
+ }
+ return nullptr;
+}
+
+nsIDocument*
+ChromeProcessController::GetRootDocument() const
+{
+ if (nsIPresShell* presShell = GetPresShell()) {
+ return presShell->GetDocument();
+ }
+ return nullptr;
+}
+
+nsIDocument*
+ChromeProcessController::GetRootContentDocument(const FrameMetrics::ViewID& aScrollId) const
+{
+ nsIContent* content = nsLayoutUtils::FindContentFor(aScrollId);
+ if (!content) {
+ return nullptr;
+ }
+ nsIPresShell* presShell = APZCCallbackHelper::GetRootContentDocumentPresShellForContent(content);
+ if (presShell) {
+ return presShell->GetDocument();
+ }
+ return nullptr;
+}
+
+void
+ChromeProcessController::HandleDoubleTap(const mozilla::CSSPoint& aPoint,
+ Modifiers aModifiers,
+ const ScrollableLayerGuid& aGuid)
+{
+ MOZ_ASSERT(MessageLoop::current() == mUILoop);
+
+ nsCOMPtr<nsIDocument> document = GetRootContentDocument(aGuid.mScrollId);
+ if (!document.get()) {
+ return;
+ }
+
+ // CalculateRectToZoomTo performs a hit test on the frame associated with the
+ // Root Content Document. Unfortunately that frame does not know about the
+ // resolution of the document and so we must remove it before calculating
+ // the zoomToRect.
+ nsIPresShell* presShell = document->GetShell();
+ const float resolution = presShell->ScaleToResolution() ? presShell->GetResolution () : 1.0f;
+ CSSPoint point(aPoint.x / resolution, aPoint.y / resolution);
+ CSSRect zoomToRect = CalculateRectToZoomTo(document, point);
+
+ uint32_t presShellId;
+ FrameMetrics::ViewID viewId;
+ if (APZCCallbackHelper::GetOrCreateScrollIdentifiers(
+ document->GetDocumentElement(), &presShellId, &viewId)) {
+ mAPZCTreeManager->ZoomToRect(
+ ScrollableLayerGuid(aGuid.mLayersId, presShellId, viewId), zoomToRect);
+ }
+}
+
+void
+ChromeProcessController::HandleTap(TapType aType,
+ const mozilla::LayoutDevicePoint& aPoint,
+ Modifiers aModifiers,
+ const ScrollableLayerGuid& aGuid,
+ uint64_t aInputBlockId)
+{
+ if (MessageLoop::current() != mUILoop) {
+ mUILoop->PostTask(NewRunnableMethod<TapType, mozilla::LayoutDevicePoint, Modifiers,
+ ScrollableLayerGuid, uint64_t>(this,
+ &ChromeProcessController::HandleTap,
+ aType, aPoint, aModifiers, aGuid, aInputBlockId));
+ return;
+ }
+
+ if (!mAPZEventState) {
+ return;
+ }
+
+ nsCOMPtr<nsIPresShell> presShell = GetPresShell();
+ if (!presShell) {
+ return;
+ }
+ if (!presShell->GetPresContext()) {
+ return;
+ }
+ CSSToLayoutDeviceScale scale(presShell->GetPresContext()->CSSToDevPixelScale());
+ CSSPoint point = APZCCallbackHelper::ApplyCallbackTransform(aPoint / scale, aGuid);
+
+ switch (aType) {
+ case TapType::eSingleTap:
+ mAPZEventState->ProcessSingleTap(point, scale, aModifiers, aGuid, 1);
+ break;
+ case TapType::eDoubleTap:
+ HandleDoubleTap(point, aModifiers, aGuid);
+ break;
+ case TapType::eSecondTap:
+ mAPZEventState->ProcessSingleTap(point, scale, aModifiers, aGuid, 2);
+ break;
+ case TapType::eLongTap:
+ mAPZEventState->ProcessLongTap(presShell, point, scale, aModifiers, aGuid,
+ aInputBlockId);
+ break;
+ case TapType::eLongTapUp:
+ mAPZEventState->ProcessLongTapUp(presShell, point, scale, aModifiers);
+ break;
+ case TapType::eSentinel:
+ // Should never happen, but we need to handle this case branch for the
+ // compiler to be happy.
+ MOZ_ASSERT(false);
+ break;
+ }
+}
+
+void
+ChromeProcessController::NotifyPinchGesture(PinchGestureInput::PinchGestureType aType,
+ const ScrollableLayerGuid& aGuid,
+ LayoutDeviceCoord aSpanChange,
+ Modifiers aModifiers)
+{
+ if (MessageLoop::current() != mUILoop) {
+ mUILoop->PostTask(NewRunnableMethod
+ <PinchGestureInput::PinchGestureType,
+ ScrollableLayerGuid,
+ LayoutDeviceCoord,
+ Modifiers>(this,
+ &ChromeProcessController::NotifyPinchGesture,
+ aType, aGuid, aSpanChange, aModifiers));
+ return;
+ }
+
+ if (mWidget) {
+ APZCCallbackHelper::NotifyPinchGesture(aType, aSpanChange, aModifiers, mWidget.get());
+ }
+}
+
+void
+ChromeProcessController::NotifyAPZStateChange(const ScrollableLayerGuid& aGuid,
+ APZStateChange aChange,
+ int aArg)
+{
+ if (MessageLoop::current() != mUILoop) {
+ mUILoop->PostTask(NewRunnableMethod
+ <ScrollableLayerGuid,
+ APZStateChange,
+ int>(this, &ChromeProcessController::NotifyAPZStateChange,
+ aGuid, aChange, aArg));
+ return;
+ }
+
+ if (!mAPZEventState) {
+ return;
+ }
+
+ mAPZEventState->ProcessAPZStateChange(aGuid.mScrollId, aChange, aArg);
+}
+
+void
+ChromeProcessController::NotifyMozMouseScrollEvent(const FrameMetrics::ViewID& aScrollId, const nsString& aEvent)
+{
+ if (MessageLoop::current() != mUILoop) {
+ mUILoop->PostTask(NewRunnableMethod
+ <FrameMetrics::ViewID,
+ nsString>(this, &ChromeProcessController::NotifyMozMouseScrollEvent,
+ aScrollId, aEvent));
+ return;
+ }
+
+ APZCCallbackHelper::NotifyMozMouseScrollEvent(aScrollId, aEvent);
+}
+
+void
+ChromeProcessController::NotifyFlushComplete()
+{
+ MOZ_ASSERT(IsRepaintThread());
+
+ APZCCallbackHelper::NotifyFlushComplete(GetPresShell());
+}
diff --git a/gfx/layers/apz/util/ChromeProcessController.h b/gfx/layers/apz/util/ChromeProcessController.h
new file mode 100644
index 000000000..9a43297d4
--- /dev/null
+++ b/gfx/layers/apz/util/ChromeProcessController.h
@@ -0,0 +1,83 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_layers_ChromeProcessController_h
+#define mozilla_layers_ChromeProcessController_h
+
+#include "mozilla/layers/GeckoContentController.h"
+#include "nsCOMPtr.h"
+#include "mozilla/RefPtr.h"
+
+class nsIDOMWindowUtils;
+class nsIDocument;
+class nsIPresShell;
+class nsIWidget;
+
+class MessageLoop;
+
+namespace mozilla {
+
+namespace layers {
+
+class IAPZCTreeManager;
+class APZEventState;
+
+/**
+ * ChromeProcessController is a GeckoContentController attached to the root of
+ * a compositor's layer tree. It's used directly by APZ by default, and remoted
+ * using PAPZ if there is a gpu process.
+ *
+ * If ChromeProcessController needs to implement a new method on GeckoContentController
+ * PAPZ, APZChild, and RemoteContentController must be updated to handle it.
+ */
+class ChromeProcessController : public mozilla::layers::GeckoContentController
+{
+protected:
+ typedef mozilla::layers::FrameMetrics FrameMetrics;
+ typedef mozilla::layers::ScrollableLayerGuid ScrollableLayerGuid;
+
+public:
+ explicit ChromeProcessController(nsIWidget* aWidget, APZEventState* aAPZEventState, IAPZCTreeManager* aAPZCTreeManager);
+ ~ChromeProcessController();
+ virtual void Destroy() override;
+
+ // GeckoContentController interface
+ virtual void RequestContentRepaint(const FrameMetrics& aFrameMetrics) override;
+ virtual void PostDelayedTask(already_AddRefed<Runnable> aTask, int aDelayMs) override;
+ virtual bool IsRepaintThread() override;
+ virtual void DispatchToRepaintThread(already_AddRefed<Runnable> aTask) override;
+ virtual void HandleTap(TapType aType,
+ const mozilla::LayoutDevicePoint& aPoint,
+ Modifiers aModifiers,
+ const ScrollableLayerGuid& aGuid,
+ uint64_t aInputBlockId) override;
+ virtual void NotifyPinchGesture(PinchGestureInput::PinchGestureType aType,
+ const ScrollableLayerGuid& aGuid,
+ LayoutDeviceCoord aSpanChange,
+ Modifiers aModifiers) override;
+ virtual void NotifyAPZStateChange(const ScrollableLayerGuid& aGuid,
+ APZStateChange aChange,
+ int aArg) override;
+ virtual void NotifyMozMouseScrollEvent(const FrameMetrics::ViewID& aScrollId,
+ const nsString& aEvent) override;
+ virtual void NotifyFlushComplete() override;
+private:
+ nsCOMPtr<nsIWidget> mWidget;
+ RefPtr<APZEventState> mAPZEventState;
+ RefPtr<IAPZCTreeManager> mAPZCTreeManager;
+ MessageLoop* mUILoop;
+
+ void InitializeRoot();
+ nsIPresShell* GetPresShell() const;
+ nsIDocument* GetRootDocument() const;
+ nsIDocument* GetRootContentDocument(const FrameMetrics::ViewID& aScrollId) const;
+ void HandleDoubleTap(const mozilla::CSSPoint& aPoint, Modifiers aModifiers,
+ const ScrollableLayerGuid& aGuid);
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif /* mozilla_layers_ChromeProcessController_h */
diff --git a/gfx/layers/apz/util/ContentProcessController.cpp b/gfx/layers/apz/util/ContentProcessController.cpp
new file mode 100644
index 000000000..eccd4179f
--- /dev/null
+++ b/gfx/layers/apz/util/ContentProcessController.cpp
@@ -0,0 +1,207 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set sw=4 ts=8 et tw=80 : */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "ContentProcessController.h"
+
+#include "mozilla/dom/TabChild.h"
+#include "mozilla/layers/APZCCallbackHelper.h"
+#include "mozilla/layers/APZChild.h"
+
+#include "InputData.h" // for InputData
+
+namespace mozilla {
+namespace layers {
+
+/**
+ * There are cases where we try to create the APZChild before the corresponding
+ * TabChild has been created, we use an observer for the "tab-child-created"
+ * topic to set the TabChild in the APZChild when it has been created.
+ */
+class TabChildCreatedObserver : public nsIObserver
+{
+public:
+ TabChildCreatedObserver(ContentProcessController* aController, const dom::TabId& aTabId)
+ : mController(aController),
+ mTabId(aTabId)
+ {}
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIOBSERVER
+
+private:
+ virtual ~TabChildCreatedObserver()
+ {}
+
+ // TabChildCreatedObserver is owned by mController, and mController outlives its
+ // TabChildCreatedObserver, so the raw pointer is fine.
+ ContentProcessController* mController;
+ dom::TabId mTabId;
+};
+
+NS_IMPL_ISUPPORTS(TabChildCreatedObserver, nsIObserver)
+
+NS_IMETHODIMP
+TabChildCreatedObserver::Observe(nsISupports* aSubject,
+ const char* aTopic,
+ const char16_t* aData)
+{
+ MOZ_ASSERT(strcmp(aTopic, "tab-child-created") == 0);
+
+ nsCOMPtr<nsITabChild> tabChild(do_QueryInterface(aSubject));
+ NS_ENSURE_TRUE(tabChild, NS_ERROR_FAILURE);
+
+ dom::TabChild* browser = static_cast<dom::TabChild*>(tabChild.get());
+
+ if (browser->GetTabId() == mTabId) {
+ mController->SetBrowser(browser);
+ }
+ return NS_OK;
+}
+
+APZChild*
+ContentProcessController::Create(const dom::TabId& aTabId)
+{
+ RefPtr<dom::TabChild> browser = dom::TabChild::FindTabChild(aTabId);
+
+ ContentProcessController* controller = new ContentProcessController();
+
+ nsAutoPtr<APZChild> apz(new APZChild(controller));
+
+ if (browser) {
+
+ controller->SetBrowser(browser);
+
+ } else {
+
+ RefPtr<TabChildCreatedObserver> observer =
+ new TabChildCreatedObserver(controller, aTabId);
+ nsCOMPtr<nsIObserverService> os = services::GetObserverService();
+ if (!os ||
+ NS_FAILED(os->AddObserver(observer, "tab-child-created", false))) {
+ return nullptr;
+ }
+ controller->SetObserver(observer);
+
+ }
+
+ return apz.forget();
+}
+
+ContentProcessController::ContentProcessController()
+ : mBrowser(nullptr)
+{
+}
+ContentProcessController::~ContentProcessController()
+{
+ if (mObserver) {
+ nsCOMPtr<nsIObserverService> os = services::GetObserverService();
+ os->RemoveObserver(mObserver, "tab-child-created");
+ }
+}
+
+void
+ContentProcessController::SetObserver(nsIObserver* aObserver)
+{
+ MOZ_ASSERT(!mBrowser);
+ mObserver = aObserver;
+}
+
+void
+ContentProcessController::SetBrowser(dom::TabChild* aBrowser)
+{
+ MOZ_ASSERT(!mBrowser);
+ mBrowser = aBrowser;
+
+ if (mObserver) {
+ nsCOMPtr<nsIObserverService> os = services::GetObserverService();
+ os->RemoveObserver(mObserver, "tab-child-created");
+ mObserver = nullptr;
+ }
+}
+void
+ContentProcessController::RequestContentRepaint(const FrameMetrics& aFrameMetrics)
+{
+ if (mBrowser) {
+ mBrowser->UpdateFrame(aFrameMetrics);
+ }
+}
+
+void
+ContentProcessController::HandleTap(
+ TapType aType,
+ const LayoutDevicePoint& aPoint,
+ Modifiers aModifiers,
+ const ScrollableLayerGuid& aGuid,
+ uint64_t aInputBlockId)
+{
+ // This should never get called
+ MOZ_ASSERT(false);
+}
+
+void
+ContentProcessController::NotifyPinchGesture(
+ PinchGestureInput::PinchGestureType aType,
+ const ScrollableLayerGuid& aGuid,
+ LayoutDeviceCoord aSpanChange,
+ Modifiers aModifiers)
+{
+ // This should never get called
+ MOZ_ASSERT_UNREACHABLE("Unexpected message to content process");
+}
+
+void
+ContentProcessController::NotifyAPZStateChange(
+ const ScrollableLayerGuid& aGuid,
+ APZStateChange aChange,
+ int aArg)
+{
+ if (mBrowser) {
+ mBrowser->NotifyAPZStateChange(aGuid.mScrollId, aChange, aArg);
+ }
+}
+
+void
+ContentProcessController::NotifyMozMouseScrollEvent(
+ const FrameMetrics::ViewID& aScrollId,
+ const nsString& aEvent)
+{
+ if (mBrowser) {
+ APZCCallbackHelper::NotifyMozMouseScrollEvent(aScrollId, aEvent);
+ }
+}
+
+void
+ContentProcessController::NotifyFlushComplete()
+{
+ if (mBrowser) {
+ nsCOMPtr<nsIPresShell> shell;
+ if (nsCOMPtr<nsIDocument> doc = mBrowser->GetDocument()) {
+ shell = doc->GetShell();
+ }
+ APZCCallbackHelper::NotifyFlushComplete(shell.get());
+ }
+}
+
+void
+ContentProcessController::PostDelayedTask(already_AddRefed<Runnable> aRunnable, int aDelayMs)
+{
+ MOZ_ASSERT_UNREACHABLE("ContentProcessController should only be used remotely.");
+}
+
+bool
+ContentProcessController::IsRepaintThread()
+{
+ return NS_IsMainThread();
+}
+
+void
+ContentProcessController::DispatchToRepaintThread(already_AddRefed<Runnable> aTask)
+{
+ NS_DispatchToMainThread(Move(aTask));
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/apz/util/ContentProcessController.h b/gfx/layers/apz/util/ContentProcessController.h
new file mode 100644
index 000000000..07d113c9e
--- /dev/null
+++ b/gfx/layers/apz/util/ContentProcessController.h
@@ -0,0 +1,90 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set sw=4 ts=8 et tw=80 : */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_layers_ContentProcessController_h
+#define mozilla_layers_ContentProcessController_h
+
+#include "mozilla/layers/GeckoContentController.h"
+
+class nsIObserver;
+
+namespace mozilla {
+
+namespace dom {
+class TabChild;
+} // namespace dom
+
+namespace layers {
+
+class APZChild;
+
+/**
+ * ContentProcessController is a GeckoContentController for a TabChild, and is always
+ * remoted using PAPZ/APZChild.
+ *
+ * ContentProcessController is created in ContentChild when a layer tree id has
+ * been allocated for a PBrowser that lives in that content process, and is destroyed
+ * when the Destroy message is received, or when the tab dies.
+ *
+ * If ContentProcessController needs to implement a new method on GeckoContentController
+ * PAPZ, APZChild, and RemoteContentController must be updated to handle it.
+ */
+class ContentProcessController final
+ : public GeckoContentController
+{
+public:
+ ~ContentProcessController();
+
+ static APZChild* Create(const dom::TabId& aTabId);
+
+ // ContentProcessController
+
+ void SetBrowser(dom::TabChild* aBrowser);
+
+ // GeckoContentController
+
+ void RequestContentRepaint(const FrameMetrics& frame) override;
+
+ void HandleTap(TapType aType,
+ const LayoutDevicePoint& aPoint,
+ Modifiers aModifiers,
+ const ScrollableLayerGuid& aGuid,
+ uint64_t aInputBlockId) override;
+
+ void NotifyPinchGesture(PinchGestureInput::PinchGestureType aType,
+ const ScrollableLayerGuid& aGuid,
+ LayoutDeviceCoord aSpanChange,
+ Modifiers aModifiers) override;
+
+ void NotifyAPZStateChange(const ScrollableLayerGuid& aGuid,
+ APZStateChange aChange,
+ int aArg) override;
+
+ void NotifyMozMouseScrollEvent(const FrameMetrics::ViewID& aScrollId,
+ const nsString& aEvent) override;
+
+ void NotifyFlushComplete() override;
+
+ void PostDelayedTask(already_AddRefed<Runnable> aRunnable, int aDelayMs) override;
+
+ bool IsRepaintThread() override;
+
+ void DispatchToRepaintThread(already_AddRefed<Runnable> aTask) override;
+
+private:
+ ContentProcessController();
+
+ void SetObserver(nsIObserver* aObserver);
+
+ RefPtr<dom::TabChild> mBrowser;
+ RefPtr<nsIObserver> mObserver;
+};
+
+} // namespace layers
+
+} // namespace mozilla
+
+#endif // mozilla_layers_ContentProcessController_h
diff --git a/gfx/layers/apz/util/DoubleTapToZoom.cpp b/gfx/layers/apz/util/DoubleTapToZoom.cpp
new file mode 100644
index 000000000..62dd5feaa
--- /dev/null
+++ b/gfx/layers/apz/util/DoubleTapToZoom.cpp
@@ -0,0 +1,178 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "DoubleTapToZoom.h"
+
+#include <algorithm> // for std::min, std::max
+
+#include "mozilla/AlreadyAddRefed.h"
+#include "mozilla/dom/Element.h"
+#include "nsCOMPtr.h"
+#include "nsIContent.h"
+#include "nsIDocument.h"
+#include "nsIDOMHTMLLIElement.h"
+#include "nsIDOMHTMLQuoteElement.h"
+#include "nsIDOMWindow.h"
+#include "nsIFrame.h"
+#include "nsIFrameInlines.h"
+#include "nsIPresShell.h"
+#include "nsLayoutUtils.h"
+#include "nsStyleConsts.h"
+
+namespace mozilla {
+namespace layers {
+
+// Returns the DOM element found at |aPoint|, interpreted as being relative to
+// the root frame of |aShell|. If the point is inside a subdocument, returns
+// an element inside the subdocument, rather than the subdocument element
+// (and does so recursively).
+// The implementation was adapted from nsDocument::ElementFromPoint(), with
+// the notable exception that we don't pass nsLayoutUtils::IGNORE_CROSS_DOC
+// to GetFrameForPoint(), so as to get the behaviour described above in the
+// presence of subdocuments.
+static already_AddRefed<dom::Element>
+ElementFromPoint(const nsCOMPtr<nsIPresShell>& aShell,
+ const CSSPoint& aPoint)
+{
+ if (nsIFrame* rootFrame = aShell->GetRootFrame()) {
+ if (nsIFrame* frame = nsLayoutUtils::GetFrameForPoint(rootFrame,
+ CSSPoint::ToAppUnits(aPoint),
+ nsLayoutUtils::IGNORE_PAINT_SUPPRESSION |
+ nsLayoutUtils::IGNORE_ROOT_SCROLL_FRAME)) {
+ while (frame && (!frame->GetContent() || frame->GetContent()->IsInAnonymousSubtree())) {
+ frame = nsLayoutUtils::GetParentOrPlaceholderFor(frame);
+ }
+ nsIContent* content = frame->GetContent();
+ if (content && !content->IsElement()) {
+ content = content->GetParent();
+ }
+ if (content) {
+ nsCOMPtr<dom::Element> result = content->AsElement();
+ return result.forget();
+ }
+ }
+ }
+ return nullptr;
+}
+
+static bool
+ShouldZoomToElement(const nsCOMPtr<dom::Element>& aElement) {
+ if (nsIFrame* frame = aElement->GetPrimaryFrame()) {
+ if (frame->GetDisplay() == StyleDisplay::Inline) {
+ return false;
+ }
+ }
+ if (aElement->IsAnyOfHTMLElements(nsGkAtoms::li, nsGkAtoms::q)) {
+ return false;
+ }
+ return true;
+}
+
+static bool
+IsRectZoomedIn(const CSSRect& aRect, const CSSRect& aCompositedArea)
+{
+ // This functions checks to see if the area of the rect visible in the
+ // composition bounds (i.e. the overlapArea variable below) is approximately
+ // the max area of the rect we can show.
+ CSSRect overlap = aCompositedArea.Intersect(aRect);
+ float overlapArea = overlap.width * overlap.height;
+ float availHeight = std::min(aRect.width * aCompositedArea.height / aCompositedArea.width,
+ aRect.height);
+ float showing = overlapArea / (aRect.width * availHeight);
+ float ratioW = aRect.width / aCompositedArea.width;
+ float ratioH = aRect.height / aCompositedArea.height;
+
+ return showing > 0.9 && (ratioW > 0.9 || ratioH > 0.9);
+}
+
+CSSRect
+CalculateRectToZoomTo(const nsCOMPtr<nsIDocument>& aRootContentDocument,
+ const CSSPoint& aPoint)
+{
+ // Ensure the layout information we get is up-to-date.
+ aRootContentDocument->FlushPendingNotifications(Flush_Layout);
+
+ // An empty rect as return value is interpreted as "zoom out".
+ const CSSRect zoomOut;
+
+ nsCOMPtr<nsIPresShell> shell = aRootContentDocument->GetShell();
+ if (!shell) {
+ return zoomOut;
+ }
+
+ nsIScrollableFrame* rootScrollFrame = shell->GetRootScrollFrameAsScrollable();
+ if (!rootScrollFrame) {
+ return zoomOut;
+ }
+
+ nsCOMPtr<dom::Element> element = ElementFromPoint(shell, aPoint);
+ if (!element) {
+ return zoomOut;
+ }
+
+ while (element && !ShouldZoomToElement(element)) {
+ element = element->GetParentElement();
+ }
+
+ if (!element) {
+ return zoomOut;
+ }
+
+ FrameMetrics metrics = nsLayoutUtils::CalculateBasicFrameMetrics(rootScrollFrame);
+ CSSRect compositedArea(metrics.GetScrollOffset(), metrics.CalculateCompositedSizeInCssPixels());
+ const CSSCoord margin = 15;
+ CSSRect rect = nsLayoutUtils::GetBoundingContentRect(element, rootScrollFrame);
+
+ // If the element is taller than the visible area of the page scale
+ // the height of the |rect| so that it has the same aspect ratio as
+ // the root frame. The clipped |rect| is centered on the y value of
+ // the touch point. This allows tall narrow elements to be zoomed.
+ if (!rect.IsEmpty() && compositedArea.width > 0.0f) {
+ const float widthRatio = rect.width / compositedArea.width;
+ float targetHeight = compositedArea.height * widthRatio;
+ if (widthRatio < 0.9 && targetHeight < rect.height) {
+ const CSSPoint scrollPoint = CSSPoint::FromAppUnits(rootScrollFrame->GetScrollPosition());
+ float newY = aPoint.y + scrollPoint.y - (targetHeight * 0.5f);
+ if ((newY + targetHeight) > (rect.y + rect.height)) {
+ rect.y += rect.height - targetHeight;
+ } else if (newY > rect.y) {
+ rect.y = newY;
+ }
+ rect.height = targetHeight;
+ }
+ }
+
+ rect = CSSRect(std::max(metrics.GetScrollableRect().x, rect.x - margin),
+ rect.y,
+ rect.width + 2 * margin,
+ rect.height);
+ // Constrict the rect to the screen's right edge
+ rect.width = std::min(rect.width, metrics.GetScrollableRect().XMost() - rect.x);
+
+ // If the rect is already taking up most of the visible area and is
+ // stretching the width of the page, then we want to zoom out instead.
+ if (IsRectZoomedIn(rect, compositedArea)) {
+ return zoomOut;
+ }
+
+ CSSRect rounded(rect);
+ rounded.Round();
+
+ // If the block we're zooming to is really tall, and the user double-tapped
+ // more than a screenful of height from the top of it, then adjust the
+ // y-coordinate so that we center the actual point the user double-tapped
+ // upon. This prevents flying to the top of the page when double-tapping
+ // to zoom in (bug 761721). The 1.2 multiplier is just a little fuzz to
+ // compensate for 'rect' including horizontal margins but not vertical ones.
+ CSSCoord cssTapY = metrics.GetScrollOffset().y + aPoint.y;
+ if ((rect.height > rounded.height) && (cssTapY > rounded.y + (rounded.height * 1.2))) {
+ rounded.y = cssTapY - (rounded.height / 2);
+ }
+
+ return rounded;
+}
+
+}
+}
diff --git a/gfx/layers/apz/util/DoubleTapToZoom.h b/gfx/layers/apz/util/DoubleTapToZoom.h
new file mode 100644
index 000000000..7b8723865
--- /dev/null
+++ b/gfx/layers/apz/util/DoubleTapToZoom.h
@@ -0,0 +1,29 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_layers_DoubleTapToZoom_h
+#define mozilla_layers_DoubleTapToZoom_h
+
+#include "Units.h"
+
+class nsIDocument;
+template<class T> class nsCOMPtr;
+
+namespace mozilla {
+namespace layers {
+
+/**
+ * For a double tap at |aPoint|, return the rect to which the browser
+ * should zoom in response, or an empty rect if the browser should zoom out.
+ * |aDocument| should be the root content document for the content that was
+ * tapped.
+ */
+CSSRect CalculateRectToZoomTo(const nsCOMPtr<nsIDocument>& aRootContentDocument,
+ const CSSPoint& aPoint);
+
+}
+}
+
+#endif /* mozilla_layers_DoubleTapToZoom_h */
diff --git a/gfx/layers/apz/util/InputAPZContext.cpp b/gfx/layers/apz/util/InputAPZContext.cpp
new file mode 100644
index 000000000..af5bd9f0f
--- /dev/null
+++ b/gfx/layers/apz/util/InputAPZContext.cpp
@@ -0,0 +1,69 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "InputAPZContext.h"
+
+namespace mozilla {
+namespace layers {
+
+ScrollableLayerGuid InputAPZContext::sGuid;
+uint64_t InputAPZContext::sBlockId = 0;
+nsEventStatus InputAPZContext::sApzResponse = nsEventStatus_eIgnore;
+bool InputAPZContext::sRoutedToChildProcess = false;
+
+/*static*/ ScrollableLayerGuid
+InputAPZContext::GetTargetLayerGuid()
+{
+ return sGuid;
+}
+
+/*static*/ uint64_t
+InputAPZContext::GetInputBlockId()
+{
+ return sBlockId;
+}
+
+/*static*/ nsEventStatus
+InputAPZContext::GetApzResponse()
+{
+ return sApzResponse;
+}
+
+/*static*/ void
+InputAPZContext::SetRoutedToChildProcess()
+{
+ sRoutedToChildProcess = true;
+}
+
+InputAPZContext::InputAPZContext(const ScrollableLayerGuid& aGuid,
+ const uint64_t& aBlockId,
+ const nsEventStatus& aApzResponse)
+ : mOldGuid(sGuid)
+ , mOldBlockId(sBlockId)
+ , mOldApzResponse(sApzResponse)
+ , mOldRoutedToChildProcess(sRoutedToChildProcess)
+{
+ sGuid = aGuid;
+ sBlockId = aBlockId;
+ sApzResponse = aApzResponse;
+ sRoutedToChildProcess = false;
+}
+
+InputAPZContext::~InputAPZContext()
+{
+ sGuid = mOldGuid;
+ sBlockId = mOldBlockId;
+ sApzResponse = mOldApzResponse;
+ sRoutedToChildProcess = mOldRoutedToChildProcess;
+}
+
+bool
+InputAPZContext::WasRoutedToChildProcess()
+{
+ return sRoutedToChildProcess;
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/apz/util/InputAPZContext.h b/gfx/layers/apz/util/InputAPZContext.h
new file mode 100644
index 000000000..0f232e1cb
--- /dev/null
+++ b/gfx/layers/apz/util/InputAPZContext.h
@@ -0,0 +1,50 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_layers_InputAPZContext_h
+#define mozilla_layers_InputAPZContext_h
+
+#include "FrameMetrics.h"
+#include "mozilla/EventForwards.h"
+
+namespace mozilla {
+namespace layers {
+
+// InputAPZContext is used to communicate the ScrollableLayerGuid,
+// input block ID, APZ response from nsIWidget to RenderFrameParent.
+// It is conceptually attached to any WidgetInputEvent
+// that has been processed by APZ directly from a widget.
+class MOZ_STACK_CLASS InputAPZContext
+{
+private:
+ static ScrollableLayerGuid sGuid;
+ static uint64_t sBlockId;
+ static nsEventStatus sApzResponse;
+ static bool sRoutedToChildProcess;
+
+public:
+ static ScrollableLayerGuid GetTargetLayerGuid();
+ static uint64_t GetInputBlockId();
+ static nsEventStatus GetApzResponse();
+ static void SetRoutedToChildProcess();
+
+ InputAPZContext(const ScrollableLayerGuid& aGuid,
+ const uint64_t& aBlockId,
+ const nsEventStatus& aApzResponse);
+ ~InputAPZContext();
+
+ bool WasRoutedToChildProcess();
+
+private:
+ ScrollableLayerGuid mOldGuid;
+ uint64_t mOldBlockId;
+ nsEventStatus mOldApzResponse;
+ bool mOldRoutedToChildProcess;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif /* mozilla_layers_InputAPZContext_h */
diff --git a/gfx/layers/apz/util/ScrollInputMethods.h b/gfx/layers/apz/util/ScrollInputMethods.h
new file mode 100644
index 000000000..ba599cd8b
--- /dev/null
+++ b/gfx/layers/apz/util/ScrollInputMethods.h
@@ -0,0 +1,62 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_layers_ScrollInputMethods_h
+#define mozilla_layers_ScrollInputMethods_h
+
+namespace mozilla {
+namespace layers {
+
+/**
+ * An enumeration that lists various input methods used to trigger scrolling.
+ * Used as the values for the SCROLL_INPUT_METHODS telemetry histogram.
+ */
+enum class ScrollInputMethod {
+
+ // === Driven by APZ ===
+
+ ApzTouch, // touch events
+ ApzWheelPixel, // wheel events, pixel scrolling mode
+ ApzWheelLine, // wheel events, line scrolling mode
+ ApzWheelPage, // wheel events, page scrolling mode
+ ApzPanGesture, // pan gesture events (generally triggered by trackpad)
+ ApzScrollbarDrag, // dragging the scrollbar
+
+ // === Driven by the main thread ===
+
+ // Keyboard
+ MainThreadScrollLine, // line scrolling
+ // (generally triggered by up/down arrow keys)
+ MainThreadScrollCharacter, // character scrolling
+ // (generally triggered by left/right arrow keys)
+ MainThreadScrollPage, // page scrolling
+ // (generally triggered by PageUp/PageDown keys)
+ MainThreadCompleteScroll, // scrolling to the end of the scroll range
+ // (generally triggered by Home/End keys)
+ MainThreadScrollCaretIntoView, // scrolling to bring the caret into view
+ // after moving the caret via the keyboard
+
+ // Touch
+ MainThreadTouch, // touch events
+
+ // Scrollbar
+ MainThreadScrollbarDrag, // dragging the scrollbar
+ MainThreadScrollbarButtonClick, // clicking the buttons at the ends of the
+ // scrollback track
+ MainThreadScrollbarTrackClick, // clicking the scrollbar track above or
+ // below the thumb
+
+ // Autoscrolling
+ MainThreadAutoscrolling, // autoscrolling
+
+ // New input methods can be added at the end, up to a maximum of 64.
+ // They should only be added at the end, to preserve the numerical values
+ // of the existing enumerators.
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif /* mozilla_layers_ScrollInputMethods_h */
diff --git a/gfx/layers/apz/util/ScrollLinkedEffectDetector.cpp b/gfx/layers/apz/util/ScrollLinkedEffectDetector.cpp
new file mode 100644
index 000000000..758b705a3
--- /dev/null
+++ b/gfx/layers/apz/util/ScrollLinkedEffectDetector.cpp
@@ -0,0 +1,49 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "ScrollLinkedEffectDetector.h"
+
+#include "nsIDocument.h"
+#include "nsThreadUtils.h"
+
+namespace mozilla {
+namespace layers {
+
+uint32_t ScrollLinkedEffectDetector::sDepth = 0;
+bool ScrollLinkedEffectDetector::sFoundScrollLinkedEffect = false;
+
+/* static */ void
+ScrollLinkedEffectDetector::PositioningPropertyMutated()
+{
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (sDepth > 0) {
+ // We are inside a scroll event dispatch
+ sFoundScrollLinkedEffect = true;
+ }
+}
+
+ScrollLinkedEffectDetector::ScrollLinkedEffectDetector(nsIDocument* aDoc)
+ : mDocument(aDoc)
+{
+ MOZ_ASSERT(NS_IsMainThread());
+ sDepth++;
+}
+
+ScrollLinkedEffectDetector::~ScrollLinkedEffectDetector()
+{
+ sDepth--;
+ if (sDepth == 0) {
+ // We have exited all (possibly-nested) scroll event dispatches,
+ // record whether or not we found an effect, and reset state
+ if (sFoundScrollLinkedEffect) {
+ mDocument->ReportHasScrollLinkedEffect();
+ sFoundScrollLinkedEffect = false;
+ }
+ }
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/apz/util/ScrollLinkedEffectDetector.h b/gfx/layers/apz/util/ScrollLinkedEffectDetector.h
new file mode 100644
index 000000000..f792586cf
--- /dev/null
+++ b/gfx/layers/apz/util/ScrollLinkedEffectDetector.h
@@ -0,0 +1,43 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_layers_ScrollLinkedEffectDetector_h
+#define mozilla_layers_ScrollLinkedEffectDetector_h
+
+#include "mozilla/RefPtr.h"
+
+class nsIDocument;
+
+namespace mozilla {
+namespace layers {
+
+// ScrollLinkedEffectDetector is used to detect the existence of a scroll-linked
+// effect on a webpage. Generally speaking, a scroll-linked effect is something
+// on the page that animates or changes with respect to the scroll position.
+// Content authors usually rely on running some JS in response to the scroll
+// event in order to implement such effects, and therefore it tends to be laggy
+// or work improperly with APZ enabled. This class helps us detect such an
+// effect so that we can warn the author and/or take other preventative
+// measures.
+class MOZ_STACK_CLASS ScrollLinkedEffectDetector
+{
+private:
+ static uint32_t sDepth;
+ static bool sFoundScrollLinkedEffect;
+
+public:
+ static void PositioningPropertyMutated();
+
+ explicit ScrollLinkedEffectDetector(nsIDocument* aDoc);
+ ~ScrollLinkedEffectDetector();
+
+private:
+ RefPtr<nsIDocument> mDocument;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif /* mozilla_layers_ScrollLinkedEffectDetector_h */
diff --git a/gfx/layers/apz/util/TouchActionHelper.cpp b/gfx/layers/apz/util/TouchActionHelper.cpp
new file mode 100644
index 000000000..b35fd2ec7
--- /dev/null
+++ b/gfx/layers/apz/util/TouchActionHelper.cpp
@@ -0,0 +1,96 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "TouchActionHelper.h"
+
+#include "mozilla/layers/APZCTreeManager.h"
+#include "nsContainerFrame.h"
+#include "nsIScrollableFrame.h"
+#include "nsLayoutUtils.h"
+
+namespace mozilla {
+namespace layers {
+
+void
+TouchActionHelper::UpdateAllowedBehavior(uint32_t aTouchActionValue,
+ bool aConsiderPanning,
+ TouchBehaviorFlags& aOutBehavior)
+{
+ if (aTouchActionValue != NS_STYLE_TOUCH_ACTION_AUTO) {
+ // Double-tap-zooming need property value AUTO
+ aOutBehavior &= ~AllowedTouchBehavior::DOUBLE_TAP_ZOOM;
+ if (aTouchActionValue != NS_STYLE_TOUCH_ACTION_MANIPULATION) {
+ // Pinch-zooming need value AUTO or MANIPULATION
+ aOutBehavior &= ~AllowedTouchBehavior::PINCH_ZOOM;
+ }
+ }
+
+ if (aConsiderPanning) {
+ if (aTouchActionValue == NS_STYLE_TOUCH_ACTION_NONE) {
+ aOutBehavior &= ~AllowedTouchBehavior::VERTICAL_PAN;
+ aOutBehavior &= ~AllowedTouchBehavior::HORIZONTAL_PAN;
+ }
+
+ // Values pan-x and pan-y set at the same time to the same element do not affect panning constraints.
+ // Therefore we need to check whether pan-x is set without pan-y and the same for pan-y.
+ if ((aTouchActionValue & NS_STYLE_TOUCH_ACTION_PAN_X) && !(aTouchActionValue & NS_STYLE_TOUCH_ACTION_PAN_Y)) {
+ aOutBehavior &= ~AllowedTouchBehavior::VERTICAL_PAN;
+ } else if ((aTouchActionValue & NS_STYLE_TOUCH_ACTION_PAN_Y) && !(aTouchActionValue & NS_STYLE_TOUCH_ACTION_PAN_X)) {
+ aOutBehavior &= ~AllowedTouchBehavior::HORIZONTAL_PAN;
+ }
+ }
+}
+
+TouchBehaviorFlags
+TouchActionHelper::GetAllowedTouchBehavior(nsIWidget* aWidget,
+ nsIFrame* aRootFrame,
+ const LayoutDeviceIntPoint& aPoint)
+{
+ TouchBehaviorFlags behavior = AllowedTouchBehavior::VERTICAL_PAN | AllowedTouchBehavior::HORIZONTAL_PAN |
+ AllowedTouchBehavior::PINCH_ZOOM | AllowedTouchBehavior::DOUBLE_TAP_ZOOM;
+
+ nsPoint relativePoint =
+ nsLayoutUtils::GetEventCoordinatesRelativeTo(aWidget, aPoint, aRootFrame);
+
+ nsIFrame *target = nsLayoutUtils::GetFrameForPoint(aRootFrame, relativePoint, nsLayoutUtils::IGNORE_ROOT_SCROLL_FRAME);
+ if (!target) {
+ return behavior;
+ }
+ nsIScrollableFrame *nearestScrollableParent = nsLayoutUtils::GetNearestScrollableFrame(target, 0);
+ nsIFrame* nearestScrollableFrame = do_QueryFrame(nearestScrollableParent);
+
+ // We're walking up the DOM tree until we meet the element with touch behavior and accumulating
+ // touch-action restrictions of all elements in this chain.
+ // The exact quote from the spec, that clarifies more:
+ // To determine the effect of a touch, find the nearest ancestor (starting from the element itself)
+ // that has a default touch behavior. Then examine the touch-action property of each element between
+ // the hit tested element and the element with the default touch behavior (including both the hit
+ // tested element and the element with the default touch behavior). If the touch-action property of
+ // any of those elements disallows the default touch behavior, do nothing. Otherwise allow the element
+ // to start considering the touch for the purposes of executing a default touch behavior.
+
+ // Currently we support only two touch behaviors: panning and zooming.
+ // For panning we walk up until we meet the first scrollable element (the element that supports panning)
+ // or root element.
+ // For zooming we walk up until the root element since Firefox currently supports only zooming of the
+ // root frame but not the subframes.
+
+ bool considerPanning = true;
+
+ for (nsIFrame *frame = target; frame && frame->GetContent() && behavior; frame = frame->GetParent()) {
+ UpdateAllowedBehavior(nsLayoutUtils::GetTouchActionFromFrame(frame), considerPanning, behavior);
+
+ if (frame == nearestScrollableFrame) {
+ // We met the scrollable element, after it we shouldn't consider touch-action
+ // values for the purpose of panning but only for zooming.
+ considerPanning = false;
+ }
+ }
+
+ return behavior;
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/apz/util/TouchActionHelper.h b/gfx/layers/apz/util/TouchActionHelper.h
new file mode 100644
index 000000000..1dacfd4c0
--- /dev/null
+++ b/gfx/layers/apz/util/TouchActionHelper.h
@@ -0,0 +1,42 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef __mozilla_layers_TouchActionHelper_h__
+#define __mozilla_layers_TouchActionHelper_h__
+
+#include "mozilla/layers/APZUtils.h" // for TouchBehaviorFlags
+
+class nsIFrame;
+class nsIWidget;
+
+namespace mozilla {
+namespace layers {
+
+/*
+ * Helper class to figure out the allowed touch behavior for frames, as per
+ * the touch-action spec.
+ */
+class TouchActionHelper
+{
+private:
+ static void UpdateAllowedBehavior(uint32_t aTouchActionValue,
+ bool aConsiderPanning,
+ TouchBehaviorFlags& aOutBehavior);
+
+public:
+ /*
+ * Performs hit testing on content, finds frame that corresponds to the aPoint and retrieves
+ * touch-action css property value from it according the rules specified in the spec:
+ * http://www.w3.org/TR/pointerevents/#the-touch-action-css-property.
+ */
+ static TouchBehaviorFlags GetAllowedTouchBehavior(nsIWidget* aWidget,
+ nsIFrame* aRootFrame,
+ const LayoutDeviceIntPoint& aPoint);
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif /*__mozilla_layers_TouchActionHelper_h__ */