Bug 1083395 - Extract an InputQueue class from the APZC input code. r=botond

This commit is contained in:
Kartikaya Gupta 2014-10-24 13:29:03 -04:00
parent 2489aa5b2e
commit 541e8f016d
7 changed files with 494 additions and 323 deletions

View File

@ -16,6 +16,7 @@
#include "GestureEventListener.h" // for GestureEventListener
#include "InputData.h" // for MultiTouchInput, etc
#include "InputBlockState.h" // for InputBlockState, TouchBlockState
#include "InputQueue.h" // for InputQueue
#include "OverscrollHandoffState.h" // for OverscrollHandoffState
#include "TaskThrottler.h" // for TaskThrottler
#include "Units.h" // for CSSRect, CSSPoint, etc
@ -885,7 +886,7 @@ AsyncPanZoomController::AsyncPanZoomController(uint64_t aLayersId,
mAsyncScrollTimeoutTask(nullptr),
mState(NOTHING),
mNotificationBlockers(0),
mTouchBlockBalance(0),
mInputQueue(new InputQueue()),
mTreeManager(aTreeManager),
mAPZCId(sAsyncPanZoomControllerCount++),
mSharedLock(nullptr),
@ -926,6 +927,13 @@ AsyncPanZoomController::GetGestureEventListener() const {
return listener.forget();
}
nsRefPtr<InputQueue>
AsyncPanZoomController::GetInputQueue() const {
MonitorAutoLock lock(mRefPtrMonitor);
MOZ_ASSERT(mInputQueue);
return mInputQueue;
}
void
AsyncPanZoomController::Destroy()
{
@ -933,12 +941,11 @@ AsyncPanZoomController::Destroy()
CancelAnimation();
mTouchBlockQueue.Clear();
{ // scope the lock
MonitorAutoLock lock(mRefPtrMonitor);
mGeckoContentController = nullptr;
mGestureEventListener = nullptr;
mInputQueue = nullptr; // XXX this is temporary and will be removed in a future patch
}
mPrevSibling = nullptr;
mLastChild = nullptr;
@ -1007,76 +1014,7 @@ AsyncPanZoomController::ArePointerEventsConsumable(TouchBlockState* aBlock, uint
}
nsEventStatus AsyncPanZoomController::ReceiveInputEvent(const InputData& aEvent) {
AssertOnControllerThread();
if (aEvent.mInputType != MULTITOUCH_INPUT) {
HandleInputEvent(aEvent);
// The return value for non-touch input isn't really used, so just return
// ConsumeDoDefault for now. This can be changed later if needed.
return nsEventStatus_eConsumeDoDefault;
}
TouchBlockState* block = nullptr;
if (aEvent.AsMultiTouchInput().mType == MultiTouchInput::MULTITOUCH_START) {
block = StartNewTouchBlock(false);
APZC_LOG("%p started new touch block %p\n", this, block);
// 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 (block == CurrentTouchBlock()) {
if (block->GetOverscrollHandoffChain()->HasFastMovingApzc()) {
// If we're already in a fast fling, then we want the touch event to stop the fling
// and to disallow the touch event from being used as part of a fling.
block->DisallowSingleTap();
}
block->GetOverscrollHandoffChain()->CancelAnimations();
}
if (NeedToWaitForContent()) {
// Content may intercept the touch events and prevent-default them. So we schedule
// a timeout to give content time to do that.
ScheduleContentResponseTimeout();
} else {
// Content won't prevent-default this, so we can just pretend like we scheduled
// a timeout and it expired. Note that we will still receive a ContentReceivedTouch
// callback for this block, and so we need to make sure we adjust the touch balance.
APZC_LOG("%p not waiting for content response on block %p\n", this, block);
mTouchBlockBalance++;
block->TimeoutContentResponse();
}
} else if (mTouchBlockQueue.IsEmpty()) {
NS_WARNING("Received a non-start touch event while no touch blocks active!");
} else {
// this touch is part of the most-recently created block
block = mTouchBlockQueue.LastElement().get();
APZC_LOG("%p received new event in block %p\n", this, block);
}
if (!block) {
return nsEventStatus_eIgnore;
}
nsEventStatus result = ArePointerEventsConsumable(block, aEvent.AsMultiTouchInput().mTouches.Length())
? nsEventStatus_eConsumeDoDefault
: nsEventStatus_eIgnore;
if (block == CurrentTouchBlock() && block->IsReadyForHandling()) {
APZC_LOG("%p's current touch block is ready with preventdefault %d\n",
this, block->IsDefaultPrevented());
if (block->IsDefaultPrevented()) {
return result;
}
HandleInputEvent(aEvent);
return result;
}
// Otherwise, add it to the queue for the touch block
block->AddEvent(aEvent.AsMultiTouchInput());
return result;
return GetInputQueue()->ReceiveInputEvent(this, aEvent);
}
nsEventStatus AsyncPanZoomController::HandleInputEvent(const InputData& aEvent) {
@ -1534,7 +1472,7 @@ nsEventStatus AsyncPanZoomController::OnPanBegin(const PanGestureInput& aEvent)
CancelAnimation();
}
mPanGestureState = MakeUnique<InputBlockState>(BuildOverscrollHandoffChain());
mPanGestureState = MakeUnique<InputBlockState>(this);
mX.StartTouch(aEvent.mPanStartPoint.x, aEvent.mTime);
mY.StartTouch(aEvent.mPanStartPoint.y, aEvent.mTime);
@ -1613,7 +1551,7 @@ nsEventStatus AsyncPanZoomController::OnPanMomentumStart(const PanGestureInput&
CancelAnimation();
}
mPanGestureState = MakeUnique<InputBlockState>(BuildOverscrollHandoffChain());
mPanGestureState = MakeUnique<InputBlockState>(this);
return nsEventStatus_eConsumeNoDefault;
}
@ -1641,8 +1579,7 @@ nsEventStatus AsyncPanZoomController::OnLongPress(const TapGestureInput& aEvent)
int32_t modifiers = WidgetModifiersToDOMModifiers(aEvent.modifiers);
CSSPoint geckoScreenPoint;
if (ConvertToGecko(aEvent.mPoint, &geckoScreenPoint)) {
StartNewTouchBlock(true);
ScheduleContentResponseTimeout();
GetInputQueue()->InjectNewTouchBlock(this);
controller->HandleLongTap(geckoScreenPoint, modifiers, GetGuid());
return nsEventStatus_eConsumeNoDefault;
}
@ -2917,115 +2854,14 @@ void AsyncPanZoomController::ZoomToRect(CSSRect aRect) {
}
}
void
AsyncPanZoomController::ScheduleContentResponseTimeout() {
APZC_LOG("%p scheduling content response timeout\n", this);
PostDelayedTask(
NewRunnableMethod(this, &AsyncPanZoomController::ContentResponseTimeout),
gfxPrefs::APZContentResponseTimeout());
}
void
AsyncPanZoomController::ContentResponseTimeout() {
AssertOnControllerThread();
mTouchBlockBalance++;
APZC_LOG("%p got a content response timeout; balance %d\n", this, mTouchBlockBalance);
if (mTouchBlockBalance > 0) {
// Find the first touch block in the queue that hasn't already received
// the content response timeout callback, and notify it.
bool found = false;
for (size_t i = 0; i < mTouchBlockQueue.Length(); i++) {
if (mTouchBlockQueue[i]->TimeoutContentResponse()) {
found = true;
break;
}
}
if (found) {
ProcessPendingInputBlocks();
} else {
NS_WARNING("APZC received more ContentResponseTimeout calls than it has unprocessed touch blocks\n");
}
}
}
void
AsyncPanZoomController::ContentReceivedTouch(bool aPreventDefault) {
AssertOnControllerThread();
mTouchBlockBalance--;
APZC_LOG("%p got a content response; balance %d\n", this, mTouchBlockBalance);
if (mTouchBlockBalance < 0) {
// Find the first touch block in the queue that hasn't already received
// its response from content, and notify it.
bool found = false;
for (size_t i = 0; i < mTouchBlockQueue.Length(); i++) {
if (mTouchBlockQueue[i]->SetContentResponse(aPreventDefault)) {
found = true;
break;
}
}
if (found) {
ProcessPendingInputBlocks();
} else {
NS_WARNING("APZC received more ContentReceivedTouch calls than it has unprocessed touch blocks\n");
}
}
GetInputQueue()->ContentReceivedTouch(aPreventDefault);
}
void
AsyncPanZoomController::SetAllowedTouchBehavior(const nsTArray<TouchBehaviorFlags>& aBehaviors) {
AssertOnControllerThread();
bool found = false;
for (size_t i = 0; i < mTouchBlockQueue.Length(); i++) {
if (mTouchBlockQueue[i]->SetAllowedTouchBehaviors(aBehaviors)) {
found = true;
break;
}
}
if (found) {
ProcessPendingInputBlocks();
} else {
NS_WARNING("APZC received more SetAllowedTouchBehavior calls than it has unprocessed touch blocks\n");
}
}
void
AsyncPanZoomController::ProcessPendingInputBlocks() {
AssertOnControllerThread();
while (true) {
TouchBlockState* curBlock = CurrentTouchBlock();
if (!curBlock->IsReadyForHandling()) {
break;
}
APZC_LOG("%p processing input block %p; preventDefault %d\n",
this, curBlock, curBlock->IsDefaultPrevented());
if (curBlock->IsDefaultPrevented()) {
curBlock->DropEvents();
ResetInputState();
} else {
while (curBlock->HasEvents()) {
HandleInputEvent(curBlock->RemoveFirstEvent());
}
}
MOZ_ASSERT(!curBlock->HasEvents());
if (mTouchBlockQueue.Length() == 1) {
// If |curBlock| is the only touch block in the queue, then it is still
// active and we cannot remove it yet. We only know that a touch block is
// over when we start the next one. This block will be removed by the code
// in StartNewTouchBlock, where new touch blocks are added.
break;
}
// If we get here, we know there are more touch blocks in the queue after
// |curBlock|, so we can remove |curBlock| and try to process the next one.
APZC_LOG("%p discarding depleted touch block %p\n", this, curBlock);
mTouchBlockQueue.RemoveElementAt(0);
}
GetInputQueue()->SetAllowedTouchBehavior(aBehaviors);
}
bool
@ -3034,6 +2870,12 @@ AsyncPanZoomController::NeedToWaitForContent() const
return (mFrameMetrics.GetMayHaveTouchListeners() || mFrameMetrics.GetMayHaveTouchCaret());
}
TouchBlockState*
AsyncPanZoomController::CurrentTouchBlock()
{
return GetInputQueue()->CurrentTouchBlock();
}
void
AsyncPanZoomController::ResetInputState()
{
@ -3046,43 +2888,10 @@ AsyncPanZoomController::ResetInputState()
}
}
TouchBlockState*
AsyncPanZoomController::StartNewTouchBlock(bool aCopyAllowedTouchBehaviorFromCurrent)
{
TouchBlockState* newBlock = new TouchBlockState(BuildOverscrollHandoffChain());
if (gfxPrefs::TouchActionEnabled() && aCopyAllowedTouchBehaviorFromCurrent) {
newBlock->CopyAllowedTouchBehaviorsFrom(*CurrentTouchBlock());
}
// We're going to start a new block, so clear out any depleted blocks at the head of the queue.
// See corresponding comment in ProcessPendingInputBlocks.
while (!mTouchBlockQueue.IsEmpty()) {
if (mTouchBlockQueue[0]->IsReadyForHandling() && !mTouchBlockQueue[0]->HasEvents()) {
APZC_LOG("%p discarding depleted touch block %p\n", this, mTouchBlockQueue[0].get());
mTouchBlockQueue.RemoveElementAt(0);
} else {
break;
}
}
// Add the new block to the queue.
mTouchBlockQueue.AppendElement(newBlock);
return newBlock;
}
TouchBlockState*
AsyncPanZoomController::CurrentTouchBlock()
{
AssertOnControllerThread();
MOZ_ASSERT(!mTouchBlockQueue.IsEmpty());
return mTouchBlockQueue[0].get();
}
bool
AsyncPanZoomController::HasReadyTouchBlock()
{
return !mTouchBlockQueue.IsEmpty() && mTouchBlockQueue[0]->IsReadyForHandling();
return GetInputQueue()->HasReadyTouchBlock();
}
AsyncPanZoomController::TouchBehaviorFlags

