1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/layout/base/PositionedEventTargeting.cpp Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,402 @@ 1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.6 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.7 + 1.8 +#include "PositionedEventTargeting.h" 1.9 + 1.10 +#include "mozilla/EventListenerManager.h" 1.11 +#include "mozilla/EventStates.h" 1.12 +#include "mozilla/MouseEvents.h" 1.13 +#include "mozilla/Preferences.h" 1.14 +#include "nsLayoutUtils.h" 1.15 +#include "nsGkAtoms.h" 1.16 +#include "nsPrintfCString.h" 1.17 +#include "mozilla/dom/Element.h" 1.18 +#include "nsRegion.h" 1.19 +#include "nsDeviceContext.h" 1.20 +#include "nsIFrame.h" 1.21 +#include <algorithm> 1.22 + 1.23 +namespace mozilla { 1.24 + 1.25 +/* 1.26 + * The basic goal of FindFrameTargetedByInputEvent() is to find a good 1.27 + * target element that can respond to mouse events. Both mouse events and touch 1.28 + * events are targeted at this element. Note that even for touch events, we 1.29 + * check responsiveness to mouse events. We assume Web authors 1.30 + * designing for touch events will take their own steps to account for 1.31 + * inaccurate touch events. 1.32 + * 1.33 + * IsElementClickable() encapsulates the heuristic that determines whether an 1.34 + * element is expected to respond to mouse events. An element is deemed 1.35 + * "clickable" if it has registered listeners for "click", "mousedown" or 1.36 + * "mouseup", or is on a whitelist of element tags (<a>, <button>, <input>, 1.37 + * <select>, <textarea>, <label>), or has role="button", or is a link, or 1.38 + * is a suitable XUL element. 1.39 + * Any descendant (in the same document) of a clickable element is also 1.40 + * deemed clickable since events will propagate to the clickable element from its 1.41 + * descendant. 1.42 + * 1.43 + * If the element directly under the event position is clickable (or 1.44 + * event radii are disabled), we always use that element. Otherwise we collect 1.45 + * all frames intersecting a rectangle around the event position (taking CSS 1.46 + * transforms into account) and choose the best candidate in GetClosest(). 1.47 + * Only IsElementClickable() candidates are considered; if none are found, 1.48 + * then we revert to targeting the element under the event position. 1.49 + * We ignore candidates outside the document subtree rooted by the 1.50 + * document of the element directly under the event position. This ensures that 1.51 + * event listeners in ancestor documents don't make it completely impossible 1.52 + * to target a non-clickable element in a child document. 1.53 + * 1.54 + * When both a frame and its ancestor are in the candidate list, we ignore 1.55 + * the ancestor. Otherwise a large ancestor element with a mouse event listener 1.56 + * and some descendant elements that need to be individually targetable would 1.57 + * disable intelligent targeting of those descendants within its bounds. 1.58 + * 1.59 + * GetClosest() computes the transformed axis-aligned bounds of each 1.60 + * candidate frame, then computes the Manhattan distance from the event point 1.61 + * to the bounds rect (which can be zero). The frame with the 1.62 + * shortest distance is chosen. For visited links we multiply the distance 1.63 + * by a specified constant weight; this can be used to make visited links 1.64 + * more or less likely to be targeted than non-visited links. 1.65 + */ 1.66 + 1.67 +struct EventRadiusPrefs 1.68 +{ 1.69 + uint32_t mVisitedWeight; // in percent, i.e. default is 100 1.70 + uint32_t mSideRadii[4]; // TRBL order, in millimetres 1.71 + bool mEnabled; 1.72 + bool mRegistered; 1.73 + bool mTouchOnly; 1.74 +}; 1.75 + 1.76 +static EventRadiusPrefs sMouseEventRadiusPrefs; 1.77 +static EventRadiusPrefs sTouchEventRadiusPrefs; 1.78 + 1.79 +static const EventRadiusPrefs* 1.80 +GetPrefsFor(nsEventStructType aEventStructType) 1.81 +{ 1.82 + EventRadiusPrefs* prefs = nullptr; 1.83 + const char* prefBranch = nullptr; 1.84 + if (aEventStructType == NS_TOUCH_EVENT) { 1.85 + prefBranch = "touch"; 1.86 + prefs = &sTouchEventRadiusPrefs; 1.87 + } else if (aEventStructType == NS_MOUSE_EVENT) { 1.88 + // Mostly for testing purposes 1.89 + prefBranch = "mouse"; 1.90 + prefs = &sMouseEventRadiusPrefs; 1.91 + } else { 1.92 + return nullptr; 1.93 + } 1.94 + 1.95 + if (!prefs->mRegistered) { 1.96 + prefs->mRegistered = true; 1.97 + 1.98 + nsPrintfCString enabledPref("ui.%s.radius.enabled", prefBranch); 1.99 + Preferences::AddBoolVarCache(&prefs->mEnabled, enabledPref.get(), false); 1.100 + 1.101 + nsPrintfCString visitedWeightPref("ui.%s.radius.visitedWeight", prefBranch); 1.102 + Preferences::AddUintVarCache(&prefs->mVisitedWeight, visitedWeightPref.get(), 100); 1.103 + 1.104 + static const char prefNames[4][9] = 1.105 + { "topmm", "rightmm", "bottommm", "leftmm" }; 1.106 + for (int32_t i = 0; i < 4; ++i) { 1.107 + nsPrintfCString radiusPref("ui.%s.radius.%s", prefBranch, prefNames[i]); 1.108 + Preferences::AddUintVarCache(&prefs->mSideRadii[i], radiusPref.get(), 0); 1.109 + } 1.110 + 1.111 + if (aEventStructType == NS_MOUSE_EVENT) { 1.112 + Preferences::AddBoolVarCache(&prefs->mTouchOnly, 1.113 + "ui.mouse.radius.inputSource.touchOnly", true); 1.114 + } else { 1.115 + prefs->mTouchOnly = false; 1.116 + } 1.117 + } 1.118 + 1.119 + return prefs; 1.120 +} 1.121 + 1.122 +static bool 1.123 +HasMouseListener(nsIContent* aContent) 1.124 +{ 1.125 + if (EventListenerManager* elm = aContent->GetExistingListenerManager()) { 1.126 + return elm->HasListenersFor(nsGkAtoms::onclick) || 1.127 + elm->HasListenersFor(nsGkAtoms::onmousedown) || 1.128 + elm->HasListenersFor(nsGkAtoms::onmouseup); 1.129 + } 1.130 + 1.131 + return false; 1.132 +} 1.133 + 1.134 +static bool gTouchEventsRegistered = false; 1.135 +static int32_t gTouchEventsEnabled = 0; 1.136 + 1.137 +static bool 1.138 +HasTouchListener(nsIContent* aContent) 1.139 +{ 1.140 + EventListenerManager* elm = aContent->GetExistingListenerManager(); 1.141 + if (!elm) { 1.142 + return false; 1.143 + } 1.144 + 1.145 + if (!gTouchEventsRegistered) { 1.146 + Preferences::AddIntVarCache(&gTouchEventsEnabled, 1.147 + "dom.w3c_touch_events.enabled", gTouchEventsEnabled); 1.148 + gTouchEventsRegistered = true; 1.149 + } 1.150 + 1.151 + if (!gTouchEventsEnabled) { 1.152 + return false; 1.153 + } 1.154 + 1.155 + return elm->HasListenersFor(nsGkAtoms::ontouchstart) || 1.156 + elm->HasListenersFor(nsGkAtoms::ontouchend); 1.157 +} 1.158 + 1.159 +static bool 1.160 +IsElementClickable(nsIFrame* aFrame, nsIAtom* stopAt = nullptr) 1.161 +{ 1.162 + // Input events propagate up the content tree so we'll follow the content 1.163 + // ancestors to look for elements accepting the click. 1.164 + for (nsIContent* content = aFrame->GetContent(); content; 1.165 + content = content->GetFlattenedTreeParent()) { 1.166 + nsIAtom* tag = content->Tag(); 1.167 + if (content->IsHTML() && stopAt && tag == stopAt) { 1.168 + break; 1.169 + } 1.170 + if (HasTouchListener(content) || HasMouseListener(content)) { 1.171 + return true; 1.172 + } 1.173 + if (content->IsHTML()) { 1.174 + if (tag == nsGkAtoms::button || 1.175 + tag == nsGkAtoms::input || 1.176 + tag == nsGkAtoms::select || 1.177 + tag == nsGkAtoms::textarea || 1.178 + tag == nsGkAtoms::label) { 1.179 + return true; 1.180 + } 1.181 + // Bug 921928: we don't have access to the content of remote iframe. 1.182 + // So fluffing won't go there. We do an optimistic assumption here: 1.183 + // that the content of the remote iframe needs to be a target. 1.184 + if (tag == nsGkAtoms::iframe && 1.185 + content->AttrValueIs(kNameSpaceID_None, nsGkAtoms::mozbrowser, 1.186 + nsGkAtoms::_true, eIgnoreCase) && 1.187 + content->AttrValueIs(kNameSpaceID_None, nsGkAtoms::Remote, 1.188 + nsGkAtoms::_true, eIgnoreCase)) { 1.189 + return true; 1.190 + } 1.191 + } else if (content->IsXUL()) { 1.192 + nsIAtom* tag = content->Tag(); 1.193 + // See nsCSSFrameConstructor::FindXULTagData. This code is not 1.194 + // really intended to be used with XUL, though. 1.195 + if (tag == nsGkAtoms::button || 1.196 + tag == nsGkAtoms::checkbox || 1.197 + tag == nsGkAtoms::radio || 1.198 + tag == nsGkAtoms::autorepeatbutton || 1.199 + tag == nsGkAtoms::menu || 1.200 + tag == nsGkAtoms::menubutton || 1.201 + tag == nsGkAtoms::menuitem || 1.202 + tag == nsGkAtoms::menulist || 1.203 + tag == nsGkAtoms::scrollbarbutton || 1.204 + tag == nsGkAtoms::resizer) { 1.205 + return true; 1.206 + } 1.207 + } 1.208 + static nsIContent::AttrValuesArray clickableRoles[] = 1.209 + { &nsGkAtoms::button, &nsGkAtoms::key, nullptr }; 1.210 + if (content->FindAttrValueIn(kNameSpaceID_None, nsGkAtoms::role, 1.211 + clickableRoles, eIgnoreCase) >= 0) { 1.212 + return true; 1.213 + } 1.214 + if (content->IsEditable()) { 1.215 + return true; 1.216 + } 1.217 + nsCOMPtr<nsIURI> linkURI; 1.218 + if (content->IsLink(getter_AddRefs(linkURI))) { 1.219 + return true; 1.220 + } 1.221 + } 1.222 + return false; 1.223 +} 1.224 + 1.225 +static nscoord 1.226 +AppUnitsFromMM(nsIFrame* aFrame, uint32_t aMM, bool aVertical) 1.227 +{ 1.228 + nsPresContext* pc = aFrame->PresContext(); 1.229 + float result = float(aMM) * 1.230 + (pc->DeviceContext()->AppUnitsPerPhysicalInch() / MM_PER_INCH_FLOAT); 1.231 + return NSToCoordRound(result); 1.232 +} 1.233 + 1.234 +/** 1.235 + * Clip aRect with the bounds of aFrame in the coordinate system of 1.236 + * aRootFrame. aRootFrame is an ancestor of aFrame. 1.237 + */ 1.238 +static nsRect 1.239 +ClipToFrame(nsIFrame* aRootFrame, nsIFrame* aFrame, nsRect& aRect) 1.240 +{ 1.241 + nsRect bound = nsLayoutUtils::TransformFrameRectToAncestor( 1.242 + aFrame, nsRect(nsPoint(0, 0), aFrame->GetSize()), aRootFrame); 1.243 + nsRect result = bound.Intersect(aRect); 1.244 + return result; 1.245 +} 1.246 + 1.247 +static nsRect 1.248 +GetTargetRect(nsIFrame* aRootFrame, const nsPoint& aPointRelativeToRootFrame, 1.249 + nsIFrame* aRestrictToDescendants, const EventRadiusPrefs* aPrefs) 1.250 +{ 1.251 + nsMargin m(AppUnitsFromMM(aRootFrame, aPrefs->mSideRadii[0], true), 1.252 + AppUnitsFromMM(aRootFrame, aPrefs->mSideRadii[1], false), 1.253 + AppUnitsFromMM(aRootFrame, aPrefs->mSideRadii[2], true), 1.254 + AppUnitsFromMM(aRootFrame, aPrefs->mSideRadii[3], false)); 1.255 + nsRect r(aPointRelativeToRootFrame, nsSize(0,0)); 1.256 + r.Inflate(m); 1.257 + return ClipToFrame(aRootFrame, aRestrictToDescendants, r); 1.258 +} 1.259 + 1.260 +static float 1.261 +ComputeDistanceFromRect(const nsPoint& aPoint, const nsRect& aRect) 1.262 +{ 1.263 + nscoord dx = std::max(0, std::max(aRect.x - aPoint.x, aPoint.x - aRect.XMost())); 1.264 + nscoord dy = std::max(0, std::max(aRect.y - aPoint.y, aPoint.y - aRect.YMost())); 1.265 + return float(NS_hypot(dx, dy)); 1.266 +} 1.267 + 1.268 +static float 1.269 +ComputeDistanceFromRegion(const nsPoint& aPoint, const nsRegion& aRegion) 1.270 +{ 1.271 + MOZ_ASSERT(!aRegion.IsEmpty(), "can't compute distance between point and empty region"); 1.272 + nsRegionRectIterator iter(aRegion); 1.273 + const nsRect* r; 1.274 + float minDist = -1; 1.275 + while ((r = iter.Next()) != nullptr) { 1.276 + float dist = ComputeDistanceFromRect(aPoint, *r); 1.277 + if (dist < minDist || minDist < 0) { 1.278 + minDist = dist; 1.279 + } 1.280 + } 1.281 + return minDist; 1.282 +} 1.283 + 1.284 +// Subtract aRegion from aExposedRegion as long as that doesn't make the 1.285 +// exposed region get too complex or removes a big chunk of the exposed region. 1.286 +static void 1.287 +SubtractFromExposedRegion(nsRegion* aExposedRegion, const nsRegion& aRegion) 1.288 +{ 1.289 + if (aRegion.IsEmpty()) 1.290 + return; 1.291 + 1.292 + nsRegion tmp; 1.293 + tmp.Sub(*aExposedRegion, aRegion); 1.294 + // Don't let *aExposedRegion get too complex, but don't let it fluff out to 1.295 + // its bounds either. Do let aExposedRegion get more complex if by doing so 1.296 + // we reduce its area by at least half. 1.297 + if (tmp.GetNumRects() <= 15 || tmp.Area() <= aExposedRegion->Area()/2) { 1.298 + *aExposedRegion = tmp; 1.299 + } 1.300 +} 1.301 + 1.302 +static nsIFrame* 1.303 +GetClosest(nsIFrame* aRoot, const nsPoint& aPointRelativeToRootFrame, 1.304 + const nsRect& aTargetRect, const EventRadiusPrefs* aPrefs, 1.305 + nsIFrame* aRestrictToDescendants, nsTArray<nsIFrame*>& aCandidates) 1.306 +{ 1.307 + nsIFrame* bestTarget = nullptr; 1.308 + // Lower is better; distance is in appunits 1.309 + float bestDistance = 1e6f; 1.310 + nsRegion exposedRegion(aTargetRect); 1.311 + for (uint32_t i = 0; i < aCandidates.Length(); ++i) { 1.312 + nsIFrame* f = aCandidates[i]; 1.313 + 1.314 + bool preservesAxisAlignedRectangles = false; 1.315 + nsRect borderBox = nsLayoutUtils::TransformFrameRectToAncestor(f, 1.316 + nsRect(nsPoint(0, 0), f->GetSize()), aRoot, &preservesAxisAlignedRectangles); 1.317 + nsRegion region; 1.318 + region.And(exposedRegion, borderBox); 1.319 + 1.320 + if (region.IsEmpty()) { 1.321 + continue; 1.322 + } 1.323 + 1.324 + if (preservesAxisAlignedRectangles) { 1.325 + // Subtract from the exposed region if we have a transform that won't make 1.326 + // the bounds include a bunch of area that we don't actually cover. 1.327 + SubtractFromExposedRegion(&exposedRegion, region); 1.328 + } 1.329 + 1.330 + if (!IsElementClickable(f)) { 1.331 + continue; 1.332 + } 1.333 + // If our current closest frame is a descendant of 'f', skip 'f' (prefer 1.334 + // the nested frame). 1.335 + if (bestTarget && nsLayoutUtils::IsProperAncestorFrameCrossDoc(f, bestTarget, aRoot)) { 1.336 + continue; 1.337 + } 1.338 + if (!nsLayoutUtils::IsAncestorFrameCrossDoc(aRestrictToDescendants, f, aRoot)) { 1.339 + continue; 1.340 + } 1.341 + 1.342 + // distance is in appunits 1.343 + float distance = ComputeDistanceFromRegion(aPointRelativeToRootFrame, region); 1.344 + nsIContent* content = f->GetContent(); 1.345 + if (content && content->IsElement() && 1.346 + content->AsElement()->State().HasState( 1.347 + EventStates(NS_EVENT_STATE_VISITED))) { 1.348 + distance *= aPrefs->mVisitedWeight / 100.0f; 1.349 + } 1.350 + if (distance < bestDistance) { 1.351 + bestDistance = distance; 1.352 + bestTarget = f; 1.353 + } 1.354 + } 1.355 + return bestTarget; 1.356 +} 1.357 + 1.358 +nsIFrame* 1.359 +FindFrameTargetedByInputEvent(const WidgetGUIEvent* aEvent, 1.360 + nsIFrame* aRootFrame, 1.361 + const nsPoint& aPointRelativeToRootFrame, 1.362 + uint32_t aFlags) 1.363 +{ 1.364 + uint32_t flags = (aFlags & INPUT_IGNORE_ROOT_SCROLL_FRAME) ? 1.365 + nsLayoutUtils::IGNORE_ROOT_SCROLL_FRAME : 0; 1.366 + nsIFrame* target = 1.367 + nsLayoutUtils::GetFrameForPoint(aRootFrame, aPointRelativeToRootFrame, flags); 1.368 + 1.369 + const EventRadiusPrefs* prefs = GetPrefsFor(aEvent->eventStructType); 1.370 + if (!prefs || !prefs->mEnabled || (target && IsElementClickable(target, nsGkAtoms::body))) { 1.371 + return target; 1.372 + } 1.373 + 1.374 + // Do not modify targeting for actual mouse hardware; only for mouse 1.375 + // events generated by touch-screen hardware. 1.376 + if (aEvent->eventStructType == NS_MOUSE_EVENT && 1.377 + prefs->mTouchOnly && 1.378 + aEvent->AsMouseEvent()->inputSource != 1.379 + nsIDOMMouseEvent::MOZ_SOURCE_TOUCH) { 1.380 + return target; 1.381 + } 1.382 + 1.383 + // If the exact target is non-null, only consider candidate targets in the same 1.384 + // document as the exact target. Otherwise, if an ancestor document has 1.385 + // a mouse event handler for example, targets that are !IsElementClickable can 1.386 + // never be targeted --- something nsSubDocumentFrame in an ancestor document 1.387 + // would be targeted instead. 1.388 + nsIFrame* restrictToDescendants = target ? 1.389 + target->PresContext()->PresShell()->GetRootFrame() : aRootFrame; 1.390 + 1.391 + nsRect targetRect = GetTargetRect(aRootFrame, aPointRelativeToRootFrame, 1.392 + restrictToDescendants, prefs); 1.393 + nsAutoTArray<nsIFrame*,8> candidates; 1.394 + nsresult rv = nsLayoutUtils::GetFramesForArea(aRootFrame, targetRect, candidates, flags); 1.395 + if (NS_FAILED(rv)) { 1.396 + return target; 1.397 + } 1.398 + 1.399 + nsIFrame* closestClickable = 1.400 + GetClosest(aRootFrame, aPointRelativeToRootFrame, targetRect, prefs, 1.401 + restrictToDescendants, candidates); 1.402 + return closestClickable ? closestClickable : target; 1.403 +} 1.404 + 1.405 +}