/* -*- 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 "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/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& 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& 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& aTargetApzc) { // note that aTargetApzc MAY be null here. mTargetApzc = aTargetApzc; mTransformToApzc = aTargetApzc ? aTargetApzc->GetTransformToThis() : ScreenToParentLayerMatrix4x4(); mOverscrollHandoffChain = (mTargetApzc ? mTargetApzc->BuildOverscrollHandoffChain() : nullptr); } const RefPtr& InputBlockState::GetTargetApzc() const { return mTargetApzc; } const RefPtr& 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& 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; } mContentResponseTimer = TimeStamp(); } DragBlockState::DragBlockState(const RefPtr& 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& 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 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& 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 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 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 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 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& 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& 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 apzc = mOverscrollHandoffChain->FindFirstScrollable(aInitialEvent); if (apzc && apzc != GetTargetApzc()) { UpdateTargetApzc(apzc); } } } bool PanGestureBlockState::SetConfirmedTargetApzc(const RefPtr& 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 apzc = aTargetApzc; if (apzc && aFirstInput) { RefPtr 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& 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& 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& 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