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/layers/InputAPZContext.h"
18 #include "nsIContent.h"
20 #include "nsLayoutUtils.h"
22 #include "PositionedEventTargeting.h"
24 using namespace mozilla::dom
;
28 StaticAutoPtr
<nsTHashMap
<nsUint32HashKey
, TouchManager::TouchInfo
>>
29 TouchManager::sCaptureTouchList
;
30 layers::LayersId
TouchManager::sCaptureTouchLayersId
;
31 TimeStamp
TouchManager::sSingleTouchStartTimeStamp
;
32 LayoutDeviceIntPoint
TouchManager::sSingleTouchStartPoint
;
35 void TouchManager::InitializeStatics() {
36 NS_ASSERTION(!sCaptureTouchList
, "InitializeStatics called multiple times!");
37 sCaptureTouchList
= new nsTHashMap
<nsUint32HashKey
, TouchManager::TouchInfo
>;
38 sCaptureTouchLayersId
= layers::LayersId
{0};
42 void TouchManager::ReleaseStatics() {
43 NS_ASSERTION(sCaptureTouchList
, "ReleaseStatics called without Initialize!");
44 sCaptureTouchList
= nullptr;
47 void TouchManager::Init(PresShell
* aPresShell
, Document
* aDocument
) {
48 mPresShell
= aPresShell
;
49 mDocument
= aDocument
;
52 void TouchManager::Destroy() {
53 EvictTouches(mDocument
);
58 static nsIContent
* GetNonAnonymousAncestor(EventTarget
* aTarget
) {
59 nsIContent
* content
= nsIContent::FromEventTargetOrNull(aTarget
);
60 if (content
&& content
->IsInNativeAnonymousSubtree()) {
61 content
= content
->FindFirstNonChromeOnlyAccessContent();
67 void TouchManager::EvictTouchPoint(RefPtr
<Touch
>& aTouch
,
68 Document
* aLimitToDocument
) {
69 nsCOMPtr
<nsINode
> node(
70 nsINode::FromEventTargetOrNull(aTouch
->mOriginalTarget
));
72 Document
* doc
= node
->GetComposedDoc();
73 if (doc
&& (!aLimitToDocument
|| aLimitToDocument
== doc
)) {
74 PresShell
* presShell
= doc
->GetPresShell();
76 nsIFrame
* frame
= presShell
->GetRootFrame();
78 nsCOMPtr
<nsIWidget
> widget
=
79 frame
->GetView()->GetNearestWidget(nullptr);
81 WidgetTouchEvent
event(true, eTouchEnd
, widget
);
82 event
.mTouches
.AppendElement(aTouch
);
84 widget
->DispatchEvent(&event
, status
);
90 if (!node
|| !aLimitToDocument
|| node
->OwnerDoc() == aLimitToDocument
) {
91 sCaptureTouchList
->Remove(aTouch
->Identifier());
96 void TouchManager::AppendToTouchList(
97 WidgetTouchEvent::TouchArrayBase
* aTouchList
) {
98 for (const auto& data
: sCaptureTouchList
->Values()) {
99 const RefPtr
<Touch
>& touch
= data
.mTouch
;
100 touch
->mChanged
= false;
101 aTouchList
->AppendElement(touch
);
105 void TouchManager::EvictTouches(Document
* aLimitToDocument
) {
106 WidgetTouchEvent::AutoTouchArray touches
;
107 AppendToTouchList(&touches
);
108 for (uint32_t i
= 0; i
< touches
.Length(); ++i
) {
109 EvictTouchPoint(touches
[i
], aLimitToDocument
);
111 sCaptureTouchLayersId
= layers::LayersId
{0};
115 nsIFrame
* TouchManager::SetupTarget(WidgetTouchEvent
* aEvent
,
119 if (!aEvent
|| aEvent
->mMessage
!= eTouchStart
) {
120 // All touch events except for touchstart use a captured target.
124 nsIFrame
* target
= aFrame
;
125 for (int32_t i
= aEvent
->mTouches
.Length(); i
;) {
127 dom::Touch
* touch
= aEvent
->mTouches
[i
];
129 int32_t id
= touch
->Identifier();
130 if (!TouchManager::HasCapturedTouch(id
)) {
131 // find the target for this touch
132 RelativeTo relativeTo
{aFrame
};
133 nsPoint eventPoint
= nsLayoutUtils::GetEventCoordinatesRelativeTo(
134 aEvent
, touch
->mRefPoint
, relativeTo
);
135 target
= FindFrameTargetedByInputEvent(aEvent
, relativeTo
, eventPoint
);
137 nsCOMPtr
<nsIContent
> targetContent
;
138 target
->GetContentForEvent(aEvent
, getter_AddRefs(targetContent
));
139 touch
->SetTouchTarget(targetContent
140 ? targetContent
->GetAsElementOrParentElement()
143 aEvent
->mTouches
.RemoveElementAt(i
);
146 // This touch is an old touch, we need to ensure that is not
147 // marked as changed and set its target correctly
148 touch
->mChanged
= false;
149 RefPtr
<dom::Touch
> oldTouch
= TouchManager::GetCapturedTouch(id
);
151 touch
->SetTouchTarget(oldTouch
->mOriginalTarget
);
159 nsIFrame
* TouchManager::SuppressInvalidPointsAndGetTargetedFrame(
160 WidgetTouchEvent
* aEvent
) {
163 if (!aEvent
|| aEvent
->mMessage
!= eTouchStart
) {
164 // All touch events except for touchstart use a captured target.
168 // if this is a continuing session, ensure that all these events are
169 // in the same document by taking the target of the events already in
171 nsCOMPtr
<nsIContent
> anyTarget
;
172 if (aEvent
->mTouches
.Length() > 1) {
173 anyTarget
= TouchManager::GetAnyCapturedTouchTarget();
176 nsIFrame
* frame
= nullptr;
177 for (uint32_t i
= aEvent
->mTouches
.Length(); i
;) {
179 dom::Touch
* touch
= aEvent
->mTouches
[i
];
180 if (TouchManager::HasCapturedTouch(touch
->Identifier())) {
184 MOZ_ASSERT(touch
->mOriginalTarget
);
185 nsIContent
* const targetContent
=
186 nsIContent::FromEventTargetOrNull(touch
->GetTarget());
187 if (MOZ_UNLIKELY(!targetContent
)) {
188 touch
->mIsTouchEventSuppressed
= true;
192 // Even if the target content is not connected, we should dispatch the touch
193 // start event except when the target content is owned by different
195 if (MOZ_UNLIKELY(!targetContent
->IsInComposedDoc())) {
196 if (anyTarget
&& anyTarget
->OwnerDoc() != targetContent
->OwnerDoc()) {
197 touch
->mIsTouchEventSuppressed
= true;
201 anyTarget
= targetContent
;
203 touch
->SetTouchTarget(targetContent
->GetAsElementOrParentElement());
204 if (PresShell
* const presShell
=
205 targetContent
->OwnerDoc()->GetPresShell()) {
206 if (nsIFrame
* rootFrame
= presShell
->GetRootFrame()) {
213 nsIFrame
* targetFrame
= targetContent
->GetPrimaryFrame();
214 if (targetFrame
&& !anyTarget
) {
215 anyTarget
= targetContent
;
217 nsIFrame
* newTargetFrame
= nullptr;
218 for (nsIFrame
* f
= targetFrame
; f
;
219 f
= nsLayoutUtils::GetParentOrPlaceholderForCrossDoc(f
)) {
220 if (f
->PresContext()->Document() == anyTarget
->OwnerDoc()) {
224 // We must be in a subdocument so jump directly to the root frame.
225 // GetParentOrPlaceholderForCrossDoc gets called immediately to
226 // jump up to the containing document.
227 f
= f
->PresShell()->GetRootFrame();
229 // if we couldn't find a target frame in the same document as
230 // anyTarget, remove the touch from the capture touch list, as
231 // well as the event->mTouches array. touchmove events that aren't
232 // in the captured touch list will be discarded
233 if (!newTargetFrame
) {
234 touch
->mIsTouchEventSuppressed
= true;
236 targetFrame
= newTargetFrame
;
237 nsCOMPtr
<nsIContent
> newTargetContent
;
238 targetFrame
->GetContentForEvent(aEvent
,
239 getter_AddRefs(newTargetContent
));
240 touch
->SetTouchTarget(
241 newTargetContent
? newTargetContent
->GetAsElementOrParentElement()
252 bool TouchManager::PreHandleEvent(WidgetEvent
* aEvent
, nsEventStatus
* aStatus
,
254 nsCOMPtr
<nsIContent
>& aCurrentEventContent
) {
255 MOZ_DIAGNOSTIC_ASSERT(aEvent
->IsTrusted());
257 // NOTE: If you need to handle new event messages here, you need to add new
258 // cases in PresShell::EventHandler::PrepareToDispatchEvent().
259 switch (aEvent
->mMessage
) {
261 WidgetTouchEvent
* touchEvent
= aEvent
->AsTouchEvent();
262 // if there is only one touch in this touchstart event, assume that it is
263 // the start of a new touch session and evict any old touches in the
265 if (touchEvent
->mTouches
.Length() == 1) {
268 // https://w3c.github.io/touch-events/#touchevent-implementer-s-note,
269 // all touch event should be dispatched to the same document that first
270 // touch event associated to. We cache layers id of the first touchstart
271 // event, all subsequent touch events will use the same layers id.
272 sCaptureTouchLayersId
= aEvent
->mLayersId
;
273 sSingleTouchStartTimeStamp
= aEvent
->mTimeStamp
;
274 sSingleTouchStartPoint
= aEvent
->AsTouchEvent()->mTouches
[0]->mRefPoint
;
276 touchEvent
->mLayersId
= sCaptureTouchLayersId
;
277 sSingleTouchStartTimeStamp
= TimeStamp();
279 // Add any new touches to the queue
280 WidgetTouchEvent::TouchArray
& touches
= touchEvent
->mTouches
;
281 for (int32_t i
= touches
.Length(); i
;) {
283 Touch
* touch
= touches
[i
];
284 int32_t id
= touch
->Identifier();
285 if (!sCaptureTouchList
->Get(id
, nullptr)) {
286 // If it is not already in the queue, it is a new touch
287 touch
->mChanged
= true;
289 touch
->mMessage
= aEvent
->mMessage
;
291 touch
, GetNonAnonymousAncestor(touch
->mOriginalTarget
), true};
292 sCaptureTouchList
->InsertOrUpdate(id
, info
);
293 if (touch
->mIsTouchEventSuppressed
) {
294 // We're going to dispatch touch event. Remove this touch instance if
296 touches
.RemoveElementAt(i
);
303 // Check for touches that changed. Mark them add to queue
304 WidgetTouchEvent
* touchEvent
= aEvent
->AsTouchEvent();
305 WidgetTouchEvent::TouchArray
& touches
= touchEvent
->mTouches
;
306 touchEvent
->mLayersId
= sCaptureTouchLayersId
;
307 bool haveChanged
= false;
308 for (int32_t i
= touches
.Length(); i
;) {
310 Touch
* touch
= touches
[i
];
314 int32_t id
= touch
->Identifier();
315 touch
->mMessage
= aEvent
->mMessage
;
318 if (!sCaptureTouchList
->Get(id
, &info
)) {
319 touches
.RemoveElementAt(i
);
322 const RefPtr
<Touch
> oldTouch
= info
.mTouch
;
323 if (!oldTouch
->Equals(touch
)) {
324 touch
->mChanged
= true;
328 nsCOMPtr
<EventTarget
> targetPtr
= oldTouch
->mOriginalTarget
;
330 touches
.RemoveElementAt(i
);
333 nsCOMPtr
<nsINode
> targetNode(do_QueryInterface(targetPtr
));
334 if (!targetNode
->IsInComposedDoc()) {
335 targetPtr
= info
.mNonAnonymousTarget
;
337 touch
->SetTouchTarget(targetPtr
);
340 // info.mNonAnonymousTarget is still valid from above
341 sCaptureTouchList
->InsertOrUpdate(id
, info
);
342 // if we're moving from touchstart to touchmove for this touch
343 // we allow preventDefault to prevent mouse events
344 if (oldTouch
->mMessage
!= touch
->mMessage
) {
347 if (oldTouch
->mIsTouchEventSuppressed
) {
348 touch
->mIsTouchEventSuppressed
= true;
349 touches
.RemoveElementAt(i
);
353 // is nothing has changed, we should just return
356 // however, if this is the first touchmove after a touchstart,
357 // it is special in that preventDefault is allowed on it, so
358 // we must dispatch it to content even if nothing changed. we
359 // arbitrarily pick the first touch point to be the "changed"
360 // touch because firing an event with no changed events doesn't
362 for (uint32_t i
= 0; i
< touchEvent
->mTouches
.Length(); ++i
) {
363 if (touchEvent
->mTouches
[i
]) {
364 touchEvent
->mTouches
[i
]->mChanged
= true;
369 // This touch event isn't going to be dispatched on the main-thread,
370 // we need to tell it to APZ because returned nsEventStatus is
371 // unreliable to tell whether the event was preventDefaulted or not.
372 layers::InputAPZContext::SetDropped();
380 // Remove the changed touches
381 // need to make sure we only remove touches that are ending here
382 WidgetTouchEvent
* touchEvent
= aEvent
->AsTouchEvent();
383 WidgetTouchEvent::TouchArray
& touches
= touchEvent
->mTouches
;
384 touchEvent
->mLayersId
= sCaptureTouchLayersId
;
385 for (int32_t i
= touches
.Length(); i
;) {
387 Touch
* touch
= touches
[i
];
391 touch
->mMessage
= aEvent
->mMessage
;
392 touch
->mChanged
= true;
394 int32_t id
= touch
->Identifier();
396 if (!sCaptureTouchList
->Get(id
, &info
)) {
399 nsCOMPtr
<EventTarget
> targetPtr
= info
.mTouch
->mOriginalTarget
;
400 nsCOMPtr
<nsINode
> targetNode(do_QueryInterface(targetPtr
));
401 if (targetNode
&& !targetNode
->IsInComposedDoc()) {
402 targetPtr
= info
.mNonAnonymousTarget
;
405 aCurrentEventContent
= do_QueryInterface(targetPtr
);
406 touch
->SetTouchTarget(targetPtr
);
407 sCaptureTouchList
->Remove(id
);
408 if (info
.mTouch
->mIsTouchEventSuppressed
) {
409 touches
.RemoveElementAt(i
);
413 // add any touches left in the touch list, but ensure changed=false
414 AppendToTouchList(&touches
);
417 case eTouchPointerCancel
: {
418 // Don't generate pointer events by touch events after eTouchPointerCancel
420 WidgetTouchEvent
* touchEvent
= aEvent
->AsTouchEvent();
421 WidgetTouchEvent::TouchArray
& touches
= touchEvent
->mTouches
;
422 touchEvent
->mLayersId
= sCaptureTouchLayersId
;
423 for (uint32_t i
= 0; i
< touches
.Length(); ++i
) {
424 Touch
* touch
= touches
[i
];
428 int32_t id
= touch
->Identifier();
430 if (!sCaptureTouchList
->Get(id
, &info
)) {
433 info
.mConvertToPointer
= false;
434 sCaptureTouchList
->InsertOrUpdate(id
, info
);
444 void TouchManager::PostHandleEvent(const WidgetEvent
* aEvent
,
445 const nsEventStatus
* aStatus
) {
446 switch (aEvent
->mMessage
) {
448 if (sSingleTouchStartTimeStamp
.IsNull()) {
451 if (*aStatus
== nsEventStatus_eConsumeNoDefault
) {
452 sSingleTouchStartTimeStamp
= TimeStamp();
455 const WidgetTouchEvent
* touchEvent
= aEvent
->AsTouchEvent();
456 if (touchEvent
->mTouches
.Length() > 1) {
457 sSingleTouchStartTimeStamp
= TimeStamp();
460 if (touchEvent
->mTouches
.Length() == 1) {
461 // If the touch moved too far from the start point, don't treat the
463 const float distance
=
464 static_cast<float>((sSingleTouchStartPoint
-
465 aEvent
->AsTouchEvent()->mTouches
[0]->mRefPoint
)
467 const float maxDistance
=
468 StaticPrefs::apz_touch_start_tolerance() *
469 (MOZ_LIKELY(touchEvent
->mWidget
) ? touchEvent
->mWidget
->GetDPI()
471 if (distance
> maxDistance
) {
472 sSingleTouchStartTimeStamp
= TimeStamp();
479 if (*aStatus
== nsEventStatus_eConsumeNoDefault
&&
480 !sSingleTouchStartTimeStamp
.IsNull()) {
481 sSingleTouchStartTimeStamp
= TimeStamp();
485 case eTouchPointerCancel
:
488 if (!sSingleTouchStartTimeStamp
.IsNull()) {
489 sSingleTouchStartTimeStamp
= TimeStamp();
499 already_AddRefed
<nsIContent
> TouchManager::GetAnyCapturedTouchTarget() {
500 nsCOMPtr
<nsIContent
> result
= nullptr;
501 if (sCaptureTouchList
->Count() == 0) {
502 return result
.forget();
504 for (const auto& data
: sCaptureTouchList
->Values()) {
505 const RefPtr
<Touch
>& touch
= data
.mTouch
;
507 EventTarget
* target
= touch
->GetTarget();
509 result
= nsIContent::FromEventTargetOrNull(target
);
514 return result
.forget();
518 bool TouchManager::HasCapturedTouch(int32_t aId
) {
519 return sCaptureTouchList
->Contains(aId
);
523 already_AddRefed
<Touch
> TouchManager::GetCapturedTouch(int32_t aId
) {
526 if (sCaptureTouchList
->Get(aId
, &info
)) {
529 return touch
.forget();
533 bool TouchManager::ShouldConvertTouchToPointer(const Touch
* aTouch
,
534 const WidgetTouchEvent
* aEvent
) {
535 if (!aTouch
|| !aTouch
->convertToPointer
) {
539 if (!sCaptureTouchList
->Get(aTouch
->Identifier(), &info
)) {
540 // This check runs before the TouchManager has the touch registered in its
541 // touch list. It's because we dispatching pointer events before handling
542 // touch events. So we convert eTouchStart to pointerdown even it's not
544 // Check WidgetTouchEvent::mMessage because Touch::mMessage is assigned when
545 // pre-handling touch events.
546 return aEvent
->mMessage
== eTouchStart
;
549 if (!info
.mConvertToPointer
) {
553 switch (aEvent
->mMessage
) {
555 // We don't want to fire duplicated pointerdown.
559 return !aTouch
->Equals(info
.mTouch
);
568 bool TouchManager::IsSingleTapEndToDoDefault(
569 const WidgetTouchEvent
* aTouchEndEvent
) {
570 MOZ_ASSERT(aTouchEndEvent
);
571 MOZ_ASSERT(aTouchEndEvent
->mFlags
.mIsSynthesizedForTests
);
572 MOZ_ASSERT(!StaticPrefs::test_events_async_enabled());
573 if (sSingleTouchStartTimeStamp
.IsNull() ||
574 aTouchEndEvent
->mTouches
.Length() != 1) {
577 // If it's pressed long time, we should not treat it as a single tap because
578 // a long press should cause opening context menu by default.
579 if ((aTouchEndEvent
->mTimeStamp
- sSingleTouchStartTimeStamp
)
580 .ToMilliseconds() > StaticPrefs::apz_max_tap_time()) {
583 NS_WARNING_ASSERTION(aTouchEndEvent
->mTouches
[0]->mChanged
,
584 "The single tap end should be changed");
588 } // namespace mozilla