1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
8 #include "TouchManager.h"
11 #include "mozilla/EventForwards.h"
12 #include "mozilla/PresShell.h"
13 #include "mozilla/StaticPrefs_test.h"
14 #include "mozilla/TimeStamp.h"
15 #include "mozilla/dom/Document.h"
16 #include "mozilla/dom/EventTarget.h"
17 #include "mozilla/dom/PointerEventHandler.h"
18 #include "mozilla/layers/InputAPZContext.h"
19 #include "nsIContent.h"
21 #include "nsLayoutUtils.h"
23 #include "PositionedEventTargeting.h"
25 using namespace mozilla::dom
;
29 StaticAutoPtr
<nsTHashMap
<nsUint32HashKey
, TouchManager::TouchInfo
>>
30 TouchManager::sCaptureTouchList
;
31 layers::LayersId
TouchManager::sCaptureTouchLayersId
;
32 TimeStamp
TouchManager::sSingleTouchStartTimeStamp
;
33 LayoutDeviceIntPoint
TouchManager::sSingleTouchStartPoint
;
34 bool TouchManager::sPrecedingTouchPointerDownConsumedByContent
= false;
37 void TouchManager::InitializeStatics() {
38 NS_ASSERTION(!sCaptureTouchList
, "InitializeStatics called multiple times!");
39 sCaptureTouchList
= new nsTHashMap
<nsUint32HashKey
, TouchManager::TouchInfo
>;
40 sCaptureTouchLayersId
= layers::LayersId
{0};
44 void TouchManager::ReleaseStatics() {
45 NS_ASSERTION(sCaptureTouchList
, "ReleaseStatics called without Initialize!");
46 sCaptureTouchList
= nullptr;
49 void TouchManager::Init(PresShell
* aPresShell
, Document
* aDocument
) {
50 mPresShell
= aPresShell
;
51 mDocument
= aDocument
;
54 void TouchManager::Destroy() {
55 EvictTouches(mDocument
);
60 static nsIContent
* GetNonAnonymousAncestor(EventTarget
* aTarget
) {
61 nsIContent
* content
= nsIContent::FromEventTargetOrNull(aTarget
);
62 if (content
&& content
->IsInNativeAnonymousSubtree()) {
63 content
= content
->FindFirstNonChromeOnlyAccessContent();
69 void TouchManager::EvictTouchPoint(RefPtr
<Touch
>& aTouch
,
70 Document
* aLimitToDocument
) {
71 nsCOMPtr
<nsINode
> node(
72 nsINode::FromEventTargetOrNull(aTouch
->mOriginalTarget
));
74 Document
* doc
= node
->GetComposedDoc();
75 if (doc
&& (!aLimitToDocument
|| aLimitToDocument
== doc
)) {
76 PresShell
* presShell
= doc
->GetPresShell();
78 nsIFrame
* frame
= presShell
->GetRootFrame();
80 nsCOMPtr
<nsIWidget
> widget
=
81 frame
->GetView()->GetNearestWidget(nullptr);
83 WidgetTouchEvent
event(true, eTouchEnd
, widget
);
84 event
.mTouches
.AppendElement(aTouch
);
86 widget
->DispatchEvent(&event
, status
);
92 if (!node
|| !aLimitToDocument
|| node
->OwnerDoc() == aLimitToDocument
) {
93 sCaptureTouchList
->Remove(aTouch
->Identifier());
98 void TouchManager::AppendToTouchList(
99 WidgetTouchEvent::TouchArrayBase
* aTouchList
) {
100 for (const auto& data
: sCaptureTouchList
->Values()) {
101 const RefPtr
<Touch
>& touch
= data
.mTouch
;
102 touch
->mChanged
= false;
103 aTouchList
->AppendElement(touch
);
107 void TouchManager::EvictTouches(Document
* aLimitToDocument
) {
108 WidgetTouchEvent::AutoTouchArray touches
;
109 AppendToTouchList(&touches
);
110 for (uint32_t i
= 0; i
< touches
.Length(); ++i
) {
111 EvictTouchPoint(touches
[i
], aLimitToDocument
);
113 sCaptureTouchLayersId
= layers::LayersId
{0};
117 nsIFrame
* TouchManager::SetupTarget(WidgetTouchEvent
* aEvent
,
121 if (!aEvent
|| aEvent
->mMessage
!= eTouchStart
) {
122 // All touch events except for touchstart use a captured target.
126 nsIFrame
* target
= aFrame
;
127 for (int32_t i
= aEvent
->mTouches
.Length(); i
;) {
129 dom::Touch
* touch
= aEvent
->mTouches
[i
];
131 int32_t id
= touch
->Identifier();
132 if (!TouchManager::HasCapturedTouch(id
)) {
133 // find the target for this touch
134 RelativeTo relativeTo
{aFrame
};
135 nsPoint eventPoint
= nsLayoutUtils::GetEventCoordinatesRelativeTo(
136 aEvent
, touch
->mRefPoint
, relativeTo
);
137 target
= FindFrameTargetedByInputEvent(aEvent
, relativeTo
, eventPoint
);
139 nsCOMPtr
<nsIContent
> targetContent
;
140 target
->GetContentForEvent(aEvent
, getter_AddRefs(targetContent
));
141 touch
->SetTouchTarget(targetContent
142 ? targetContent
->GetAsElementOrParentElement()
145 aEvent
->mTouches
.RemoveElementAt(i
);
148 // This touch is an old touch, we need to ensure that is not
149 // marked as changed and set its target correctly
150 touch
->mChanged
= false;
151 RefPtr
<dom::Touch
> oldTouch
= TouchManager::GetCapturedTouch(id
);
153 touch
->SetTouchTarget(oldTouch
->mOriginalTarget
);
161 nsIFrame
* TouchManager::SuppressInvalidPointsAndGetTargetedFrame(
162 WidgetTouchEvent
* aEvent
) {
165 if (!aEvent
|| aEvent
->mMessage
!= eTouchStart
) {
166 // All touch events except for touchstart use a captured target.
170 // if this is a continuing session, ensure that all these events are
171 // in the same document by taking the target of the events already in
173 nsCOMPtr
<nsIContent
> anyTarget
;
174 if (aEvent
->mTouches
.Length() > 1) {
175 anyTarget
= TouchManager::GetAnyCapturedTouchTarget();
178 nsIFrame
* frame
= nullptr;
179 for (uint32_t i
= aEvent
->mTouches
.Length(); i
;) {
181 dom::Touch
* touch
= aEvent
->mTouches
[i
];
182 if (TouchManager::HasCapturedTouch(touch
->Identifier())) {
186 MOZ_ASSERT(touch
->mOriginalTarget
);
187 nsIContent
* const targetContent
=
188 nsIContent::FromEventTargetOrNull(touch
->GetTarget());
189 if (MOZ_UNLIKELY(!targetContent
)) {
190 touch
->mIsTouchEventSuppressed
= true;
194 // Even if the target content is not connected, we should dispatch the touch
195 // start event except when the target content is owned by different
197 if (MOZ_UNLIKELY(!targetContent
->IsInComposedDoc())) {
198 if (anyTarget
&& anyTarget
->OwnerDoc() != targetContent
->OwnerDoc()) {
199 touch
->mIsTouchEventSuppressed
= true;
203 anyTarget
= targetContent
;
205 touch
->SetTouchTarget(targetContent
->GetAsElementOrParentElement());
206 if (PresShell
* const presShell
=
207 targetContent
->OwnerDoc()->GetPresShell()) {
208 if (nsIFrame
* rootFrame
= presShell
->GetRootFrame()) {
215 nsIFrame
* targetFrame
= targetContent
->GetPrimaryFrame();
216 if (targetFrame
&& !anyTarget
) {
217 anyTarget
= targetContent
;
219 nsIFrame
* newTargetFrame
= nullptr;
220 for (nsIFrame
* f
= targetFrame
; f
;
221 f
= nsLayoutUtils::GetParentOrPlaceholderForCrossDoc(f
)) {
222 if (f
->PresContext()->Document() == anyTarget
->OwnerDoc()) {
226 // We must be in a subdocument so jump directly to the root frame.
227 // GetParentOrPlaceholderForCrossDoc gets called immediately to
228 // jump up to the containing document.
229 f
= f
->PresShell()->GetRootFrame();
231 // if we couldn't find a target frame in the same document as
232 // anyTarget, remove the touch from the capture touch list, as
233 // well as the event->mTouches array. touchmove events that aren't
234 // in the captured touch list will be discarded
235 if (!newTargetFrame
) {
236 touch
->mIsTouchEventSuppressed
= true;
238 targetFrame
= newTargetFrame
;
239 nsCOMPtr
<nsIContent
> newTargetContent
;
240 targetFrame
->GetContentForEvent(aEvent
,
241 getter_AddRefs(newTargetContent
));
242 touch
->SetTouchTarget(
243 newTargetContent
? newTargetContent
->GetAsElementOrParentElement()
254 bool TouchManager::PreHandleEvent(WidgetEvent
* aEvent
, nsEventStatus
* aStatus
,
256 nsCOMPtr
<nsIContent
>& aCurrentEventContent
) {
257 MOZ_DIAGNOSTIC_ASSERT(aEvent
->IsTrusted());
259 // NOTE: If you need to handle new event messages here, you need to add new
260 // cases in PresShell::EventHandler::PrepareToDispatchEvent().
261 switch (aEvent
->mMessage
) {
263 WidgetTouchEvent
* touchEvent
= aEvent
->AsTouchEvent();
264 // if there is only one touch in this touchstart event, assume that it is
265 // the start of a new touch session and evict any old touches in the
267 if (touchEvent
->mTouches
.Length() == 1) {
270 // https://w3c.github.io/touch-events/#touchevent-implementer-s-note,
271 // all touch event should be dispatched to the same document that first
272 // touch event associated to. We cache layers id of the first touchstart
273 // event, all subsequent touch events will use the same layers id.
274 sCaptureTouchLayersId
= aEvent
->mLayersId
;
275 sSingleTouchStartTimeStamp
= aEvent
->mTimeStamp
;
276 sSingleTouchStartPoint
= touchEvent
->mTouches
[0]->mRefPoint
;
277 const PointerInfo
* pointerInfo
= PointerEventHandler::GetPointerInfo(
278 touchEvent
->mTouches
[0]->Identifier());
279 sPrecedingTouchPointerDownConsumedByContent
=
280 pointerInfo
&& pointerInfo
->mPreventMouseEventByContent
;
282 touchEvent
->mLayersId
= sCaptureTouchLayersId
;
283 sSingleTouchStartTimeStamp
= TimeStamp();
285 // Add any new touches to the queue
286 WidgetTouchEvent::TouchArray
& touches
= touchEvent
->mTouches
;
287 for (int32_t i
= touches
.Length(); i
;) {
289 Touch
* touch
= touches
[i
];
290 int32_t id
= touch
->Identifier();
291 if (!sCaptureTouchList
->Get(id
, nullptr)) {
292 // If it is not already in the queue, it is a new touch
293 touch
->mChanged
= true;
295 touch
->mMessage
= aEvent
->mMessage
;
297 touch
, GetNonAnonymousAncestor(touch
->mOriginalTarget
), true};
298 sCaptureTouchList
->InsertOrUpdate(id
, info
);
299 if (touch
->mIsTouchEventSuppressed
) {
300 // We're going to dispatch touch event. Remove this touch instance if
302 touches
.RemoveElementAt(i
);
309 // Check for touches that changed. Mark them add to queue
310 WidgetTouchEvent
* touchEvent
= aEvent
->AsTouchEvent();
311 WidgetTouchEvent::TouchArray
& touches
= touchEvent
->mTouches
;
312 touchEvent
->mLayersId
= sCaptureTouchLayersId
;
313 bool haveChanged
= false;
314 for (int32_t i
= touches
.Length(); i
;) {
316 Touch
* touch
= touches
[i
];
320 int32_t id
= touch
->Identifier();
321 touch
->mMessage
= aEvent
->mMessage
;
324 if (!sCaptureTouchList
->Get(id
, &info
)) {
325 touches
.RemoveElementAt(i
);
328 const RefPtr
<Touch
> oldTouch
= info
.mTouch
;
329 if (!oldTouch
->Equals(touch
)) {
330 touch
->mChanged
= true;
334 nsCOMPtr
<EventTarget
> targetPtr
= oldTouch
->mOriginalTarget
;
336 touches
.RemoveElementAt(i
);
339 nsCOMPtr
<nsINode
> targetNode(do_QueryInterface(targetPtr
));
340 if (!targetNode
->IsInComposedDoc()) {
341 targetPtr
= info
.mNonAnonymousTarget
;
343 touch
->SetTouchTarget(targetPtr
);
346 // info.mNonAnonymousTarget is still valid from above
347 sCaptureTouchList
->InsertOrUpdate(id
, info
);
348 // if we're moving from touchstart to touchmove for this touch
349 // we allow preventDefault to prevent mouse events
350 if (oldTouch
->mMessage
!= touch
->mMessage
) {
353 if (oldTouch
->mIsTouchEventSuppressed
) {
354 touch
->mIsTouchEventSuppressed
= true;
355 touches
.RemoveElementAt(i
);
359 // is nothing has changed, we should just return
362 // however, if this is the first touchmove after a touchstart,
363 // it is special in that preventDefault is allowed on it, so
364 // we must dispatch it to content even if nothing changed. we
365 // arbitrarily pick the first touch point to be the "changed"
366 // touch because firing an event with no changed events doesn't
368 for (uint32_t i
= 0; i
< touchEvent
->mTouches
.Length(); ++i
) {
369 if (touchEvent
->mTouches
[i
]) {
370 touchEvent
->mTouches
[i
]->mChanged
= true;
375 // This touch event isn't going to be dispatched on the main-thread,
376 // we need to tell it to APZ because returned nsEventStatus is
377 // unreliable to tell whether the event was preventDefaulted or not.
378 layers::InputAPZContext::SetDropped();
386 // Remove the changed touches
387 // need to make sure we only remove touches that are ending here
388 WidgetTouchEvent
* touchEvent
= aEvent
->AsTouchEvent();
389 WidgetTouchEvent::TouchArray
& touches
= touchEvent
->mTouches
;
390 touchEvent
->mLayersId
= sCaptureTouchLayersId
;
391 for (int32_t i
= touches
.Length(); i
;) {
393 Touch
* touch
= touches
[i
];
397 touch
->mMessage
= aEvent
->mMessage
;
398 touch
->mChanged
= true;
400 int32_t id
= touch
->Identifier();
402 if (!sCaptureTouchList
->Get(id
, &info
)) {
405 nsCOMPtr
<EventTarget
> targetPtr
= info
.mTouch
->mOriginalTarget
;
406 nsCOMPtr
<nsINode
> targetNode(do_QueryInterface(targetPtr
));
407 if (targetNode
&& !targetNode
->IsInComposedDoc()) {
408 targetPtr
= info
.mNonAnonymousTarget
;
411 aCurrentEventContent
= do_QueryInterface(targetPtr
);
412 touch
->SetTouchTarget(targetPtr
);
413 sCaptureTouchList
->Remove(id
);
414 if (info
.mTouch
->mIsTouchEventSuppressed
) {
415 touches
.RemoveElementAt(i
);
419 // add any touches left in the touch list, but ensure changed=false
420 AppendToTouchList(&touches
);
423 case eTouchPointerCancel
: {
424 // Don't generate pointer events by touch events after eTouchPointerCancel
426 WidgetTouchEvent
* touchEvent
= aEvent
->AsTouchEvent();
427 WidgetTouchEvent::TouchArray
& touches
= touchEvent
->mTouches
;
428 touchEvent
->mLayersId
= sCaptureTouchLayersId
;
429 for (uint32_t i
= 0; i
< touches
.Length(); ++i
) {
430 Touch
* touch
= touches
[i
];
434 int32_t id
= touch
->Identifier();
436 if (!sCaptureTouchList
->Get(id
, &info
)) {
439 info
.mConvertToPointer
= false;
440 sCaptureTouchList
->InsertOrUpdate(id
, info
);
450 void TouchManager::PostHandleEvent(const WidgetEvent
* aEvent
,
451 const nsEventStatus
* aStatus
) {
452 switch (aEvent
->mMessage
) {
454 if (sSingleTouchStartTimeStamp
.IsNull()) {
457 if (*aStatus
== nsEventStatus_eConsumeNoDefault
) {
458 sSingleTouchStartTimeStamp
= TimeStamp();
461 const WidgetTouchEvent
* touchEvent
= aEvent
->AsTouchEvent();
462 if (touchEvent
->mTouches
.Length() > 1) {
463 sSingleTouchStartTimeStamp
= TimeStamp();
466 if (touchEvent
->mTouches
.Length() == 1) {
467 // If the touch moved too far from the start point, don't treat the
469 const float distance
=
470 static_cast<float>((sSingleTouchStartPoint
-
471 aEvent
->AsTouchEvent()->mTouches
[0]->mRefPoint
)
473 const float maxDistance
=
474 StaticPrefs::apz_touch_start_tolerance() *
475 (MOZ_LIKELY(touchEvent
->mWidget
) ? touchEvent
->mWidget
->GetDPI()
477 if (distance
> maxDistance
) {
478 sSingleTouchStartTimeStamp
= TimeStamp();
485 if (*aStatus
== nsEventStatus_eConsumeNoDefault
&&
486 !sSingleTouchStartTimeStamp
.IsNull()) {
487 sSingleTouchStartTimeStamp
= TimeStamp();
491 case eTouchPointerCancel
:
494 if (!sSingleTouchStartTimeStamp
.IsNull()) {
495 sSingleTouchStartTimeStamp
= TimeStamp();
505 already_AddRefed
<nsIContent
> TouchManager::GetAnyCapturedTouchTarget() {
506 nsCOMPtr
<nsIContent
> result
= nullptr;
507 if (sCaptureTouchList
->Count() == 0) {
508 return result
.forget();
510 for (const auto& data
: sCaptureTouchList
->Values()) {
511 const RefPtr
<Touch
>& touch
= data
.mTouch
;
513 EventTarget
* target
= touch
->GetTarget();
515 result
= nsIContent::FromEventTargetOrNull(target
);
520 return result
.forget();
524 bool TouchManager::HasCapturedTouch(int32_t aId
) {
525 return sCaptureTouchList
->Contains(aId
);
529 already_AddRefed
<Touch
> TouchManager::GetCapturedTouch(int32_t aId
) {
532 if (sCaptureTouchList
->Get(aId
, &info
)) {
535 return touch
.forget();
539 bool TouchManager::ShouldConvertTouchToPointer(const Touch
* aTouch
,
540 const WidgetTouchEvent
* aEvent
) {
541 if (!aTouch
|| !aTouch
->convertToPointer
) {
545 if (!sCaptureTouchList
->Get(aTouch
->Identifier(), &info
)) {
546 // This check runs before the TouchManager has the touch registered in its
547 // touch list. It's because we dispatching pointer events before handling
548 // touch events. So we convert eTouchStart to pointerdown even it's not
550 // Check WidgetTouchEvent::mMessage because Touch::mMessage is assigned when
551 // pre-handling touch events.
552 return aEvent
->mMessage
== eTouchStart
;
555 if (!info
.mConvertToPointer
) {
559 switch (aEvent
->mMessage
) {
561 // We don't want to fire duplicated pointerdown.
565 return !aTouch
->Equals(info
.mTouch
);
574 bool TouchManager::IsSingleTapEndToDoDefault(
575 const WidgetTouchEvent
* aTouchEndEvent
) {
576 MOZ_ASSERT(aTouchEndEvent
);
577 MOZ_ASSERT(aTouchEndEvent
->mFlags
.mIsSynthesizedForTests
);
578 MOZ_ASSERT(!StaticPrefs::test_events_async_enabled());
579 if (sSingleTouchStartTimeStamp
.IsNull() ||
580 aTouchEndEvent
->mTouches
.Length() != 1) {
583 // If it's pressed long time, we should not treat it as a single tap because
584 // a long press should cause opening context menu by default.
585 if ((aTouchEndEvent
->mTimeStamp
- sSingleTouchStartTimeStamp
)
586 .ToMilliseconds() > StaticPrefs::apz_max_tap_time()) {
589 NS_WARNING_ASSERTION(aTouchEndEvent
->mTouches
[0]->mChanged
,
590 "The single tap end should be changed");
595 bool TouchManager::IsPrecedingTouchPointerDownConsumedByContent() {
596 return sPrecedingTouchPointerDownConsumedByContent
;
599 } // namespace mozilla