TouchManager.cpp (20615B)
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/. 6 */ 7 8 #include "TouchManager.h" 9 10 #include "PositionedEventTargeting.h" 11 #include "Units.h" 12 #include "mozilla/EventForwards.h" 13 #include "mozilla/PresShell.h" 14 #include "mozilla/StaticPrefs_apz.h" 15 #include "mozilla/StaticPrefs_test.h" 16 #include "mozilla/TimeStamp.h" 17 #include "mozilla/dom/Document.h" 18 #include "mozilla/dom/EventTarget.h" 19 #include "mozilla/dom/PointerEventHandler.h" 20 #include "mozilla/layers/InputAPZContext.h" 21 #include "nsIContent.h" 22 #include "nsIFrame.h" 23 #include "nsLayoutUtils.h" 24 25 using namespace mozilla::dom; 26 27 namespace mozilla { 28 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; 35 36 /*static*/ 37 void TouchManager::InitializeStatics() { 38 NS_ASSERTION(!sCaptureTouchList, "InitializeStatics called multiple times!"); 39 sCaptureTouchList = new nsTHashMap<nsUint32HashKey, TouchManager::TouchInfo>; 40 sCaptureTouchLayersId = layers::LayersId{0}; 41 } 42 43 /*static*/ 44 void TouchManager::ReleaseStatics() { 45 NS_ASSERTION(sCaptureTouchList, "ReleaseStatics called without Initialize!"); 46 sCaptureTouchList = nullptr; 47 } 48 49 void TouchManager::Init(PresShell* aPresShell, Document* aDocument) { 50 mPresShell = aPresShell; 51 mDocument = aDocument; 52 } 53 54 void TouchManager::Destroy() { 55 EvictTouches(mDocument); 56 mDocument = nullptr; 57 mPresShell = nullptr; 58 } 59 60 static nsIContent* GetNonAnonymousAncestor(EventTarget* aTarget) { 61 nsIContent* content = nsIContent::FromEventTargetOrNull(aTarget); 62 if (content && content->IsInNativeAnonymousSubtree()) { 63 content = content->FindFirstNonChromeOnlyAccessContent(); 64 } 65 return content; 66 } 67 68 /*static*/ 69 void TouchManager::EvictTouchPoint(RefPtr<Touch>& aTouch, 70 Document* aLimitToDocument) { 71 nsCOMPtr<nsINode> node( 72 nsINode::FromEventTargetOrNull(aTouch->mOriginalTarget)); 73 if (node) { 74 Document* doc = node->GetComposedDoc(); 75 if (doc && (!aLimitToDocument || aLimitToDocument == doc)) { 76 if (PresShell* presShell = doc->GetPresShell()) { 77 if (nsIFrame* frame = presShell->GetRootFrame()) { 78 if (nsCOMPtr<nsIWidget> widget = frame->GetNearestWidget()) { 79 WidgetTouchEvent event(true, eTouchEnd, widget); 80 event.mTouches.AppendElement(aTouch); 81 widget->DispatchEvent(&event); 82 } 83 } 84 } 85 } 86 } 87 if (!node || !aLimitToDocument || node->OwnerDoc() == aLimitToDocument) { 88 sCaptureTouchList->Remove(aTouch->Identifier()); 89 } 90 } 91 92 /*static*/ 93 void TouchManager::AppendToTouchList( 94 WidgetTouchEvent::TouchArrayBase* aTouchList) { 95 for (const auto& data : sCaptureTouchList->Values()) { 96 const RefPtr<Touch>& touch = data.mTouch; 97 touch->mChanged = false; 98 aTouchList->AppendElement(touch); 99 } 100 } 101 102 void TouchManager::EvictTouches(Document* aLimitToDocument) { 103 WidgetTouchEvent::AutoTouchArray touches; 104 AppendToTouchList(&touches); 105 for (uint32_t i = 0; i < touches.Length(); ++i) { 106 EvictTouchPoint(touches[i], aLimitToDocument); 107 } 108 sCaptureTouchLayersId = layers::LayersId{0}; 109 } 110 111 /* static */ 112 nsIFrame* TouchManager::SetupTarget(WidgetTouchEvent* aEvent, 113 nsIFrame* aFrame) { 114 MOZ_ASSERT(aEvent); 115 116 if (!aEvent || aEvent->mMessage != eTouchStart) { 117 // All touch events except for touchstart use a captured target. 118 return aFrame; 119 } 120 121 nsIFrame* target = aFrame; 122 for (int32_t i = aEvent->mTouches.Length(); i;) { 123 --i; 124 dom::Touch* touch = aEvent->mTouches[i]; 125 126 int32_t id = touch->Identifier(); 127 if (!TouchManager::HasCapturedTouch(id)) { 128 // find the target for this touch 129 RelativeTo relativeTo{aFrame}; 130 nsPoint eventPoint = nsLayoutUtils::GetEventCoordinatesRelativeTo( 131 aEvent, touch->mRefPoint, relativeTo); 132 target = FindFrameTargetedByInputEvent(aEvent, relativeTo, eventPoint); 133 if (target) { 134 nsIContent* targetContent = target->GetContentForEvent(aEvent); 135 touch->SetTouchTarget(targetContent 136 ? targetContent->GetAsElementOrParentElement() 137 : nullptr); 138 } else { 139 aEvent->mTouches.RemoveElementAt(i); 140 } 141 } else { 142 // This touch is an old touch, we need to ensure that is not 143 // marked as changed and set its target correctly 144 touch->mChanged = false; 145 RefPtr<dom::Touch> oldTouch = TouchManager::GetCapturedTouch(id); 146 if (oldTouch) { 147 touch->SetTouchTarget(oldTouch->mOriginalTarget); 148 } 149 } 150 } 151 return target; 152 } 153 154 /* static */ 155 nsIFrame* TouchManager::SuppressInvalidPointsAndGetTargetedFrame( 156 WidgetTouchEvent* aEvent) { 157 MOZ_ASSERT(aEvent); 158 159 if (!aEvent || aEvent->mMessage != eTouchStart) { 160 // All touch events except for touchstart use a captured target. 161 return nullptr; 162 } 163 164 // if this is a continuing session, ensure that all these events are 165 // in the same document by taking the target of the events already in 166 // the capture list 167 nsCOMPtr<nsIContent> anyTarget; 168 if (aEvent->mTouches.Length() > 1) { 169 anyTarget = TouchManager::GetAnyCapturedTouchTarget(); 170 } 171 172 nsIFrame* frame = nullptr; 173 for (uint32_t i = aEvent->mTouches.Length(); i;) { 174 --i; 175 dom::Touch* touch = aEvent->mTouches[i]; 176 if (TouchManager::HasCapturedTouch(touch->Identifier())) { 177 continue; 178 } 179 180 MOZ_ASSERT(touch->mOriginalTarget); 181 nsIContent* const targetContent = 182 nsIContent::FromEventTargetOrNull(touch->GetTarget()); 183 if (MOZ_UNLIKELY(!targetContent)) { 184 touch->mIsTouchEventSuppressed = true; 185 continue; 186 } 187 188 // Even if the target content is not connected, we should dispatch the touch 189 // start event except when the target content is owned by different 190 // document. 191 if (MOZ_UNLIKELY(!targetContent->IsInComposedDoc())) { 192 if (anyTarget && anyTarget->OwnerDoc() != targetContent->OwnerDoc()) { 193 touch->mIsTouchEventSuppressed = true; 194 continue; 195 } 196 if (!anyTarget) { 197 anyTarget = targetContent; 198 } 199 touch->SetTouchTarget(targetContent->GetAsElementOrParentElement()); 200 if (PresShell* const presShell = 201 targetContent->OwnerDoc()->GetPresShell()) { 202 if (nsIFrame* rootFrame = presShell->GetRootFrame()) { 203 frame = rootFrame; 204 } 205 } 206 continue; 207 } 208 209 nsIFrame* targetFrame = targetContent->GetPrimaryFrame(); 210 if (targetFrame && !anyTarget) { 211 anyTarget = targetContent; 212 } else { 213 nsIFrame* newTargetFrame = nullptr; 214 for (nsIFrame* f = targetFrame; f; 215 f = nsLayoutUtils::GetParentOrPlaceholderForCrossDoc(f)) { 216 if (f->PresContext()->Document() == anyTarget->OwnerDoc()) { 217 newTargetFrame = f; 218 break; 219 } 220 // We must be in a subdocument so jump directly to the root frame. 221 // GetParentOrPlaceholderForCrossDoc gets called immediately to 222 // jump up to the containing document. 223 f = f->PresShell()->GetRootFrame(); 224 } 225 // if we couldn't find a target frame in the same document as 226 // anyTarget, remove the touch from the capture touch list, as 227 // well as the event->mTouches array. touchmove events that aren't 228 // in the captured touch list will be discarded 229 if (!newTargetFrame) { 230 touch->mIsTouchEventSuppressed = true; 231 } else { 232 targetFrame = newTargetFrame; 233 nsIContent* newTargetContent = targetFrame->GetContentForEvent(aEvent); 234 touch->SetTouchTarget( 235 newTargetContent ? newTargetContent->GetAsElementOrParentElement() 236 : nullptr); 237 } 238 } 239 if (targetFrame) { 240 frame = targetFrame; 241 } 242 } 243 return frame; 244 } 245 246 bool TouchManager::PreHandleEvent(WidgetEvent* aEvent, nsEventStatus* aStatus, 247 bool& aTouchIsNew, 248 nsCOMPtr<nsIContent>& aCurrentEventContent) { 249 MOZ_DIAGNOSTIC_ASSERT(aEvent->IsTrusted()); 250 251 // NOTE: If you need to handle new event messages here, you need to add new 252 // cases in PresShell::EventHandler::PrepareToDispatchEvent(). 253 switch (aEvent->mMessage) { 254 case eTouchStart: { 255 WidgetTouchEvent* touchEvent = aEvent->AsTouchEvent(); 256 // if there is only one touch in this touchstart event, assume that it is 257 // the start of a new touch session and evict any old touches in the 258 // queue 259 if (touchEvent->mTouches.Length() == 1) { 260 EvictTouches(); 261 // Per 262 // https://w3c.github.io/touch-events/#touchevent-implementer-s-note, 263 // all touch event should be dispatched to the same document that first 264 // touch event associated to. We cache layers id of the first touchstart 265 // event, all subsequent touch events will use the same layers id. 266 sCaptureTouchLayersId = aEvent->mLayersId; 267 sSingleTouchStartTimeStamp = aEvent->mTimeStamp; 268 sSingleTouchStartPoint = touchEvent->mTouches[0]->mRefPoint; 269 const PointerInfo* pointerInfo = PointerEventHandler::GetPointerInfo( 270 touchEvent->mTouches[0]->Identifier()); 271 sPrecedingTouchPointerDownConsumedByContent = 272 pointerInfo && pointerInfo->mPreventMouseEventByContent; 273 } else { 274 touchEvent->mLayersId = sCaptureTouchLayersId; 275 sSingleTouchStartTimeStamp = TimeStamp(); 276 } 277 // Add any new touches to the queue 278 WidgetTouchEvent::TouchArray& touches = touchEvent->mTouches; 279 for (int32_t i = touches.Length(); i;) { 280 --i; 281 Touch* touch = touches[i]; 282 int32_t id = touch->Identifier(); 283 if (!sCaptureTouchList->Get(id, nullptr)) { 284 // If it is not already in the queue, it is a new touch 285 touch->mChanged = true; 286 } 287 touch->mMessage = aEvent->mMessage; 288 TouchInfo info = { 289 touch, GetNonAnonymousAncestor(touch->mOriginalTarget), true}; 290 sCaptureTouchList->InsertOrUpdate(id, info); 291 if (touch->mIsTouchEventSuppressed) { 292 // We're going to dispatch touch event. Remove this touch instance if 293 // it is suppressed. 294 touches.RemoveElementAt(i); 295 continue; 296 } 297 } 298 break; 299 } 300 case eTouchRawUpdate: 301 MOZ_ASSERT_UNREACHABLE("eTouchRawUpdate shouldn't be handled as a touch"); 302 break; 303 case eTouchMove: { 304 // Check for touches that changed. Mark them add to queue 305 WidgetTouchEvent* touchEvent = aEvent->AsTouchEvent(); 306 WidgetTouchEvent::TouchArray& touches = touchEvent->mTouches; 307 touchEvent->mLayersId = sCaptureTouchLayersId; 308 bool haveChanged = false; 309 for (int32_t i = touches.Length(); i;) { 310 --i; 311 Touch* touch = touches[i]; 312 if (!touch) { 313 continue; 314 } 315 int32_t id = touch->Identifier(); 316 touch->mMessage = aEvent->mMessage; 317 318 TouchInfo info; 319 if (!sCaptureTouchList->Get(id, &info)) { 320 touches.RemoveElementAt(i); 321 continue; 322 } 323 const RefPtr<Touch> oldTouch = info.mTouch; 324 if (!oldTouch->Equals(touch)) { 325 touch->mChanged = true; 326 haveChanged = true; 327 } 328 329 nsCOMPtr<EventTarget> targetPtr = oldTouch->mOriginalTarget; 330 if (!targetPtr) { 331 touches.RemoveElementAt(i); 332 continue; 333 } 334 nsCOMPtr<nsINode> targetNode(do_QueryInterface(targetPtr)); 335 if (!targetNode->IsInComposedDoc()) { 336 targetPtr = info.mNonAnonymousTarget; 337 } 338 touch->SetTouchTarget(targetPtr); 339 340 info.mTouch = touch; 341 // info.mNonAnonymousTarget is still valid from above 342 sCaptureTouchList->InsertOrUpdate(id, info); 343 // if we're moving from touchstart to touchmove for this touch 344 // we allow preventDefault to prevent mouse events 345 if (oldTouch->mMessage != touch->mMessage) { 346 aTouchIsNew = true; 347 } 348 if (oldTouch->mIsTouchEventSuppressed) { 349 touch->mIsTouchEventSuppressed = true; 350 touches.RemoveElementAt(i); 351 continue; 352 } 353 } 354 // is nothing has changed, we should just return 355 if (!haveChanged) { 356 if (aTouchIsNew) { 357 // however, if this is the first touchmove after a touchstart, 358 // it is special in that preventDefault is allowed on it, so 359 // we must dispatch it to content even if nothing changed. we 360 // arbitrarily pick the first touch point to be the "changed" 361 // touch because firing an event with no changed events doesn't 362 // work. 363 for (uint32_t i = 0; i < touchEvent->mTouches.Length(); ++i) { 364 if (touchEvent->mTouches[i]) { 365 touchEvent->mTouches[i]->mChanged = true; 366 break; 367 } 368 } 369 } else { 370 // This touch event isn't going to be dispatched on the main-thread, 371 // we need to tell it to APZ because returned nsEventStatus is 372 // unreliable to tell whether the event was preventDefaulted or not. 373 layers::InputAPZContext::SetDropped(); 374 return false; 375 } 376 } 377 break; 378 } 379 case eTouchEnd: 380 case eTouchCancel: { 381 // Remove the changed touches 382 // need to make sure we only remove touches that are ending here 383 WidgetTouchEvent* touchEvent = aEvent->AsTouchEvent(); 384 WidgetTouchEvent::TouchArray& touches = touchEvent->mTouches; 385 touchEvent->mLayersId = sCaptureTouchLayersId; 386 for (int32_t i = touches.Length(); i;) { 387 --i; 388 Touch* touch = touches[i]; 389 if (!touch) { 390 continue; 391 } 392 touch->mMessage = aEvent->mMessage; 393 touch->mChanged = true; 394 395 int32_t id = touch->Identifier(); 396 TouchInfo info; 397 if (!sCaptureTouchList->Get(id, &info)) { 398 continue; 399 } 400 nsCOMPtr<EventTarget> targetPtr = info.mTouch->mOriginalTarget; 401 nsCOMPtr<nsINode> targetNode(do_QueryInterface(targetPtr)); 402 if (targetNode && !targetNode->IsInComposedDoc()) { 403 targetPtr = info.mNonAnonymousTarget; 404 } 405 406 aCurrentEventContent = do_QueryInterface(targetPtr); 407 touch->SetTouchTarget(targetPtr); 408 sCaptureTouchList->Remove(id); 409 if (info.mTouch->mIsTouchEventSuppressed) { 410 touches.RemoveElementAt(i); 411 continue; 412 } 413 } 414 // add any touches left in the touch list, but ensure changed=false 415 AppendToTouchList(&touches); 416 break; 417 } 418 case eTouchPointerCancel: { 419 // Don't generate pointer events by touch events after eTouchPointerCancel 420 // is received. 421 WidgetTouchEvent* touchEvent = aEvent->AsTouchEvent(); 422 WidgetTouchEvent::TouchArray& touches = touchEvent->mTouches; 423 touchEvent->mLayersId = sCaptureTouchLayersId; 424 for (uint32_t i = 0; i < touches.Length(); ++i) { 425 Touch* touch = touches[i]; 426 if (!touch) { 427 continue; 428 } 429 int32_t id = touch->Identifier(); 430 TouchInfo info; 431 if (!sCaptureTouchList->Get(id, &info)) { 432 continue; 433 } 434 info.mConvertToPointer = false; 435 sCaptureTouchList->InsertOrUpdate(id, info); 436 } 437 break; 438 } 439 default: 440 break; 441 } 442 return true; 443 } 444 445 void TouchManager::PostHandleEvent(const WidgetEvent* aEvent, 446 const nsEventStatus* aStatus) { 447 switch (aEvent->mMessage) { 448 case eTouchRawUpdate: 449 MOZ_ASSERT_UNREACHABLE("eTouchRawUpdate shouldn't be handled as a touch"); 450 break; 451 case eTouchMove: { 452 if (sSingleTouchStartTimeStamp.IsNull()) { 453 break; 454 } 455 if (*aStatus == nsEventStatus_eConsumeNoDefault) { 456 sSingleTouchStartTimeStamp = TimeStamp(); 457 break; 458 } 459 const WidgetTouchEvent* touchEvent = aEvent->AsTouchEvent(); 460 if (touchEvent->mTouches.Length() > 1) { 461 sSingleTouchStartTimeStamp = TimeStamp(); 462 break; 463 } 464 if (touchEvent->mTouches.Length() == 1) { 465 // If the touch moved too far from the start point, don't treat the 466 // touch as a tap. 467 const float distance = 468 static_cast<float>((sSingleTouchStartPoint - 469 aEvent->AsTouchEvent()->mTouches[0]->mRefPoint) 470 .Length()); 471 const float maxDistance = 472 StaticPrefs::apz_touch_start_tolerance() * 473 (MOZ_LIKELY(touchEvent->mWidget) ? touchEvent->mWidget->GetDPI() 474 : 96.0f); 475 if (distance > maxDistance) { 476 sSingleTouchStartTimeStamp = TimeStamp(); 477 } 478 } 479 break; 480 } 481 case eTouchStart: 482 case eTouchEnd: 483 if (*aStatus == nsEventStatus_eConsumeNoDefault && 484 !sSingleTouchStartTimeStamp.IsNull()) { 485 sSingleTouchStartTimeStamp = TimeStamp(); 486 } 487 break; 488 case eTouchCancel: 489 case eTouchPointerCancel: 490 case eMouseLongTap: 491 case eContextMenu: { 492 if (!sSingleTouchStartTimeStamp.IsNull()) { 493 sSingleTouchStartTimeStamp = TimeStamp(); 494 } 495 break; 496 } 497 default: 498 break; 499 } 500 } 501 502 /*static*/ 503 already_AddRefed<nsIContent> TouchManager::GetAnyCapturedTouchTarget() { 504 nsCOMPtr<nsIContent> result = nullptr; 505 if (sCaptureTouchList->Count() == 0) { 506 return result.forget(); 507 } 508 for (const auto& data : sCaptureTouchList->Values()) { 509 const RefPtr<Touch>& touch = data.mTouch; 510 if (touch) { 511 EventTarget* target = touch->GetTarget(); 512 if (target) { 513 result = nsIContent::FromEventTargetOrNull(target); 514 break; 515 } 516 } 517 } 518 return result.forget(); 519 } 520 521 /*static*/ 522 bool TouchManager::HasCapturedTouch(int32_t aId) { 523 return sCaptureTouchList->Contains(aId); 524 } 525 526 /*static*/ 527 already_AddRefed<Touch> TouchManager::GetCapturedTouch(int32_t aId) { 528 RefPtr<Touch> touch; 529 TouchInfo info; 530 if (sCaptureTouchList->Get(aId, &info)) { 531 touch = info.mTouch; 532 } 533 return touch.forget(); 534 } 535 536 /*static*/ 537 bool TouchManager::ShouldConvertTouchToPointer(const Touch* aTouch, 538 const WidgetTouchEvent* aEvent) { 539 if (!aTouch || !aTouch->convertToPointer) { 540 return false; 541 } 542 TouchInfo info; 543 if (!sCaptureTouchList->Get(aTouch->Identifier(), &info)) { 544 // This check runs before the TouchManager has the touch registered in its 545 // touch list. It's because we dispatching pointer events before handling 546 // touch events. So we convert eTouchStart to pointerdown even it's not 547 // registered. 548 // Check WidgetTouchEvent::mMessage because Touch::mMessage is assigned when 549 // pre-handling touch events. 550 return aEvent->mMessage == eTouchStart; 551 } 552 553 if (!info.mConvertToPointer) { 554 return false; 555 } 556 557 switch (aEvent->mMessage) { 558 case eTouchStart: { 559 // We don't want to fire duplicated pointerdown. 560 return false; 561 } 562 case eTouchMove: 563 case eTouchRawUpdate: { 564 return !aTouch->Equals(info.mTouch); 565 } 566 default: 567 break; 568 } 569 return true; 570 } 571 572 /* static */ 573 bool TouchManager::IsSingleTapEndToDoDefault( 574 const WidgetTouchEvent* aTouchEndEvent) { 575 MOZ_ASSERT(aTouchEndEvent); 576 MOZ_ASSERT(aTouchEndEvent->mFlags.mIsSynthesizedForTests); 577 MOZ_ASSERT(!StaticPrefs::test_events_async_enabled()); 578 if (sSingleTouchStartTimeStamp.IsNull() || 579 aTouchEndEvent->mTouches.Length() != 1) { 580 return false; 581 } 582 // If it's pressed long time, we should not treat it as a single tap because 583 // a long press should cause opening context menu by default. 584 if ((aTouchEndEvent->mTimeStamp - sSingleTouchStartTimeStamp) 585 .ToMilliseconds() > StaticPrefs::apz_max_tap_time()) { 586 return false; 587 } 588 NS_WARNING_ASSERTION(aTouchEndEvent->mTouches[0]->mChanged, 589 "The single tap end should be changed"); 590 return true; 591 } 592 593 /* static */ 594 bool TouchManager::IsPrecedingTouchPointerDownConsumedByContent() { 595 return sPrecedingTouchPointerDownConsumedByContent; 596 } 597 598 } // namespace mozilla