Source code

Revision control

Copy as Markdown

Other Tools

/* -*- 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