1 /* 2 * Copyright (C) 2012 Nokia Corporation and/or its subsidiary(-ies) 3 * 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. 8 * 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. 13 * 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. 18 */ 19 20 #include "config.h" 21 22 #include "core/page/TouchAdjustment.h" 23 24 #include "core/dom/ContainerNode.h" 25 #include "core/dom/Node.h" 26 #include "core/dom/NodeRenderStyle.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/rendering/RenderBox.h" 33 #include "core/rendering/RenderObject.h" 34 #include "core/rendering/RenderText.h" 35 #include "core/rendering/style/RenderStyle.h" 36 #include "platform/geometry/FloatPoint.h" 37 #include "platform/geometry/FloatQuad.h" 38 #include "platform/geometry/IntSize.h" 39 #include "platform/text/TextBreakIterator.h" 40 41 namespace blink { 42 43 namespace TouchAdjustment { 44 45 const float zeroTolerance = 1e-6f; 46 47 // Class for remembering absolute quads of a target node and what node they represent. 48 class SubtargetGeometry { 49 ALLOW_ONLY_INLINE_ALLOCATION(); 50 public: 51 SubtargetGeometry(Node* node, const FloatQuad& quad) 52 : m_node(node) 53 , m_quad(quad) 54 { } 55 void trace(Visitor* visitor) { visitor->trace(m_node); } 56 57 Node* node() const { return m_node; } 58 FloatQuad quad() const { return m_quad; } 59 IntRect boundingBox() const { return m_quad.enclosingBoundingBox(); } 60 61 private: 62 RawPtrWillBeMember<Node> m_node; 63 FloatQuad m_quad; 64 }; 65 66 } 67 68 } 69 70 WTF_ALLOW_MOVE_INIT_AND_COMPARE_WITH_MEM_FUNCTIONS(blink::TouchAdjustment::SubtargetGeometry) 71 72 namespace blink { 73 74 namespace TouchAdjustment { 75 76 typedef WillBeHeapVector<SubtargetGeometry> SubtargetGeometryList; 77 typedef bool (*NodeFilter)(Node*); 78 typedef void (*AppendSubtargetsForNode)(Node*, SubtargetGeometryList&); 79 typedef float (*DistanceFunction)(const IntPoint&, const IntRect&, const SubtargetGeometry&); 80 81 // Takes non-const Node* because isContentEditable is a non-const function. 82 bool nodeRespondsToTapGesture(Node* node) 83 { 84 if (node->willRespondToMouseClickEvents() || node->willRespondToMouseMoveEvents()) 85 return true; 86 if (node->isElementNode()) { 87 Element* element = toElement(node); 88 // Tapping on a text field or other focusable item should trigger adjustment, except 89 // that iframe elements are hard-coded to support focus but the effect is often invisible 90 // so they should be excluded. 91 if (element->isMouseFocusable() && !isHTMLIFrameElement(element)) 92 return true; 93 // Accept nodes that has a CSS effect when touched. 94 if (element->childrenOrSiblingsAffectedByActive() || element->childrenOrSiblingsAffectedByHover()) 95 return true; 96 } 97 if (RenderStyle* renderStyle = node->renderStyle()) { 98 if (renderStyle->affectedByActive() || renderStyle->affectedByHover()) 99 return true; 100 } 101 return false; 102 } 103 104 bool nodeIsZoomTarget(Node* node) 105 { 106 if (node->isTextNode() || node->isShadowRoot()) 107 return false; 108 109 ASSERT(node->renderer()); 110 return node->renderer()->isBox(); 111 } 112 113 bool providesContextMenuItems(Node* node) 114 { 115 // This function tries to match the nodes that receive special context-menu items in 116 // ContextMenuController::populate(), and should be kept uptodate with those. 117 ASSERT(node->renderer() || node->isShadowRoot()); 118 if (!node->renderer()) 119 return false; 120 if (node->isContentEditable()) 121 return true; 122 if (node->isLink()) 123 return true; 124 if (node->renderer()->isImage()) 125 return true; 126 if (node->renderer()->isMedia()) 127 return true; 128 if (node->renderer()->canBeSelectionLeaf()) { 129 // If the context menu gesture will trigger a selection all selectable nodes are valid targets. 130 if (node->renderer()->frame()->editor().behavior().shouldSelectOnContextualMenuClick()) 131 return true; 132 // Only the selected part of the renderer is a valid target, but this will be corrected in 133 // appendContextSubtargetsForNode. 134 if (node->renderer()->selectionState() != RenderObject::SelectionNone) 135 return true; 136 } 137 return false; 138 } 139 140 static inline void appendQuadsToSubtargetList(Vector<FloatQuad>& quads, Node* node, SubtargetGeometryList& subtargets) 141 { 142 Vector<FloatQuad>::const_iterator it = quads.begin(); 143 const Vector<FloatQuad>::const_iterator end = quads.end(); 144 for (; it != end; ++it) 145 subtargets.append(SubtargetGeometry(node, *it)); 146 } 147 148 static inline void appendBasicSubtargetsForNode(Node* node, SubtargetGeometryList& subtargets) 149 { 150 // Node guaranteed to have renderer due to check in node filter. 151 ASSERT(node->renderer()); 152 153 Vector<FloatQuad> quads; 154 node->renderer()->absoluteQuads(quads); 155 156 appendQuadsToSubtargetList(quads, node, subtargets); 157 } 158 159 static inline void appendContextSubtargetsForNode(Node* node, SubtargetGeometryList& subtargets) 160 { 161 // This is a variant of appendBasicSubtargetsForNode that adds special subtargets for 162 // selected or auto-selectable parts of text nodes. 163 ASSERT(node->renderer()); 164 165 if (!node->isTextNode()) 166 return appendBasicSubtargetsForNode(node, subtargets); 167 168 Text* textNode = toText(node); 169 RenderText* textRenderer = textNode->renderer(); 170 171 if (textRenderer->frame()->editor().behavior().shouldSelectOnContextualMenuClick()) { 172 // Make subtargets out of every word. 173 String textValue = textNode->data(); 174 TextBreakIterator* wordIterator = wordBreakIterator(textValue, 0, textValue.length()); 175 int lastOffset = wordIterator->first(); 176 if (lastOffset == -1) 177 return; 178 int offset; 179 while ((offset = wordIterator->next()) != -1) { 180 if (isWordTextBreak(wordIterator)) { 181 Vector<FloatQuad> quads; 182 textRenderer->absoluteQuadsForRange(quads, lastOffset, offset); 183 appendQuadsToSubtargetList(quads, textNode, subtargets); 184 } 185 lastOffset = offset; 186 } 187 } else { 188 if (textRenderer->selectionState() == RenderObject::SelectionNone) 189 return appendBasicSubtargetsForNode(node, subtargets); 190 // If selected, make subtargets out of only the selected part of the text. 191 int startPos, endPos; 192 switch (textRenderer->selectionState()) { 193 case RenderObject::SelectionInside: 194 startPos = 0; 195 endPos = textRenderer->textLength(); 196 break; 197 case RenderObject::SelectionStart: 198 textRenderer->selectionStartEnd(startPos, endPos); 199 endPos = textRenderer->textLength(); 200 break; 201 case RenderObject::SelectionEnd: 202 textRenderer->selectionStartEnd(startPos, endPos); 203 startPos = 0; 204 break; 205 case RenderObject::SelectionBoth: 206 textRenderer->selectionStartEnd(startPos, endPos); 207 break; 208 default: 209 ASSERT_NOT_REACHED(); 210 return; 211 } 212 Vector<FloatQuad> quads; 213 textRenderer->absoluteQuadsForRange(quads, startPos, endPos); 214 appendQuadsToSubtargetList(quads, textNode, subtargets); 215 } 216 } 217 218 static inline void appendZoomableSubtargets(Node* node, SubtargetGeometryList& subtargets) 219 { 220 RenderBox* renderer = toRenderBox(node->renderer()); 221 ASSERT(renderer); 222 223 Vector<FloatQuad> quads; 224 FloatRect borderBoxRect = renderer->borderBoxRect(); 225 FloatRect contentBoxRect = renderer->contentBoxRect(); 226 quads.append(renderer->localToAbsoluteQuad(borderBoxRect)); 227 if (borderBoxRect != contentBoxRect) 228 quads.append(renderer->localToAbsoluteQuad(contentBoxRect)); 229 // FIXME: For RenderBlocks, add column boxes and content boxes cleared for floats. 230 231 Vector<FloatQuad>::const_iterator it = quads.begin(); 232 const Vector<FloatQuad>::const_iterator end = quads.end(); 233 for (; it != end; ++it) 234 subtargets.append(SubtargetGeometry(node, *it)); 235 } 236 237 static inline Node* parentShadowHostOrOwner(const Node* node) 238 { 239 if (Node* ancestor = node->parentOrShadowHostNode()) 240 return ancestor; 241 if (node->isDocumentNode()) 242 return toDocument(node)->ownerElement(); 243 return 0; 244 } 245 246 // Compiles a list of subtargets of all the relevant target nodes. 247 void compileSubtargetList(const WillBeHeapVector<RefPtrWillBeMember<Node> >& intersectedNodes, SubtargetGeometryList& subtargets, NodeFilter nodeFilter, AppendSubtargetsForNode appendSubtargetsForNode) 248 { 249 // Find candidates responding to tap gesture events in O(n) time. 250 WillBeHeapHashMap<RawPtrWillBeMember<Node>, RawPtrWillBeMember<Node> > responderMap; 251 WillBeHeapHashSet<RawPtrWillBeMember<Node> > ancestorsToRespondersSet; 252 WillBeHeapVector<RawPtrWillBeMember<Node> > candidates; 253 WillBeHeapHashSet<RawPtrWillBeMember<Node> > editableAncestors; 254 255 // A node matching the NodeFilter is called a responder. Candidate nodes must either be a 256 // responder or have an ancestor that is a responder. 257 // This iteration tests all ancestors at most once by caching earlier results. 258 for (unsigned i = 0; i < intersectedNodes.size(); ++i) { 259 Node* node = intersectedNodes[i].get(); 260 WillBeHeapVector<RawPtrWillBeMember<Node> > visitedNodes; 261 Node* respondingNode = 0; 262 for (Node* visitedNode = node; visitedNode; visitedNode = visitedNode->parentOrShadowHostNode()) { 263 // Check if we already have a result for a common ancestor from another candidate. 264 respondingNode = responderMap.get(visitedNode); 265 if (respondingNode) 266 break; 267 visitedNodes.append(visitedNode); 268 // Check if the node filter applies, which would mean we have found a responding node. 269 if (nodeFilter(visitedNode)) { 270 respondingNode = visitedNode; 271 // Continue the iteration to collect the ancestors of the responder, which we will need later. 272 for (visitedNode = parentShadowHostOrOwner(visitedNode); visitedNode; visitedNode = parentShadowHostOrOwner(visitedNode)) { 273 WillBeHeapHashSet<RawPtrWillBeMember<Node> >::AddResult addResult = ancestorsToRespondersSet.add(visitedNode); 274 if (!addResult.isNewEntry) 275 break; 276 } 277 break; 278 } 279 } 280 // Insert the detected responder for all the visited nodes. 281 for (unsigned j = 0; j < visitedNodes.size(); j++) 282 responderMap.add(visitedNodes[j], respondingNode); 283 284 if (respondingNode) 285 candidates.append(node); 286 } 287 288 // We compile the list of component absolute quads instead of using the bounding rect 289 // to be able to perform better hit-testing on inline links on line-breaks. 290 for (unsigned i = 0; i < candidates.size(); i++) { 291 Node* candidate = candidates[i]; 292 // Skip nodes who's responders are ancestors of other responders. This gives preference to 293 // the inner-most event-handlers. So that a link is always preferred even when contained 294 // in an element that monitors all click-events. 295 Node* respondingNode = responderMap.get(candidate); 296 ASSERT(respondingNode); 297 if (ancestorsToRespondersSet.contains(respondingNode)) 298 continue; 299 // Consolidate bounds for editable content. 300 if (editableAncestors.contains(candidate)) 301 continue; 302 if (candidate->isContentEditable()) { 303 Node* replacement = candidate; 304 Node* parent = candidate->parentOrShadowHostNode(); 305 while (parent && parent->isContentEditable()) { 306 replacement = parent; 307 if (editableAncestors.contains(replacement)) { 308 replacement = 0; 309 break; 310 } 311 editableAncestors.add(replacement); 312 parent = parent->parentOrShadowHostNode(); 313 } 314 candidate = replacement; 315 } 316 if (candidate) 317 appendSubtargetsForNode(candidate, subtargets); 318 } 319 } 320 321 // Compiles a list of zoomable subtargets. 322 void compileZoomableSubtargets(const WillBeHeapVector<RefPtrWillBeMember<Node> >& intersectedNodes, SubtargetGeometryList& subtargets) 323 { 324 for (unsigned i = 0; i < intersectedNodes.size(); ++i) { 325 Node* candidate = intersectedNodes[i].get(); 326 if (nodeIsZoomTarget(candidate)) 327 appendZoomableSubtargets(candidate, subtargets); 328 } 329 } 330 331 // This returns quotient of the target area and its intersection with the touch area. 332 // This will prioritize largest intersection and smallest area, while balancing the two against each other. 333 float zoomableIntersectionQuotient(const IntPoint& touchHotspot, const IntRect& touchArea, const SubtargetGeometry& subtarget) 334 { 335 IntRect rect = subtarget.boundingBox(); 336 337 // Convert from frame coordinates to window coordinates. 338 rect = subtarget.node()->document().view()->contentsToWindow(rect); 339 340 // Check the rectangle is meaningful zoom target. It should at least contain the hotspot. 341 if (!rect.contains(touchHotspot)) 342 return std::numeric_limits<float>::infinity(); 343 IntRect intersection = rect; 344 intersection.intersect(touchArea); 345 346 // Return the quotient of the intersection. 347 return rect.size().area() / (float)intersection.size().area(); 348 } 349 350 // Uses a hybrid of distance to adjust and intersect ratio, normalizing each score between 0 and 1 351 // and combining them. The distance to adjust works best for disambiguating clicks on targets such 352 // as links, where the width may be significantly larger than the touch width. Using area of overlap 353 // in such cases can lead to a bias towards shorter links. Conversely, percentage of overlap can 354 // provide strong confidence in tapping on a small target, where the overlap is often quite high, 355 // and works well for tightly packed controls. 356 float hybridDistanceFunction(const IntPoint& touchHotspot, const IntRect& touchRect, const SubtargetGeometry& subtarget) 357 { 358 IntRect rect = subtarget.boundingBox(); 359 360 // Convert from frame coordinates to window coordinates. 361 rect = subtarget.node()->document().view()->contentsToWindow(rect); 362 363 float radiusSquared = 0.25f * (touchRect.size().diagonalLengthSquared()); 364 float distanceToAdjustScore = rect.distanceSquaredToPoint(touchHotspot) / radiusSquared; 365 366 int maxOverlapWidth = std::min(touchRect.width(), rect.width()); 367 int maxOverlapHeight = std::min(touchRect.height(), rect.height()); 368 float maxOverlapArea = std::max(maxOverlapWidth * maxOverlapHeight, 1); 369 rect.intersect(touchRect); 370 float intersectArea = rect.size().area(); 371 float intersectionScore = 1 - intersectArea / maxOverlapArea; 372 373 float hybridScore = intersectionScore + distanceToAdjustScore; 374 375 return hybridScore; 376 } 377 378 FloatPoint contentsToWindow(FrameView *view, FloatPoint pt) 379 { 380 int x = static_cast<int>(pt.x() + 0.5f); 381 int y = static_cast<int>(pt.y() + 0.5f); 382 IntPoint adjusted = view->contentsToWindow(IntPoint(x, y)); 383 return FloatPoint(adjusted.x(), adjusted.y()); 384 } 385 386 // Adjusts 'point' to the nearest point inside rect, and leaves it unchanged if already inside. 387 void adjustPointToRect(FloatPoint& point, const FloatRect& rect) 388 { 389 if (point.x() < rect.x()) 390 point.setX(rect.x()); 391 else if (point.x() > rect.maxX()) 392 point.setX(rect.maxX()); 393 394 if (point.y() < rect.y()) 395 point.setY(rect.y()); 396 else if (point.y() > rect.maxY()) 397 point.setY(rect.maxY()); 398 } 399 400 bool snapTo(const SubtargetGeometry& geom, const IntPoint& touchPoint, const IntRect& touchArea, IntPoint& adjustedPoint) 401 { 402 FrameView* view = geom.node()->document().view(); 403 FloatQuad quad = geom.quad(); 404 405 if (quad.isRectilinear()) { 406 IntRect contentBounds = geom.boundingBox(); 407 // Convert from frame coordinates to window coordinates. 408 IntRect bounds = view->contentsToWindow(contentBounds); 409 if (bounds.contains(touchPoint)) { 410 adjustedPoint = touchPoint; 411 return true; 412 } 413 if (bounds.intersects(touchArea)) { 414 bounds.intersect(touchArea); 415 adjustedPoint = bounds.center(); 416 return true; 417 } 418 return false; 419 } 420 421 // The following code tries to adjust the point to place inside a both the touchArea and the non-rectilinear quad. 422 // FIXME: This will return the point inside the touch area that is the closest to the quad center, but does not 423 // guarantee that the point will be inside the quad. Corner-cases exist where the quad will intersect but this 424 // will fail to adjust the point to somewhere in the intersection. 425 426 // Convert quad from content to window coordinates. 427 FloatPoint p1 = contentsToWindow(view, quad.p1()); 428 FloatPoint p2 = contentsToWindow(view, quad.p2()); 429 FloatPoint p3 = contentsToWindow(view, quad.p3()); 430 FloatPoint p4 = contentsToWindow(view, quad.p4()); 431 quad = FloatQuad(p1, p2, p3, p4); 432 433 if (quad.containsPoint(touchPoint)) { 434 adjustedPoint = touchPoint; 435 return true; 436 } 437 438 // Pull point towards the center of the element. 439 FloatPoint center = quad.center(); 440 441 adjustPointToRect(center, touchArea); 442 adjustedPoint = roundedIntPoint(center); 443 444 return quad.containsPoint(adjustedPoint); 445 } 446 447 // A generic function for finding the target node with the lowest distance metric. A distance metric here is the result 448 // of a distance-like function, that computes how well the touch hits the node. 449 // Distance functions could for instance be distance squared or area of intersection. 450 bool findNodeWithLowestDistanceMetric(Node*& targetNode, IntPoint& targetPoint, IntRect& targetArea, const IntPoint& touchHotspot, const IntRect& touchArea, SubtargetGeometryList& subtargets, DistanceFunction distanceFunction) 451 { 452 targetNode = 0; 453 float bestDistanceMetric = std::numeric_limits<float>::infinity(); 454 SubtargetGeometryList::const_iterator it = subtargets.begin(); 455 const SubtargetGeometryList::const_iterator end = subtargets.end(); 456 IntPoint adjustedPoint; 457 458 for (; it != end; ++it) { 459 Node* node = it->node(); 460 float distanceMetric = distanceFunction(touchHotspot, touchArea, *it); 461 if (distanceMetric < bestDistanceMetric) { 462 if (snapTo(*it, touchHotspot, touchArea, adjustedPoint)) { 463 targetPoint = adjustedPoint; 464 targetArea = it->boundingBox(); 465 targetNode = node; 466 bestDistanceMetric = distanceMetric; 467 } 468 } else if (distanceMetric - bestDistanceMetric < zeroTolerance) { 469 if (snapTo(*it, touchHotspot, touchArea, adjustedPoint)) { 470 if (node->isDescendantOf(targetNode)) { 471 // Try to always return the inner-most element. 472 targetPoint = adjustedPoint; 473 targetNode = node; 474 targetArea = it->boundingBox(); 475 } 476 } 477 } 478 } 479 480 // As for HitTestResult.innerNode, we skip over pseudo elements. 481 if (targetNode && targetNode->isPseudoElement()) 482 targetNode = targetNode->parentOrShadowHostNode(); 483 484 if (targetNode) { 485 targetArea = targetNode->document().view()->contentsToWindow(targetArea); 486 } 487 return (targetNode); 488 } 489 490 } // namespace TouchAdjustment 491 492 bool findBestClickableCandidate(Node*& targetNode, IntPoint& targetPoint, const IntPoint& touchHotspot, const IntRect& touchArea, const WillBeHeapVector<RefPtrWillBeMember<Node> >& nodes) 493 { 494 IntRect targetArea; 495 TouchAdjustment::SubtargetGeometryList subtargets; 496 TouchAdjustment::compileSubtargetList(nodes, subtargets, TouchAdjustment::nodeRespondsToTapGesture, TouchAdjustment::appendBasicSubtargetsForNode); 497 return TouchAdjustment::findNodeWithLowestDistanceMetric(targetNode, targetPoint, targetArea, touchHotspot, touchArea, subtargets, TouchAdjustment::hybridDistanceFunction); 498 } 499 500 bool findBestContextMenuCandidate(Node*& targetNode, IntPoint& targetPoint, const IntPoint& touchHotspot, const IntRect& touchArea, const WillBeHeapVector<RefPtrWillBeMember<Node> >& nodes) 501 { 502 IntRect targetArea; 503 TouchAdjustment::SubtargetGeometryList subtargets; 504 TouchAdjustment::compileSubtargetList(nodes, subtargets, TouchAdjustment::providesContextMenuItems, TouchAdjustment::appendContextSubtargetsForNode); 505 return TouchAdjustment::findNodeWithLowestDistanceMetric(targetNode, targetPoint, targetArea, touchHotspot, touchArea, subtargets, TouchAdjustment::hybridDistanceFunction); 506 } 507 508 bool findBestZoomableArea(Node*& targetNode, IntRect& targetArea, const IntPoint& touchHotspot, const IntRect& touchArea, const WillBeHeapVector<RefPtrWillBeMember<Node> >& nodes) 509 { 510 IntPoint targetPoint; 511 TouchAdjustment::SubtargetGeometryList subtargets; 512 TouchAdjustment::compileZoomableSubtargets(nodes, subtargets); 513 return TouchAdjustment::findNodeWithLowestDistanceMetric(targetNode, targetPoint, targetArea, touchHotspot, touchArea, subtargets, TouchAdjustment::zoomableIntersectionQuotient); 514 } 515 516 } // namespace blink 517