PositionedEventTargeting.cpp (35877B)
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 #include "PositionedEventTargeting.h" 8 9 #include <algorithm> 10 11 #include "Units.h" 12 #include "mozilla/EventListenerManager.h" 13 #include "mozilla/MouseEvents.h" 14 #include "mozilla/Preferences.h" 15 #include "mozilla/PresShell.h" 16 #include "mozilla/Result.h" 17 #include "mozilla/StaticPrefs_dom.h" 18 #include "mozilla/StaticPrefs_ui.h" 19 #include "mozilla/ToString.h" 20 #include "mozilla/ViewportUtils.h" 21 #include "mozilla/dom/DOMIntersectionObserver.h" 22 #include "mozilla/dom/Element.h" 23 #include "mozilla/dom/MouseEventBinding.h" 24 #include "mozilla/dom/TouchEvent.h" 25 #include "mozilla/gfx/Matrix.h" 26 #include "mozilla/layers/LayersTypes.h" 27 #include "nsContainerFrame.h" 28 #include "nsCoord.h" 29 #include "nsDeviceContext.h" 30 #include "nsFontMetrics.h" 31 #include "nsFrameList.h" // for DEBUG_FRAME_DUMP 32 #include "nsGkAtoms.h" 33 #include "nsHTMLParts.h" 34 #include "nsIContentInlines.h" 35 #include "nsIFrame.h" 36 #include "nsLayoutUtils.h" 37 #include "nsPresContext.h" 38 #include "nsPrintfCString.h" 39 #include "nsRect.h" 40 #include "nsRegion.h" 41 42 using namespace mozilla; 43 using namespace mozilla::dom; 44 45 // If debugging this code you may wish to enable this logging, via 46 // the env var MOZ_LOG="event.retarget:4". For extra logging (getting 47 // frame dumps, use MOZ_LOG="event.retarget:5". 48 static mozilla::LazyLogModule sEvtTgtLog("event.retarget"); 49 #define PET_LOG(...) MOZ_LOG(sEvtTgtLog, LogLevel::Debug, (__VA_ARGS__)) 50 #define PET_LOG_ENABLED() MOZ_LOG_TEST(sEvtTgtLog, LogLevel::Debug) 51 52 namespace mozilla { 53 54 /* 55 * The basic goal of FindFrameTargetedByInputEvent() is to find a good 56 * target element that can respond to mouse events. Both mouse events and touch 57 * events are targeted at this element. Note that even for touch events, we 58 * check responsiveness to mouse events. We assume Web authors 59 * designing for touch events will take their own steps to account for 60 * inaccurate touch events. 61 * 62 * GetClickableAncestor() encapsulates the heuristic that determines whether an 63 * element is expected to respond to mouse events. An element is deemed 64 * "clickable" if it has registered listeners for "click", "mousedown" or 65 * "mouseup", or is on a whitelist of element tags (<a>, <button>, <input>, 66 * <select>, <textarea>, <label>), or has role="button", or is a link, or 67 * is a suitable XUL element. 68 * Any descendant (in the same document) of a clickable element is also 69 * deemed clickable since events will propagate to the clickable element from 70 * its descendant. 71 * 72 * If the element directly under the event position is clickable (or 73 * event radii are disabled), we always use that element. Otherwise we collect 74 * all frames intersecting a rectangle around the event position (taking CSS 75 * transforms into account) and choose the best candidate in GetClosest(). 76 * Only GetClickableAncestor() candidates are considered; if none are found, 77 * then we revert to targeting the element under the event position. 78 * We ignore candidates outside the document subtree rooted by the 79 * document of the element directly under the event position. This ensures that 80 * event listeners in ancestor documents don't make it completely impossible 81 * to target a non-clickable element in a child document. 82 * 83 * When both a frame and its ancestor are in the candidate list, we ignore 84 * the ancestor. Otherwise a large ancestor element with a mouse event listener 85 * and some descendant elements that need to be individually targetable would 86 * disable intelligent targeting of those descendants within its bounds. 87 * 88 * GetClosest() computes the transformed axis-aligned bounds of each 89 * candidate frame, then computes the Manhattan distance from the event point 90 * to the bounds rect (which can be zero). The frame with the 91 * shortest distance is chosen. For visited links we multiply the distance 92 * by a specified constant weight; this can be used to make visited links 93 * more or less likely to be targeted than non-visited links. 94 */ 95 96 // Enum that determines which type of elements to count as targets in the 97 // search. Clickable elements are generally ones that respond to click events, 98 // like form inputs and links and things with click event listeners. 99 // Touchable elements are a much narrower set of elements; ones with touchstart 100 // and touchend listeners. 101 enum class SearchType { 102 None, 103 Clickable, 104 Touchable, 105 TouchableOrClickable, 106 }; 107 108 struct EventRadiusPrefs { 109 bool mEnabled; // other fields are valid iff this field is true 110 uint32_t mVisitedWeight; // in percent, i.e. default is 100 111 uint32_t mRadiusTopmm; 112 uint32_t mRadiusRightmm; 113 uint32_t mRadiusBottommm; 114 uint32_t mRadiusLeftmm; 115 bool mTouchOnly; 116 bool mReposition; 117 SearchType mSearchType; 118 119 explicit EventRadiusPrefs(WidgetGUIEvent* aMouseOrTouchEvent) { 120 if (aMouseOrTouchEvent->mClass == eTouchEventClass) { 121 mEnabled = StaticPrefs::ui_touch_radius_enabled(); 122 mVisitedWeight = StaticPrefs::ui_touch_radius_visitedWeight(); 123 mRadiusTopmm = StaticPrefs::ui_touch_radius_topmm(); 124 mRadiusRightmm = StaticPrefs::ui_touch_radius_rightmm(); 125 mRadiusBottommm = StaticPrefs::ui_touch_radius_bottommm(); 126 mRadiusLeftmm = StaticPrefs::ui_touch_radius_leftmm(); 127 mTouchOnly = false; // Always false, unlike mouse events. 128 mReposition = false; // Always false, unlike mouse events. 129 if (StaticPrefs:: 130 ui_touch_radius_single_touch_treat_clickable_as_touchable() && 131 aMouseOrTouchEvent->mMessage == eTouchStart && 132 aMouseOrTouchEvent->AsTouchEvent()->mTouches.Length() == 1) { 133 // If it may cause a single tap, we need to refer clickable target too 134 // because the touchstart target will be captured implicitly if the 135 // web app does not capture the touch explicitly. 136 mSearchType = SearchType::TouchableOrClickable; 137 } else { 138 mSearchType = SearchType::Touchable; 139 } 140 141 } else if (aMouseOrTouchEvent->mClass == eMouseEventClass) { 142 mEnabled = StaticPrefs::ui_mouse_radius_enabled(); 143 mVisitedWeight = StaticPrefs::ui_mouse_radius_visitedWeight(); 144 mRadiusTopmm = StaticPrefs::ui_mouse_radius_topmm(); 145 mRadiusRightmm = StaticPrefs::ui_mouse_radius_rightmm(); 146 mRadiusBottommm = StaticPrefs::ui_mouse_radius_bottommm(); 147 mRadiusLeftmm = StaticPrefs::ui_mouse_radius_leftmm(); 148 mTouchOnly = StaticPrefs::ui_mouse_radius_inputSource_touchOnly(); 149 mReposition = StaticPrefs::ui_mouse_radius_reposition(); 150 mSearchType = SearchType::Clickable; 151 152 } else { 153 mEnabled = false; 154 mVisitedWeight = 0; 155 mRadiusTopmm = 0; 156 mRadiusRightmm = 0; 157 mRadiusBottommm = 0; 158 mRadiusLeftmm = 0; 159 mTouchOnly = false; 160 mReposition = false; 161 mSearchType = SearchType::None; 162 } 163 } 164 }; 165 166 static bool HasMouseListener(const nsIContent* aContent) { 167 if (EventListenerManager* elm = aContent->GetExistingListenerManager()) { 168 return elm->HasListenersFor(nsGkAtoms::onclick) || 169 elm->HasListenersFor(nsGkAtoms::onmousedown) || 170 elm->HasListenersFor(nsGkAtoms::onmouseup); 171 } 172 173 return false; 174 } 175 176 static bool HasTouchListener(const nsIContent* aContent) { 177 EventListenerManager* elm = aContent->GetExistingListenerManager(); 178 if (!elm) { 179 return false; 180 } 181 182 if (!TouchEvent::PrefEnabled(aContent->OwnerDoc()->GetDocShell())) { 183 return false; 184 } 185 186 return elm->HasNonSystemGroupListenersFor(nsGkAtoms::ontouchstart) || 187 elm->HasNonSystemGroupListenersFor(nsGkAtoms::ontouchend); 188 } 189 190 static bool HasPointerListener(const nsIContent* aContent) { 191 EventListenerManager* elm = aContent->GetExistingListenerManager(); 192 if (!elm) { 193 return false; 194 } 195 196 return elm->HasListenersFor(nsGkAtoms::onpointerdown) || 197 elm->HasListenersFor(nsGkAtoms::onpointerup); 198 } 199 200 static bool IsDescendant(nsIFrame* aFrame, nsIContent* aAncestor, 201 nsAutoString* aLabelTargetId) { 202 for (nsIContent* content = aFrame->GetContent(); content; 203 content = content->GetFlattenedTreeParent()) { 204 if (aLabelTargetId && content->IsHTMLElement(nsGkAtoms::label)) { 205 content->AsElement()->GetAttr(nsGkAtoms::_for, *aLabelTargetId); 206 } 207 if (content == aAncestor) { 208 return true; 209 } 210 } 211 return false; 212 } 213 214 static nsIContent* GetTouchableAncestor(nsIFrame* aFrame, 215 nsAtom* aStopAt = nullptr) { 216 // Input events propagate up the content tree so we'll follow the content 217 // ancestors to look for elements accepting the touch event. 218 for (nsIContent* content = aFrame->GetContent(); content; 219 content = content->GetFlattenedTreeParent()) { 220 if (aStopAt && content->IsHTMLElement(aStopAt)) { 221 break; 222 } 223 if (HasTouchListener(content)) { 224 return content; 225 } 226 } 227 return nullptr; 228 } 229 230 static bool IsClickableContent(const nsIContent* aContent, 231 nsAutoString* aLabelTargetId = nullptr) { 232 if (HasTouchListener(aContent) || HasMouseListener(aContent) || 233 HasPointerListener(aContent)) { 234 return true; 235 } 236 if (aContent->IsAnyOfHTMLElements(nsGkAtoms::button, nsGkAtoms::input, 237 nsGkAtoms::select, nsGkAtoms::textarea)) { 238 return true; 239 } 240 if (aContent->IsHTMLElement(nsGkAtoms::label)) { 241 if (aLabelTargetId) { 242 aContent->AsElement()->GetAttr(nsGkAtoms::_for, *aLabelTargetId); 243 } 244 return aContent; 245 } 246 247 // See nsCSSFrameConstructor::FindXULTagData. This code is not 248 // really intended to be used with XUL, though. 249 if (aContent->IsAnyOfXULElements( 250 nsGkAtoms::button, nsGkAtoms::checkbox, nsGkAtoms::radio, 251 nsGkAtoms::menu, nsGkAtoms::menuitem, nsGkAtoms::menulist, 252 nsGkAtoms::scrollbarbutton, nsGkAtoms::resizer)) { 253 return true; 254 } 255 256 static Element::AttrValuesArray clickableRoles[] = {nsGkAtoms::button, 257 nsGkAtoms::key, nullptr}; 258 if (const auto* element = Element::FromNode(*aContent)) { 259 if (element->IsLink()) { 260 return true; 261 } 262 if (element->FindAttrValueIn(kNameSpaceID_None, nsGkAtoms::role, 263 clickableRoles, eIgnoreCase) >= 0) { 264 return true; 265 } 266 } 267 return aContent->IsEditable(); 268 } 269 270 static nsIContent* GetMostDistantAncestorWhoseCursorIsPointer( 271 nsIFrame* aFrame, nsINode* aAncestorLimiter = nullptr, 272 nsAtom* aStopAt = nullptr) { 273 nsIFrame* lastCursorPointerFrame = nullptr; 274 for (nsIFrame* frame = aFrame; frame; frame = frame->GetParent()) { 275 if (frame->StyleUI()->Cursor().keyword != StyleCursorKind::Pointer) { 276 break; 277 } 278 nsIContent* content = frame->GetContent(); 279 if (MOZ_UNLIKELY(!content)) { 280 break; 281 } 282 lastCursorPointerFrame = frame; 283 if (content == aAncestorLimiter || 284 (aStopAt && content->IsHTMLElement(aStopAt))) { 285 break; 286 } 287 } 288 return lastCursorPointerFrame ? lastCursorPointerFrame->GetContent() 289 : nullptr; 290 } 291 292 static nsIContent* GetClickableAncestor( 293 nsIFrame* aFrame, nsAtom* aStopAt = nullptr, 294 nsAutoString* aLabelTargetId = nullptr) { 295 // Input events propagate up the content tree so we'll follow the content 296 // ancestors to look for elements accepting the click. 297 nsIContent* deepestClickableTarget = nullptr; 298 for (nsIContent* content = aFrame->GetContent(); content; 299 content = content->GetFlattenedTreeParent()) { 300 if (aStopAt && content->IsHTMLElement(aStopAt)) { 301 break; 302 } 303 if (IsClickableContent(content, aLabelTargetId)) { 304 deepestClickableTarget = content; 305 break; 306 } 307 } 308 309 // If the frame is `cursor:pointer` or inherits `cursor:pointer` from an 310 // ancestor, treat it as clickable. This is a heuristic to deal with pages 311 // where the click event listener is on the <body> or <html> element but it 312 // triggers an action on some specific element. We want the specific element 313 // to be considered clickable, and at least some pages that do this indicate 314 // the clickability by setting `cursor:pointer`, so we use that here. 315 // Note that descendants of `cursor:pointer` elements that override the 316 // inherited `pointer` to `auto` or any other value are NOT treated as 317 // clickable, because it seems like the content author is trying to express 318 // non-clickability on that sub-element. 319 // In the future depending on real-world cases it might make sense to expand 320 // this check to any non-auto cursor. Such a change would also pick up things 321 // like contenteditable or input fields, which can then be removed from the 322 // loop below, and would have better performance. 323 if (nsIContent* const mostDistantCursorPointerContent = 324 GetMostDistantAncestorWhoseCursorIsPointer( 325 aFrame, deepestClickableTarget, aStopAt)) { 326 if (!deepestClickableTarget || 327 (mostDistantCursorPointerContent != deepestClickableTarget && 328 mostDistantCursorPointerContent->IsInclusiveFlatTreeDescendantOf( 329 deepestClickableTarget))) { 330 // XXX Shouldn't we set aLabelTargetId if mostDistantCursorPointerContent 331 // is a <label>? 332 if (aLabelTargetId) { 333 aLabelTargetId->Truncate(); 334 } 335 return mostDistantCursorPointerContent; 336 } 337 } 338 return deepestClickableTarget; 339 } 340 341 static nsIContent* GetTouchableOrClickableAncestor( 342 nsIFrame* aFrame, nsAtom* aStopAt = nullptr, 343 nsAutoString* aLabelTargetId = nullptr) { 344 nsIContent* deepestClickableTarget = nullptr; 345 for (nsIContent* content = aFrame->GetContent(); content; 346 content = content->GetFlattenedTreeParent()) { 347 if (aStopAt && content->IsHTMLElement(aStopAt)) { 348 break; 349 } 350 // If we find a touchable content, let's target it. 351 if (HasTouchListener(content)) { 352 if (aLabelTargetId) { 353 aLabelTargetId->Truncate(); 354 } 355 return content; 356 } 357 // If we find a clickable content, let's store it and use it as the last 358 // resort if there is no touchable ancestor. 359 if (!deepestClickableTarget && 360 IsClickableContent(content, aLabelTargetId)) { 361 deepestClickableTarget = content; 362 } 363 } 364 365 // See comment in GetClickableAncestor for the detail of referring CSS 366 // `cursor`. 367 if (nsIContent* const mostDistantCursorPointerContent = 368 GetMostDistantAncestorWhoseCursorIsPointer( 369 aFrame, deepestClickableTarget, aStopAt)) { 370 if (!deepestClickableTarget || 371 (mostDistantCursorPointerContent != deepestClickableTarget && 372 mostDistantCursorPointerContent->IsInclusiveFlatTreeDescendantOf( 373 deepestClickableTarget))) { 374 // XXX Shouldn't we set aLabelTargetId if mostDistantCursorPointerContent 375 // is a <label>? 376 if (aLabelTargetId) { 377 aLabelTargetId->Truncate(); 378 } 379 return mostDistantCursorPointerContent; 380 } 381 } 382 return deepestClickableTarget; 383 } 384 385 static Scale2D AppUnitsToMMScale(RelativeTo aFrame) { 386 nsPresContext* presContext = aFrame.mFrame->PresContext(); 387 388 const int32_t appUnitsPerInch = 389 presContext->DeviceContext()->AppUnitsPerPhysicalInch(); 390 const float appUnits = 391 static_cast<float>(appUnitsPerInch) / MM_PER_INCH_FLOAT; 392 393 // Visual coordinates are only used for quantities relative to the 394 // cross-process root content document's root frame. There should 395 // not be an enclosing resolution or transform scale above that. 396 if (aFrame.mViewportType != ViewportType::Layout) { 397 const nscoord scale = NSToCoordRound(appUnits); 398 return Scale2D{static_cast<float>(scale), static_cast<float>(scale)}; 399 } 400 401 Scale2D localResolution{1.0f, 1.0f}; 402 Scale2D enclosingResolution{1.0f, 1.0f}; 403 404 if (auto* pc = presContext->GetInProcessRootContentDocumentPresContext()) { 405 PresShell* presShell = pc->PresShell(); 406 localResolution = {presShell->GetResolution(), presShell->GetResolution()}; 407 enclosingResolution = ViewportUtils::TryInferEnclosingResolution(presShell); 408 } 409 410 const gfx::MatrixScales parentScale = 411 nsLayoutUtils::GetTransformToAncestorScale(aFrame.mFrame); 412 const Scale2D resolution = 413 localResolution * parentScale * enclosingResolution; 414 415 const nscoord scaleX = NSToCoordRound(appUnits / resolution.xScale); 416 const nscoord scaleY = NSToCoordRound(appUnits / resolution.yScale); 417 418 return {static_cast<float>(scaleX), static_cast<float>(scaleY)}; 419 } 420 421 /** 422 * Clip aRect with the bounds of aFrame in the coordinate system of 423 * aRootFrame. aRootFrame is an ancestor of aFrame. 424 */ 425 static nsRect ClipToFrame(RelativeTo aRootFrame, const nsIFrame* aFrame, 426 nsRect& aRect) { 427 nsRect bound = nsLayoutUtils::TransformFrameRectToAncestor( 428 aFrame, nsRect(nsPoint(0, 0), aFrame->GetSize()), aRootFrame); 429 nsRect result = bound.Intersect(aRect); 430 return result; 431 } 432 433 static nsRect GetTargetRect(RelativeTo aRootFrame, 434 const nsPoint& aPointRelativeToRootFrame, 435 const nsIFrame* aRestrictToDescendants, 436 const EventRadiusPrefs& aPrefs, uint32_t aFlags) { 437 const Scale2D scale = AppUnitsToMMScale(aRootFrame); 438 nsMargin m(aPrefs.mRadiusTopmm * scale.yScale, 439 aPrefs.mRadiusRightmm * scale.xScale, 440 aPrefs.mRadiusBottommm * scale.yScale, 441 aPrefs.mRadiusLeftmm * scale.xScale); 442 nsRect r(aPointRelativeToRootFrame, nsSize(0, 0)); 443 r.Inflate(m); 444 if (!(aFlags & INPUT_IGNORE_ROOT_SCROLL_FRAME)) { 445 // Don't clip this rect to the root scroll frame if the flag to ignore the 446 // root scroll frame is set. Note that the GetClosest code will still 447 // enforce that the target found is a descendant of aRestrictToDescendants. 448 r = ClipToFrame(aRootFrame, aRestrictToDescendants, r); 449 } 450 return r; 451 } 452 453 static double ComputeDistanceFromRect(const nsPoint& aPoint, 454 const nsRect& aRect) { 455 nscoord dx = 456 std::max(0, std::max(aRect.x - aPoint.x, aPoint.x - aRect.XMost())); 457 nscoord dy = 458 std::max(0, std::max(aRect.y - aPoint.y, aPoint.y - aRect.YMost())); 459 return NS_hypot(dx, dy); 460 } 461 462 static double ComputeDistanceFromRegion(const nsPoint& aPoint, 463 const nsRegion& aRegion) { 464 MOZ_ASSERT(!aRegion.IsEmpty(), 465 "can't compute distance between point and empty region"); 466 double minDist = std::numeric_limits<double>::infinity(); 467 for (auto iter = aRegion.RectIter(); !iter.Done(); iter.Next()) { 468 double dist = ComputeDistanceFromRect(aPoint, iter.Get()); 469 if (dist < minDist) { 470 minDist = dist; 471 if (minDist == 0.0) { 472 break; 473 } 474 } 475 } 476 return minDist; 477 } 478 479 // Subtract aRegion from aExposedRegion as long as that doesn't make the 480 // exposed region get too complex or removes a big chunk of the exposed region. 481 static void SubtractFromExposedRegion(nsRegion* aExposedRegion, 482 const nsRegion& aRegion) { 483 if (aRegion.IsEmpty()) { 484 return; 485 } 486 487 nsRegion tmp; 488 tmp.Sub(*aExposedRegion, aRegion); 489 // Don't let *aExposedRegion get too complex, but don't let it fluff out to 490 // its bounds either. Do let aExposedRegion get more complex if by doing so 491 // we reduce its area by at least half. 492 if (tmp.GetNumRects() <= 15 || tmp.Area() <= aExposedRegion->Area() / 2) { 493 *aExposedRegion = tmp; 494 } 495 } 496 497 /** 498 * Return the border box of aFrame which is clipped by the ancestors. 499 */ 500 static Result<nsRect, nsresult> GetClippedBorderBox( 501 RelativeTo aRoot, nsIFrame* aFrame, const IntersectionInput& aInput, 502 bool* aPreservesAxisAlignedRectangles) { 503 MOZ_ASSERT(aPreservesAxisAlignedRectangles); 504 505 const IntersectionOutput intersectionOutput = 506 DOMIntersectionObserver::Intersect( 507 aInput, aFrame, DOMIntersectionObserver::BoxToUse::Border); 508 if (!intersectionOutput.Intersects()) { 509 return Err(NS_ERROR_FAILURE); 510 } 511 *aPreservesAxisAlignedRectangles = 512 intersectionOutput.mPreservesAxisAlignedRectangles; 513 // IntersectionOutput::mIntersectionRect is relative to the container 514 // block of aInput.mRootFrame. Therefore, we need to adjust the offset to 515 // relative to aRoot.mFrame. 516 nsIFrame* const containerBlock = 517 nsLayoutUtils::GetContainingBlockForClientRect(aInput.mRootFrame); 518 const nsRect& clippedBorderBoxRelativeToContainerBlock = 519 intersectionOutput.mIntersectionRect.ref(); 520 if (containerBlock == aRoot.mFrame) { 521 return clippedBorderBoxRelativeToContainerBlock; 522 } 523 nsRect clippedBorderBoxRelativeToRoot( 524 clippedBorderBoxRelativeToContainerBlock); 525 nsLayoutUtils::TransformRect(containerBlock, aRoot.mFrame, 526 clippedBorderBoxRelativeToRoot); 527 return clippedBorderBoxRelativeToRoot; 528 } 529 530 class MOZ_STACK_CLASS FramePrettyPrinter : public nsAutoCString { 531 public: 532 explicit FramePrettyPrinter(const nsIFrame* aFrame) { 533 #ifdef DEBUG_FRAME_DUMP 534 if (!aFrame) { 535 Assign(nsPrintfCString("%p", aFrame)); 536 return; 537 } 538 Assign(aFrame->ListTag()); 539 #else 540 Assign(nsPrintfCString("%p", aFrame)); 541 #endif 542 } 543 }; 544 545 static void LogClippedBorderBoxOfCandidateFrame( 546 RelativeTo aRoot, nsIFrame* aFrame, const nsRect& aClippedBorderBox, 547 bool aPreservesAxisAlignedRectangles) { 548 const nsRect borderBox = nsLayoutUtils::TransformFrameRectToAncestor( 549 aFrame, nsRect(nsPoint(0, 0), aFrame->GetSize()), aRoot); 550 PET_LOG( 551 "Checking candidate %s with clipped border box %s%s " 552 "(preservesAxisAlignedRectangles=%s)\n", 553 FramePrettyPrinter(aFrame).get(), ToString(aClippedBorderBox).c_str(), 554 aClippedBorderBox == borderBox 555 ? "" 556 : nsPrintfCString(" (non-clipped border box: %s)", 557 ToString(borderBox).c_str()) 558 .get(), 559 TrueOrFalse(aPreservesAxisAlignedRectangles)); 560 } 561 562 static nsIFrame* GetClosest(RelativeTo aRoot, 563 const nsPoint& aPointRelativeToRootFrame, 564 const nsRect& aTargetRect, 565 const EventRadiusPrefs& aPrefs, 566 const nsIFrame* aRestrictToDescendants, 567 nsIContent* aClickableAncestor, 568 nsTArray<nsIFrame*>& aCandidates) { 569 nsIFrame* bestTarget = nullptr; 570 // When we find a bestTarget, it or its ancestor is clickable or touchable. 571 // Then, the element is stored with this. 572 nsIContent* bestTargetHandler = nullptr; 573 // Lower is better; distance is in appunits 574 double bestDistance = std::numeric_limits<double>::infinity(); 575 nsRegion exposedRegion(aTargetRect); 576 MOZ_ASSERT(aRestrictToDescendants); 577 Document* const doc = aRestrictToDescendants->PresContext()->Document(); 578 MOZ_ASSERT(doc); 579 const IntersectionInput intersectionInput = 580 DOMIntersectionObserver::ComputeInput(*doc, doc, nullptr, nullptr); 581 for (nsIFrame* const f : aCandidates) { 582 bool preservesAxisAlignedRectangles = false; 583 Result<nsRect, nsresult> clippedBorderBoxOrError = GetClippedBorderBox( 584 aRoot, f, intersectionInput, &preservesAxisAlignedRectangles); 585 if (MOZ_UNLIKELY(clippedBorderBoxOrError.isErr())) { 586 PET_LOG(" candidate %s is not visible\n", FramePrettyPrinter(f).get()); 587 continue; 588 } 589 const nsRect clippedBorderBox = clippedBorderBoxOrError.unwrap(); 590 if (MOZ_UNLIKELY(PET_LOG_ENABLED())) { 591 LogClippedBorderBoxOfCandidateFrame(aRoot, f, clippedBorderBox, 592 preservesAxisAlignedRectangles); 593 } 594 nsRegion region; 595 region.And(exposedRegion, clippedBorderBox); 596 if (region.IsEmpty()) { 597 PET_LOG(" candidate %s had empty hit region\n", 598 FramePrettyPrinter(f).get()); 599 continue; 600 } 601 602 if (MOZ_LIKELY(preservesAxisAlignedRectangles)) { 603 // Subtract from the exposed region if we have a transform that won't make 604 // the bounds include a bunch of area that we don't actually cover. 605 SubtractFromExposedRegion(&exposedRegion, region); 606 } 607 608 nsAutoString labelTargetId; 609 if (aClickableAncestor && 610 !IsDescendant(f, aClickableAncestor, &labelTargetId)) { 611 PET_LOG(" candidate %s is not a descendant of required ancestor\n", 612 FramePrettyPrinter(f).get()); 613 continue; 614 } 615 616 nsIContent* handlerContent = nullptr; 617 switch (aPrefs.mSearchType) { 618 case SearchType::Clickable: { 619 nsIContent* clickableContent = 620 GetClickableAncestor(f, nsGkAtoms::body, &labelTargetId); 621 if (!aClickableAncestor && !clickableContent) { 622 PET_LOG(" candidate %s was not clickable\n", 623 FramePrettyPrinter(f).get()); 624 continue; 625 } 626 handlerContent = 627 clickableContent ? clickableContent : aClickableAncestor; 628 break; 629 } 630 case SearchType::Touchable: { 631 nsIContent* touchableContent = GetTouchableAncestor(f, nsGkAtoms::body); 632 if (!touchableContent) { 633 PET_LOG(" candidate %s was not touchable\n", 634 FramePrettyPrinter(f).get()); 635 continue; 636 } 637 handlerContent = touchableContent; 638 break; 639 } 640 case SearchType::TouchableOrClickable: { 641 nsIContent* touchableOrClickableContent = 642 GetTouchableOrClickableAncestor(f, nsGkAtoms::body, &labelTargetId); 643 if (!touchableOrClickableContent) { 644 PET_LOG(" candidate %s was not touchable nor clickable\n", 645 FramePrettyPrinter(f).get()); 646 continue; 647 } 648 handlerContent = touchableOrClickableContent; 649 break; 650 } 651 case SearchType::None: 652 MOZ_ASSERT_UNREACHABLE("Why is it enabled with seaching none?"); 653 break; 654 } 655 656 // If our current closest frame is a descendant of 'f', we may be able to 657 // skip 'f' (prefer the nested frame). 658 if (bestTarget && nsLayoutUtils::IsProperAncestorFrameCrossDoc( 659 f, bestTarget, aRoot.mFrame)) { 660 // If the bestTarget is a descendant of `f` but the handler is not in an 661 // independent clickable/touchable element in `f`, e.g., the <span> in the 662 // following case, 663 // 664 // <div onclick="foo()" style="padding: 5px"> 665 // <span>bar</span> 666 // </div> 667 // 668 // We shouldn't redirect to the <span> because when the user directly 669 // clicks/taps the clickable <div>, we should keep targeting the <div>. 670 // 671 // On the other hand, if the bestTarget is a frame in an independent 672 // clickable/touchable element, e.g., in the following case, 673 // 674 // <div onclick="foo()" style="padding: 5px"> 675 // <span onclick="bar()">bar</span> 676 // </div> 677 // 678 // We should retarget the event to the <span> because users may want to 679 // click the smaller target. 680 if (!bestTargetHandler || handlerContent != bestTargetHandler) { 681 PET_LOG( 682 " candidate %s (handler: %s) was ancestor for bestTarget %s " 683 "(handler: %s)\n", 684 FramePrettyPrinter(f).get(), ToString(*handlerContent).c_str(), 685 FramePrettyPrinter(bestTarget).get(), 686 ToString(RefPtr{bestTargetHandler}).c_str()); 687 continue; 688 } 689 } 690 691 if (!aClickableAncestor && !nsLayoutUtils::IsAncestorFrameCrossDoc( 692 aRestrictToDescendants, f, aRoot.mFrame)) { 693 PET_LOG(" candidate %s was not descendant of restrictroot %s\n", 694 FramePrettyPrinter(f).get(), 695 FramePrettyPrinter(aRestrictToDescendants).get()); 696 continue; 697 } 698 699 // distance is in appunit 700 double distance = 701 ComputeDistanceFromRegion(aPointRelativeToRootFrame, region); 702 nsIContent* content = f->GetContent(); 703 // XXX Well, some users may want to tap unvisited link, however, some other 704 // users may not. For example, click a link, and go back, then, want to go 705 // forward, but click the visited link instead. This scenario may occur if 706 // clicking the link is easier to do "go forward" and I think it's true for 707 // the most users. So, it might be better to do this. 708 if (content && content->IsElement() && 709 content->AsElement()->State().HasState( 710 ElementState(ElementState::VISITED))) { 711 distance *= aPrefs.mVisitedWeight / 100.0; 712 } 713 // XXX When we look for a touchable or clickable target, should we give 714 // lower weight for clickable target? 715 if (distance < bestDistance) { 716 PET_LOG(" candidate %s is the new best (%f)\n", 717 FramePrettyPrinter(f).get(), distance); 718 bestDistance = distance; 719 bestTarget = f; 720 bestTargetHandler = handlerContent; 721 if (bestDistance == 0.0) { 722 break; 723 } 724 } 725 } 726 return bestTarget; 727 } 728 729 // Walk from aTarget up to aRoot, and return the first frame found with an 730 // explicit z-index set on it. If no such frame is found, aRoot is returned. 731 static const nsIFrame* FindZIndexAncestor(const nsIFrame* aTarget, 732 const nsIFrame* aRoot) { 733 const nsIFrame* candidate = aTarget; 734 while (candidate && candidate != aRoot) { 735 if (candidate->ZIndex().valueOr(0) > 0) { 736 PET_LOG("Restricting search to z-index root %s\n", 737 FramePrettyPrinter(candidate).get()); 738 return candidate; 739 } 740 candidate = candidate->GetParent(); 741 } 742 return aRoot; 743 } 744 745 nsIFrame* FindFrameTargetedByInputEvent( 746 WidgetGUIEvent* aEvent, RelativeTo aRootFrame, 747 const nsPoint& aPointRelativeToRootFrame, uint32_t aFlags) { 748 using FrameForPointOption = nsLayoutUtils::FrameForPointOption; 749 EnumSet<FrameForPointOption> options; 750 if (aFlags & INPUT_IGNORE_ROOT_SCROLL_FRAME) { 751 options += FrameForPointOption::IgnoreRootScrollFrame; 752 } 753 nsIFrame* target = nsLayoutUtils::GetFrameForPoint( 754 aRootFrame, aPointRelativeToRootFrame, options); 755 nsIFrame* initialTarget = target; 756 PET_LOG( 757 "Found initial target %s for event class %s message %s point %s " 758 "relative to root frame %s\n", 759 FramePrettyPrinter(target).get(), ToChar(aEvent->mClass), 760 ToChar(aEvent->mMessage), ToString(aPointRelativeToRootFrame).c_str(), 761 ToString(aRootFrame).c_str()); 762 763 EventRadiusPrefs prefs(aEvent); 764 if (!prefs.mEnabled || EventRetargetSuppression::IsActive()) { 765 PET_LOG("Retargeting disabled\n"); 766 return target; 767 } 768 769 // Do not modify targeting for actual mouse hardware; only for mouse 770 // events generated by touch-screen hardware. 771 if (aEvent->mClass == eMouseEventClass && prefs.mTouchOnly && 772 aEvent->AsMouseEvent()->mInputSource != 773 MouseEvent_Binding::MOZ_SOURCE_TOUCH) { 774 PET_LOG("Mouse input event is not from a touch source\n"); 775 return target; 776 } 777 778 // If the exact target is non-null, only consider candidate targets in the 779 // same document as the exact target. Otherwise, if an ancestor document has 780 // a mouse event handler for example, targets that are !GetClickableAncestor 781 // can never be targeted --- something nsSubDocumentFrame in an ancestor 782 // document would be targeted instead. 783 const nsIFrame* restrictToDescendants = [&]() -> const nsIFrame* { 784 if (target && target->PresContext() != aRootFrame.mFrame->PresContext()) { 785 return target->PresShell()->GetRootFrame(); 786 } 787 return aRootFrame.mFrame; 788 }(); 789 790 // Ignore retarget if target is editable. 791 nsIContent* targetContent = target ? target->GetContent() : nullptr; 792 if (targetContent && targetContent->IsEditable()) { 793 PET_LOG("Target %s is editable\n", FramePrettyPrinter(target).get()); 794 return target; 795 } 796 797 // If the target element inside an element with a z-index, restrict the 798 // search to other elements inside that z-index. This is a heuristic 799 // intended to help with a class of scenarios involving web modals or 800 // web popup type things. In particular it helps alleviate bug 1666792. 801 restrictToDescendants = FindZIndexAncestor(target, restrictToDescendants); 802 803 nsRect targetRect = GetTargetRect(aRootFrame, aPointRelativeToRootFrame, 804 restrictToDescendants, prefs, aFlags); 805 PET_LOG("Expanded point to target rect %s\n", ToString(targetRect).c_str()); 806 AutoTArray<nsIFrame*, 8> candidates; 807 nsresult rv = nsLayoutUtils::GetFramesForArea(aRootFrame, targetRect, 808 candidates, options); 809 if (NS_FAILED(rv)) { 810 return target; 811 } 812 813 nsIContent* clickableAncestor = nullptr; 814 if (target) { 815 clickableAncestor = GetClickableAncestor(target, nsGkAtoms::body); 816 if (clickableAncestor) { 817 PET_LOG("Target %s is clickable\n", FramePrettyPrinter(target).get()); 818 // If the target that was directly hit has a clickable ancestor, that 819 // means it too is clickable. And since it is the same as or a 820 // descendant of clickableAncestor, it should become the root for the 821 // GetClosest search. 822 clickableAncestor = target->GetContent(); 823 } 824 } 825 826 nsIFrame* closest = 827 GetClosest(aRootFrame, aPointRelativeToRootFrame, targetRect, prefs, 828 restrictToDescendants, clickableAncestor, candidates); 829 if (closest) { 830 target = closest; 831 } 832 833 PET_LOG("Final target is %s\n", FramePrettyPrinter(target).get()); 834 835 #ifdef DEBUG_FRAME_DUMP 836 // At verbose logging level, dump the frame tree to help with debugging. 837 // Note that dumping the frame tree at the top of the function may flood 838 // logcat on Android devices and cause the PET_LOGs to get dropped. 839 if (MOZ_LOG_TEST(sEvtTgtLog, LogLevel::Verbose)) { 840 if (target) { 841 target->DumpFrameTree(); 842 } else { 843 aRootFrame.mFrame->DumpFrameTree(); 844 } 845 } 846 #endif 847 848 if (!target || !prefs.mReposition || target == initialTarget) { 849 // No repositioning required for this event 850 return target; 851 } 852 853 // Take the point relative to the root frame, make it relative to the target, 854 // clamp it to the bounds, and then make it relative to the root frame again. 855 nsPoint point = aPointRelativeToRootFrame; 856 if (nsLayoutUtils::TRANSFORM_SUCCEEDED != 857 nsLayoutUtils::TransformPoint(aRootFrame, RelativeTo{target}, point)) { 858 return target; 859 } 860 point = target->GetRectRelativeToSelf().ClampPoint(point); 861 if (nsLayoutUtils::TRANSFORM_SUCCEEDED != 862 nsLayoutUtils::TransformPoint(RelativeTo{target}, aRootFrame, point)) { 863 return target; 864 } 865 // Now we basically undo the operations in GetEventCoordinatesRelativeTo, to 866 // get back the (now-clamped) coordinates in the event's widget's space. 867 nsPresContext* pc = aRootFrame.mFrame->PresContext(); 868 // TODO: Consider adding an optimization similar to the one in 869 // GetEventCoordinatesRelativeTo, where we detect cases where 870 // there is no transform to apply and avoid calling 871 // TransformFramePointToRoot() in those cases. 872 point = nsLayoutUtils::TransformFramePointToRoot(ViewportType::Visual, 873 aRootFrame, point); 874 if (auto widgetPoint = nsLayoutUtils::FrameToWidgetOffset(aRootFrame.mFrame, 875 aEvent->mWidget)) { 876 // If that succeeded, we update the point in the event 877 aEvent->mRefPoint = LayoutDeviceIntPoint::FromAppUnitsRounded( 878 *widgetPoint + point, pc->AppUnitsPerDevPixel()); 879 } 880 return target; 881 } 882 883 uint32_t EventRetargetSuppression::sSuppressionCount = 0; 884 885 EventRetargetSuppression::EventRetargetSuppression() { sSuppressionCount++; } 886 887 EventRetargetSuppression::~EventRetargetSuppression() { sSuppressionCount--; } 888 889 bool EventRetargetSuppression::IsActive() { return sSuppressionCount > 0; } 890 891 } // namespace mozilla