diff -r 000000000000 -r 6474c204b198 gfx/layers/apz/src/AsyncPanZoomController.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/gfx/layers/apz/src/AsyncPanZoomController.cpp Wed Dec 31 06:09:35 2014 +0100 @@ -0,0 +1,2209 @@ +/* -*- 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 // for fabsf, fabs, atan2 +#include // for uint32_t, uint64_t +#include // for int32_t +#include // for max, min +#include "AnimationCommon.h" // for ComputedTimingFunction +#include "AsyncPanZoomController.h" // for AsyncPanZoomController, etc +#include "CompositorParent.h" // for CompositorParent +#include "FrameMetrics.h" // for FrameMetrics, etc +#include "GestureEventListener.h" // for GestureEventListener +#include "InputData.h" // for MultiTouchInput, etc +#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 "base/tracked.h" // for FROM_HERE +#include "gfxPrefs.h" // for gfxPrefs +#include "gfxTypes.h" // for gfxFloat +#include "mozilla/Assertions.h" // for MOZ_ASSERT, etc +#include "mozilla/BasicEvents.h" // for Modifiers, MODIFIER_* +#include "mozilla/ClearOnShutdown.h" // for ClearOnShutdown +#include "mozilla/Constants.h" // for M_PI +#include "mozilla/EventForwards.h" // for nsEventStatus_* +#include "mozilla/Preferences.h" // for Preferences +#include "mozilla/ReentrantMonitor.h" // for ReentrantMonitorAutoEnter, etc +#include "mozilla/StaticPtr.h" // for StaticAutoPtr +#include "mozilla/TimeStamp.h" // for TimeDuration, TimeStamp +#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/AsyncCompositionManager.h" // for ViewTransform +#include "mozilla/layers/Axis.h" // for AxisX, AxisY, Axis, etc +#include "mozilla/layers/LayerTransactionParent.h" // for LayerTransactionParent +#include "mozilla/layers/PCompositorParent.h" // for PCompositorParent +#include "mozilla/layers/TaskThrottler.h" // for TaskThrottler +#include "mozilla/mozalloc.h" // for operator new, etc +#include "mozilla/unused.h" // for unused +#include "mozilla/FloatingPoint.h" // for FuzzyEqualsMultiplicative +#include "nsAlgorithm.h" // for clamped +#include "nsAutoPtr.h" // for nsRefPtr +#include "nsCOMPtr.h" // for already_AddRefed +#include "nsDebug.h" // for NS_WARNING +#include "nsIDOMWindowUtils.h" // for nsIDOMWindowUtils +#include "nsISupportsImpl.h" // for MOZ_COUNT_CTOR, etc +#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 "SharedMemoryBasic.h" // for SharedMemoryBasic + +// #define APZC_ENABLE_RENDERTRACE + +#define APZC_LOG(...) +// #define APZC_LOG(...) printf_stderr("APZC: " __VA_ARGS__) +#define APZC_LOG_FM(fm, prefix, ...) \ + APZC_LOG(prefix ":" \ + " i=(%ld %lld) cb=(%d %d %d %d) rcs=(%.3f %.3f) dp=(%.3f %.3f %.3f %.3f) dpm=(%.3f %.3f %.3f %.3f) um=%d " \ + "v=(%.3f %.3f %.3f %.3f) s=(%.3f %.3f) sr=(%.3f %.3f %.3f %.3f) z=(%.3f %.3f %.3f %.3f) u=(%d %lu)\n", \ + __VA_ARGS__, \ + fm.mPresShellId, fm.GetScrollId(), \ + fm.mCompositionBounds.x, fm.mCompositionBounds.y, fm.mCompositionBounds.width, fm.mCompositionBounds.height, \ + fm.GetRootCompositionSize().width, fm.GetRootCompositionSize().height, \ + fm.mDisplayPort.x, fm.mDisplayPort.y, fm.mDisplayPort.width, fm.mDisplayPort.height, \ + fm.GetDisplayPortMargins().top, fm.GetDisplayPortMargins().right, fm.GetDisplayPortMargins().bottom, fm.GetDisplayPortMargins().left, \ + fm.GetUseDisplayPortMargins() ? 1 : 0, \ + fm.mViewport.x, fm.mViewport.y, fm.mViewport.width, fm.mViewport.height, \ + fm.GetScrollOffset().x, fm.GetScrollOffset().y, \ + fm.mScrollableRect.x, fm.mScrollableRect.y, fm.mScrollableRect.width, fm.mScrollableRect.height, \ + fm.mDevPixelsPerCSSPixel.scale, fm.mResolution.scale, fm.mCumulativeResolution.scale, fm.GetZoom().scale, \ + fm.GetScrollOffsetUpdated(), fm.GetScrollGeneration()); \ + +// 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_NUMLOCK) { + result |= nsIDOMWindowUtils::MODIFIER_NUMLOCK; + } + if (aModifiers & mozilla::MODIFIER_SCROLLLOCK) { + result |= nsIDOMWindowUtils::MODIFIER_SCROLLLOCK; + } + if (aModifiers & mozilla::MODIFIER_SYMBOLLOCK) { + result |= nsIDOMWindowUtils::MODIFIER_SYMBOLLOCK; + } + if (aModifiers & mozilla::MODIFIER_OS) { + result |= nsIDOMWindowUtils::MODIFIER_OS; + } + return result; +} + +} + +using namespace mozilla::css; + +namespace mozilla { +namespace layers { + +typedef mozilla::layers::AllowedTouchBehavior AllowedTouchBehavior; +typedef GeckoContentController::APZStateChange APZStateChange; + +/* + * The following prefs are used to control the behaviour of the APZC. + * The default values are provided in gfxPrefs.h. + * + * "apz.allow-checkerboarding" + * Pref that allows or disallows checkerboarding + * + * "apz.asyncscroll.throttle" + * The time period in ms that throttles mozbrowserasyncscroll event. + * + * "apz.asyncscroll.timeout" + * The timeout in ms for mAsyncScrollTimeoutTask delay task. + * + * "apz.axis_lock_mode" + * The preferred axis locking style. See AxisLockMode for possible values. + * + * "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. + * + * "apz.cross_slide_enabled" + * Pref that enables integration with the Metro "cross-slide" gesture. + * + * "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. + * + * "apz.fling_friction" + * Amount of friction applied during flings. + * + * "apz.fling_repaint_interval" + * Maximum amount of time flinging before sending a viewport change. This will + * asynchronously repaint the page. + * + * "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. + * + * "apz.max_velocity_inches_per_ms" + * Maximum velocity in inches per millisecond. Velocity will be capped at this + * value if a faster fling occurs. Negative values indicate unlimited velocity. + * + * "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. + * + * "apz.min_skate_speed" + * Minimum amount of speed along an axis before we switch to "skate" multipliers + * rather than using the "stationary" multipliers. + * + * "apz.num_paint_duration_samples" + * Number of samples to store of how long it took to paint after the previous + * requests. + * + * "apz.pan_repaint_interval" + * Maximum amount of time while panning before sending a viewport change. This + * will asynchronously repaint the page. It is also forced when panning stops. + * + * "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. + * + * "apz.use_paint_duration" + * Whether or not to use the estimated paint duration as a factor when projecting + * the displayport in the direction of scrolling. If this value is set to false, + * a constant 50ms paint time is used; the projection can be scaled as desired + * using the apz.velocity_bias pref below. + * + * "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. + * + * "apz.x_skate_size_multiplier", "apz.y_skate_size_multiplier" + * The multiplier we apply to the displayport size if it is skating (current + * velocity is above 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 + * that scrolls up/down. Note that one, both or neither of these may be used + * at any instant. + * In general we want 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. + * + * "apz.x_stationary_size_multiplier", "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). + */ + +/** + * Default touch behavior (is used when not touch behavior is set). + */ +static const uint32_t DefaultTouchBehavior = AllowedTouchBehavior::VERTICAL_PAN | + AllowedTouchBehavior::HORIZONTAL_PAN | + AllowedTouchBehavior::PINCH_ZOOM | + AllowedTouchBehavior::DOUBLE_TAP_ZOOM; + +/** + * Angle from axis within which we stay axis-locked + */ +static const double AXIS_LOCK_ANGLE = M_PI / 6.0; // 30 degrees + +/** + * The distance in inches the user must pan before axis lock can be broken + */ +static const float AXIS_BREAKOUT_THRESHOLD = 1.0f/32.0f; + +/** + * The angle at which axis lock can be broken + */ +static const double AXIS_BREAKOUT_ANGLE = M_PI / 8.0; // 22.5 degrees + +/** + * Angle from axis to the line drawn by pan move. + * If angle is less than this value we can assume that panning + * can be done in allowed direction (horizontal or vertical). + * Currently used only for touch-action css property stuff and was + * added to keep behavior consistent with IE. + */ +static const double ALLOWED_DIRECT_PAN_ANGLE = M_PI / 3.0; // 60 degrees + +/** + * Duration of a zoom to animation. + */ +static const TimeDuration ZOOM_TO_DURATION = TimeDuration::FromSeconds(0.25); + +/** + * Computed time function used for sampling frames of a zoom to animation. + */ +StaticAutoPtr gComputedTimingFunction; + +/** + * Maximum zoom amount, always used, even if a page asks for higher. + */ +static const CSSToScreenScale MAX_ZOOM(8.0f); + +/** + * Minimum zoom amount, always used, even if a page asks for lower. + */ +static const CSSToScreenScale MIN_ZOOM(0.125f); + +/** + * 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); +} + +template +static bool IsZero(const gfx::PointTyped& aPoint) +{ + return FuzzyEqualsMultiplicative(aPoint.x, 0.0f) + && FuzzyEqualsMultiplicative(aPoint.y, 0.0f); +} + +static inline void LogRendertraceRect(const ScrollableLayerGuid& aGuid, const char* aDesc, const char* aColor, const CSSRect& aRect) +{ +#ifdef APZC_ENABLE_RENDERTRACE + static const TimeStamp sRenderStart = TimeStamp::Now(); + TimeDuration delta = TimeStamp::Now() - sRenderStart; + printf_stderr("(%llu,%lu,%llu)%s RENDERTRACE %f rect %s %f %f %f %f\n", + aGuid.mLayersId, aGuid.mPresShellId, aGuid.GetScrollId(), + aDesc, delta.ToMilliseconds(), aColor, + aRect.x, aRect.y, aRect.width, aRect.height); +#endif +} + +static TimeStamp sFrameTime; + +// Counter used to give each APZC a unique id +static uint32_t sAsyncPanZoomControllerCount = 0; + +static TimeStamp +GetFrameTime() { + if (sFrameTime.IsNull()) { + return TimeStamp::Now(); + } + return sFrameTime; +} + +class FlingAnimation: public AsyncPanZoomAnimation { +public: + FlingAnimation(AsyncPanZoomController& aApzc) + : AsyncPanZoomAnimation(TimeDuration::FromMilliseconds(gfxPrefs::APZFlingRepaintInterval())) + , mApzc(aApzc) + {} + /** + * 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 Sample(FrameMetrics& aFrameMetrics, + const TimeDuration& aDelta); + +private: + AsyncPanZoomController& mApzc; +}; + +class ZoomAnimation: public AsyncPanZoomAnimation { +public: + ZoomAnimation(CSSPoint aStartOffset, CSSToScreenScale aStartZoom, + CSSPoint aEndOffset, CSSToScreenScale aEndZoom) + : mStartOffset(aStartOffset) + , mStartZoom(aStartZoom) + , mEndOffset(aEndOffset) + , mEndZoom(aEndZoom) + {} + + virtual bool Sample(FrameMetrics& aFrameMetrics, + const TimeDuration& aDelta); + +private: + TimeDuration mDuration; + + // 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; + CSSToScreenScale 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; + CSSToScreenScale mEndZoom; +}; + +void +AsyncPanZoomController::SetFrameTime(const TimeStamp& aTime) { + sFrameTime = aTime; +} + +/*static*/ void +AsyncPanZoomController::InitializeGlobalState() +{ + MOZ_ASSERT(NS_IsMainThread()); + + static bool sInitialized = false; + if (sInitialized) + return; + sInitialized = true; + + gComputedTimingFunction = new ComputedTimingFunction(); + gComputedTimingFunction->Init( + nsTimingFunction(NS_STYLE_TRANSITION_TIMING_FUNCTION_EASE)); + ClearOnShutdown(&gComputedTimingFunction); +} + +AsyncPanZoomController::AsyncPanZoomController(uint64_t aLayersId, + APZCTreeManager* aTreeManager, + GeckoContentController* aGeckoContentController, + GestureBehavior aGestures) + : mLayersId(aLayersId), + mCrossProcessCompositorParent(nullptr), + mPaintThrottler(GetFrameTime()), + mGeckoContentController(aGeckoContentController), + mRefPtrMonitor("RefPtrMonitor"), + mMonitor("AsyncPanZoomController"), + mTouchActionPropertyEnabled(gfxPrefs::TouchActionEnabled()), + mContentResponseTimeoutTask(nullptr), + mX(MOZ_THIS_IN_INITIALIZER_LIST()), + mY(MOZ_THIS_IN_INITIALIZER_LIST()), + mPanDirRestricted(false), + mZoomConstraints(false, false, MIN_ZOOM, MAX_ZOOM), + mLastSampleTime(GetFrameTime()), + mState(NOTHING), + mLastAsyncScrollTime(GetFrameTime()), + mLastAsyncScrollOffset(0, 0), + mCurrentAsyncScrollOffset(0, 0), + mAsyncScrollTimeoutTask(nullptr), + mHandlingTouchQueue(false), + mTreeManager(aTreeManager), + mScrollParentId(FrameMetrics::NULL_SCROLL_ID), + mAPZCId(sAsyncPanZoomControllerCount++), + mSharedFrameMetricsBuffer(nullptr), + mSharedLock(nullptr) +{ + MOZ_COUNT_CTOR(AsyncPanZoomController); + + if (aGestures == USE_GESTURE_DETECTOR) { + mGestureEventListener = new GestureEventListener(this); + } +} + +AsyncPanZoomController::~AsyncPanZoomController() { + + PCompositorParent* compositor = + (mCrossProcessCompositorParent ? mCrossProcessCompositorParent : mCompositorParent.get()); + + // Only send the release message if the SharedFrameMetrics has been created. + if (compositor && mSharedFrameMetricsBuffer) { + unused << compositor->SendReleaseSharedCompositorFrameMetrics(mFrameMetrics.GetScrollId(), mAPZCId); + } + + delete mSharedFrameMetricsBuffer; + delete mSharedLock; + + MOZ_COUNT_DTOR(AsyncPanZoomController); +} + +already_AddRefed +AsyncPanZoomController::GetGeckoContentController() { + MonitorAutoLock lock(mRefPtrMonitor); + nsRefPtr controller = mGeckoContentController; + return controller.forget(); +} + +already_AddRefed +AsyncPanZoomController::GetGestureEventListener() { + MonitorAutoLock lock(mRefPtrMonitor); + nsRefPtr listener = mGestureEventListener; + return listener.forget(); +} + +void +AsyncPanZoomController::Destroy() +{ + { // scope the lock + MonitorAutoLock lock(mRefPtrMonitor); + mGeckoContentController = nullptr; + mGestureEventListener = nullptr; + } + mPrevSibling = nullptr; + mLastChild = nullptr; + mParent = nullptr; + mTreeManager = nullptr; +} + +bool +AsyncPanZoomController::IsDestroyed() +{ + return mTreeManager == nullptr; +} + +/* static */float +AsyncPanZoomController::GetTouchStartTolerance() +{ + return (gfxPrefs::APZTouchStartTolerance() * APZCTreeManager::GetDPI()); +} + +/* static */AsyncPanZoomController::AxisLockMode AsyncPanZoomController::GetAxisLockMode() +{ + return static_cast(gfxPrefs::APZAxisLockMode()); +} + +nsEventStatus AsyncPanZoomController::ReceiveInputEvent(const InputData& aEvent) { + if (aEvent.mInputType == MULTITOUCH_INPUT && + aEvent.AsMultiTouchInput().mType == MultiTouchInput::MULTITOUCH_START) { + // Starting a new touch block, clear old touch block state. + mTouchBlockState = TouchBlockState(); + } + + // If we may have touch listeners and touch action property is enabled, we + // enable the machinery that allows touch listeners to preventDefault any touch inputs + // and also waits for the allowed touch behavior values to be received from the outside. + // This should not happen unless there are actually touch listeners and touch-action property + // enable as it introduces potentially unbounded lag because it causes a round-trip through + // content. Usually, if content is responding in a timely fashion, this only introduces a + // nearly constant few hundred ms of lag. + if (mFrameMetrics.mMayHaveTouchListeners && aEvent.mInputType == MULTITOUCH_INPUT && + (mState == NOTHING || mState == TOUCHING || IsPanningState(mState))) { + const MultiTouchInput& multiTouchInput = aEvent.AsMultiTouchInput(); + if (multiTouchInput.mType == MultiTouchInput::MULTITOUCH_START) { + SetState(WAITING_CONTENT_RESPONSE); + } + } + + if (mState == WAITING_CONTENT_RESPONSE || mHandlingTouchQueue) { + if (aEvent.mInputType == MULTITOUCH_INPUT) { + const MultiTouchInput& multiTouchInput = aEvent.AsMultiTouchInput(); + mTouchQueue.AppendElement(multiTouchInput); + + SetContentResponseTimer(); + } + return nsEventStatus_eIgnore; + } + + return HandleInputEvent(aEvent); +} + +nsEventStatus AsyncPanZoomController::HandleInputEvent(const InputData& aEvent) { + nsEventStatus rv = nsEventStatus_eIgnore; + + switch (aEvent.mInputType) { + case MULTITOUCH_INPUT: { + const MultiTouchInput& multiTouchInput = aEvent.AsMultiTouchInput(); + + nsRefPtr 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; + } + default: NS_WARNING("Unhandled input event"); break; + } + + mLastEventTime = aEvent.mTime; + return rv; +} + +nsEventStatus AsyncPanZoomController::HandleGestureEvent(const InputData& aEvent) +{ + 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_CANCEL: rv = OnCancelTap(tapGestureInput); break; + default: NS_WARNING("Unhandled tap gesture"); break; + } + break; + } + default: NS_WARNING("Unhandled input event"); break; + } + + mLastEventTime = aEvent.mTime; + return rv; +} + +nsEventStatus AsyncPanZoomController::OnTouchStart(const MultiTouchInput& aEvent) { + APZC_LOG("%p got a touch-start in state %d\n", this, mState); + mPanDirRestricted = false; + ScreenIntPoint point = GetFirstTouchScreenPoint(aEvent); + + switch (mState) { + case ANIMATING_ZOOM: + // We just interrupted a double-tap animation, so force a redraw in case + // this touchstart is just a tap that doesn't end up triggering a redraw. + { + ReentrantMonitorAutoEnter lock(mMonitor); + RequestContentRepaint(); + ScheduleComposite(); + UpdateSharedCompositorFrameMetrics(); + } + // Fall through. + case FLING: + CancelAnimation(); + // Fall through. + case NOTHING: { + mX.StartTouch(point.x); + mY.StartTouch(point.y); + APZCTreeManager* treeManagerLocal = mTreeManager; + nsRefPtr controller = GetGeckoContentController(); + if (treeManagerLocal && controller) { + bool touchCanBePan = treeManagerLocal->CanBePanned(this); + controller->NotifyAPZStateChange( + GetGuid(), APZStateChange::StartTouch, touchCanBePan); + } + SetState(TOUCHING); + break; + } + case TOUCHING: + case PANNING: + case PANNING_LOCKED_X: + case PANNING_LOCKED_Y: + case CROSS_SLIDING_X: + case CROSS_SLIDING_Y: + case PINCHING: + case WAITING_CONTENT_RESPONSE: + 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 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 CROSS_SLIDING_X: + case CROSS_SLIDING_Y: + // While cross-sliding, we don't want to consume any touchmove events for + // panning or zooming, and let the caller handle them instead. + return nsEventStatus_eIgnore; + + case TOUCHING: { + float panThreshold = GetTouchStartTolerance(); + UpdateWithTouchAtDevicePoint(aEvent); + + if (PanDistance() < panThreshold) { + return nsEventStatus_eIgnore; + } + + if (mTouchActionPropertyEnabled && + (GetTouchBehavior(0) & AllowedTouchBehavior::VERTICAL_PAN) && + (GetTouchBehavior(0) & AllowedTouchBehavior::HORIZONTAL_PAN)) { + // 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: + 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 WAITING_CONTENT_RESPONSE: + 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); + + 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); + SendAsyncScrollEvent(); + } + + switch (mState) { + case FLING: + // Should never happen. + NS_WARNING("Received impossible touch end in OnTouchEnd."); + // Fall through. + case ANIMATING_ZOOM: + 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: + case CROSS_SLIDING_X: + case CROSS_SLIDING_Y: + SetState(NOTHING); + return nsEventStatus_eIgnore; + + case PANNING: + case PANNING_LOCKED_X: + case PANNING_LOCKED_Y: + { + // Make a local copy of the tree manager pointer and check if it's not + // null before calling FlushRepaintsForOverscrollHandoffChain(). + // This is necessary because Destroy(), which nulls out mTreeManager, + // could be called concurrently. + APZCTreeManager* treeManagerLocal = mTreeManager; + if (treeManagerLocal) { + if (!treeManagerLocal->FlushRepaintsForOverscrollHandoffChain()) { + NS_WARNING("Overscroll handoff chain was empty during panning! This should not be the case."); + // Graceful handling of error condition + FlushRepaintForOverscrollHandoff(); + } + } + } + mX.EndTouch(); + mY.EndTouch(); + SetState(FLING); + StartAnimation(new FlingAnimation(*this)); + 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 WAITING_CONTENT_RESPONSE: + 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(); + SetState(NOTHING); + return nsEventStatus_eConsumeNoDefault; +} + +nsEventStatus AsyncPanZoomController::OnScaleBegin(const PinchGestureInput& aEvent) { + APZC_LOG("%p got a scale-begin in state %d\n", this, mState); + + if (!TouchActionAllowPinchZoom()) { + return nsEventStatus_eIgnore; + } + + if (!mZoomConstraints.mAllowZoom) { + return nsEventStatus_eConsumeNoDefault; + } + + SetState(PINCHING); + mLastZoomFocus = ToParentLayerCoords(aEvent.mFocusPoint) - mFrameMetrics.mCompositionBounds.TopLeft(); + + return nsEventStatus_eConsumeNoDefault; +} + +nsEventStatus AsyncPanZoomController::OnScale(const PinchGestureInput& aEvent) { + APZC_LOG("%p got a scale in state %d\n", this, mState); + if (mState != PINCHING) { + return nsEventStatus_eConsumeNoDefault; + } + + float prevSpan = aEvent.mPreviousSpan; + if (fabsf(prevSpan) <= EPSILON || fabsf(aEvent.mCurrentSpan) <= EPSILON) { + // We're still handling it; we've just decided to throw this event away. + return nsEventStatus_eConsumeNoDefault; + } + + float spanRatio = aEvent.mCurrentSpan / aEvent.mPreviousSpan; + + { + ReentrantMonitorAutoEnter lock(mMonitor); + + CSSToParentLayerScale userZoom = mFrameMetrics.GetZoomToParent(); + ParentLayerPoint focusPoint = ToParentLayerCoords(aEvent.mFocusPoint) - mFrameMetrics.mCompositionBounds.TopLeft(); + CSSPoint cssFocusPoint = focusPoint / userZoom; + + CSSPoint focusChange = (mLastZoomFocus - focusPoint) / userZoom; + // If displacing by the change in focus point will take us off page bounds, + // then reduce the displacement such that it doesn't. + if (mX.DisplacementWillOverscroll(focusChange.x) != Axis::OVERSCROLL_NONE) { + focusChange.x -= mX.DisplacementWillOverscrollAmount(focusChange.x); + } + if (mY.DisplacementWillOverscroll(focusChange.y) != Axis::OVERSCROLL_NONE) { + focusChange.y -= mY.DisplacementWillOverscrollAmount(focusChange.y); + } + ScrollBy(focusChange); + + // 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 * mFrameMetrics.mTransformScale; + CSSToParentLayerScale realMaxZoom = mZoomConstraints.mMaxZoom * mFrameMetrics.mTransformScale; + realMinZoom.scale = std::max(realMinZoom.scale, + mFrameMetrics.mCompositionBounds.width / mFrameMetrics.mScrollableRect.width); + realMinZoom.scale = std::max(realMinZoom.scale, + mFrameMetrics.mCompositionBounds.height / mFrameMetrics.mScrollableRect.height); + if (realMaxZoom < realMinZoom) { + realMaxZoom = realMinZoom; + } + + bool doScale = (spanRatio > 1.0 && userZoom < realMaxZoom) || + (spanRatio < 1.0 && userZoom > realMinZoom); + + 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); + } + + ScheduleComposite(); + // We don't want to redraw on every scale, so don't use + // RequestContentRepaint() + UpdateSharedCompositorFrameMetrics(); + } + + mLastZoomFocus = focusPoint; + } + + return nsEventStatus_eConsumeNoDefault; +} + +nsEventStatus AsyncPanZoomController::OnScaleEnd(const PinchGestureInput& aEvent) { + APZC_LOG("%p got a scale-end in state %d\n", this, mState); + + SetState(NOTHING); + + { + ReentrantMonitorAutoEnter lock(mMonitor); + ScheduleComposite(); + RequestContentRepaint(); + UpdateSharedCompositorFrameMetrics(); + } + + return nsEventStatus_eConsumeNoDefault; +} + +bool +AsyncPanZoomController::ConvertToGecko(const ScreenPoint& aPoint, CSSPoint* aOut) +{ + APZCTreeManager* treeManagerLocal = mTreeManager; + if (treeManagerLocal) { + gfx3DMatrix transformToApzc; + gfx3DMatrix transformToGecko; + treeManagerLocal->GetInputTransforms(this, transformToApzc, transformToGecko); + gfxPoint result = transformToGecko.Transform(gfxPoint(aPoint.x, aPoint.y)); + // NOTE: This isn't *quite* LayoutDevicePoint, we just don't have a name + // for this coordinate space and it maps the closest to LayoutDevicePoint. + LayoutDevicePoint layoutPoint = LayoutDevicePoint(result.x, result.y); + { // scoped lock to access mFrameMetrics + ReentrantMonitorAutoEnter lock(mMonitor); + *aOut = layoutPoint / mFrameMetrics.mDevPixelsPerCSSPixel; + } + return true; + } + return false; +} + +nsEventStatus AsyncPanZoomController::OnLongPress(const TapGestureInput& aEvent) { + APZC_LOG("%p got a long-press in state %d\n", this, mState); + nsRefPtr controller = GetGeckoContentController(); + if (controller) { + int32_t modifiers = WidgetModifiersToDOMModifiers(aEvent.modifiers); + CSSPoint geckoScreenPoint; + if (ConvertToGecko(aEvent.mPoint, &geckoScreenPoint)) { + SetState(WAITING_CONTENT_RESPONSE); + SetContentResponseTimer(); + controller->HandleLongTap(geckoScreenPoint, modifiers, GetGuid()); + 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); + nsRefPtr controller = GetGeckoContentController(); + if (controller) { + int32_t modifiers = WidgetModifiersToDOMModifiers(aEvent.modifiers); + CSSPoint geckoScreenPoint; + if (ConvertToGecko(aEvent.mPoint, &geckoScreenPoint)) { + controller->HandleLongTapUp(geckoScreenPoint, modifiers, GetGuid()); + return nsEventStatus_eConsumeNoDefault; + } + } + return nsEventStatus_eIgnore; +} + +nsEventStatus AsyncPanZoomController::GenerateSingleTap(const ScreenIntPoint& aPoint, mozilla::Modifiers aModifiers) { + nsRefPtr controller = GetGeckoContentController(); + if (controller) { + CSSPoint geckoScreenPoint; + if (ConvertToGecko(aPoint, &geckoScreenPoint)) { + int32_t modifiers = WidgetModifiersToDOMModifiers(aModifiers); + // Because this may be being running as part of APZCTreeManager::ReceiveInputEvent, + // calling controller->HandleSingleTap 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. + controller->PostDelayedTask( + NewRunnableMethod(controller.get(), &GeckoContentController::HandleSingleTap, + geckoScreenPoint, modifiers, GetGuid()), + 0); + mTouchBlockState.mSingleTapOccurred = true; + return nsEventStatus_eConsumeNoDefault; + } + } + return nsEventStatus_eIgnore; +} + +void AsyncPanZoomController::OnTouchEndOrCancel() { + if (nsRefPtr controller = GetGeckoContentController()) { + controller->NotifyAPZStateChange( + GetGuid(), APZStateChange::EndTouch, mTouchBlockState.mSingleTapOccurred); + } +} + +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 + if (!(mZoomConstraints.mAllowDoubleTapZoom && TouchActionAllowDoubleTapZoom())) { + return GenerateSingleTap(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(aEvent.mPoint, aEvent.modifiers); +} + +nsEventStatus AsyncPanZoomController::OnDoubleTap(const TapGestureInput& aEvent) { + APZC_LOG("%p got a double-tap in state %d\n", this, mState); + nsRefPtr controller = GetGeckoContentController(); + if (controller) { + if (mZoomConstraints.mAllowDoubleTapZoom && TouchActionAllowDoubleTapZoom()) { + int32_t modifiers = WidgetModifiersToDOMModifiers(aEvent.modifiers); + CSSPoint geckoScreenPoint; + if (ConvertToGecko(aEvent.mPoint, &geckoScreenPoint)) { + controller->HandleDoubleTap(geckoScreenPoint, modifiers, GetGuid()); + } + } + return nsEventStatus_eConsumeNoDefault; + } + return nsEventStatus_eIgnore; +} + +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; +} + +float AsyncPanZoomController::PanDistance() { + ReentrantMonitorAutoEnter lock(mMonitor); + return NS_hypot(mX.PanDistance(), mY.PanDistance()); +} + +const ScreenPoint AsyncPanZoomController::GetVelocityVector() { + return ScreenPoint(mX.GetVelocity(), mY.GetVelocity()); +} + +void AsyncPanZoomController::HandlePanningWithTouchAction(double aAngle, TouchBehaviorFlags aBehavior) { + // Handling of cross sliding will need to be added in this method after touch-action released + // enabled by default. + if ((aBehavior & AllowedTouchBehavior::VERTICAL_PAN) && (aBehavior & AllowedTouchBehavior::HORIZONTAL_PAN)) { + if (mX.Scrollable() && mY.Scrollable()) { + if (IsCloseToHorizontal(aAngle, AXIS_LOCK_ANGLE)) { + mY.SetAxisLocked(true); + SetState(PANNING_LOCKED_X); + } else if (IsCloseToVertical(aAngle, AXIS_LOCK_ANGLE)) { + mX.SetAxisLocked(true); + SetState(PANNING_LOCKED_Y); + } else { + SetState(PANNING); + } + } else if (mX.Scrollable() || mY.Scrollable()) { + SetState(PANNING); + } else { + SetState(NOTHING); + } + } else if (aBehavior & AllowedTouchBehavior::HORIZONTAL_PAN) { + // Using bigger angle for panning to keep behavior consistent + // with IE. + if (IsCloseToHorizontal(aAngle, ALLOWED_DIRECT_PAN_ANGLE)) { + 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 (aBehavior & AllowedTouchBehavior::VERTICAL_PAN) { + if (IsCloseToVertical(aAngle, ALLOWED_DIRECT_PAN_ANGLE)) { + mX.SetAxisLocked(true); + SetState(PANNING_LOCKED_Y); + mPanDirRestricted = true; + } else { + SetState(NOTHING); + } + } else { + SetState(NOTHING); + } +} + +void AsyncPanZoomController::HandlePanning(double aAngle) { + if (!gfxPrefs::APZCrossSlideEnabled() && (!mX.Scrollable() || !mY.Scrollable())) { + SetState(PANNING); + } else if (IsCloseToHorizontal(aAngle, AXIS_LOCK_ANGLE)) { + mY.SetAxisLocked(true); + if (mX.Scrollable()) { + SetState(PANNING_LOCKED_X); + } else { + SetState(CROSS_SLIDING_X); + mX.SetAxisLocked(true); + } + } else if (IsCloseToVertical(aAngle, AXIS_LOCK_ANGLE)) { + mX.SetAxisLocked(true); + if (mY.Scrollable()) { + SetState(PANNING_LOCKED_Y); + } else { + SetState(CROSS_SLIDING_Y); + mY.SetAxisLocked(true); + } + } else { + SetState(PANNING); + } +} + +nsEventStatus AsyncPanZoomController::StartPanning(const MultiTouchInput& aEvent) { + ReentrantMonitorAutoEnter lock(mMonitor); + + ScreenIntPoint point = GetFirstTouchScreenPoint(aEvent); + float dx = mX.PanDistance(point.x); + float dy = mY.PanDistance(point.y); + + // When the touch move breaks through the pan threshold, reposition the touch down origin + // so the page won't jump when we start panning. + mX.StartTouch(point.x); + mY.StartTouch(point.y); + mLastEventTime = aEvent.mTime; + + double angle = atan2(dy, dx); // range [-pi, pi] + angle = fabs(angle); // range [0, pi] + + if (mTouchActionPropertyEnabled) { + HandlePanningWithTouchAction(angle, GetTouchBehavior(0)); + } else { + if (GetAxisLockMode() == FREE) { + SetState(PANNING); + } else { + HandlePanning(angle); + } + } + + if (IsPanningState(mState)) { + if (nsRefPtr controller = GetGeckoContentController()) { + controller->NotifyAPZStateChange(GetGuid(), APZStateChange::StartPanning); + } + return nsEventStatus_eConsumeNoDefault; + } + // Don't consume an event that didn't trigger a panning. + return nsEventStatus_eIgnore; +} + +void AsyncPanZoomController::UpdateWithTouchAtDevicePoint(const MultiTouchInput& aEvent) { + ScreenIntPoint point = GetFirstTouchScreenPoint(aEvent); + TimeDuration timeDelta = TimeDuration().FromMilliseconds(aEvent.mTime - mLastEventTime); + + // Probably a duplicate event, just throw it away. + if (timeDelta.ToMilliseconds() <= EPSILON) { + return; + } + + mX.UpdateWithTouchAtDevicePoint(point.x, timeDelta); + mY.UpdateWithTouchAtDevicePoint(point.y, timeDelta); +} + +void AsyncPanZoomController::AttemptScroll(const ScreenPoint& aStartPoint, + const ScreenPoint& aEndPoint, + uint32_t aOverscrollHandoffChainIndex) { + + // "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. + ScreenPoint displacement = aStartPoint - aEndPoint; + + ScreenPoint overscroll; // will be used outside monitor block + { + ReentrantMonitorAutoEnter lock(mMonitor); + + CSSToScreenScale zoom = mFrameMetrics.GetZoom(); + + // Inversely scale the offset by the resolution (when you're zoomed further in, + // the same swipe should move you a shorter distance). + CSSPoint cssDisplacement = displacement / zoom; + + CSSPoint cssOverscroll; + CSSPoint allowedDisplacement(mX.AdjustDisplacement(cssDisplacement.x, + cssOverscroll.x), + mY.AdjustDisplacement(cssDisplacement.y, + cssOverscroll.y)); + overscroll = cssOverscroll * zoom; + + if (!IsZero(allowedDisplacement)) { + ScrollBy(allowedDisplacement); + ScheduleComposite(); + + TimeDuration timePaintDelta = mPaintThrottler.TimeSinceLastRequest(GetFrameTime()); + if (timePaintDelta.ToMilliseconds() > gfxPrefs::APZPanRepaintInterval()) { + RequestContentRepaint(); + } + UpdateSharedCompositorFrameMetrics(); + } + } + + if (!IsZero(overscroll)) { + // "+ overscroll" rather than "- overscroll" because "overscroll" is what's + // left of "displacement", and "displacement" is "start - end". + CallDispatchScroll(aEndPoint + overscroll, aEndPoint, aOverscrollHandoffChainIndex + 1); + } +} + +void AsyncPanZoomController::TakeOverFling(ScreenPoint aVelocity) { + // We may have a pre-existing velocity for whatever reason (for example, + // a previously handed off fling). We don't want to clobber that. + mX.SetVelocity(mX.GetVelocity() + aVelocity.x); + mY.SetVelocity(mY.GetVelocity() + aVelocity.y); + SetState(FLING); + StartAnimation(new FlingAnimation(*this)); +} + +void AsyncPanZoomController::CallDispatchScroll(const ScreenPoint& aStartPoint, const ScreenPoint& aEndPoint, + uint32_t aOverscrollHandoffChainIndex) { + // 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 = mTreeManager; + if (treeManagerLocal) { + treeManagerLocal->DispatchScroll(this, aStartPoint, aEndPoint, + aOverscrollHandoffChainIndex); + } +} + +void AsyncPanZoomController::TrackTouch(const MultiTouchInput& aEvent) { + ScreenIntPoint prevTouchPoint(mX.GetPos(), mY.GetPos()); + ScreenIntPoint touchPoint = GetFirstTouchScreenPoint(aEvent); + TimeDuration timeDelta = TimeDuration().FromMilliseconds(aEvent.mTime - mLastEventTime); + + // Probably a duplicate event, just throw it away. + if (timeDelta.ToMilliseconds() <= EPSILON) { + return; + } + + // If we're axis-locked, check if the user is trying to break the lock + if (GetAxisLockMode() == STICKY && !mPanDirRestricted) { + ScreenIntPoint point = GetFirstTouchScreenPoint(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] + + float breakThreshold = AXIS_BREAKOUT_THRESHOLD * APZCTreeManager::GetDPI(); + + if (fabs(dx) > breakThreshold || fabs(dy) > breakThreshold) { + if (mState == PANNING_LOCKED_X || mState == CROSS_SLIDING_X) { + if (!IsCloseToHorizontal(angle, AXIS_BREAKOUT_ANGLE)) { + mY.SetAxisLocked(false); + SetState(PANNING); + } + } else if (mState == PANNING_LOCKED_Y || mState == CROSS_SLIDING_Y) { + if (!IsCloseToVertical(angle, AXIS_BREAKOUT_ANGLE)) { + mX.SetAxisLocked(false); + SetState(PANNING); + } + } + } + } + + UpdateWithTouchAtDevicePoint(aEvent); + + CallDispatchScroll(prevTouchPoint, touchPoint, 0); +} + +ScreenIntPoint& AsyncPanZoomController::GetFirstTouchScreenPoint(const MultiTouchInput& aEvent) { + return ((SingleTouchData&)aEvent.mTouches[0]).mScreenPoint; +} + +bool FlingAnimation::Sample(FrameMetrics& aFrameMetrics, + const TimeDuration& aDelta) { + + // If the fling is handed off to our APZC from a child, on the first call to + // Sample() aDelta might be negative because it's computed as the sample time + // from SampleContentTransformForFrame() minus our APZC's mLastSampleTime + // which is the time the child handed off the fling from its call to + // SampleContentTransformForFrame() with the same sample time. If we allow + // the negative aDelta to be processed, it will yield a displacement in the + // direction opposite to the fling, which can cause us to overscroll and + // hand off the fling to _our_ parent, which effectively kills the fling. + if (aDelta.ToMilliseconds() <= 0) { + return true; + } + + bool shouldContinueFlingX = mApzc.mX.FlingApplyFrictionOrCancel(aDelta), + shouldContinueFlingY = mApzc.mY.FlingApplyFrictionOrCancel(aDelta); + // If we shouldn't continue the fling, let's just stop and repaint. + if (!shouldContinueFlingX && !shouldContinueFlingY) { + 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 ScreenVector instead of ScreenPoint if we had + // vector classes. + ScreenPoint velocity(mApzc.mX.GetVelocity(), mApzc.mY.GetVelocity()); + + ScreenPoint offset = velocity * aDelta.ToMilliseconds(); + + // Inversely scale the offset by the resolution (when you're zoomed further in, + // the same swipe should move you a shorter distance). + CSSPoint cssOffset = offset / aFrameMetrics.GetZoom(); + CSSPoint overscroll; + aFrameMetrics.ScrollBy(CSSPoint( + mApzc.mX.AdjustDisplacement(cssOffset.x, overscroll.x), + mApzc.mY.AdjustDisplacement(cssOffset.y, overscroll.y) + )); + + // If the fling has caused us to reach the end of our scroll range, hand + // off the fling to the next APZC in the overscroll handoff chain. + if (!IsZero(overscroll)) { + // 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 (FuzzyEqualsMultiplicative(overscroll.x, 0.0f)) { + velocity.x = 0; + } else if (FuzzyEqualsMultiplicative(overscroll.y, 0.0f)) { + velocity.y = 0; + } + + // To hand off the fling, we call APZCTreeManager::HandleFlingOverscroll() + // which starts a new fling in the next APZC in the handoff chain with + // the same velocity. 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. + + // Make a local copy of the tree manager pointer and check if it's not + // null before calling HandleFlingOverscroll(). This is necessary because + // Destroy(), which nulls out mTreeManager, could be called concurrently. + APZCTreeManager* treeManagerLocal = mApzc.mTreeManager; + if (treeManagerLocal) { + // 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. + mDeferredTasks.append(NewRunnableMethod(treeManagerLocal, + &APZCTreeManager::HandOffFling, + &mApzc, + velocity)); + } + } + + return true; +} + +void AsyncPanZoomController::StartAnimation(AsyncPanZoomAnimation* aAnimation) +{ + ReentrantMonitorAutoEnter lock(mMonitor); + mAnimation = aAnimation; + mLastSampleTime = GetFrameTime(); + ScheduleComposite(); +} + +void AsyncPanZoomController::CancelAnimation() { + ReentrantMonitorAutoEnter lock(mMonitor); + SetState(NOTHING); + mAnimation = nullptr; +} + +void AsyncPanZoomController::SetCompositorParent(CompositorParent* aCompositorParent) { + mCompositorParent = aCompositorParent; +} + +void AsyncPanZoomController::SetCrossProcessCompositorParent(PCompositorParent* aCrossProcessCompositorParent) { + mCrossProcessCompositorParent = aCrossProcessCompositorParent; +} + +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) +{ + float xMultiplier = fabsf(aVelocity.x) < gfxPrefs::APZMinSkateSpeed() + ? gfxPrefs::APZXStationarySizeMultiplier() + : gfxPrefs::APZXSkateSizeMultiplier(); + float yMultiplier = fabsf(aVelocity.y) < gfxPrefs::APZMinSkateSpeed() + ? gfxPrefs::APZYStationarySizeMultiplier() + : gfxPrefs::APZYSkateSizeMultiplier(); + return CSSSize(aCompositionSize.width * xMultiplier, + aCompositionSize.height * yMultiplier); +} + +/** + * 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) +{ + float xSlack = std::max(0.0f, aDisplayPortSize.width - aScrollableRect.width); + float ySlack = std::max(0.0f, aDisplayPortSize.height - aScrollableRect.height); + + if (ySlack > 0) { + // Reassign wasted y-axis displayport to the x-axis + aDisplayPortSize.height -= ySlack; + float xExtra = ySlack * aDisplayPortSize.width / aDisplayPortSize.height; + aDisplayPortSize.width += xExtra; + } else if (xSlack > 0) { + // Reassign wasted x-axis displayport to the y-axis + aDisplayPortSize.width -= xSlack; + float yExtra = xSlack * aDisplayPortSize.height / aDisplayPortSize.width; + aDisplayPortSize.height += yExtra; + } +} + +/* static */ +const LayerMargin AsyncPanZoomController::CalculatePendingDisplayPort( + const FrameMetrics& aFrameMetrics, + const ScreenPoint& aVelocity, + double aEstimatedPaintDuration) +{ + CSSSize compositionBounds = aFrameMetrics.CalculateCompositedSizeInCssPixels(); + CSSSize compositionSize = aFrameMetrics.GetRootCompositionSize(); + compositionSize = + CSSSize(std::min(compositionBounds.width, compositionSize.width), + std::min(compositionBounds.height, compositionSize.height)); + CSSPoint velocity = aVelocity / aFrameMetrics.GetZoom(); + CSSPoint scrollOffset = aFrameMetrics.GetScrollOffset(); + CSSRect scrollableRect = aFrameMetrics.GetExpandedScrollableRect(); + + // Calculate the displayport size based on how fast we're moving along each axis. + CSSSize displayPortSize = CalculateDisplayPortSize(compositionSize, velocity); + + if (gfxPrefs::APZEnlargeDisplayPortWhenClipped()) { + RedistributeDisplayPortExcess(displayPortSize, scrollableRect); + } + + // Offset the displayport, depending on how fast we're moving and the + // estimated time it takes to paint, to try to minimise checkerboarding. + float estimatedPaintDurationMillis = (float)(aEstimatedPaintDuration * 1000.0); + float paintFactor = (gfxPrefs::APZUsePaintDuration() ? estimatedPaintDurationMillis : 50.0f); + CSSRect displayPort = CSSRect(scrollOffset + (velocity * paintFactor * gfxPrefs::APZVelocityBias()), + displayPortSize); + + // Re-center the displayport based on its expansion over the composition size. + displayPort.MoveBy((compositionSize.width - displayPort.width)/2.0f, + (compositionSize.height - displayPort.height)/2.0f); + + // Make sure the displayport remains within the scrollable rect. + displayPort = displayPort.ForceInside(scrollableRect) - scrollOffset; + + APZC_LOG_FM(aFrameMetrics, + "Calculated displayport as (%f %f %f %f) from velocity (%f %f) paint time %f metrics", + displayPort.x, displayPort.y, displayPort.width, displayPort.height, + aVelocity.x, aVelocity.y, (float)estimatedPaintDurationMillis); + + 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; + + LayerMargin layerMargins = cssMargins * aFrameMetrics.LayersPixelsPerCSSPixel(); + + return layerMargins; +} + +void AsyncPanZoomController::ScheduleComposite() { + if (mCompositorParent) { + mCompositorParent->ScheduleRenderOnCompositorThread(); + } +} + +void AsyncPanZoomController::FlushRepaintForOverscrollHandoff() { + ReentrantMonitorAutoEnter lock(mMonitor); + RequestContentRepaint(); + UpdateSharedCompositorFrameMetrics(); +} + +bool AsyncPanZoomController::IsPannable() const { + ReentrantMonitorAutoEnter lock(mMonitor); + return mX.HasRoomToPan() || mY.HasRoomToPan(); +} + +void AsyncPanZoomController::RequestContentRepaint() { + RequestContentRepaint(mFrameMetrics); +} + +void AsyncPanZoomController::RequestContentRepaint(FrameMetrics& aFrameMetrics) { + aFrameMetrics.SetDisplayPortMargins( + CalculatePendingDisplayPort(aFrameMetrics, + GetVelocityVector(), + mPaintThrottler.AverageDuration().ToSeconds())); + aFrameMetrics.SetUseDisplayPortMargins(); + + // If we're trying to paint what we already think is painted, discard this + // request since it's a pointless paint. + LayerMargin 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.GetZoom() == mLastPaintRequestMetrics.GetZoom() && + fabsf(aFrameMetrics.mViewport.width - mLastPaintRequestMetrics.mViewport.width) < EPSILON && + fabsf(aFrameMetrics.mViewport.height - mLastPaintRequestMetrics.mViewport.height) < EPSILON) { + return; + } + + SendAsyncScrollEvent(); + mPaintThrottler.PostTask( + FROM_HERE, + NewRunnableMethod(this, + &AsyncPanZoomController::DispatchRepaintRequest, + aFrameMetrics), + GetFrameTime()); + + aFrameMetrics.mPresShellId = mLastContentPaintMetrics.mPresShellId; + mLastPaintRequestMetrics = aFrameMetrics; +} + +/*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(), + CSSSize(std::min(aFrameMetrics.CalculateCompositedSizeInCssPixels().width, + aFrameMetrics.GetRootCompositionSize().width), + std::min(aFrameMetrics.CalculateCompositedSizeInCssPixels().height, + aFrameMetrics.GetRootCompositionSize().height))); + baseRect.Inflate(aFrameMetrics.GetDisplayPortMargins() / aFrameMetrics.LayersPixelsPerCSSPixel()); + return baseRect; +} + +void +AsyncPanZoomController::DispatchRepaintRequest(const FrameMetrics& aFrameMetrics) { + nsRefPtr controller = GetGeckoContentController(); + if (controller) { + APZC_LOG_FM(aFrameMetrics, "%p requesting content repaint", this); + LogRendertraceRect(GetGuid(), "requested displayport", "yellow", GetDisplayPortRect(aFrameMetrics)); + + controller->RequestContentRepaint(aFrameMetrics); + mLastDispatchedPaintMetrics = aFrameMetrics; + } +} + +void +AsyncPanZoomController::FireAsyncScrollOnTimeout() +{ + if (mCurrentAsyncScrollOffset != mLastAsyncScrollOffset) { + ReentrantMonitorAutoEnter lock(mMonitor); + SendAsyncScrollEvent(); + } + mAsyncScrollTimeoutTask = nullptr; +} + +bool ZoomAnimation::Sample(FrameMetrics& aFrameMetrics, + const TimeDuration& aDelta) { + mDuration += aDelta; + double animPosition = mDuration / ZOOM_TO_DURATION; + + 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. + double sampledPosition = gComputedTimingFunction->GetValue(animPosition); + + // We scale the scrollOffset linearly with sampledPosition, so the zoom + // needs to scale inversely to match. + aFrameMetrics.SetZoom(CSSToScreenScale(1 / + (sampledPosition / mEndZoom.scale + + (1 - sampledPosition) / mStartZoom.scale))); + + aFrameMetrics.SetScrollOffset(CSSPoint::FromUnknownPoint(gfx::Point( + mEndOffset.x * sampledPosition + mStartOffset.x * (1 - sampledPosition), + mEndOffset.y * sampledPosition + mStartOffset.y * (1 - sampledPosition) + ))); + + return true; +} + +bool AsyncPanZoomController::UpdateAnimation(const TimeStamp& aSampleTime) +{ + if (mAnimation) { + if (mAnimation->Sample(mFrameMetrics, aSampleTime - mLastSampleTime)) { + if (mPaintThrottler.TimeSinceLastRequest(aSampleTime) > + mAnimation->mRepaintInterval) { + RequestContentRepaint(); + } + } else { + mAnimation = nullptr; + SetState(NOTHING); + SendAsyncScrollEvent(); + RequestContentRepaint(); + } + UpdateSharedCompositorFrameMetrics(); + mLastSampleTime = aSampleTime; + return true; + } + return false; +} + +bool AsyncPanZoomController::SampleContentTransformForFrame(const TimeStamp& aSampleTime, + ViewTransform* aNewTransform, + ScreenPoint& aScrollOffset) { + // 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. + bool requestAnimationFrame = false; + Vector deferredTasks; + + { + ReentrantMonitorAutoEnter lock(mMonitor); + + requestAnimationFrame = UpdateAnimation(aSampleTime); + + aScrollOffset = mFrameMetrics.GetScrollOffset() * mFrameMetrics.GetZoom(); + *aNewTransform = GetCurrentAsyncTransform(); + + LogRendertraceRect(GetGuid(), "viewport", "red", + CSSRect(mFrameMetrics.GetScrollOffset(), + ParentLayerSize(mFrameMetrics.mCompositionBounds.Size()) / mFrameMetrics.GetZoomToParent())); + + mCurrentAsyncScrollOffset = mFrameMetrics.GetScrollOffset(); + + // Get any deferred tasks queued up by mAnimation's Sample() (called by + // UpdateAnimation()). This needs to be done here since mAnimation can + // be destroyed by another thread when we release the monitor, but + // the tasks need to be executed after we release the monitor since they + // are allowed to call APZCTreeManager methods which can grab the tree lock. + if (mAnimation) { + deferredTasks = mAnimation->TakeDeferredTasks(); + } + } + + for (uint32_t i = 0; i < deferredTasks.length(); ++i) { + deferredTasks[i]->Run(); + delete deferredTasks[i]; + } + + // Cancel the mAsyncScrollTimeoutTask because we will fire a + // mozbrowserasyncscroll event or renew the mAsyncScrollTimeoutTask again. + if (mAsyncScrollTimeoutTask) { + mAsyncScrollTimeoutTask->Cancel(); + mAsyncScrollTimeoutTask = nullptr; + } + // Fire the mozbrowserasyncscroll event immediately if it's been + // sAsyncScrollThrottleTime ms since the last time we fired the event and the + // current scroll offset is different than the mLastAsyncScrollOffset we sent + // with the last event. + // Otherwise, start a timer to fire the event sAsyncScrollTimeout ms from now. + TimeDuration delta = aSampleTime - mLastAsyncScrollTime; + if (delta.ToMilliseconds() > gfxPrefs::APZAsyncScrollThrottleTime() && + mCurrentAsyncScrollOffset != mLastAsyncScrollOffset) { + ReentrantMonitorAutoEnter lock(mMonitor); + mLastAsyncScrollTime = aSampleTime; + mLastAsyncScrollOffset = mCurrentAsyncScrollOffset; + SendAsyncScrollEvent(); + } + else { + mAsyncScrollTimeoutTask = + NewRunnableMethod(this, &AsyncPanZoomController::FireAsyncScrollOnTimeout); + MessageLoop::current()->PostDelayedTask(FROM_HERE, + mAsyncScrollTimeoutTask, + gfxPrefs::APZAsyncScrollTimeout()); + } + + return requestAnimationFrame; +} + +ViewTransform AsyncPanZoomController::GetCurrentAsyncTransform() { + ReentrantMonitorAutoEnter lock(mMonitor); + + 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.mDisplayPort.IsEmpty()) { + CSSSize compositedSize = mLastContentPaintMetrics.CalculateCompositedSizeInCssPixels(); + CSSPoint maxScrollOffset = lastPaintScrollOffset + + CSSPoint(mLastContentPaintMetrics.mDisplayPort.XMost() - compositedSize.width, + mLastContentPaintMetrics.mDisplayPort.YMost() - compositedSize.height); + CSSPoint minScrollOffset = lastPaintScrollOffset + mLastContentPaintMetrics.mDisplayPort.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); + } + } + + LayerPoint translation = (currentScrollOffset - lastPaintScrollOffset) + * mLastContentPaintMetrics.LayersPixelsPerCSSPixel(); + + return ViewTransform(-translation, + mFrameMetrics.GetZoom() + / mLastContentPaintMetrics.mDevPixelsPerCSSPixel + / mFrameMetrics.GetParentResolution()); +} + +gfx3DMatrix AsyncPanZoomController::GetNontransientAsyncTransform() { + ReentrantMonitorAutoEnter lock(mMonitor); + return gfx3DMatrix::ScalingMatrix(mLastContentPaintMetrics.mResolution.scale, + mLastContentPaintMetrics.mResolution.scale, + 1.0f); +} + +gfx3DMatrix AsyncPanZoomController::GetTransformToLastDispatchedPaint() { + ReentrantMonitorAutoEnter lock(mMonitor); + LayerPoint scrollChange = (mLastContentPaintMetrics.GetScrollOffset() - mLastDispatchedPaintMetrics.GetScrollOffset()) + * mLastContentPaintMetrics.LayersPixelsPerCSSPixel(); + float zoomChange = mLastContentPaintMetrics.GetZoom().scale / mLastDispatchedPaintMetrics.GetZoom().scale; + return gfx3DMatrix::Translation(scrollChange.x, scrollChange.y, 0) * + gfx3DMatrix::ScalingMatrix(zoomChange, zoomChange, 1); +} + +void AsyncPanZoomController::NotifyLayersUpdated(const FrameMetrics& aLayerMetrics, bool aIsFirstPaint) { + ReentrantMonitorAutoEnter lock(mMonitor); + + mLastContentPaintMetrics = aLayerMetrics; + UpdateTransformScale(); + + bool isDefault = mFrameMetrics.IsDefault(); + mFrameMetrics.mMayHaveTouchListeners = aLayerMetrics.mMayHaveTouchListeners; + APZC_LOG_FM(aLayerMetrics, "%p got a NotifyLayersUpdated with aIsFirstPaint=%d", this, aIsFirstPaint); + + LogRendertraceRect(GetGuid(), "page", "brown", aLayerMetrics.mScrollableRect); + LogRendertraceRect(GetGuid(), "painted displayport", "green", + aLayerMetrics.mDisplayPort + aLayerMetrics.GetScrollOffset()); + + mPaintThrottler.TaskComplete(GetFrameTime()); + bool needContentRepaint = false; + if (aLayerMetrics.mCompositionBounds.width == mFrameMetrics.mCompositionBounds.width && + aLayerMetrics.mCompositionBounds.height == mFrameMetrics.mCompositionBounds.height) { + // Remote content has sync'd up to the composition geometry + // change, so we can accept the viewport it's calculated. + if (mFrameMetrics.mViewport.width != aLayerMetrics.mViewport.width || + mFrameMetrics.mViewport.height != aLayerMetrics.mViewport.height) { + needContentRepaint = true; + } + mFrameMetrics.mViewport = aLayerMetrics.mViewport; + } + + // 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 (aIsFirstPaint || isDefault) { + // Initialize our internal state to something sane when the content + // that was just painted is something we knew nothing about previously + mPaintThrottler.ClearHistory(); + mPaintThrottler.SetMaxDurations(gfxPrefs::APZNumPaintDurationSamples()); + + mX.CancelTouch(); + mY.CancelTouch(); + SetState(NOTHING); + + mFrameMetrics = aLayerMetrics; + mLastDispatchedPaintMetrics = aLayerMetrics; + ShareCompositorFrameMetrics(); + } 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 (mFrameMetrics.mCompositionBounds.width == aLayerMetrics.mCompositionBounds.width && + mFrameMetrics.mDevPixelsPerCSSPixel == aLayerMetrics.mDevPixelsPerCSSPixel) { + float parentResolutionChange = aLayerMetrics.GetParentResolution().scale + / mFrameMetrics.GetParentResolution().scale; + mFrameMetrics.ZoomBy(parentResolutionChange); + } else { + // Take the new zoom as either device scale or composition width or both + // got changed (e.g. due to orientation change). + mFrameMetrics.SetZoom(aLayerMetrics.GetZoom()); + mFrameMetrics.mDevPixelsPerCSSPixel.scale = aLayerMetrics.mDevPixelsPerCSSPixel.scale; + } + mFrameMetrics.mScrollableRect = aLayerMetrics.mScrollableRect; + mFrameMetrics.mCompositionBounds = aLayerMetrics.mCompositionBounds; + mFrameMetrics.SetRootCompositionSize(aLayerMetrics.GetRootCompositionSize()); + mFrameMetrics.mResolution = aLayerMetrics.mResolution; + mFrameMetrics.mCumulativeResolution = aLayerMetrics.mCumulativeResolution; + mFrameMetrics.mHasScrollgrab = aLayerMetrics.mHasScrollgrab; + + if (scrollOffsetUpdated) { + APZC_LOG("%p updating scroll offset from (%f, %f) to (%f, %f)\n", this, + mFrameMetrics.GetScrollOffset().x, mFrameMetrics.GetScrollOffset().y, + aLayerMetrics.GetScrollOffset().x, aLayerMetrics.GetScrollOffset().y); + + mFrameMetrics.CopyScrollInfoFrom(aLayerMetrics); + + // Because of the scroll offset update, any inflight paint requests are + // going to be ignored by layout, and so mLastDispatchedPaintMetrics + // becomes incorrect for the purposes of calculating the LD transform. To + // correct this we need to update mLastDispatchedPaintMetrics to be the + // last thing we know was painted by Gecko. + mLastDispatchedPaintMetrics = aLayerMetrics; + } + } + + if (scrollOffsetUpdated) { + // Once layout issues a scroll offset update, it becomes impervious to + // scroll offset updates from APZ until we acknowledge the update it sent. + // This prevents APZ updates from clobbering scroll updates from other + // more "legitimate" sources like content scripts. + nsRefPtr controller = GetGeckoContentController(); + if (controller) { + APZC_LOG("%p sending scroll update acknowledgement with gen %lu\n", this, aLayerMetrics.GetScrollGeneration()); + controller->AcknowledgeScrollUpdate(aLayerMetrics.GetScrollId(), + aLayerMetrics.GetScrollGeneration()); + } + } + + if (needContentRepaint) { + RequestContentRepaint(); + } + UpdateSharedCompositorFrameMetrics(); +} + +const FrameMetrics& AsyncPanZoomController::GetFrameMetrics() { + mMonitor.AssertCurrentThreadIn(); + return mFrameMetrics; +} + +void AsyncPanZoomController::ZoomToRect(CSSRect aRect) { + if (!aRect.IsFinite()) { + NS_WARNING("ZoomToRect got called with a non-finite rect; ignoring...\n"); + return; + } + + SetState(ANIMATING_ZOOM); + + { + ReentrantMonitorAutoEnter lock(mMonitor); + + ParentLayerIntRect compositionBounds = mFrameMetrics.mCompositionBounds; + CSSRect cssPageRect = mFrameMetrics.mScrollableRect; + CSSPoint scrollOffset = mFrameMetrics.GetScrollOffset(); + CSSToParentLayerScale currentZoom = mFrameMetrics.GetZoomToParent(); + 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 * mFrameMetrics.mTransformScale).scale, + std::max(compositionBounds.width / cssPageRect.width, + compositionBounds.height / cssPageRect.height))); + CSSToParentLayerScale localMaxZoom = mZoomConstraints.mMaxZoom * mFrameMetrics.mTransformScale; + + 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, request received from browserElementScrolling.js + // 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. + if (aRect.IsEmpty() || + (currentZoom == localMaxZoom && targetZoom >= localMaxZoom) || + (currentZoom == localMinZoom && targetZoom <= localMinZoom)) { + 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; + endZoomToMetrics.SetZoom(targetZoom / mFrameMetrics.mTransformScale); + + // Adjust the zoomToRect to a sensible position to prevent overscrolling. + CSSSize sizeAfterZoom = endZoomToMetrics.CalculateCompositedSizeInCssPixels(); + + // 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()); + endZoomToMetrics.SetDisplayPortMargins( + CalculatePendingDisplayPort(endZoomToMetrics, + ScreenPoint(0,0), + 0)); + endZoomToMetrics.SetUseDisplayPortMargins(); + + 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. + RequestContentRepaint(endZoomToMetrics); + } +} + +void AsyncPanZoomController::ContentReceivedTouch(bool aPreventDefault) { + mTouchBlockState.mPreventDefaultSet = true; + mTouchBlockState.mPreventDefault = aPreventDefault; + CheckContentResponse(); +} + +void AsyncPanZoomController::CheckContentResponse() { + bool canProceedToTouchState = true; + + if (mFrameMetrics.mMayHaveTouchListeners) { + canProceedToTouchState &= mTouchBlockState.mPreventDefaultSet; + } + + if (mTouchActionPropertyEnabled) { + canProceedToTouchState &= mTouchBlockState.mAllowedTouchBehaviorSet; + } + + if (!canProceedToTouchState) { + return; + } + + if (mContentResponseTimeoutTask) { + mContentResponseTimeoutTask->Cancel(); + mContentResponseTimeoutTask = nullptr; + } + + if (mState == WAITING_CONTENT_RESPONSE) { + if (!mTouchBlockState.mPreventDefault) { + SetState(NOTHING); + } + + mHandlingTouchQueue = true; + + while (!mTouchQueue.IsEmpty()) { + if (!mTouchBlockState.mPreventDefault) { + HandleInputEvent(mTouchQueue[0]); + } + + if (mTouchQueue[0].mType == MultiTouchInput::MULTITOUCH_END || + mTouchQueue[0].mType == MultiTouchInput::MULTITOUCH_CANCEL) { + mTouchQueue.RemoveElementAt(0); + break; + } + + mTouchQueue.RemoveElementAt(0); + } + + mHandlingTouchQueue = false; + } +} + +bool AsyncPanZoomController::TouchActionAllowPinchZoom() { + if (!mTouchActionPropertyEnabled) { + return true; + } + // Pointer events specification implies all touch points to allow zoom + // to perform it. + for (size_t i = 0; i < mTouchBlockState.mAllowedTouchBehaviors.Length(); i++) { + if (!(mTouchBlockState.mAllowedTouchBehaviors[i] & AllowedTouchBehavior::PINCH_ZOOM)) { + return false; + } + } + return true; +} + +bool AsyncPanZoomController::TouchActionAllowDoubleTapZoom() { + if (!mTouchActionPropertyEnabled) { + return true; + } + for (size_t i = 0; i < mTouchBlockState.mAllowedTouchBehaviors.Length(); i++) { + if (!(mTouchBlockState.mAllowedTouchBehaviors[i] & AllowedTouchBehavior::DOUBLE_TAP_ZOOM)) { + return false; + } + } + return true; +} + +AsyncPanZoomController::TouchBehaviorFlags +AsyncPanZoomController::GetTouchBehavior(uint32_t touchIndex) { + if (touchIndex < mTouchBlockState.mAllowedTouchBehaviors.Length()) { + return mTouchBlockState.mAllowedTouchBehaviors[touchIndex]; + } + return DefaultTouchBehavior; +} + +AsyncPanZoomController::TouchBehaviorFlags +AsyncPanZoomController::GetAllowedTouchBehavior(ScreenIntPoint& aPoint) { + // Here we need to perform a hit testing over the touch-action regions attached to the + // layer associated with current apzc. + // Currently they are in progress, for more info see bug 928833. + return AllowedTouchBehavior::UNKNOWN; +} + +void AsyncPanZoomController::SetAllowedTouchBehavior(const nsTArray& aBehaviors) { + mTouchBlockState.mAllowedTouchBehaviors.Clear(); + mTouchBlockState.mAllowedTouchBehaviors.AppendElements(aBehaviors); + mTouchBlockState.mAllowedTouchBehaviorSet = true; + CheckContentResponse(); +} + +void AsyncPanZoomController::SetState(PanZoomState aNewState) { + + PanZoomState oldState; + + // Intentional scoping for mutex + { + ReentrantMonitorAutoEnter lock(mMonitor); + oldState = mState; + mState = aNewState; + } + + if (nsRefPtr controller = GetGeckoContentController()) { + if (!IsTransformingState(oldState) && IsTransformingState(aNewState)) { + controller->NotifyAPZStateChange( + GetGuid(), APZStateChange::TransformBegin); + } else if (IsTransformingState(oldState) && !IsTransformingState(aNewState)) { + controller->NotifyAPZStateChange( + GetGuid(), APZStateChange::TransformEnd); + } + } +} + +bool AsyncPanZoomController::IsTransformingState(PanZoomState aState) { + return !(aState == NOTHING || aState == TOUCHING || aState == WAITING_CONTENT_RESPONSE); +} + +bool AsyncPanZoomController::IsPanningState(PanZoomState aState) { + return (aState == PANNING || aState == PANNING_LOCKED_X || aState == PANNING_LOCKED_Y); +} + +void AsyncPanZoomController::SetContentResponseTimer() { + if (!mContentResponseTimeoutTask) { + mContentResponseTimeoutTask = + NewRunnableMethod(this, &AsyncPanZoomController::TimeoutContentResponse); + + PostDelayedTask(mContentResponseTimeoutTask, gfxPrefs::APZContentResponseTimeout()); + } +} + +void AsyncPanZoomController::TimeoutContentResponse() { + mContentResponseTimeoutTask = nullptr; + ContentReceivedTouch(false); +} + +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...\n"); + return; + } + // 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_ZOOM > aConstraints.mMinZoom ? MIN_ZOOM : aConstraints.mMinZoom); + mZoomConstraints.mMaxZoom = (MAX_ZOOM > aConstraints.mMaxZoom ? aConstraints.mMaxZoom : MAX_ZOOM); + if (mZoomConstraints.mMaxZoom < mZoomConstraints.mMinZoom) { + mZoomConstraints.mMaxZoom = mZoomConstraints.mMinZoom; + } +} + +ZoomConstraints +AsyncPanZoomController::GetZoomConstraints() const +{ + return mZoomConstraints; +} + + +void AsyncPanZoomController::PostDelayedTask(Task* aTask, int aDelayMs) { + nsRefPtr controller = GetGeckoContentController(); + if (controller) { + controller->PostDelayedTask(aTask, aDelayMs); + } +} + +void AsyncPanZoomController::SendAsyncScrollEvent() { + nsRefPtr controller = GetGeckoContentController(); + if (!controller) { + return; + } + + bool isRoot; + CSSRect contentRect; + CSSSize scrollableSize; + { + ReentrantMonitorAutoEnter lock(mMonitor); + + isRoot = mFrameMetrics.mIsRoot; + scrollableSize = mFrameMetrics.mScrollableRect.Size(); + contentRect = mFrameMetrics.CalculateCompositedRectInCssPixels(); + contentRect.MoveTo(mCurrentAsyncScrollOffset); + } + + controller->SendAsyncScrollDOMEvent(isRoot, contentRect, scrollableSize); +} + +bool AsyncPanZoomController::Matches(const ScrollableLayerGuid& aGuid) +{ + return aGuid == GetGuid(); +} + +void AsyncPanZoomController::GetGuid(ScrollableLayerGuid* aGuidOut) +{ + if (aGuidOut) { + *aGuidOut = GetGuid(); + } +} + +ScrollableLayerGuid AsyncPanZoomController::GetGuid() +{ + return ScrollableLayerGuid(mLayersId, mFrameMetrics); +} + +void AsyncPanZoomController::UpdateSharedCompositorFrameMetrics() +{ + mMonitor.AssertCurrentThreadIn(); + + FrameMetrics* frame = mSharedFrameMetricsBuffer ? + static_cast(mSharedFrameMetricsBuffer->memory()) : nullptr; + + if (frame && mSharedLock && gfxPrefs::UseProgressiveTilePainting()) { + mSharedLock->Lock(); + *frame = mFrameMetrics; + mSharedLock->Unlock(); + } +} + +void AsyncPanZoomController::ShareCompositorFrameMetrics() { + + PCompositorParent* compositor = + (mCrossProcessCompositorParent ? mCrossProcessCompositorParent : mCompositorParent.get()); + + // Only create the shared memory buffer if it hasn't already been created, + // we are using progressive tile painting, and we have a + // compositor to pass the shared memory back to the content process/thread. + if (!mSharedFrameMetricsBuffer && compositor && gfxPrefs::UseProgressiveTilePainting()) { + + // 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(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::ProcessHandle processHandle = compositor->OtherProcess(); + ipc::SharedMemoryBasic::Handle mem = ipc::SharedMemoryBasic::NULLHandle(); + + // Get the shared memory handle to share with the content process + mSharedFrameMetricsBuffer->ShareToProcess(processHandle, &mem); + + // Get the cross process mutex handle to share with the content process + mSharedLock = new CrossProcessMutex("AsyncPanZoomControlLock"); + CrossProcessMutexHandle handle = mSharedLock->ShareToProcess(processHandle); + + // 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 (!compositor->SendSharedCompositorFrameMetrics(mem, handle, mAPZCId)) { + APZC_LOG("%p failed to share FrameMetrics with content process.", this); + } + } + } +} + +ParentLayerPoint AsyncPanZoomController::ToParentLayerCoords(const ScreenPoint& aPoint) +{ + return TransformTo(GetNontransientAsyncTransform() * GetCSSTransform(), aPoint); +} + +void AsyncPanZoomController::UpdateTransformScale() +{ + gfx3DMatrix nontransientTransforms = GetNontransientAsyncTransform() * GetCSSTransform(); + if (!FuzzyEqualsMultiplicative(nontransientTransforms.GetXScale(), nontransientTransforms.GetYScale())) { + NS_WARNING("The x- and y-scales of the nontransient transforms should be equal"); + } + mFrameMetrics.mTransformScale.scale = nontransientTransforms.GetXScale(); +} + +} +}