2 * Copyright (C) 2012 Nokia Corporation and/or its subsidiary(-ies)
4 * This library is free software; you can redistribute it and/or
5 * modify it under the terms of the GNU Library General Public
6 * License as published by the Free Software Foundation; either
7 * version 2 of the License, or (at your option) any later version.
9 * This library is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12 * Library General Public License for more details.
14 * You should have received a copy of the GNU Library General Public License
15 * along with this library; see the file COPYING.LIB. If not, write to
16 * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
17 * Boston, MA 02110-1301, USA.
22 #include "core/page/TouchAdjustment.h"
24 #include "core/dom/ContainerNode.h"
25 #include "core/dom/Node.h"
26 #include "core/dom/NodeComputedStyle.h"
27 #include "core/dom/Text.h"
28 #include "core/editing/Editor.h"
29 #include "core/frame/FrameView.h"
30 #include "core/frame/LocalFrame.h"
31 #include "core/html/HTMLFrameOwnerElement.h"
32 #include "core/layout/LayoutBox.h"
33 #include "core/layout/LayoutObject.h"
34 #include "core/layout/LayoutText.h"
35 #include "core/layout/api/SelectionState.h"
36 #include "core/style/ComputedStyle.h"
37 #include "platform/geometry/FloatPoint.h"
38 #include "platform/geometry/FloatQuad.h"
39 #include "platform/geometry/IntSize.h"
40 #include "platform/text/TextBreakIterator.h"
44 namespace TouchAdjustment
{
46 const float zeroTolerance
= 1e-6f
;
48 // Class for remembering absolute quads of a target node and what node they represent.
49 class SubtargetGeometry
{
50 ALLOW_ONLY_INLINE_ALLOCATION();
52 SubtargetGeometry(Node
* node
, const FloatQuad
& quad
)
56 DEFINE_INLINE_TRACE() { visitor
->trace(m_node
); }
58 Node
* node() const { return m_node
; }
59 FloatQuad
quad() const { return m_quad
; }
60 IntRect
boundingBox() const { return m_quad
.enclosingBoundingBox(); }
63 RawPtrWillBeMember
<Node
> m_node
;
71 WTF_ALLOW_MOVE_INIT_AND_COMPARE_WITH_MEM_FUNCTIONS(blink::TouchAdjustment::SubtargetGeometry
)
75 namespace TouchAdjustment
{
77 typedef WillBeHeapVector
<SubtargetGeometry
> SubtargetGeometryList
;
78 typedef bool (*NodeFilter
)(Node
*);
79 typedef void (*AppendSubtargetsForNode
)(Node
*, SubtargetGeometryList
&);
80 typedef float (*DistanceFunction
)(const IntPoint
&, const IntRect
&, const SubtargetGeometry
&);
82 // Takes non-const Node* because isContentEditable is a non-const function.
83 bool nodeRespondsToTapGesture(Node
* node
)
85 if (node
->willRespondToMouseClickEvents() || node
->willRespondToMouseMoveEvents())
87 if (node
->isElementNode()) {
88 Element
* element
= toElement(node
);
89 // Tapping on a text field or other focusable item should trigger adjustment, except
90 // that iframe elements are hard-coded to support focus but the effect is often invisible
91 // so they should be excluded.
92 if (element
->isMouseFocusable() && !isHTMLIFrameElement(element
))
94 // Accept nodes that has a CSS effect when touched.
95 if (element
->childrenOrSiblingsAffectedByActive() || element
->childrenOrSiblingsAffectedByHover())
98 if (const ComputedStyle
* computedStyle
= node
->computedStyle()) {
99 if (computedStyle
->affectedByActive() || computedStyle
->affectedByHover())
105 bool nodeIsZoomTarget(Node
* node
)
107 if (node
->isTextNode() || node
->isShadowRoot())
110 ASSERT(node
->layoutObject());
111 return node
->layoutObject()->isBox();
114 bool providesContextMenuItems(Node
* node
)
116 // This function tries to match the nodes that receive special context-menu items in
117 // ContextMenuController::populate(), and should be kept uptodate with those.
118 ASSERT(node
->layoutObject() || node
->isShadowRoot());
119 if (!node
->layoutObject())
121 if (node
->isContentEditable())
125 if (node
->layoutObject()->isImage())
127 if (node
->layoutObject()->isMedia())
129 if (node
->layoutObject()->canBeSelectionLeaf()) {
130 // If the context menu gesture will trigger a selection all selectable nodes are valid targets.
131 if (node
->layoutObject()->frame()->editor().behavior().shouldSelectOnContextualMenuClick())
133 // Only the selected part of the layoutObject is a valid target, but this will be corrected in
134 // appendContextSubtargetsForNode.
135 if (node
->layoutObject()->selectionState() != SelectionNone
)
141 static inline void appendQuadsToSubtargetList(Vector
<FloatQuad
>& quads
, Node
* node
, SubtargetGeometryList
& subtargets
)
143 Vector
<FloatQuad
>::const_iterator it
= quads
.begin();
144 const Vector
<FloatQuad
>::const_iterator end
= quads
.end();
145 for (; it
!= end
; ++it
)
146 subtargets
.append(SubtargetGeometry(node
, *it
));
149 static inline void appendBasicSubtargetsForNode(Node
* node
, SubtargetGeometryList
& subtargets
)
151 // Node guaranteed to have layoutObject due to check in node filter.
152 ASSERT(node
->layoutObject());
154 Vector
<FloatQuad
> quads
;
155 node
->layoutObject()->absoluteQuads(quads
);
157 appendQuadsToSubtargetList(quads
, node
, subtargets
);
160 static inline void appendContextSubtargetsForNode(Node
* node
, SubtargetGeometryList
& subtargets
)
162 // This is a variant of appendBasicSubtargetsForNode that adds special subtargets for
163 // selected or auto-selectable parts of text nodes.
164 ASSERT(node
->layoutObject());
166 if (!node
->isTextNode())
167 return appendBasicSubtargetsForNode(node
, subtargets
);
169 Text
* textNode
= toText(node
);
170 LayoutText
* textLayoutObject
= textNode
->layoutObject();
172 if (textLayoutObject
->frame()->editor().behavior().shouldSelectOnContextualMenuClick()) {
173 // Make subtargets out of every word.
174 String textValue
= textNode
->data();
175 TextBreakIterator
* wordIterator
= wordBreakIterator(textValue
, 0, textValue
.length());
176 int lastOffset
= wordIterator
->first();
177 if (lastOffset
== -1)
180 while ((offset
= wordIterator
->next()) != -1) {
181 if (isWordTextBreak(wordIterator
)) {
182 Vector
<FloatQuad
> quads
;
183 textLayoutObject
->absoluteQuadsForRange(quads
, lastOffset
, offset
);
184 appendQuadsToSubtargetList(quads
, textNode
, subtargets
);
189 if (textLayoutObject
->selectionState() == SelectionNone
)
190 return appendBasicSubtargetsForNode(node
, subtargets
);
191 // If selected, make subtargets out of only the selected part of the text.
192 int startPos
, endPos
;
193 switch (textLayoutObject
->selectionState()) {
194 case SelectionInside
:
196 endPos
= textLayoutObject
->textLength();
199 textLayoutObject
->selectionStartEnd(startPos
, endPos
);
200 endPos
= textLayoutObject
->textLength();
203 textLayoutObject
->selectionStartEnd(startPos
, endPos
);
207 textLayoutObject
->selectionStartEnd(startPos
, endPos
);
210 ASSERT_NOT_REACHED();
213 Vector
<FloatQuad
> quads
;
214 textLayoutObject
->absoluteQuadsForRange(quads
, startPos
, endPos
);
215 appendQuadsToSubtargetList(quads
, textNode
, subtargets
);
219 static inline void appendZoomableSubtargets(Node
* node
, SubtargetGeometryList
& subtargets
)
221 LayoutBox
* layoutObject
= toLayoutBox(node
->layoutObject());
222 ASSERT(layoutObject
);
224 Vector
<FloatQuad
> quads
;
225 FloatRect
borderBoxRect(layoutObject
->borderBoxRect());
226 FloatRect
contentBoxRect(layoutObject
->contentBoxRect());
227 quads
.append(layoutObject
->localToAbsoluteQuad(borderBoxRect
));
228 if (borderBoxRect
!= contentBoxRect
)
229 quads
.append(layoutObject
->localToAbsoluteQuad(contentBoxRect
));
230 // FIXME: For LayoutBlocks, add column boxes and content boxes cleared for floats.
232 Vector
<FloatQuad
>::const_iterator it
= quads
.begin();
233 const Vector
<FloatQuad
>::const_iterator end
= quads
.end();
234 for (; it
!= end
; ++it
)
235 subtargets
.append(SubtargetGeometry(node
, *it
));
238 static inline Node
* parentShadowHostOrOwner(const Node
* node
)
240 if (Node
* ancestor
= node
->parentOrShadowHostNode())
242 if (node
->isDocumentNode())
243 return toDocument(node
)->ownerElement();
247 // Compiles a list of subtargets of all the relevant target nodes.
248 void compileSubtargetList(const WillBeHeapVector
<RefPtrWillBeMember
<Node
>>& intersectedNodes
, SubtargetGeometryList
& subtargets
, NodeFilter nodeFilter
, AppendSubtargetsForNode appendSubtargetsForNode
)
250 // Find candidates responding to tap gesture events in O(n) time.
251 WillBeHeapHashMap
<RawPtrWillBeMember
<Node
>, RawPtrWillBeMember
<Node
>> responderMap
;
252 WillBeHeapHashSet
<RawPtrWillBeMember
<Node
>> ancestorsToRespondersSet
;
253 WillBeHeapVector
<RawPtrWillBeMember
<Node
>> candidates
;
254 WillBeHeapHashSet
<RawPtrWillBeMember
<Node
>> editableAncestors
;
256 // A node matching the NodeFilter is called a responder. Candidate nodes must either be a
257 // responder or have an ancestor that is a responder.
258 // This iteration tests all ancestors at most once by caching earlier results.
259 for (unsigned i
= 0; i
< intersectedNodes
.size(); ++i
) {
260 Node
* node
= intersectedNodes
[i
].get();
261 WillBeHeapVector
<RawPtrWillBeMember
<Node
>> visitedNodes
;
262 Node
* respondingNode
= nullptr;
263 for (Node
* visitedNode
= node
; visitedNode
; visitedNode
= visitedNode
->parentOrShadowHostNode()) {
264 // Check if we already have a result for a common ancestor from another candidate.
265 respondingNode
= responderMap
.get(visitedNode
);
268 visitedNodes
.append(visitedNode
);
269 // Check if the node filter applies, which would mean we have found a responding node.
270 if (nodeFilter(visitedNode
)) {
271 respondingNode
= visitedNode
;
272 // Continue the iteration to collect the ancestors of the responder, which we will need later.
273 for (visitedNode
= parentShadowHostOrOwner(visitedNode
); visitedNode
; visitedNode
= parentShadowHostOrOwner(visitedNode
)) {
274 WillBeHeapHashSet
<RawPtrWillBeMember
<Node
>>::AddResult addResult
= ancestorsToRespondersSet
.add(visitedNode
);
275 if (!addResult
.isNewEntry
)
281 // Insert the detected responder for all the visited nodes.
282 for (unsigned j
= 0; j
< visitedNodes
.size(); j
++)
283 responderMap
.add(visitedNodes
[j
], respondingNode
);
286 candidates
.append(node
);
289 // We compile the list of component absolute quads instead of using the bounding rect
290 // to be able to perform better hit-testing on inline links on line-breaks.
291 for (unsigned i
= 0; i
< candidates
.size(); i
++) {
292 Node
* candidate
= candidates
[i
];
293 // Skip nodes who's responders are ancestors of other responders. This gives preference to
294 // the inner-most event-handlers. So that a link is always preferred even when contained
295 // in an element that monitors all click-events.
296 Node
* respondingNode
= responderMap
.get(candidate
);
297 ASSERT(respondingNode
);
298 if (ancestorsToRespondersSet
.contains(respondingNode
))
300 // Consolidate bounds for editable content.
301 if (editableAncestors
.contains(candidate
))
303 if (candidate
->isContentEditable()) {
304 Node
* replacement
= candidate
;
305 Node
* parent
= candidate
->parentOrShadowHostNode();
306 while (parent
&& parent
->isContentEditable()) {
307 replacement
= parent
;
308 if (editableAncestors
.contains(replacement
)) {
309 replacement
= nullptr;
312 editableAncestors
.add(replacement
);
313 parent
= parent
->parentOrShadowHostNode();
315 candidate
= replacement
;
318 appendSubtargetsForNode(candidate
, subtargets
);
322 // Compiles a list of zoomable subtargets.
323 void compileZoomableSubtargets(const WillBeHeapVector
<RefPtrWillBeMember
<Node
>>& intersectedNodes
, SubtargetGeometryList
& subtargets
)
325 for (unsigned i
= 0; i
< intersectedNodes
.size(); ++i
) {
326 Node
* candidate
= intersectedNodes
[i
].get();
327 if (nodeIsZoomTarget(candidate
))
328 appendZoomableSubtargets(candidate
, subtargets
);
332 // This returns quotient of the target area and its intersection with the touch area.
333 // This will prioritize largest intersection and smallest area, while balancing the two against each other.
334 float zoomableIntersectionQuotient(const IntPoint
& touchHotspot
, const IntRect
& touchArea
, const SubtargetGeometry
& subtarget
)
336 IntRect rect
= subtarget
.node()->document().view()->contentsToRootFrame(subtarget
.boundingBox());
338 // Check the rectangle is meaningful zoom target. It should at least contain the hotspot.
339 if (!rect
.contains(touchHotspot
))
340 return std::numeric_limits
<float>::infinity();
341 IntRect intersection
= rect
;
342 intersection
.intersect(touchArea
);
344 // Return the quotient of the intersection.
345 return rect
.size().area() / (float)intersection
.size().area();
348 // Uses a hybrid of distance to adjust and intersect ratio, normalizing each score between 0 and 1
349 // and combining them. The distance to adjust works best for disambiguating clicks on targets such
350 // as links, where the width may be significantly larger than the touch width. Using area of overlap
351 // in such cases can lead to a bias towards shorter links. Conversely, percentage of overlap can
352 // provide strong confidence in tapping on a small target, where the overlap is often quite high,
353 // and works well for tightly packed controls.
354 float hybridDistanceFunction(const IntPoint
& touchHotspot
, const IntRect
& touchRect
, const SubtargetGeometry
& subtarget
)
356 IntRect rect
= subtarget
.node()->document().view()->contentsToRootFrame(subtarget
.boundingBox());
358 float radiusSquared
= 0.25f
* (touchRect
.size().diagonalLengthSquared());
359 float distanceToAdjustScore
= rect
.distanceSquaredToPoint(touchHotspot
) / radiusSquared
;
361 int maxOverlapWidth
= std::min(touchRect
.width(), rect
.width());
362 int maxOverlapHeight
= std::min(touchRect
.height(), rect
.height());
363 float maxOverlapArea
= std::max(maxOverlapWidth
* maxOverlapHeight
, 1);
364 rect
.intersect(touchRect
);
365 float intersectArea
= rect
.size().area();
366 float intersectionScore
= 1 - intersectArea
/ maxOverlapArea
;
368 float hybridScore
= intersectionScore
+ distanceToAdjustScore
;
373 FloatPoint
contentsToRootFrame(FrameView
*view
, FloatPoint pt
)
375 int x
= static_cast<int>(pt
.x() + 0.5f
);
376 int y
= static_cast<int>(pt
.y() + 0.5f
);
377 IntPoint adjusted
= view
->contentsToRootFrame(IntPoint(x
, y
));
378 return FloatPoint(adjusted
.x(), adjusted
.y());
381 // Adjusts 'point' to the nearest point inside rect, and leaves it unchanged if already inside.
382 void adjustPointToRect(FloatPoint
& point
, const FloatRect
& rect
)
384 if (point
.x() < rect
.x())
385 point
.setX(rect
.x());
386 else if (point
.x() > rect
.maxX())
387 point
.setX(rect
.maxX());
389 if (point
.y() < rect
.y())
390 point
.setY(rect
.y());
391 else if (point
.y() > rect
.maxY())
392 point
.setY(rect
.maxY());
395 bool snapTo(const SubtargetGeometry
& geom
, const IntPoint
& touchPoint
, const IntRect
& touchArea
, IntPoint
& adjustedPoint
)
397 FrameView
* view
= geom
.node()->document().view();
398 FloatQuad quad
= geom
.quad();
400 if (quad
.isRectilinear()) {
401 IntRect bounds
= view
->contentsToRootFrame(geom
.boundingBox());
402 if (bounds
.contains(touchPoint
)) {
403 adjustedPoint
= touchPoint
;
406 if (bounds
.intersects(touchArea
)) {
407 bounds
.intersect(touchArea
);
408 adjustedPoint
= bounds
.center();
414 // The following code tries to adjust the point to place inside a both the touchArea and the non-rectilinear quad.
415 // FIXME: This will return the point inside the touch area that is the closest to the quad center, but does not
416 // guarantee that the point will be inside the quad. Corner-cases exist where the quad will intersect but this
417 // will fail to adjust the point to somewhere in the intersection.
419 FloatPoint p1
= contentsToRootFrame(view
, quad
.p1());
420 FloatPoint p2
= contentsToRootFrame(view
, quad
.p2());
421 FloatPoint p3
= contentsToRootFrame(view
, quad
.p3());
422 FloatPoint p4
= contentsToRootFrame(view
, quad
.p4());
423 quad
= FloatQuad(p1
, p2
, p3
, p4
);
425 if (quad
.containsPoint(touchPoint
)) {
426 adjustedPoint
= touchPoint
;
430 // Pull point towards the center of the element.
431 FloatPoint center
= quad
.center();
433 adjustPointToRect(center
, touchArea
);
434 adjustedPoint
= roundedIntPoint(center
);
436 return quad
.containsPoint(adjustedPoint
);
439 // A generic function for finding the target node with the lowest distance metric. A distance metric here is the result
440 // of a distance-like function, that computes how well the touch hits the node.
441 // Distance functions could for instance be distance squared or area of intersection.
442 bool findNodeWithLowestDistanceMetric(Node
*& targetNode
, IntPoint
& targetPoint
, IntRect
& targetArea
, const IntPoint
& touchHotspot
, const IntRect
& touchArea
, SubtargetGeometryList
& subtargets
, DistanceFunction distanceFunction
)
444 targetNode
= nullptr;
445 float bestDistanceMetric
= std::numeric_limits
<float>::infinity();
446 SubtargetGeometryList::const_iterator it
= subtargets
.begin();
447 const SubtargetGeometryList::const_iterator end
= subtargets
.end();
448 IntPoint adjustedPoint
;
450 for (; it
!= end
; ++it
) {
451 Node
* node
= it
->node();
452 float distanceMetric
= distanceFunction(touchHotspot
, touchArea
, *it
);
453 if (distanceMetric
< bestDistanceMetric
) {
454 if (snapTo(*it
, touchHotspot
, touchArea
, adjustedPoint
)) {
455 targetPoint
= adjustedPoint
;
456 targetArea
= it
->boundingBox();
458 bestDistanceMetric
= distanceMetric
;
460 } else if (distanceMetric
- bestDistanceMetric
< zeroTolerance
) {
461 if (snapTo(*it
, touchHotspot
, touchArea
, adjustedPoint
)) {
462 if (node
->isDescendantOf(targetNode
)) {
463 // Try to always return the inner-most element.
464 targetPoint
= adjustedPoint
;
466 targetArea
= it
->boundingBox();
472 // As for HitTestResult.innerNode, we skip over pseudo elements.
473 if (targetNode
&& targetNode
->isPseudoElement())
474 targetNode
= targetNode
->parentOrShadowHostNode();
477 targetArea
= targetNode
->document().view()->contentsToRootFrame(targetArea
);
482 } // namespace TouchAdjustment
484 bool findBestClickableCandidate(Node
*& targetNode
, IntPoint
& targetPoint
, const IntPoint
& touchHotspot
, const IntRect
& touchArea
, const WillBeHeapVector
<RefPtrWillBeMember
<Node
>>& nodes
)
487 TouchAdjustment::SubtargetGeometryList subtargets
;
488 TouchAdjustment::compileSubtargetList(nodes
, subtargets
, TouchAdjustment::nodeRespondsToTapGesture
, TouchAdjustment::appendBasicSubtargetsForNode
);
489 return TouchAdjustment::findNodeWithLowestDistanceMetric(targetNode
, targetPoint
, targetArea
, touchHotspot
, touchArea
, subtargets
, TouchAdjustment::hybridDistanceFunction
);
492 bool findBestContextMenuCandidate(Node
*& targetNode
, IntPoint
& targetPoint
, const IntPoint
& touchHotspot
, const IntRect
& touchArea
, const WillBeHeapVector
<RefPtrWillBeMember
<Node
>>& nodes
)
495 TouchAdjustment::SubtargetGeometryList subtargets
;
496 TouchAdjustment::compileSubtargetList(nodes
, subtargets
, TouchAdjustment::providesContextMenuItems
, TouchAdjustment::appendContextSubtargetsForNode
);
497 return TouchAdjustment::findNodeWithLowestDistanceMetric(targetNode
, targetPoint
, targetArea
, touchHotspot
, touchArea
, subtargets
, TouchAdjustment::hybridDistanceFunction
);
500 bool findBestZoomableArea(Node
*& targetNode
, IntRect
& targetArea
, const IntPoint
& touchHotspot
, const IntRect
& touchArea
, const WillBeHeapVector
<RefPtrWillBeMember
<Node
>>& nodes
)
502 IntPoint targetPoint
;
503 TouchAdjustment::SubtargetGeometryList subtargets
;
504 TouchAdjustment::compileZoomableSubtargets(nodes
, subtargets
);
505 return TouchAdjustment::findNodeWithLowestDistanceMetric(targetNode
, targetPoint
, targetArea
, touchHotspot
, touchArea
, subtargets
, TouchAdjustment::zoomableIntersectionQuotient
);