diff options
Diffstat (limited to '')
-rw-r--r-- | layout/base/TouchManager.cpp | 476 |
1 files changed, 476 insertions, 0 deletions
diff --git a/layout/base/TouchManager.cpp b/layout/base/TouchManager.cpp new file mode 100644 index 0000000000..0c34567b65 --- /dev/null +++ b/layout/base/TouchManager.cpp @@ -0,0 +1,476 @@ +/* -*- 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 "TouchManager.h" + +#include "mozilla/dom/Document.h" +#include "mozilla/dom/EventTarget.h" +#include "mozilla/PresShell.h" +#include "mozilla/layers/InputAPZContext.h" +#include "nsIContent.h" +#include "nsIFrame.h" +#include "nsLayoutUtils.h" +#include "nsView.h" +#include "PositionedEventTargeting.h" + +using namespace mozilla::dom; + +namespace mozilla { + +StaticAutoPtr<nsTHashMap<nsUint32HashKey, TouchManager::TouchInfo>> + TouchManager::sCaptureTouchList; +layers::LayersId TouchManager::sCaptureTouchLayersId; + +/*static*/ +void TouchManager::InitializeStatics() { + NS_ASSERTION(!sCaptureTouchList, "InitializeStatics called multiple times!"); + sCaptureTouchList = new nsTHashMap<nsUint32HashKey, TouchManager::TouchInfo>; + sCaptureTouchLayersId = layers::LayersId{0}; +} + +/*static*/ +void TouchManager::ReleaseStatics() { + NS_ASSERTION(sCaptureTouchList, "ReleaseStatics called without Initialize!"); + sCaptureTouchList = nullptr; +} + +void TouchManager::Init(PresShell* aPresShell, Document* aDocument) { + mPresShell = aPresShell; + mDocument = aDocument; +} + +void TouchManager::Destroy() { + EvictTouches(mDocument); + mDocument = nullptr; + mPresShell = nullptr; +} + +static nsIContent* GetNonAnonymousAncestor(EventTarget* aTarget) { + nsIContent* content = nsIContent::FromEventTargetOrNull(aTarget); + if (content && content->IsInNativeAnonymousSubtree()) { + content = content->FindFirstNonChromeOnlyAccessContent(); + } + return content; +} + +/*static*/ +void TouchManager::EvictTouchPoint(RefPtr<Touch>& aTouch, + Document* aLimitToDocument) { + nsCOMPtr<nsINode> node( + nsINode::FromEventTargetOrNull(aTouch->mOriginalTarget)); + if (node) { + Document* doc = node->GetComposedDoc(); + if (doc && (!aLimitToDocument || aLimitToDocument == doc)) { + PresShell* presShell = doc->GetPresShell(); + if (presShell) { + nsIFrame* frame = presShell->GetRootFrame(); + if (frame) { + nsCOMPtr<nsIWidget> widget = + frame->GetView()->GetNearestWidget(nullptr); + if (widget) { + WidgetTouchEvent event(true, eTouchEnd, widget); + event.mTouches.AppendElement(aTouch); + nsEventStatus status; + widget->DispatchEvent(&event, status); + } + } + } + } + } + if (!node || !aLimitToDocument || node->OwnerDoc() == aLimitToDocument) { + sCaptureTouchList->Remove(aTouch->Identifier()); + } +} + +/*static*/ +void TouchManager::AppendToTouchList( + WidgetTouchEvent::TouchArrayBase* aTouchList) { + for (const auto& data : sCaptureTouchList->Values()) { + const RefPtr<Touch>& touch = data.mTouch; + touch->mChanged = false; + aTouchList->AppendElement(touch); + } +} + +void TouchManager::EvictTouches(Document* aLimitToDocument) { + WidgetTouchEvent::AutoTouchArray touches; + AppendToTouchList(&touches); + for (uint32_t i = 0; i < touches.Length(); ++i) { + EvictTouchPoint(touches[i], aLimitToDocument); + } + sCaptureTouchLayersId = layers::LayersId{0}; +} + +/* static */ +nsIFrame* TouchManager::SetupTarget(WidgetTouchEvent* aEvent, + nsIFrame* aFrame) { + MOZ_ASSERT(aEvent); + + if (!aEvent || aEvent->mMessage != eTouchStart) { + // All touch events except for touchstart use a captured target. + return aFrame; + } + + nsIFrame* target = aFrame; + for (int32_t i = aEvent->mTouches.Length(); i;) { + --i; + dom::Touch* touch = aEvent->mTouches[i]; + + int32_t id = touch->Identifier(); + if (!TouchManager::HasCapturedTouch(id)) { + // find the target for this touch + RelativeTo relativeTo{aFrame}; + nsPoint eventPoint = nsLayoutUtils::GetEventCoordinatesRelativeTo( + aEvent, touch->mRefPoint, relativeTo); + target = FindFrameTargetedByInputEvent(aEvent, relativeTo, eventPoint); + if (target) { + nsCOMPtr<nsIContent> targetContent; + target->GetContentForEvent(aEvent, getter_AddRefs(targetContent)); + touch->SetTouchTarget(targetContent + ? targetContent->GetAsElementOrParentElement() + : nullptr); + } else { + aEvent->mTouches.RemoveElementAt(i); + } + } else { + // This touch is an old touch, we need to ensure that is not + // marked as changed and set its target correctly + touch->mChanged = false; + RefPtr<dom::Touch> oldTouch = TouchManager::GetCapturedTouch(id); + if (oldTouch) { + touch->SetTouchTarget(oldTouch->mOriginalTarget); + } + } + } + return target; +} + +/* static */ +nsIFrame* TouchManager::SuppressInvalidPointsAndGetTargetedFrame( + WidgetTouchEvent* aEvent) { + MOZ_ASSERT(aEvent); + + if (!aEvent || aEvent->mMessage != eTouchStart) { + // All touch events except for touchstart use a captured target. + return nullptr; + } + + // if this is a continuing session, ensure that all these events are + // in the same document by taking the target of the events already in + // the capture list + nsCOMPtr<nsIContent> anyTarget; + if (aEvent->mTouches.Length() > 1) { + anyTarget = TouchManager::GetAnyCapturedTouchTarget(); + } + + nsIFrame* frame = nullptr; + for (int32_t i = aEvent->mTouches.Length(); i;) { + --i; + dom::Touch* touch = aEvent->mTouches[i]; + if (TouchManager::HasCapturedTouch(touch->Identifier())) { + continue; + } + + MOZ_ASSERT(touch->mOriginalTarget); + nsCOMPtr<nsIContent> targetContent = do_QueryInterface(touch->GetTarget()); + nsIFrame* targetFrame = + targetContent ? targetContent->GetPrimaryFrame() : nullptr; + if (targetFrame && !anyTarget) { + anyTarget = targetContent; + } else { + nsIFrame* newTargetFrame = nullptr; + for (nsIFrame* f = targetFrame; f; + f = nsLayoutUtils::GetParentOrPlaceholderForCrossDoc(f)) { + if (f->PresContext()->Document() == anyTarget->OwnerDoc()) { + newTargetFrame = f; + break; + } + // We must be in a subdocument so jump directly to the root frame. + // GetParentOrPlaceholderForCrossDoc gets called immediately to + // jump up to the containing document. + f = f->PresShell()->GetRootFrame(); + } + // if we couldn't find a target frame in the same document as + // anyTarget, remove the touch from the capture touch list, as + // well as the event->mTouches array. touchmove events that aren't + // in the captured touch list will be discarded + if (!newTargetFrame) { + touch->mIsTouchEventSuppressed = true; + } else { + targetFrame = newTargetFrame; + targetFrame->GetContentForEvent(aEvent, getter_AddRefs(targetContent)); + touch->SetTouchTarget(targetContent + ? targetContent->GetAsElementOrParentElement() + : nullptr); + } + } + if (targetFrame) { + frame = targetFrame; + } + } + return frame; +} + +bool TouchManager::PreHandleEvent(WidgetEvent* aEvent, nsEventStatus* aStatus, + bool& aTouchIsNew, + nsCOMPtr<nsIContent>& aCurrentEventContent) { + MOZ_DIAGNOSTIC_ASSERT(aEvent->IsTrusted()); + + // NOTE: If you need to handle new event messages here, you need to add new + // cases in PresShell::EventHandler::PrepareToDispatchEvent(). + switch (aEvent->mMessage) { + case eTouchStart: { + WidgetTouchEvent* touchEvent = aEvent->AsTouchEvent(); + // if there is only one touch in this touchstart event, assume that it is + // the start of a new touch session and evict any old touches in the + // queue + if (touchEvent->mTouches.Length() == 1) { + EvictTouches(); + // Per + // https://w3c.github.io/touch-events/#touchevent-implementer-s-note, + // all touch event should be dispatched to the same document that first + // touch event associated to. We cache layers id of the first touchstart + // event, all subsequent touch events will use the same layers id. + sCaptureTouchLayersId = aEvent->mLayersId; + } else { + touchEvent->mLayersId = sCaptureTouchLayersId; + } + // Add any new touches to the queue + WidgetTouchEvent::TouchArray& touches = touchEvent->mTouches; + for (int32_t i = touches.Length(); i;) { + --i; + Touch* touch = touches[i]; + int32_t id = touch->Identifier(); + if (!sCaptureTouchList->Get(id, nullptr)) { + // If it is not already in the queue, it is a new touch + touch->mChanged = true; + } + touch->mMessage = aEvent->mMessage; + TouchInfo info = { + touch, GetNonAnonymousAncestor(touch->mOriginalTarget), true}; + sCaptureTouchList->InsertOrUpdate(id, info); + if (touch->mIsTouchEventSuppressed) { + // We're going to dispatch touch event. Remove this touch instance if + // it is suppressed. + touches.RemoveElementAt(i); + continue; + } + } + break; + } + case eTouchMove: { + // Check for touches that changed. Mark them add to queue + WidgetTouchEvent* touchEvent = aEvent->AsTouchEvent(); + WidgetTouchEvent::TouchArray& touches = touchEvent->mTouches; + touchEvent->mLayersId = sCaptureTouchLayersId; + bool haveChanged = false; + for (int32_t i = touches.Length(); i;) { + --i; + Touch* touch = touches[i]; + if (!touch) { + continue; + } + int32_t id = touch->Identifier(); + touch->mMessage = aEvent->mMessage; + + TouchInfo info; + if (!sCaptureTouchList->Get(id, &info)) { + touches.RemoveElementAt(i); + continue; + } + const RefPtr<Touch> oldTouch = info.mTouch; + if (!oldTouch->Equals(touch)) { + touch->mChanged = true; + haveChanged = true; + } + + nsCOMPtr<EventTarget> targetPtr = oldTouch->mOriginalTarget; + if (!targetPtr) { + touches.RemoveElementAt(i); + continue; + } + nsCOMPtr<nsINode> targetNode(do_QueryInterface(targetPtr)); + if (!targetNode->IsInComposedDoc()) { + targetPtr = info.mNonAnonymousTarget; + } + touch->SetTouchTarget(targetPtr); + + info.mTouch = touch; + // info.mNonAnonymousTarget is still valid from above + sCaptureTouchList->InsertOrUpdate(id, info); + // if we're moving from touchstart to touchmove for this touch + // we allow preventDefault to prevent mouse events + if (oldTouch->mMessage != touch->mMessage) { + aTouchIsNew = true; + } + if (oldTouch->mIsTouchEventSuppressed) { + touch->mIsTouchEventSuppressed = true; + touches.RemoveElementAt(i); + continue; + } + } + // is nothing has changed, we should just return + if (!haveChanged) { + if (aTouchIsNew) { + // however, if this is the first touchmove after a touchstart, + // it is special in that preventDefault is allowed on it, so + // we must dispatch it to content even if nothing changed. we + // arbitrarily pick the first touch point to be the "changed" + // touch because firing an event with no changed events doesn't + // work. + for (uint32_t i = 0; i < touchEvent->mTouches.Length(); ++i) { + if (touchEvent->mTouches[i]) { + touchEvent->mTouches[i]->mChanged = true; + break; + } + } + } else { + // This touch event isn't going to be dispatched on the main-thread, + // we need to tell it to APZ because returned nsEventStatus is + // unreliable to tell whether the event was preventDefaulted or not. + layers::InputAPZContext::SetDropped(); + return false; + } + } + break; + } + case eTouchEnd: + case eTouchCancel: { + // Remove the changed touches + // need to make sure we only remove touches that are ending here + WidgetTouchEvent* touchEvent = aEvent->AsTouchEvent(); + WidgetTouchEvent::TouchArray& touches = touchEvent->mTouches; + touchEvent->mLayersId = sCaptureTouchLayersId; + for (int32_t i = touches.Length(); i;) { + --i; + Touch* touch = touches[i]; + if (!touch) { + continue; + } + touch->mMessage = aEvent->mMessage; + touch->mChanged = true; + + int32_t id = touch->Identifier(); + TouchInfo info; + if (!sCaptureTouchList->Get(id, &info)) { + continue; + } + nsCOMPtr<EventTarget> targetPtr = info.mTouch->mOriginalTarget; + nsCOMPtr<nsINode> targetNode(do_QueryInterface(targetPtr)); + if (targetNode && !targetNode->IsInComposedDoc()) { + targetPtr = info.mNonAnonymousTarget; + } + + aCurrentEventContent = do_QueryInterface(targetPtr); + touch->SetTouchTarget(targetPtr); + sCaptureTouchList->Remove(id); + if (info.mTouch->mIsTouchEventSuppressed) { + touches.RemoveElementAt(i); + continue; + } + } + // add any touches left in the touch list, but ensure changed=false + AppendToTouchList(&touches); + break; + } + case eTouchPointerCancel: { + // Don't generate pointer events by touch events after eTouchPointerCancel + // is received. + WidgetTouchEvent* touchEvent = aEvent->AsTouchEvent(); + WidgetTouchEvent::TouchArray& touches = touchEvent->mTouches; + touchEvent->mLayersId = sCaptureTouchLayersId; + for (uint32_t i = 0; i < touches.Length(); ++i) { + Touch* touch = touches[i]; + if (!touch) { + continue; + } + int32_t id = touch->Identifier(); + TouchInfo info; + if (!sCaptureTouchList->Get(id, &info)) { + continue; + } + info.mConvertToPointer = false; + sCaptureTouchList->InsertOrUpdate(id, info); + } + break; + } + default: + break; + } + return true; +} + +/*static*/ +already_AddRefed<nsIContent> TouchManager::GetAnyCapturedTouchTarget() { + nsCOMPtr<nsIContent> result = nullptr; + if (sCaptureTouchList->Count() == 0) { + return result.forget(); + } + for (const auto& data : sCaptureTouchList->Values()) { + const RefPtr<Touch>& touch = data.mTouch; + if (touch) { + EventTarget* target = touch->GetTarget(); + if (target) { + result = nsIContent::FromEventTargetOrNull(target); + break; + } + } + } + return result.forget(); +} + +/*static*/ +bool TouchManager::HasCapturedTouch(int32_t aId) { + return sCaptureTouchList->Contains(aId); +} + +/*static*/ +already_AddRefed<Touch> TouchManager::GetCapturedTouch(int32_t aId) { + RefPtr<Touch> touch; + TouchInfo info; + if (sCaptureTouchList->Get(aId, &info)) { + touch = info.mTouch; + } + return touch.forget(); +} + +/*static*/ +bool TouchManager::ShouldConvertTouchToPointer(const Touch* aTouch, + const WidgetTouchEvent* aEvent) { + if (!aTouch || !aTouch->convertToPointer) { + return false; + } + TouchInfo info; + if (!sCaptureTouchList->Get(aTouch->Identifier(), &info)) { + // This check runs before the TouchManager has the touch registered in its + // touch list. It's because we dispatching pointer events before handling + // touch events. So we convert eTouchStart to pointerdown even it's not + // registered. + // Check WidgetTouchEvent::mMessage because Touch::mMessage is assigned when + // pre-handling touch events. + return aEvent->mMessage == eTouchStart; + } + + if (!info.mConvertToPointer) { + return false; + } + + switch (aEvent->mMessage) { + case eTouchStart: { + // We don't want to fire duplicated pointerdown. + return false; + } + case eTouchMove: { + return !aTouch->Equals(info.mTouch); + } + default: + break; + } + return true; +} + +} // namespace mozilla |