View File

@ -19,6 +19,7 @@
#include "mozilla/Atomics.h"
#include "InputData.h"
#include "Axis.h"
#include "InputQueue.h"
#include "TaskThrottler.h"
#include "mozilla/gfx/Matrix.h"
#include "nsRegion.h"
@ -583,15 +584,6 @@ protected:
*/
void FireAsyncScrollOnTimeout();
private:
/**
* Given the number of touch points in an input event and touch block they
* belong to, check if the event can result in a panning/zooming behavior.
* This is primarily used to figure out when to dispatch the pointercancel
* event for the pointer events spec.
*/
bool ArePointerEventsConsumable(TouchBlockState* aBlock, uint32_t aTouchPoints);
/**
* Convert ScreenPoint relative to this APZC to CSSPoint relative
* to the parent document. This excludes the transient compositor transform.
@ -638,6 +630,7 @@ private:
/* Utility functions that return a addrefed pointer to the corresponding fields. */
already_AddRefed<GeckoContentController> GetGeckoContentController() const;
already_AddRefed<GestureEventListener> GetGestureEventListener() const;
nsRefPtr<InputQueue> GetInputQueue() const;
// If we are sharing our frame metrics with content across processes
bool mSharingFrameMetricsAcrossProcesses;
@ -781,20 +774,12 @@ private:
*/
public:
/**
* This function is invoked by the APZCTreeManager which in turn is invoked
* by the widget when web content decides whether or not it wants to
* cancel a block of events. This automatically gets applied to the next
* block of events that has not yet been responded to. This function MUST
* be invoked exactly once for each touch block.
* See InputQueue::ContentReceivedTouch
*/
void ContentReceivedTouch(bool aPreventDefault);
/**
* Sets allowed touch behavior for current touch session.
* This method is invoked by the APZCTreeManager which in its turn invoked by
* the widget after performing touch-action values retrieving.
* Must be called after receiving the TOUCH_START even that started the
* touch session.
* See InputQueue::SetAllowedTouchBehavior
*/
void SetAllowedTouchBehavior(const nsTArray<TouchBehaviorFlags>& aBehaviors);
@ -804,74 +789,29 @@ public:
*/
void FlushRepaintForNewInputBlock();
private:
void ScheduleContentResponseTimeout();
void ContentResponseTimeout();
/**
* Processes any pending input blocks that are ready for processing. There
* must be at least one input block in the queue when this function is called.
* Given the number of touch points in an input event and touch block they
* belong to, check if the event can result in a panning/zooming behavior.
* This is primarily used to figure out when to dispatch the pointercancel
* event for the pointer events spec.
*/
bool ArePointerEventsConsumable(TouchBlockState* aBlock, uint32_t aTouchPoints);
/**
* Return true if there are are touch listeners registered on content
* scrolled by this APZC.
*/
void ProcessPendingInputBlocks();
TouchBlockState* StartNewTouchBlock(bool aCopyAllowedTouchBehaviorFromCurrent);
TouchBlockState* CurrentTouchBlock();
bool HasReadyTouchBlock();
bool NeedToWaitForContent() const;
/**
* Clear internal state relating to input handling.
*/
void ResetInputState();
private:
// The queue of touch blocks that have not yet been processed by this APZC.
// This member must only be accessed on the controller/UI thread.
nsTArray<UniquePtr<TouchBlockState>> mTouchBlockQueue;
// This variable requires some explanation. Strap yourself in.
//
// For each block of events, we do two things: (1) send the events to gecko and expect
// exactly one call to ContentReceivedTouch in return, and (2) kick off a timeout
// that triggers in case we don't hear from web content in a timely fashion.
// Since events are constantly coming in, we need to be able to handle more than one
// block of input events sitting in the queue.
//
// There are ordering restrictions on events that we can take advantage of, and that
// we need to abide by. Blocks of events in the queue will always be in the order that
// the user generated them. Responses we get from content will be in the same order as
// as the blocks of events in the queue. The timeout callbacks that have been posted
// will also fire in the same order as the blocks of events in the queue.
// HOWEVER, we may get multiple responses from content interleaved with multiple
// timeout expirations, and that interleaving is not predictable.
//
// Therefore, we need to make sure that for each block of events, we process the queued
// events exactly once, either when we get the response from content, or when the
// timeout expires (whichever happens first). There is no way to associate the timeout
// or response from content with a particular block of events other than via ordering.
//
// So, what we do to accomplish this is to track a "touch block balance", which is the
// number of timeout expirations that have fired, minus the number of content responses
// that have been received. (Think "balance" as in teeter-totter balance). This
// value is:
// - zero when we are in a state where the next content response we expect to receive
// and the next timeout expiration we expect to fire both correspond to the next
// unprocessed block of events in the queue.
// - negative when we are in a state where we have received more content responses than
// timeout expirations. This means that the next content repsonse we receive will
// correspond to the first unprocessed block, but the next n timeout expirations need
// to be ignored as they are for blocks we have already processed. (n is the absolute
// value of the balance.)
// - positive when we are in a state where we have received more timeout expirations
// than content responses. This means that the next timeout expiration that we will
// receive will correspond to the first unprocessed block, but the next n content
// responses need to be ignored as they are for blocks we have already processed.
// (n is the absolute value of the balance.)
//
// Note that each touch block internally carries flags that indicate whether or not it
// has received a content response and/or timeout expiration. However, we cannot rely
// on that alone to deliver these notifications to the right input block, because
// once an input block has been processed, it can potentially be removed from the queue.
// Therefore the information in that block is lost. An alternative approach would
// be to keep around those blocks until they have received both the content response
// and timeout expiration, but that involves a higher level of memory usage.
//
// This member must only be accessed on the controller/UI thread.
int32_t mTouchBlockBalance;
nsRefPtr<InputQueue> mInputQueue;
TouchBlockState* CurrentTouchBlock();
bool HasReadyTouchBlock();
/* ===================================================================
@ -1030,6 +970,27 @@ public:
*/
bool SnapBackIfOverscrolled();
/**
* Build the chain of APZCs along which scroll will be handed off when
* this APZC receives input events.
*
* Notes on lifetime and const-correctness:
* - The returned handoff chain is |const|, to indicate that it cannot be
* changed after being built.
* - When passing the chain to a function that uses it without storing it,
* pass it by reference-to-const (as in |const OverscrollHandoffChain&|).
* - When storing the chain, store it by RefPtr-to-const (as in
* |nsRefPtr<const OverscrollHandoffChain>|). This ensures the chain is
* kept alive. Note that queueing a task that uses the chain as an
* argument constitutes storing, as the task may outlive its queuer.
* - When passing the chain to a function that will store it, pass it as
* |const nsRefPtr<const OverscrollHandoffChain>&|. This allows the
* function to copy it into the |nsRefPtr<const OverscrollHandoffChain>|
* that will store it, while avoiding an unnecessary copy (and thus
* AddRef() and Release()) when passing it.
*/
nsRefPtr<const OverscrollHandoffChain> BuildOverscrollHandoffChain();
private:
/**
* A helper function for calling APZCTreeManager::DispatchScroll().
@ -1056,26 +1017,6 @@ private:
*/
bool OverscrollBy(const ScreenPoint& aOverscroll);
/**
* Build the chain of APZCs along which scroll will be handed off when
* this APZC receives input events.
*
* Notes on lifetime and const-correctness:
* - The returned handoff chain is |const|, to indicate that it cannot be
* changed after being built.
* - When passing the chain to a function that uses it without storing it,
* pass it by reference-to-const (as in |const OverscrollHandoffChain&|).
* - When storing the chain, store it by RefPtr-to-const (as in
* |nsRefPtr<const OverscrollHandoffChain>|). This ensures the chain is
* kept alive. Note that queueing a task that uses the chain as an
* argument constitutes storing, as the task may outlive its queuer.
* - When passing the chain to a function that will store it, pass it as
* |const nsRefPtr<const OverscrollHandoffChain>&|. This allows the
* function to copy it into the |nsRefPtr<const OverscrollHandoffChain>|
* that will store it, while avoiding an unnecessary copy (and thus
* AddRef() and Release()) when passing it.
*/
nsRefPtr<const OverscrollHandoffChain> BuildOverscrollHandoffChain();
/* ===================================================================
* The functions and members in this section are used to maintain the

View File

@ -15,11 +15,18 @@
namespace mozilla {
namespace layers {
InputBlockState::InputBlockState(const nsRefPtr<const OverscrollHandoffChain>& aOverscrollHandoffChain)
: mOverscrollHandoffChain(aOverscrollHandoffChain)
InputBlockState::InputBlockState(const nsRefPtr<AsyncPanZoomController>& aTargetApzc)
: mTargetApzc(aTargetApzc)
{
// We should never be constructed with a nullptr handoff chain.
MOZ_ASSERT(mOverscrollHandoffChain);
// We should never be constructed with a nullptr target.
MOZ_ASSERT(mTargetApzc);
mOverscrollHandoffChain = mTargetApzc->BuildOverscrollHandoffChain();
}
const nsRefPtr<AsyncPanZoomController>&
InputBlockState::GetTargetApzc() const
{
return mTargetApzc;
}
const nsRefPtr<const OverscrollHandoffChain>&
@ -28,8 +35,8 @@ InputBlockState::GetOverscrollHandoffChain() const
return mOverscrollHandoffChain;
}
TouchBlockState::TouchBlockState(const nsRefPtr<const OverscrollHandoffChain>& aOverscrollHandoffChain)
: InputBlockState(aOverscrollHandoffChain)
TouchBlockState::TouchBlockState(const nsRefPtr<AsyncPanZoomController>& aTargetApzc)
: InputBlockState(aTargetApzc)
, mAllowedTouchBehaviorSet(false)
, mPreventDefault(false)
, mContentResponded(false)

View File

@ -23,10 +23,13 @@ class OverscrollHandoffChain;
class InputBlockState
{
public:
explicit InputBlockState(const nsRefPtr<const OverscrollHandoffChain>& aOverscrollHandoffChain);
explicit InputBlockState(const nsRefPtr<AsyncPanZoomController>& aTargetApzc);
const nsRefPtr<AsyncPanZoomController>& GetTargetApzc() const;
const nsRefPtr<const OverscrollHandoffChain>& GetOverscrollHandoffChain() const;
private:
nsRefPtr<AsyncPanZoomController> mTargetApzc;
nsRefPtr<const OverscrollHandoffChain> mOverscrollHandoffChain;
};
@ -66,7 +69,7 @@ class TouchBlockState : public InputBlockState
public:
typedef uint32_t TouchBehaviorFlags;
explicit TouchBlockState(const nsRefPtr<const OverscrollHandoffChain>& aOverscrollHandoffChain);
explicit TouchBlockState(const nsRefPtr<AsyncPanZoomController>& aTargetApzc);
/**
* Record whether or not content cancelled this block of events.

View File

@ -0,0 +1,263 @@
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set sw=2 ts=8 et tw=80 : */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "InputQueue.h"
#include "AsyncPanZoomController.h"
#include "gfxPrefs.h"
#include "InputBlockState.h"
#include "OverscrollHandoffState.h"
#define INPQ_LOG(...)
// #define INPQ_LOG(...) printf_stderr("INPQ: " __VA_ARGS__)
namespace mozilla {
namespace layers {
InputQueue::InputQueue()
: mTouchBlockBalance(0)
{
}
InputQueue::~InputQueue() {
mTouchBlockQueue.Clear();
}
nsEventStatus
InputQueue::ReceiveInputEvent(const nsRefPtr<AsyncPanZoomController>& aTarget, const InputData& aEvent) {
AsyncPanZoomController::AssertOnControllerThread();
if (aEvent.mInputType != MULTITOUCH_INPUT) {
aTarget->HandleInputEvent(aEvent);
// The return value for non-touch input isn't really used, so just return
// ConsumeDoDefault for now. This can be changed later if needed.
return nsEventStatus_eConsumeDoDefault;
}
TouchBlockState* block = nullptr;
if (aEvent.AsMultiTouchInput().mType == MultiTouchInput::MULTITOUCH_START) {
block = StartNewTouchBlock(aTarget, false);
INPQ_LOG("%p started new touch block %p for target %p\n", this, block, aTarget.get());
// 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 (block == CurrentTouchBlock()) {
if (block->GetOverscrollHandoffChain()->HasFastMovingApzc()) {
// If we're already in a fast fling, then we want the touch event to stop the fling
// and to disallow the touch event from being used as part of a fling.
block->DisallowSingleTap();
}
block->GetOverscrollHandoffChain()->CancelAnimations();
}
if (aTarget->NeedToWaitForContent()) {
// Content may intercept the touch events and prevent-default them. So we schedule
// a timeout to give content time to do that.
ScheduleContentResponseTimeout(aTarget);
} else {
// Content won't prevent-default this, so we can just pretend like we scheduled
// a timeout and it expired. Note that we will still receive a ContentReceivedTouch
// callback for this block, and so we need to make sure we adjust the touch balance.
INPQ_LOG("%p not waiting for content response on block %p\n", this, block);
mTouchBlockBalance++;
block->TimeoutContentResponse();
}
} else if (mTouchBlockQueue.IsEmpty()) {
NS_WARNING("Received a non-start touch event while no touch blocks active!");
} else {
// this touch is part of the most-recently created block
block = mTouchBlockQueue.LastElement().get();
INPQ_LOG("%p received new event in block %p\n", this, block);
}
if (!block) {
return nsEventStatus_eIgnore;
}
nsEventStatus result = aTarget->ArePointerEventsConsumable(block, aEvent.AsMultiTouchInput().mTouches.Length())
? nsEventStatus_eConsumeDoDefault
: nsEventStatus_eIgnore;
if (block == CurrentTouchBlock() && block->IsReadyForHandling()) {
INPQ_LOG("%p's current touch block is ready with preventdefault %d\n",
this, block->IsDefaultPrevented());
if (block->IsDefaultPrevented()) {
return result;
}
aTarget->HandleInputEvent(aEvent);
return result;
}
// Otherwise, add it to the queue for the touch block
block->AddEvent(aEvent.AsMultiTouchInput());
return result;
}
void
InputQueue::InjectNewTouchBlock(AsyncPanZoomController* aTarget)
{
StartNewTouchBlock(aTarget, true);
ScheduleContentResponseTimeout(aTarget);
}
TouchBlockState*
InputQueue::StartNewTouchBlock(const nsRefPtr<AsyncPanZoomController>& aTarget, bool aCopyAllowedTouchBehaviorFromCurrent)
{
TouchBlockState* newBlock = new TouchBlockState(aTarget);
if (gfxPrefs::TouchActionEnabled() && aCopyAllowedTouchBehaviorFromCurrent) {
newBlock->CopyAllowedTouchBehaviorsFrom(*CurrentTouchBlock());
}
// We're going to start a new block, so clear out any depleted blocks at the head of the queue.
// See corresponding comment in ProcessPendingInputBlocks.
while (!mTouchBlockQueue.IsEmpty()) {
if (mTouchBlockQueue[0]->IsReadyForHandling() && !mTouchBlockQueue[0]->HasEvents()) {
INPQ_LOG("%p discarding depleted touch block %p\n", this, mTouchBlockQueue[0].get());
mTouchBlockQueue.RemoveElementAt(0);
} else {
break;
}
}
// Add the new block to the queue.
mTouchBlockQueue.AppendElement(newBlock);
return newBlock;
}
TouchBlockState*
InputQueue::CurrentTouchBlock() const
{
AsyncPanZoomController::AssertOnControllerThread();
MOZ_ASSERT(!mTouchBlockQueue.IsEmpty());
return mTouchBlockQueue[0].get();
}
bool
InputQueue::HasReadyTouchBlock() const
{
return !mTouchBlockQueue.IsEmpty() && mTouchBlockQueue[0]->IsReadyForHandling();
}
void
InputQueue::ScheduleContentResponseTimeout(const nsRefPtr<AsyncPanZoomController>& aTarget) {
INPQ_LOG("%p scheduling content response timeout for target %p\n", this, aTarget.get());
aTarget->PostDelayedTask(
NewRunnableMethod(this, &InputQueue::ContentResponseTimeout),
gfxPrefs::APZContentResponseTimeout());
}
void
InputQueue::ContentResponseTimeout() {
AsyncPanZoomController::AssertOnControllerThread();
mTouchBlockBalance++;
INPQ_LOG("%p got a content response timeout; balance %d\n", this, mTouchBlockBalance);
if (mTouchBlockBalance > 0) {
// Find the first touch block in the queue that hasn't already received
// the content response timeout callback, and notify it.
bool found = false;
for (size_t i = 0; i < mTouchBlockQueue.Length(); i++) {
if (mTouchBlockQueue[i]->TimeoutContentResponse()) {
found = true;
break;
}
}
if (found) {
ProcessPendingInputBlocks();
} else {
NS_WARNING("INPQ received more ContentResponseTimeout calls than it has unprocessed touch blocks\n");
}
}
}
void
InputQueue::ContentReceivedTouch(bool aPreventDefault) {
AsyncPanZoomController::AssertOnControllerThread();
mTouchBlockBalance--;
INPQ_LOG("%p got a content response; balance %d\n", this, mTouchBlockBalance);
if (mTouchBlockBalance < 0) {
// Find the first touch block in the queue that hasn't already received
// its response from content, and notify it.
bool found = false;
for (size_t i = 0; i < mTouchBlockQueue.Length(); i++) {
if (mTouchBlockQueue[i]->SetContentResponse(aPreventDefault)) {
found = true;
break;
}
}
if (found) {
ProcessPendingInputBlocks();
} else {
NS_WARNING("INPQ received more ContentReceivedTouch calls than it has unprocessed touch blocks\n");
}
}
}
void
InputQueue::SetAllowedTouchBehavior(const nsTArray<TouchBehaviorFlags>& aBehaviors) {
AsyncPanZoomController::AssertOnControllerThread();
bool found = false;
for (size_t i = 0; i < mTouchBlockQueue.Length(); i++) {
if (mTouchBlockQueue[i]->SetAllowedTouchBehaviors(aBehaviors)) {
found = true;
break;
}
}
if (found) {
ProcessPendingInputBlocks();
} else {
NS_WARNING("INPQ received more SetAllowedTouchBehavior calls than it has unprocessed touch blocks\n");
}
}
void
InputQueue::ProcessPendingInputBlocks() {
AsyncPanZoomController::AssertOnControllerThread();
while (true) {
TouchBlockState* curBlock = CurrentTouchBlock();
if (!curBlock->IsReadyForHandling()) {
break;
}
INPQ_LOG("%p processing input block %p; preventDefault %d target %p\n",
this, curBlock, curBlock->IsDefaultPrevented(),
curBlock->GetTargetApzc().get());
nsRefPtr<AsyncPanZoomController> target = curBlock->GetTargetApzc();
if (curBlock->IsDefaultPrevented()) {
curBlock->DropEvents();
target->ResetInputState();
} else {
while (curBlock->HasEvents()) {
target->HandleInputEvent(curBlock->RemoveFirstEvent());
}
}
MOZ_ASSERT(!curBlock->HasEvents());
if (mTouchBlockQueue.Length() == 1) {
// If |curBlock| is the only touch block in the queue, then it is still
// active and we cannot remove it yet. We only know that a touch block is
// over when we start the next one. This block will be removed by the code
// in StartNewTouchBlock, where new touch blocks are added.
break;
}
// If we get here, we know there are more touch blocks in the queue after
// |curBlock|, so we can remove |curBlock| and try to process the next one.
INPQ_LOG("%p discarding depleted touch block %p\n", this, curBlock);
mTouchBlockQueue.RemoveElementAt(0);
}
}
} // namespace layers
} // namespace mozilla

