diff options
Diffstat (limited to '')
-rw-r--r-- | gfx/layers/apz/src/InputQueue.cpp | 1211 |
1 files changed, 1211 insertions, 0 deletions
diff --git a/gfx/layers/apz/src/InputQueue.cpp b/gfx/layers/apz/src/InputQueue.cpp new file mode 100644 index 0000000000..8ad75a9794 --- /dev/null +++ b/gfx/layers/apz/src/InputQueue.cpp @@ -0,0 +1,1211 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "InputQueue.h" + +#include <inttypes.h> + +#include "AsyncPanZoomController.h" + +#include "GestureEventListener.h" +#include "InputBlockState.h" +#include "mozilla/Assertions.h" +#include "mozilla/EventForwards.h" +#include "mozilla/layers/APZInputBridge.h" +#include "mozilla/layers/APZThreadUtils.h" +#include "mozilla/RefPtr.h" +#include "mozilla/ToString.h" +#include "OverscrollHandoffState.h" +#include "QueuedInput.h" +#include "mozilla/StaticPrefs_apz.h" +#include "mozilla/StaticPrefs_layout.h" +#include "mozilla/StaticPrefs_ui.h" + +static mozilla::LazyLogModule sApzInpLog("apz.inputqueue"); +#define INPQ_LOG(...) MOZ_LOG(sApzInpLog, LogLevel::Debug, (__VA_ARGS__)) + +namespace mozilla { +namespace layers { + +InputQueue::InputQueue() = default; + +InputQueue::~InputQueue() { mQueuedInputs.Clear(); } + +APZEventResult InputQueue::ReceiveInputEvent( + const RefPtr<AsyncPanZoomController>& aTarget, + TargetConfirmationFlags aFlags, InputData& aEvent, + const Maybe<nsTArray<TouchBehaviorFlags>>& aTouchBehaviors) { + APZThreadUtils::AssertOnControllerThread(); + + AutoRunImmediateTimeout timeoutRunner{this}; + + switch (aEvent.mInputType) { + case MULTITOUCH_INPUT: { + const MultiTouchInput& event = aEvent.AsMultiTouchInput(); + return ReceiveTouchInput(aTarget, aFlags, event, aTouchBehaviors); + } + + case SCROLLWHEEL_INPUT: { + const ScrollWheelInput& event = aEvent.AsScrollWheelInput(); + return ReceiveScrollWheelInput(aTarget, aFlags, event); + } + + case PANGESTURE_INPUT: { + const PanGestureInput& event = aEvent.AsPanGestureInput(); + return ReceivePanGestureInput(aTarget, aFlags, event); + } + + case PINCHGESTURE_INPUT: { + const PinchGestureInput& event = aEvent.AsPinchGestureInput(); + return ReceivePinchGestureInput(aTarget, aFlags, event); + } + + case MOUSE_INPUT: { + MouseInput& event = aEvent.AsMouseInput(); + return ReceiveMouseInput(aTarget, aFlags, event); + } + + case KEYBOARD_INPUT: { + // Every keyboard input must have a confirmed target + MOZ_ASSERT(aTarget && aFlags.mTargetConfirmed); + + const KeyboardInput& event = aEvent.AsKeyboardInput(); + return ReceiveKeyboardInput(aTarget, aFlags, event); + } + + default: { + // The `mStatus` for other input type is only used by tests, so just + // pass through the return value of HandleInputEvent() for now. + APZEventResult result(aTarget, aFlags); + nsEventStatus status = + aTarget->HandleInputEvent(aEvent, aTarget->GetTransformToThis()); + switch (status) { + case nsEventStatus_eIgnore: + result.SetStatusAsIgnore(); + break; + case nsEventStatus_eConsumeNoDefault: + result.SetStatusAsConsumeNoDefault(); + break; + case nsEventStatus_eConsumeDoDefault: + result.SetStatusAsConsumeDoDefault(aTarget); + break; + default: + MOZ_ASSERT_UNREACHABLE("An invalid status"); + break; + } + return result; + } + } +} + +APZEventResult InputQueue::ReceiveTouchInput( + const RefPtr<AsyncPanZoomController>& aTarget, + TargetConfirmationFlags aFlags, const MultiTouchInput& aEvent, + const Maybe<nsTArray<TouchBehaviorFlags>>& aTouchBehaviors) { + APZEventResult result(aTarget, aFlags); + + RefPtr<TouchBlockState> block; + bool waitingForContentResponse = false; + if (aEvent.mType == MultiTouchInput::MULTITOUCH_START) { + nsTArray<TouchBehaviorFlags> currentBehaviors; + bool haveBehaviors = false; + if (mActiveTouchBlock) { + haveBehaviors = + mActiveTouchBlock->GetAllowedTouchBehaviors(currentBehaviors); + // If the behaviours aren't set, but the main-thread response timer on + // the block is expired we still treat it as though it has behaviors, + // because in that case we still want to interrupt the fast-fling and + // use the default behaviours. + haveBehaviors |= mActiveTouchBlock->IsContentResponseTimerExpired(); + } + + block = StartNewTouchBlock(aTarget, aFlags); + INPQ_LOG("started new touch block %p id %" PRIu64 " for target %p\n", + block.get(), block->GetBlockId(), aTarget.get()); + + // XXX using the chain from |block| here may be wrong in cases where the + // target isn't confirmed and the real target turns out to be something + // else. For now assume this is rare enough that it's not an issue. + if (mQueuedInputs.IsEmpty() && aEvent.mTouches.Length() == 1 && + block->GetOverscrollHandoffChain()->HasFastFlungApzc() && + haveBehaviors) { + // If we're already in a fast fling, and a single finger goes down, then + // we want special handling for the touch event, because it shouldn't get + // delivered to content. Note that we don't set this flag when going + // from a fast fling to a pinch state (i.e. second finger goes down while + // the first finger is moving). + block->SetDuringFastFling(); + block->SetConfirmedTargetApzc( + aTarget, InputBlockState::TargetConfirmationState::eConfirmed, + nullptr /* the block was just created so it has no events */, + false /* not a scrollbar drag */); + block->SetAllowedTouchBehaviors(currentBehaviors); + INPQ_LOG("block %p tagged as fast-motion\n", block.get()); + } else if (aTouchBehaviors) { + // If this block isn't started during a fast-fling, and APZCTM has + // provided touch behavior information, then put it on the block so + // that the ArePointerEventsConsumable call below can use it. + block->SetAllowedTouchBehaviors(*aTouchBehaviors); + } + + CancelAnimationsForNewBlock(block); + + waitingForContentResponse = MaybeRequestContentResponse(aTarget, block); + } else { + // for touch inputs that don't start a block, APZCTM shouldn't be giving + // us any touch behaviors. + MOZ_ASSERT(aTouchBehaviors.isNothing()); + + // If the active touch block is for a long tap, add new touch events into + // the original touch block, to ensure that they're only processed if the + // original touch block is not prevented. + block = mActiveTouchBlock && mActiveTouchBlock->ForLongTap() + ? mPrevActiveTouchBlock.get() + : mActiveTouchBlock.get(); + if (!block) { + NS_WARNING( + "Received a non-start touch event while no touch blocks active!"); + return result; + } + + INPQ_LOG("received new touch event (type=%d) in block %p\n", aEvent.mType, + block.get()); + } + + result.mInputBlockId = block->GetBlockId(); + + // Note that the |aTarget| the APZCTM sent us may contradict the confirmed + // target set on the block. In this case the confirmed target (which may be + // null) should take priority. This is equivalent to just always using the + // target (confirmed or not) from the block. + RefPtr<AsyncPanZoomController> target = block->GetTargetApzc(); + + // XXX calling ArePointerEventsConsumable on |target| may be wrong here if + // the target isn't confirmed and the real target turns out to be something + // else. For now assume this is rare enough that it's not an issue. + PointerEventsConsumableFlags consumableFlags; + if (target) { + consumableFlags = target->ArePointerEventsConsumable(block, aEvent); + } + if (block->IsDuringFastFling()) { + INPQ_LOG("dropping event due to block %p being in fast motion\n", + block.get()); + result.SetStatusForFastFling(*block, aFlags, consumableFlags, target); + } else { // handling depends on ArePointerEventsConsumable() + bool consumable = consumableFlags.IsConsumable(); + const bool wasInSlop = block->IsInSlop(); + if (block->UpdateSlopState(aEvent, consumable)) { + INPQ_LOG("dropping event due to block %p being in %sslop\n", block.get(), + consumable ? "" : "mini-"); + result.SetStatusAsConsumeNoDefault(); + } else { + // If all following conditions are met, we need to wait for a content + // response (again); + // 1) this is the first event bailing out from in-slop state after a + // long-tap event has been fired + // 2) there's any APZ-aware event listeners + // 3) the event block hasn't yet been prevented + // + // An example scenario; + // in the content there are two event listeners for `touchstart` and + // `touchmove` respectively, and doing `preventDefault()` in the + // `touchmove` event listener. Then if the user kept touching at a point + // until a long-tap event happens, then if the user started moving their + // finger, we have to wait for a content response twice, one is for + // `touchstart` and one is for `touchmove`. + if (wasInSlop && + (block->WasLongTapProcessed() || block->IsWaitingLongTapResult()) && + !block->IsTargetOriginallyConfirmed() && !block->ShouldDropEvents()) { + INPQ_LOG( + "bailing out from in-stop state in block %p after a long-tap " + "happened\n", + block.get()); + block->ResetContentResponseTimerExpired(); + ScheduleMainThreadTimeout(aTarget, block); + } + block->SetNeedsToWaitTouchMove(false); + result.SetStatusForTouchEvent(*block, aFlags, consumableFlags, target); + } + } + mQueuedInputs.AppendElement(MakeUnique<QueuedInput>(aEvent, *block)); + ProcessQueue(); + + // If this block just started and is waiting for a content response, but + // also in a slop state (i.e. touchstart gets delivered to content but + // not any touchmoves), then we might end up in a situation where we don't + // get the content response until the timeout is hit because we never exit + // the slop state. But if that timeout is longer than the long-press timeout, + // then the long-press gets delayed too. Avoid that by scheduling a callback + // with the long-press timeout that will force the block to get processed. + int32_t longTapTimeout = StaticPrefs::ui_click_hold_context_menus_delay(); + int32_t contentTimeout = StaticPrefs::apz_content_response_timeout(); + if (waitingForContentResponse && longTapTimeout < contentTimeout && + block->IsInSlop() && GestureEventListener::IsLongTapEnabled()) { + MOZ_ASSERT(aEvent.mType == MultiTouchInput::MULTITOUCH_START); + MOZ_ASSERT(!block->IsDuringFastFling()); + RefPtr<Runnable> maybeLongTap = NewRunnableMethod<uint64_t>( + "layers::InputQueue::MaybeLongTapTimeout", this, + &InputQueue::MaybeLongTapTimeout, block->GetBlockId()); + INPQ_LOG("scheduling maybe-long-tap timeout for target %p\n", + aTarget.get()); + aTarget->PostDelayedTask(maybeLongTap.forget(), longTapTimeout); + } + + return result; +} + +APZEventResult InputQueue::ReceiveMouseInput( + const RefPtr<AsyncPanZoomController>& aTarget, + TargetConfirmationFlags aFlags, MouseInput& aEvent) { + APZEventResult result(aTarget, aFlags); + + // On a new mouse down we can have a new target so we must force a new block + // with a new target. + bool newBlock = DragTracker::StartsDrag(aEvent); + + RefPtr<DragBlockState> block = newBlock ? nullptr : mActiveDragBlock.get(); + if (block && block->HasReceivedMouseUp()) { + block = nullptr; + } + + if (!block && mDragTracker.InDrag()) { + // If there's no current drag block, but we're getting a move with a button + // down, we need to start a new drag block because we're obviously already + // in the middle of a drag (it probably got interrupted by something else). + INPQ_LOG( + "got a drag event outside a drag block, need to create a block to hold " + "it\n"); + newBlock = true; + } + + mDragTracker.Update(aEvent); + + if (!newBlock && !block) { + // This input event is not in a drag block, so we're not doing anything + // with it, return eIgnore. + return result; + } + + if (!block) { + MOZ_ASSERT(newBlock); + block = new DragBlockState(aTarget, aFlags, aEvent); + + INPQ_LOG( + "started new drag block %p id %" PRIu64 + "for %sconfirmed target %p; on scrollbar: %d; on scrollthumb: %d\n", + block.get(), block->GetBlockId(), aFlags.mTargetConfirmed ? "" : "un", + aTarget.get(), aFlags.mHitScrollbar, aFlags.mHitScrollThumb); + + mActiveDragBlock = block; + + if (aFlags.mHitScrollThumb || !aFlags.mHitScrollbar) { + // If we're running autoscroll, we'll always cancel it during the + // following call of CancelAnimationsForNewBlock. At this time, + // we don't want to fire `click` event on the web content for web-compat + // with Chrome. Therefore, we notify widget of it with the flag. + if ((aEvent.mType == MouseInput::MOUSE_DOWN || + aEvent.mType == MouseInput::MOUSE_UP) && + block->GetOverscrollHandoffChain()->HasAutoscrollApzc()) { + aEvent.mPreventClickEvent = true; + } + CancelAnimationsForNewBlock(block); + } + MaybeRequestContentResponse(aTarget, block); + } + + result.mInputBlockId = block->GetBlockId(); + + mQueuedInputs.AppendElement(MakeUnique<QueuedInput>(aEvent, *block)); + ProcessQueue(); + + if (DragTracker::EndsDrag(aEvent)) { + block->MarkMouseUpReceived(); + } + + // The event is part of a drag block and could potentially cause + // scrolling, so return DoDefault. + result.SetStatusAsConsumeDoDefault(*block); + return result; +} + +APZEventResult InputQueue::ReceiveScrollWheelInput( + const RefPtr<AsyncPanZoomController>& aTarget, + TargetConfirmationFlags aFlags, const ScrollWheelInput& aEvent) { + APZEventResult result(aTarget, aFlags); + + RefPtr<WheelBlockState> block = mActiveWheelBlock.get(); + // If the block is not accepting new events we'll create a new input block + // (and therefore a new wheel transaction). + if (block && + (!block->ShouldAcceptNewEvent() || block->MaybeTimeout(aEvent))) { + block = nullptr; + } + + MOZ_ASSERT(!block || block->InTransaction()); + + if (!block) { + block = new WheelBlockState(aTarget, aFlags, aEvent); + INPQ_LOG("started new scroll wheel block %p id %" PRIu64 + " for %starget %p\n", + block.get(), block->GetBlockId(), + aFlags.mTargetConfirmed ? "confirmed " : "", aTarget.get()); + + mActiveWheelBlock = block; + + CancelAnimationsForNewBlock(block, ExcludeWheel); + MaybeRequestContentResponse(aTarget, block); + } else { + INPQ_LOG("received new wheel event in block %p\n", block.get()); + } + + result.mInputBlockId = block->GetBlockId(); + + // Note that the |aTarget| the APZCTM sent us may contradict the confirmed + // target set on the block. In this case the confirmed target (which may be + // null) should take priority. This is equivalent to just always using the + // target (confirmed or not) from the block, which is what + // ProcessQueue() does. + mQueuedInputs.AppendElement(MakeUnique<QueuedInput>(aEvent, *block)); + + // The WheelBlockState needs to affix a counter to the event before we process + // it. Note that the counter is affixed to the copy in the queue rather than + // |aEvent|. + block->Update(mQueuedInputs.LastElement()->Input()->AsScrollWheelInput()); + + ProcessQueue(); + + result.SetStatusAsConsumeDoDefault(*block); + return result; +} + +APZEventResult InputQueue::ReceiveKeyboardInput( + const RefPtr<AsyncPanZoomController>& aTarget, + TargetConfirmationFlags aFlags, const KeyboardInput& aEvent) { + APZEventResult result(aTarget, aFlags); + + RefPtr<KeyboardBlockState> block = mActiveKeyboardBlock.get(); + + // If the block is targeting a different Apzc than this keyboard event then + // we'll create a new input block + if (block && block->GetTargetApzc() != aTarget) { + block = nullptr; + } + + if (!block) { + block = new KeyboardBlockState(aTarget); + INPQ_LOG("started new keyboard block %p id %" PRIu64 " for target %p\n", + block.get(), block->GetBlockId(), aTarget.get()); + + mActiveKeyboardBlock = block; + } else { + INPQ_LOG("received new keyboard event in block %p\n", block.get()); + } + + result.mInputBlockId = block->GetBlockId(); + + mQueuedInputs.AppendElement(MakeUnique<QueuedInput>(aEvent, *block)); + + ProcessQueue(); + + // If APZ is allowing passive listeners then we must dispatch the event to + // content, otherwise we can consume the event. + if (StaticPrefs::apz_keyboard_passive_listeners()) { + result.SetStatusAsConsumeDoDefault(*block); + } else { + result.SetStatusAsConsumeNoDefault(); + } + return result; +} + +static bool CanScrollTargetHorizontally(const PanGestureInput& aInitialEvent, + PanGestureBlockState* aBlock) { + PanGestureInput horizontalComponent = aInitialEvent; + horizontalComponent.mPanDisplacement.y = 0; + ScrollDirections allowedScrollDirections; + RefPtr<AsyncPanZoomController> horizontallyScrollableAPZC = + aBlock->GetOverscrollHandoffChain()->FindFirstScrollable( + horizontalComponent, &allowedScrollDirections, + OverscrollHandoffChain::IncludeOverscroll::No); + return horizontallyScrollableAPZC && + horizontallyScrollableAPZC == aBlock->GetTargetApzc() && + allowedScrollDirections.contains(ScrollDirection::eHorizontal); +} + +APZEventResult InputQueue::ReceivePanGestureInput( + const RefPtr<AsyncPanZoomController>& aTarget, + TargetConfirmationFlags aFlags, const PanGestureInput& aEvent) { + APZEventResult result(aTarget, aFlags); + + if (aEvent.mType == PanGestureInput::PANGESTURE_MAYSTART || + aEvent.mType == PanGestureInput::PANGESTURE_CANCELLED) { + // Ignore these events for now. + result.SetStatusAsConsumeDoDefault(aTarget); + return result; + } + + if (aEvent.mType == PanGestureInput::PANGESTURE_INTERRUPTED) { + if (RefPtr<PanGestureBlockState> block = mActivePanGestureBlock.get()) { + mQueuedInputs.AppendElement(MakeUnique<QueuedInput>(aEvent, *block)); + ProcessQueue(); + } + result.SetStatusAsIgnore(); + return result; + } + + RefPtr<PanGestureBlockState> block; + if (aEvent.mType != PanGestureInput::PANGESTURE_START) { + block = mActivePanGestureBlock.get(); + } + + PanGestureInput event = aEvent; + + // Below `SetStatusAsConsumeDoDefault()` preserves `mHandledResult` of + // `result` which was set in the ctor of APZEventResult at the top of this + // function based on `aFlag` so that the `mHandledResult` value is reliable to + // tell whether the event will be handled by the root content APZC at least + // for swipe-navigation stuff. E.g. if a pan-start event scrolled the root + // scroll container, we don't need to anything for swipe-navigation. + result.SetStatusAsConsumeDoDefault(); + + if (!block || block->WasInterrupted()) { + if (event.mType == PanGestureInput::PANGESTURE_MOMENTUMSTART || + event.mType == PanGestureInput::PANGESTURE_MOMENTUMPAN || + event.mType == PanGestureInput::PANGESTURE_MOMENTUMEND) { + // If there are momentum events after an interruption, discard them. + // However, if there is a non-momentum event (indicating the user + // continued scrolling on the touchpad), a new input block is started + // by turning the event into a pan-start below. + return result; + } + if (event.mType != PanGestureInput::PANGESTURE_START) { + // Only PANGESTURE_START events are allowed to start a new pan gesture + // block, but we really want to start a new block here, so we magically + // turn this input into a PANGESTURE_START. + INPQ_LOG( + "transmogrifying pan input %d to PANGESTURE_START for new block\n", + event.mType); + event.mType = PanGestureInput::PANGESTURE_START; + } + block = new PanGestureBlockState(aTarget, aFlags, event); + INPQ_LOG("started new pan gesture block %p id %" PRIu64 " for target %p\n", + block.get(), block->GetBlockId(), aTarget.get()); + + mActivePanGestureBlock = block; + + CancelAnimationsForNewBlock(block); + const bool waitingForContentResponse = + MaybeRequestContentResponse(aTarget, block); + + if (event.AllowsSwipe() && !CanScrollTargetHorizontally(event, block)) { + // We will ask the browser whether this pan event is going to be used for + // swipe or not, so we need to wait the response. + block->SetNeedsToWaitForBrowserGestureResponse(true); + if (!waitingForContentResponse) { + ScheduleMainThreadTimeout(aTarget, block); + } + if (aFlags.mTargetConfirmed) { + // This event may trigger a swipe gesture, depending on what our caller + // wants to do it. We need to suspend handling of this block until we + // get a content response which will tell us whether to proceed or abort + // the block. + block->SetNeedsToWaitForContentResponse(true); + + // Inform our caller that we haven't scrolled in response to the event + // and that a swipe can be started from this event if desired. + result.SetStatusAsIgnore(); + } + } + } else { + INPQ_LOG("received new pan event (type=%d) in block %p\n", aEvent.mType, + block.get()); + } + + result.mInputBlockId = block->GetBlockId(); + + // Note that the |aTarget| the APZCTM sent us may contradict the confirmed + // target set on the block. In this case the confirmed target (which may be + // null) should take priority. This is equivalent to just always using the + // target (confirmed or not) from the block, which is what + // ProcessQueue() does. + mQueuedInputs.AppendElement(MakeUnique<QueuedInput>(event, *block)); + ProcessQueue(); + + return result; +} + +APZEventResult InputQueue::ReceivePinchGestureInput( + const RefPtr<AsyncPanZoomController>& aTarget, + TargetConfirmationFlags aFlags, const PinchGestureInput& aEvent) { + APZEventResult result(aTarget, aFlags); + + RefPtr<PinchGestureBlockState> block; + if (aEvent.mType != PinchGestureInput::PINCHGESTURE_START) { + block = mActivePinchGestureBlock.get(); + } + + result.SetStatusAsConsumeDoDefault(aTarget); + + if (!block || block->WasInterrupted()) { + if (aEvent.mType != PinchGestureInput::PINCHGESTURE_START) { + // Only PINCHGESTURE_START events are allowed to start a new pinch gesture + // block. + INPQ_LOG("pinchgesture block %p was interrupted %d\n", block.get(), + block ? block->WasInterrupted() : 0); + return result; + } + block = new PinchGestureBlockState(aTarget, aFlags); + INPQ_LOG("started new pinch gesture block %p id %" PRIu64 + " for target %p\n", + block.get(), block->GetBlockId(), aTarget.get()); + + mActivePinchGestureBlock = block; + block->SetNeedsToWaitForContentResponse(true); + + CancelAnimationsForNewBlock(block); + MaybeRequestContentResponse(aTarget, block); + } else { + INPQ_LOG("received new pinch event (type=%d) in block %p\n", aEvent.mType, + block.get()); + } + + result.mInputBlockId = block->GetBlockId(); + + // Note that the |aTarget| the APZCTM sent us may contradict the confirmed + // target set on the block. In this case the confirmed target (which may be + // null) should take priority. This is equivalent to just always using the + // target (confirmed or not) from the block, which is what + // ProcessQueue() does. + mQueuedInputs.AppendElement(MakeUnique<QueuedInput>(aEvent, *block)); + ProcessQueue(); + + return result; +} + +void InputQueue::CancelAnimationsForNewBlock(InputBlockState* aBlock, + CancelAnimationFlags aExtraFlags) { + // We want to cancel animations here as soon as possible (i.e. without waiting + // for content responses) because a finger has gone down and we don't want to + // keep moving the content under the finger. However, to prevent "future" + // touchstart events from interfering with "past" animations (i.e. from a + // previous touch block that is still being processed) we only do this + // animation-cancellation if there are no older touch blocks still in the + // queue. + if (mQueuedInputs.IsEmpty()) { + aBlock->GetOverscrollHandoffChain()->CancelAnimations( + aExtraFlags | ExcludeOverscroll | ScrollSnap); + } +} + +bool InputQueue::MaybeRequestContentResponse( + const RefPtr<AsyncPanZoomController>& aTarget, + CancelableBlockState* aBlock) { + bool waitForMainThread = false; + if (aBlock->IsTargetConfirmed()) { + // Content won't prevent-default this, so we can just set the flag directly. + INPQ_LOG("not waiting for content response on block %p\n", aBlock); + aBlock->SetContentResponse(false); + } else { + waitForMainThread = true; + } + if (aBlock->AsTouchBlock() && + !aBlock->AsTouchBlock()->HasAllowedTouchBehaviors()) { + INPQ_LOG("waiting for main thread touch-action info on block %p\n", aBlock); + waitForMainThread = true; + } + if (waitForMainThread) { + // We either don't know for sure if aTarget is the right APZC, or we may + // need to wait to give content the opportunity to prevent-default the + // touch events. Either way we schedule a timeout so the main thread stuff + // can run. + ScheduleMainThreadTimeout(aTarget, aBlock); + } + return waitForMainThread; +} + +uint64_t InputQueue::InjectNewTouchBlock(AsyncPanZoomController* aTarget) { + AutoRunImmediateTimeout timeoutRunner{this}; + TouchBlockState* block = StartNewTouchBlockForLongTap(aTarget); + INPQ_LOG("injecting new touch block %p with id %" PRIu64 " and target %p\n", + block, block->GetBlockId(), aTarget); + ScheduleMainThreadTimeout(aTarget, block); + return block->GetBlockId(); +} + +TouchBlockState* InputQueue::StartNewTouchBlock( + const RefPtr<AsyncPanZoomController>& aTarget, + TargetConfirmationFlags aFlags) { + TouchBlockState* newBlock = + new TouchBlockState(aTarget, aFlags, mTouchCounter); + + mActiveTouchBlock = newBlock; + return newBlock; +} + +TouchBlockState* InputQueue::StartNewTouchBlockForLongTap( + const RefPtr<AsyncPanZoomController>& aTarget) { + TouchBlockState* newBlock = new TouchBlockState( + aTarget, TargetConfirmationFlags{true}, mTouchCounter); + + TouchBlockState* currentBlock = GetCurrentTouchBlock(); + // We should never enter here without a current touch block, because this + // codepath is invoked from the OnLongPress handler in + // AsyncPanZoomController, which should bail out if there is no current + // touch block. + MOZ_ASSERT(currentBlock); + newBlock->CopyPropertiesFrom(*currentBlock); + newBlock->SetForLongTap(); + + // Tell the original touch block that we are going to fire a long tap event. + // NOTE: If we get a new touch-move event while we are waiting for a response + // of the long-tap event, we need to wait it before processing the original + // touch block because if the long-tap event response prevents us from + // scrolling we must stop processing any subsequent touch-move events in the + // same block. + currentBlock->SetWaitingLongTapResult(); + + // We need to keep the current block alive, it will be used once after this + // new touch block for long-tap was processed. + mPrevActiveTouchBlock = currentBlock; + mActiveTouchBlock = newBlock; + return newBlock; +} + +InputBlockState* InputQueue::GetCurrentBlock() const { + APZThreadUtils::AssertOnControllerThread(); + return mQueuedInputs.IsEmpty() ? nullptr : mQueuedInputs[0]->Block(); +} + +TouchBlockState* InputQueue::GetCurrentTouchBlock() const { + InputBlockState* block = GetCurrentBlock(); + return block ? block->AsTouchBlock() : mActiveTouchBlock.get(); +} + +WheelBlockState* InputQueue::GetCurrentWheelBlock() const { + InputBlockState* block = GetCurrentBlock(); + return block ? block->AsWheelBlock() : mActiveWheelBlock.get(); +} + +DragBlockState* InputQueue::GetCurrentDragBlock() const { + InputBlockState* block = GetCurrentBlock(); + return block ? block->AsDragBlock() : mActiveDragBlock.get(); +} + +PanGestureBlockState* InputQueue::GetCurrentPanGestureBlock() const { + InputBlockState* block = GetCurrentBlock(); + return block ? block->AsPanGestureBlock() : mActivePanGestureBlock.get(); +} + +PinchGestureBlockState* InputQueue::GetCurrentPinchGestureBlock() const { + InputBlockState* block = GetCurrentBlock(); + return block ? block->AsPinchGestureBlock() : mActivePinchGestureBlock.get(); +} + +KeyboardBlockState* InputQueue::GetCurrentKeyboardBlock() const { + InputBlockState* block = GetCurrentBlock(); + return block ? block->AsKeyboardBlock() : mActiveKeyboardBlock.get(); +} + +WheelBlockState* InputQueue::GetActiveWheelTransaction() const { + WheelBlockState* block = mActiveWheelBlock.get(); + if (!block || !block->InTransaction()) { + return nullptr; + } + return block; +} + +bool InputQueue::HasReadyTouchBlock() const { + return !mQueuedInputs.IsEmpty() && + mQueuedInputs[0]->Block()->AsTouchBlock() && + mQueuedInputs[0]->Block()->AsTouchBlock()->IsReadyForHandling(); +} + +bool InputQueue::AllowScrollHandoff() const { + if (GetCurrentWheelBlock()) { + return GetCurrentWheelBlock()->AllowScrollHandoff(); + } + if (GetCurrentPanGestureBlock()) { + return GetCurrentPanGestureBlock()->AllowScrollHandoff(); + } + if (GetCurrentKeyboardBlock()) { + return GetCurrentKeyboardBlock()->AllowScrollHandoff(); + } + return true; +} + +bool InputQueue::IsDragOnScrollbar(bool aHitScrollbar) { + if (!mDragTracker.InDrag()) { + return false; + } + // Now that we know we are in a drag, get the info from the drag tracker. + // We keep it in the tracker rather than the block because the block can get + // interrupted by something else (like a wheel event) and then a new block + // will get created without the info we want. The tracker will persist though. + return mDragTracker.IsOnScrollbar(aHitScrollbar); +} + +void InputQueue::ScheduleMainThreadTimeout( + const RefPtr<AsyncPanZoomController>& aTarget, + CancelableBlockState* aBlock) { + INPQ_LOG("scheduling main thread timeout for target %p\n", aTarget.get()); + RefPtr<Runnable> timeoutTask = NewRunnableMethod<uint64_t>( + "layers::InputQueue::MainThreadTimeout", this, + &InputQueue::MainThreadTimeout, aBlock->GetBlockId()); + int32_t timeout = StaticPrefs::apz_content_response_timeout(); + if (timeout == 0) { + // If the timeout is zero, treat it as a request to ignore any main + // thread confirmation and unconditionally use fallback behaviour for + // when a timeout is reached. This codepath is used by tests that + // want to exercise the fallback behaviour. + // To ensure the fallback behaviour is used unconditionally, the timeout + // is run right away instead of using PostDelayedTask(). However, + // we can't run it right here, because MainThreadTimeout() expects that + // the input block has at least one input event in mQueuedInputs, and + // the event that triggered this call may not have been added to + // mQueuedInputs yet. + mImmediateTimeout = std::move(timeoutTask); + } else { + aTarget->PostDelayedTask(timeoutTask.forget(), timeout); + } +} + +InputBlockState* InputQueue::GetBlockForId(uint64_t aInputBlockId) { + return FindBlockForId(aInputBlockId, nullptr); +} + +void InputQueue::AddInputBlockCallback(uint64_t aInputBlockId, + InputBlockCallbackInfo&& aCallbackInfo) { + mInputBlockCallbacks.insert(InputBlockCallbackMap::value_type( + aInputBlockId, std::move(aCallbackInfo))); +} + +InputBlockState* InputQueue::FindBlockForId(uint64_t aInputBlockId, + InputData** aOutFirstInput) { + for (const auto& queuedInput : mQueuedInputs) { + if (queuedInput->Block()->GetBlockId() == aInputBlockId) { + if (aOutFirstInput) { + *aOutFirstInput = queuedInput->Input(); + } + return queuedInput->Block(); + } + } + + InputBlockState* block = nullptr; + if (mActiveTouchBlock && mActiveTouchBlock->GetBlockId() == aInputBlockId) { + block = mActiveTouchBlock.get(); + } else if (mPrevActiveTouchBlock && + mPrevActiveTouchBlock->GetBlockId() == aInputBlockId) { + block = mPrevActiveTouchBlock.get(); + } else if (mActiveWheelBlock && + mActiveWheelBlock->GetBlockId() == aInputBlockId) { + block = mActiveWheelBlock.get(); + } else if (mActiveDragBlock && + mActiveDragBlock->GetBlockId() == aInputBlockId) { + block = mActiveDragBlock.get(); + } else if (mActivePanGestureBlock && + mActivePanGestureBlock->GetBlockId() == aInputBlockId) { + block = mActivePanGestureBlock.get(); + } else if (mActivePinchGestureBlock && + mActivePinchGestureBlock->GetBlockId() == aInputBlockId) { + block = mActivePinchGestureBlock.get(); + } else if (mActiveKeyboardBlock && + mActiveKeyboardBlock->GetBlockId() == aInputBlockId) { + block = mActiveKeyboardBlock.get(); + } + // Since we didn't encounter this block while iterating through mQueuedInputs, + // it must have no events associated with it at the moment. + if (aOutFirstInput) { + *aOutFirstInput = nullptr; + } + return block; +} + +void InputQueue::MainThreadTimeout(uint64_t aInputBlockId) { + // It's possible that this function gets called after the controller thread + // was discarded during shutdown. + if (!APZThreadUtils::IsControllerThreadAlive()) { + return; + } + APZThreadUtils::AssertOnControllerThread(); + + INPQ_LOG("got a main thread timeout; block=%" PRIu64 "\n", aInputBlockId); + bool success = false; + InputData* firstInput = nullptr; + InputBlockState* inputBlock = FindBlockForId(aInputBlockId, &firstInput); + if (inputBlock && inputBlock->AsCancelableBlock()) { + CancelableBlockState* block = inputBlock->AsCancelableBlock(); + // time out the touch-listener response and also confirm the existing + // target apzc in the case where the main thread doesn't get back to us + // fast enough. + success = block->TimeoutContentResponse(); + success |= block->SetConfirmedTargetApzc( + block->GetTargetApzc(), + InputBlockState::TargetConfirmationState::eTimedOut, firstInput, + // This actually could be a scrollbar drag, but we pass + // aForScrollbarDrag=false because for scrollbar drags, + // SetConfirmedTargetApzc() will also be called by ConfirmDragBlock(), + // and we pass aForScrollbarDrag=true there. + false); + } else if (inputBlock) { + NS_WARNING("input block is not a cancelable block"); + } + if (success) { + if (inputBlock->AsTouchBlock() && inputBlock->AsTouchBlock()->IsInSlop()) { + // If the touch block is still in slop, it's still possible this block + // needs to send a touchmove to content after the long-press gesture + // since preventDefault() in a touchmove event handler should stop + // handling the block at all. + inputBlock->AsTouchBlock()->SetNeedsToWaitTouchMove(true); + } + ProcessQueue(); + } +} + +void InputQueue::MaybeLongTapTimeout(uint64_t aInputBlockId) { + // It's possible that this function gets called after the controller thread + // was discarded during shutdown. + if (!APZThreadUtils::IsControllerThreadAlive()) { + return; + } + APZThreadUtils::AssertOnControllerThread(); + + INPQ_LOG("got a maybe-long-tap timeout; block=%" PRIu64 "\n", aInputBlockId); + + InputBlockState* inputBlock = FindBlockForId(aInputBlockId, nullptr); + MOZ_ASSERT(!inputBlock || inputBlock->AsTouchBlock()); + if (inputBlock && inputBlock->AsTouchBlock()->IsInSlop()) { + // If the block is still in slop, it won't have sent a touchmove to content + // and so content will not have sent a content response. But also it means + // the touchstart should trigger a long-press gesture so let's force the + // block to get processed now. + MainThreadTimeout(aInputBlockId); + } +} + +void InputQueue::ContentReceivedInputBlock(uint64_t aInputBlockId, + bool aPreventDefault) { + APZThreadUtils::AssertOnControllerThread(); + + INPQ_LOG("got a content response; block=%" PRIu64 " preventDefault=%d\n", + aInputBlockId, aPreventDefault); + bool success = false; + InputBlockState* inputBlock = FindBlockForId(aInputBlockId, nullptr); + if (inputBlock && inputBlock->AsCancelableBlock()) { + CancelableBlockState* block = inputBlock->AsCancelableBlock(); + success = block->SetContentResponse(aPreventDefault); + } else if (inputBlock) { + NS_WARNING("input block is not a cancelable block"); + } else { + INPQ_LOG("couldn't find block=%" PRIu64 "\n", aInputBlockId); + } + if (success) { + if (ProcessQueue()) { + // If we've switched the active touch block back to the original touch + // block from the block for long-tap, run ProcessQueue again. + // If we haven't yet received new touch-move events which need to be + // processed (e.g. we are waiting for a content response for a touch-move + // event), below ProcessQueue call is mostly no-op. + ProcessQueue(); + } + } +} + +void InputQueue::SetConfirmedTargetApzc( + uint64_t aInputBlockId, const RefPtr<AsyncPanZoomController>& aTargetApzc) { + APZThreadUtils::AssertOnControllerThread(); + + INPQ_LOG("got a target apzc; block=%" PRIu64 " guid=%s\n", aInputBlockId, + aTargetApzc ? ToString(aTargetApzc->GetGuid()).c_str() : ""); + bool success = false; + InputData* firstInput = nullptr; + InputBlockState* inputBlock = FindBlockForId(aInputBlockId, &firstInput); + if (inputBlock && inputBlock->AsCancelableBlock()) { + CancelableBlockState* block = inputBlock->AsCancelableBlock(); + success = block->SetConfirmedTargetApzc( + aTargetApzc, InputBlockState::TargetConfirmationState::eConfirmed, + firstInput, + // This actually could be a scrollbar drag, but we pass + // aForScrollbarDrag=false because for scrollbar drags, + // SetConfirmedTargetApzc() will also be called by ConfirmDragBlock(), + // and we pass aForScrollbarDrag=true there. + false); + } else if (inputBlock) { + NS_WARNING("input block is not a cancelable block"); + } + if (success) { + ProcessQueue(); + } +} + +void InputQueue::ConfirmDragBlock( + uint64_t aInputBlockId, const RefPtr<AsyncPanZoomController>& aTargetApzc, + const AsyncDragMetrics& aDragMetrics) { + APZThreadUtils::AssertOnControllerThread(); + + INPQ_LOG("got a target apzc; block=%" PRIu64 " guid=%s dragtarget=%" PRIu64 + "\n", + aInputBlockId, + aTargetApzc ? ToString(aTargetApzc->GetGuid()).c_str() : "", + aDragMetrics.mViewId); + bool success = false; + InputData* firstInput = nullptr; + InputBlockState* inputBlock = FindBlockForId(aInputBlockId, &firstInput); + if (inputBlock && inputBlock->AsDragBlock()) { + DragBlockState* block = inputBlock->AsDragBlock(); + + // We use the target initial scrollable rect for updating the thumb position + // during dragging the thumb even if the scrollable rect got expanded during + // the drag. + block->SetDragMetrics(aDragMetrics, aTargetApzc->GetScrollableRect()); + success = block->SetConfirmedTargetApzc( + aTargetApzc, InputBlockState::TargetConfirmationState::eConfirmed, + firstInput, + /* aForScrollbarDrag = */ true); + } + if (success) { + ProcessQueue(); + } +} + +void InputQueue::SetAllowedTouchBehavior( + uint64_t aInputBlockId, const nsTArray<TouchBehaviorFlags>& aBehaviors) { + APZThreadUtils::AssertOnControllerThread(); + + INPQ_LOG("got allowed touch behaviours; block=%" PRIu64 "\n", aInputBlockId); + bool success = false; + InputBlockState* inputBlock = FindBlockForId(aInputBlockId, nullptr); + if (inputBlock && inputBlock->AsTouchBlock()) { + TouchBlockState* block = inputBlock->AsTouchBlock(); + success = block->SetAllowedTouchBehaviors(aBehaviors); + } else if (inputBlock) { + NS_WARNING("input block is not a touch block"); + } + if (success) { + ProcessQueue(); + } +} + +void InputQueue::SetBrowserGestureResponse(uint64_t aInputBlockId, + BrowserGestureResponse aResponse) { + InputBlockState* inputBlock = FindBlockForId(aInputBlockId, nullptr); + + if (inputBlock && inputBlock->AsPanGestureBlock()) { + PanGestureBlockState* block = inputBlock->AsPanGestureBlock(); + block->SetBrowserGestureResponse(aResponse); + } else if (inputBlock) { + NS_WARNING("input block is not a pan gesture block"); + } + ProcessQueue(); +} + +static APZHandledResult GetHandledResultFor( + const AsyncPanZoomController* aApzc, + const InputBlockState& aCurrentInputBlock, nsEventStatus aEagerStatus) { + if (aCurrentInputBlock.ShouldDropEvents()) { + return APZHandledResult{APZHandledPlace::HandledByContent, aApzc}; + } + + if (!aApzc) { + return APZHandledResult{APZHandledPlace::HandledByContent, aApzc}; + } + + if (aApzc->IsRootContent()) { + // If the eager status was eIgnore, we would have returned an eager result + // of Unhandled if there had been no event handler. Now that we know the + // event handler did not preventDefault() the input block, return Unhandled + // as the delayed result. + // FIXME: A more accurate implementation would be to re-do the entire + // computation that determines the status (i.e. calling + // ArePointerEventsConsumable()) with the confirmed target APZC. + return (aEagerStatus == nsEventStatus_eConsumeDoDefault && + aApzc->CanVerticalScrollWithDynamicToolbar()) + ? APZHandledResult{APZHandledPlace::HandledByRoot, aApzc} + : APZHandledResult{APZHandledPlace::Unhandled, aApzc, true}; + } + + bool mayTriggerPullToRefresh = + aCurrentInputBlock.GetOverscrollHandoffChain() + ->ScrollingUpWillTriggerPullToRefresh(aApzc); + if (mayTriggerPullToRefresh) { + return APZHandledResult{APZHandledPlace::Unhandled, aApzc, true}; + } + + auto [willMoveDynamicToolbar, rootApzc] = + aCurrentInputBlock.GetOverscrollHandoffChain() + ->ScrollingDownWillMoveDynamicToolbar(aApzc); + if (!willMoveDynamicToolbar) { + return APZHandledResult{APZHandledPlace::HandledByContent, aApzc}; + } + + // Return `HandledByRoot` if scroll positions in all relevant APZC are at the + // bottom edge and if there are contents covered by the dynamic toolbar. + MOZ_ASSERT(rootApzc && rootApzc->IsRootContent()); + return APZHandledResult{APZHandledPlace::HandledByRoot, rootApzc}; +} + +bool InputQueue::ProcessQueue() { + APZThreadUtils::AssertOnControllerThread(); + + while (!mQueuedInputs.IsEmpty()) { + InputBlockState* curBlock = mQueuedInputs[0]->Block(); + CancelableBlockState* cancelable = curBlock->AsCancelableBlock(); + if (cancelable && !cancelable->IsReadyForHandling()) { + break; + } + + INPQ_LOG( + "processing input from block %p; preventDefault %d shouldDropEvents %d " + "target %p\n", + curBlock, cancelable && cancelable->IsDefaultPrevented(), + curBlock->ShouldDropEvents(), curBlock->GetTargetApzc().get()); + RefPtr<AsyncPanZoomController> target = curBlock->GetTargetApzc(); + + // If there is an input block callback registered for this + // input block, invoke it. + // + // NOTE: In the case where the block is a touch block and the block is not + // ready to invoke the callback because of waiting a touch move response + // from content, we skip the block. + if (!curBlock->AsTouchBlock() || + curBlock->AsTouchBlock()->IsReadyForCallback()) { + auto it = mInputBlockCallbacks.find(curBlock->GetBlockId()); + if (it != mInputBlockCallbacks.end()) { + INPQ_LOG("invoking the callback for input from block %p id %" PRIu64 + "\n", + curBlock, curBlock->GetBlockId()); + APZHandledResult handledResult = + GetHandledResultFor(target, *curBlock, it->second.mEagerStatus); + it->second.mCallback(curBlock->GetBlockId(), handledResult); + // The callback is one-shot; discard it after calling it. + mInputBlockCallbacks.erase(it); + } + } + + // target may be null here if the initial target was unconfirmed and then + // we later got a confirmed null target. in that case drop the events. + if (target) { + // If the event is targeting a different APZC than the previous one, + // we want to clear the previous APZC's gesture state regardless of + // whether we're actually dispatching the event or not. + if (mLastActiveApzc && mLastActiveApzc != target && + mTouchCounter.GetActiveTouchCount() > 0) { + mLastActiveApzc->ResetTouchInputState(); + } + if (curBlock->ShouldDropEvents()) { + if (curBlock->AsTouchBlock()) { + target->ResetTouchInputState(); + } else if (curBlock->AsPanGestureBlock()) { + target->ResetPanGestureInputState(); + } + } else { + UpdateActiveApzc(target); + curBlock->DispatchEvent(*(mQueuedInputs[0]->Input())); + } + } + mQueuedInputs.RemoveElementAt(0); + } + + bool processQueueAgain = false; + if (CanDiscardBlock(mActiveTouchBlock)) { + const bool forLongTap = mActiveTouchBlock->ForLongTap(); + const bool wasDefaultPrevented = mActiveTouchBlock->IsDefaultPrevented(); + INPQ_LOG("discarding a touch block %p id %" PRIu64 "\n", + mActiveTouchBlock.get(), mActiveTouchBlock->GetBlockId()); + mActiveTouchBlock = nullptr; + MOZ_ASSERT_IF(forLongTap, mPrevActiveTouchBlock); + if (forLongTap) { + INPQ_LOG("switching back to the original touch block %p id %" PRIu64 "\n", + mPrevActiveTouchBlock.get(), + mPrevActiveTouchBlock->GetBlockId()); + + mPrevActiveTouchBlock->SetLongTapProcessed(); + if (wasDefaultPrevented && !mPrevActiveTouchBlock->IsDefaultPrevented()) { + // Take over the preventDefaulted info for the long-tap event (i.e. for + // the contextmenu event) to the original touch block so that the + // original touch block will never process incoming touch events. + mPrevActiveTouchBlock->ResetContentResponseTimerExpired(); + mPrevActiveTouchBlock->SetContentResponse(true); + } + mActiveTouchBlock = mPrevActiveTouchBlock; + mPrevActiveTouchBlock = nullptr; + processQueueAgain = true; + } + } + if (CanDiscardBlock(mActiveWheelBlock)) { + mActiveWheelBlock = nullptr; + } + if (CanDiscardBlock(mActiveDragBlock)) { + mActiveDragBlock = nullptr; + } + if (CanDiscardBlock(mActivePanGestureBlock)) { + mActivePanGestureBlock = nullptr; + } + if (CanDiscardBlock(mActivePinchGestureBlock)) { + mActivePinchGestureBlock = nullptr; + } + if (CanDiscardBlock(mActiveKeyboardBlock)) { + mActiveKeyboardBlock = nullptr; + } + + return processQueueAgain; +} + +bool InputQueue::CanDiscardBlock(InputBlockState* aBlock) { + if (!aBlock || + (aBlock->AsCancelableBlock() && + !aBlock->AsCancelableBlock()->IsReadyForHandling()) || + aBlock->MustStayActive()) { + return false; + } + InputData* firstInput = nullptr; + FindBlockForId(aBlock->GetBlockId(), &firstInput); + if (firstInput) { + // The block has at least one input event still in the queue, so it's + // not depleted + return false; + } + return true; +} + +void InputQueue::UpdateActiveApzc( + const RefPtr<AsyncPanZoomController>& aNewActive) { + mLastActiveApzc = aNewActive; +} + +void InputQueue::Clear() { + // On Android, where the controller thread is the Android UI thread, + // it's possible for this to be called after the main thread has + // already run the shutdown task that clears the state used to + // implement APZThreadUtils::AssertOnControllerThread(). + // In such cases, we still want to perform the cleanup. + if (APZThreadUtils::IsControllerThreadAlive()) { + APZThreadUtils::AssertOnControllerThread(); + } + + mQueuedInputs.Clear(); + mActiveTouchBlock = nullptr; + mPrevActiveTouchBlock = nullptr; + mActiveWheelBlock = nullptr; + mActiveDragBlock = nullptr; + mActivePanGestureBlock = nullptr; + mActivePinchGestureBlock = nullptr; + mActiveKeyboardBlock = nullptr; + mLastActiveApzc = nullptr; +} + +InputQueue::AutoRunImmediateTimeout::AutoRunImmediateTimeout(InputQueue* aQueue) + : mQueue(aQueue) { + MOZ_ASSERT(!mQueue->mImmediateTimeout); +} + +InputQueue::AutoRunImmediateTimeout::~AutoRunImmediateTimeout() { + if (mQueue->mImmediateTimeout) { + mQueue->mImmediateTimeout->Run(); + mQueue->mImmediateTimeout = nullptr; + } +} + +} // namespace layers +} // namespace mozilla |