diff options
author | Matt A. Tobin <mattatobin@localhost.localdomain> | 2018-02-02 04:16:08 -0500 |
---|---|---|
committer | Matt A. Tobin <mattatobin@localhost.localdomain> | 2018-02-02 04:16:08 -0500 |
commit | 5f8de423f190bbb79a62f804151bc24824fa32d8 (patch) | |
tree | 10027f336435511475e392454359edea8e25895d /gfx/layers/apz | |
parent | 49ee0794b5d912db1f95dce6eb52d781dc210db5 (diff) | |
download | UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.gz UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.lz UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.xz UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.zip |
Add m-esr52 at 52.6.0
Diffstat (limited to 'gfx/layers/apz')
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(¤tX); + mOverScroller->GetCurrY(¤tY); + 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__ */ |