/* -*- 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 #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& aTarget, TargetConfirmationFlags aFlags, InputData& aEvent, const Maybe>& 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& aTarget, TargetConfirmationFlags aFlags, const MultiTouchInput& aEvent, const Maybe>& aTouchBehaviors) { APZEventResult result(aTarget, aFlags); RefPtr block; bool waitingForContentResponse = false; if (aEvent.mType == MultiTouchInput::MULTITOUCH_START) { nsTArray 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 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(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 maybeLongTap = NewRunnableMethod( "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& 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 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(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& aTarget, TargetConfirmationFlags aFlags, const ScrollWheelInput& aEvent) { APZEventResult result(aTarget, aFlags); RefPtr 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(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& aTarget, TargetConfirmationFlags aFlags, const KeyboardInput& aEvent) { APZEventResult result(aTarget, aFlags); RefPtr 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(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 horizontallyScrollableAPZC = aBlock->GetOverscrollHandoffChain()->FindFirstScrollable( horizontalComponent, &allowedScrollDirections, OverscrollHandoffChain::IncludeOverscroll::No); return horizontallyScrollableAPZC && horizontallyScrollableAPZC == aBlock->GetTargetApzc() && allowedScrollDirections.contains(ScrollDirection::eHorizontal); } APZEventResult InputQueue::ReceivePanGestureInput( const RefPtr& 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 block = mActivePanGestureBlock.get()) { mQueuedInputs.AppendElement(MakeUnique(aEvent, *block)); ProcessQueue(); } result.SetStatusAsIgnore(); return result; } RefPtr 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(event, *block)); ProcessQueue(); return result; } APZEventResult InputQueue::ReceivePinchGestureInput( const RefPtr& aTarget, TargetConfirmationFlags aFlags, const PinchGestureInput& aEvent) { APZEventResult result(aTarget, aFlags); RefPtr 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(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& 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& aTarget, TargetConfirmationFlags aFlags) { TouchBlockState* newBlock = new TouchBlockState(aTarget, aFlags, mTouchCounter); mActiveTouchBlock = newBlock; return newBlock; } TouchBlockState* InputQueue::StartNewTouchBlockForLongTap( const RefPtr& 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& aTarget, CancelableBlockState* aBlock) { INPQ_LOG("scheduling main thread timeout for target %p\n", aTarget.get()); RefPtr timeoutTask = NewRunnableMethod( "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& 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& 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& 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 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& 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