tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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