View File

@ -0,0 +1,147 @@
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#ifndef mozilla_layers_InputQueue_h
#define mozilla_layers_InputQueue_h
#include "mozilla/EventForwards.h"
#include "mozilla/UniquePtr.h"
#include "nsAutoPtr.h"
#include "nsTArray.h"
namespace mozilla {
class InputData;
namespace layers {
class AsyncPanZoomController;
class OverscrollHandoffChain;
class TouchBlockState;
/**
* This class stores incoming input events, separated into "input blocks", until
* they are ready for handling. Currently input blocks are only created from
* touch input.
*/
class InputQueue {
NS_INLINE_DECL_THREADSAFE_REFCOUNTING(InputQueue)
public:
typedef uint32_t TouchBehaviorFlags;
public:
InputQueue();
/**
* Notifies the InputQueue of a new incoming input event. The APZC that the
* input event was targeted to should be provided in the |aTarget| parameter.
* See the documentation on APZCTreeManager::ReceiveInputEvent for info on
* return values from this function.
*/
nsEventStatus ReceiveInputEvent(const nsRefPtr<AsyncPanZoomController>& aTarget, const InputData& aEvent);
/**
* This function should be invoked to notify the InputQueue when web content
* decides whether or not it wants to cancel a block of events. This
* automatically gets applied to the next block of events that has not yet
* been responded to. This function MUST be invoked exactly once for each
* touch block, after the touch-start event that creates the block is sent to
* ReceiveInputEvent.
*/
void ContentReceivedTouch(bool aPreventDefault);
/**
* This function should be invoked to notify the InputQueue of the touch-
* action properties for the different touch points in an input block. This
* automatically gets applied to the next block of events that has not yet
* received a touch behaviour notification. This function MUST be invoked
* exactly once for each touch block, after the touch-start event that creates
* the block is sent to ReceiveInputEvent. If touch-action is not enabled on
* the platform, this function does nothing and need not be called.
*/
void SetAllowedTouchBehavior(const nsTArray<TouchBehaviorFlags>& aBehaviors);
/**
* Adds a new touch block at the end of the input queue that has the same
* allowed touch behaviour flags as the the touch block currently being
* processed. This should only be called when processing of a touch block
* triggers the creation of a new touch block.
*/
void InjectNewTouchBlock(AsyncPanZoomController* aTarget);
/**
* Returns the touch block at the head of the queue.
*/
TouchBlockState* CurrentTouchBlock() const;
/**
* Returns true iff the touch block at the head of the queue is ready for
* handling.
*/
bool HasReadyTouchBlock() const;
private:
~InputQueue();
TouchBlockState* StartNewTouchBlock(const nsRefPtr<AsyncPanZoomController>& aTarget, bool aCopyAllowedTouchBehaviorFromCurrent);
void ScheduleContentResponseTimeout(const nsRefPtr<AsyncPanZoomController>& aTarget);
void ContentResponseTimeout();
void ProcessPendingInputBlocks();
private:
// The queue of touch blocks that have not yet been processed.
// This member must only be accessed on the controller/UI thread.
nsTArray<UniquePtr<TouchBlockState>> mTouchBlockQueue;
// This variable requires some explanation. Strap yourself in.
//
// For each block of events, we do two things: (1) send the events to gecko and expect
// exactly one call to ContentReceivedTouch in return, and (2) kick off a timeout
// that triggers in case we don't hear from web content in a timely fashion.
// Since events are constantly coming in, we need to be able to handle more than one
// block of input events sitting in the queue.
//
// There are ordering restrictions on events that we can take advantage of, and that
// we need to abide by. Blocks of events in the queue will always be in the order that
// the user generated them. Responses we get from content will be in the same order as
// as the blocks of events in the queue. The timeout callbacks that have been posted
// will also fire in the same order as the blocks of events in the queue.
// HOWEVER, we may get multiple responses from content interleaved with multiple
// timeout expirations, and that interleaving is not predictable.
//
// Therefore, we need to make sure that for each block of events, we process the queued
// events exactly once, either when we get the response from content, or when the
// timeout expires (whichever happens first). There is no way to associate the timeout
// or response from content with a particular block of events other than via ordering.
//
// So, what we do to accomplish this is to track a "touch block balance", which is the
// number of timeout expirations that have fired, minus the number of content responses
// that have been received. (Think "balance" as in teeter-totter balance). This
// value is:
// - zero when we are in a state where the next content response we expect to receive
// and the next timeout expiration we expect to fire both correspond to the next
// unprocessed block of events in the queue.
// - negative when we are in a state where we have received more content responses than
// timeout expirations. This means that the next content repsonse we receive will
// correspond to the first unprocessed block, but the next n timeout expirations need
// to be ignored as they are for blocks we have already processed. (n is the absolute
// value of the balance.)
// - positive when we are in a state where we have received more timeout expirations
// than content responses. This means that the next timeout expiration that we will
// receive will correspond to the first unprocessed block, but the next n content
// responses need to be ignored as they are for blocks we have already processed.
// (n is the absolute value of the balance.)
//
// Note that each touch block internally carries flags that indicate whether or not it
// has received a content response and/or timeout expiration. However, we cannot rely
// on that alone to deliver these notifications to the right input block, because
// once an input block has been processed, it can potentially be removed from the queue.
// Therefore the information in that block is lost. An alternative approach would
// be to keep around those blocks until they have received both the content response
// and timeout expiration, but that involves a higher level of memory usage.
//
// This member must only be accessed on the controller/UI thread.
int32_t mTouchBlockBalance;
};
}
}
#endif // mozilla_layers_InputQueue_h

View File

@ -240,6 +240,7 @@ UNIFIED_SOURCES += [
'apz/src/Axis.cpp',
'apz/src/GestureEventListener.cpp',
'apz/src/InputBlockState.cpp',
'apz/src/InputQueue.cpp',
'apz/src/OverscrollHandoffState.cpp',
'apz/src/TaskThrottler.cpp',
'apz/testutil/APZTestData.cpp',