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