| OLD | NEW |
| (Empty) |
| 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 "sky/engine/config.h" | |
| 21 | |
| 22 #include "sky/engine/core/page/TouchAdjustment.h" | |
| 23 | |
| 24 #include "sky/engine/core/dom/ContainerNode.h" | |
| 25 #include "sky/engine/core/dom/Node.h" | |
| 26 #include "sky/engine/core/dom/NodeRenderStyle.h" | |
| 27 #include "sky/engine/core/dom/Text.h" | |
| 28 #include "sky/engine/core/editing/Editor.h" | |
| 29 #include "sky/engine/core/frame/FrameView.h" | |
| 30 #include "sky/engine/core/frame/LocalFrame.h" | |
| 31 #include "sky/engine/core/rendering/RenderBox.h" | |
| 32 #include "sky/engine/core/rendering/RenderObject.h" | |
| 33 #include "sky/engine/core/rendering/RenderText.h" | |
| 34 #include "sky/engine/core/rendering/style/RenderStyle.h" | |
| 35 #include "sky/engine/platform/geometry/FloatPoint.h" | |
| 36 #include "sky/engine/platform/geometry/FloatQuad.h" | |
| 37 #include "sky/engine/platform/geometry/IntSize.h" | |
| 38 #include "sky/engine/platform/text/TextBreakIterator.h" | |
| 39 | |
| 40 namespace blink { | |
| 41 | |
| 42 namespace TouchAdjustment { | |
| 43 | |
| 44 const float zeroTolerance = 1e-6f; | |
| 45 | |
| 46 // Class for remembering absolute quads of a target node and what node they repr
esent. | |
| 47 class SubtargetGeometry { | |
| 48 ALLOW_ONLY_INLINE_ALLOCATION(); | |
| 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 RawPtr<Node> m_node; | |
| 61 FloatQuad m_quad; | |
| 62 }; | |
| 63 | |
| 64 } | |
| 65 | |
| 66 } | |
| 67 | |
| 68 WTF_ALLOW_MOVE_INIT_AND_COMPARE_WITH_MEM_FUNCTIONS(blink::TouchAdjustment::Subta
rgetGeometry) | |
| 69 | |
| 70 namespace blink { | |
| 71 | |
| 72 namespace TouchAdjustment { | |
| 73 | |
| 74 typedef Vector<SubtargetGeometry> SubtargetGeometryList; | |
| 75 typedef bool (*NodeFilter)(Node*); | |
| 76 typedef void (*AppendSubtargetsForNode)(Node*, SubtargetGeometryList&); | |
| 77 typedef float (*DistanceFunction)(const IntPoint&, const IntRect&, const Subtarg
etGeometry&); | |
| 78 | |
| 79 // Takes non-const Node* because isContentEditable is a non-const function. | |
| 80 bool nodeRespondsToTapGesture(Node* node) | |
| 81 { | |
| 82 if (node->willRespondToMouseClickEvents() || node->willRespondToMouseMoveEve
nts()) | |
| 83 return true; | |
| 84 if (node->isElementNode()) { | |
| 85 Element* element = toElement(node); | |
| 86 // Tapping on a text field or other focusable item should trigger adjust
ment, except | |
| 87 // that iframe elements are hard-coded to support focus but the effect i
s often invisible | |
| 88 // so they should be excluded. | |
| 89 if (element->isMouseFocusable()) | |
| 90 return true; | |
| 91 } | |
| 92 if (RenderStyle* renderStyle = node->renderStyle()) { | |
| 93 if (renderStyle->affectedByActive() || renderStyle->affectedByHover()) | |
| 94 return true; | |
| 95 } | |
| 96 return false; | |
| 97 } | |
| 98 | |
| 99 static inline void appendQuadsToSubtargetList(Vector<FloatQuad>& quads, Node* no
de, SubtargetGeometryList& subtargets) | |
| 100 { | |
| 101 Vector<FloatQuad>::const_iterator it = quads.begin(); | |
| 102 const Vector<FloatQuad>::const_iterator end = quads.end(); | |
| 103 for (; it != end; ++it) | |
| 104 subtargets.append(SubtargetGeometry(node, *it)); | |
| 105 } | |
| 106 | |
| 107 static inline void appendBasicSubtargetsForNode(Node* node, SubtargetGeometryLis
t& subtargets) | |
| 108 { | |
| 109 // Node guaranteed to have renderer due to check in node filter. | |
| 110 ASSERT(node->renderer()); | |
| 111 | |
| 112 Vector<FloatQuad> quads; | |
| 113 node->renderer()->absoluteQuads(quads); | |
| 114 | |
| 115 appendQuadsToSubtargetList(quads, node, subtargets); | |
| 116 } | |
| 117 | |
| 118 // Compiles a list of subtargets of all the relevant target nodes. | |
| 119 void compileSubtargetList(const Vector<RefPtr<Node> >& intersectedNodes, Subtarg
etGeometryList& subtargets, NodeFilter nodeFilter, AppendSubtargetsForNode appen
dSubtargetsForNode) | |
| 120 { | |
| 121 // Find candidates responding to tap gesture events in O(n) time. | |
| 122 HashMap<RawPtr<Node>, RawPtr<Node> > responderMap; | |
| 123 HashSet<RawPtr<Node> > ancestorsToRespondersSet; | |
| 124 Vector<RawPtr<Node> > candidates; | |
| 125 HashSet<RawPtr<Node> > editableAncestors; | |
| 126 | |
| 127 // A node matching the NodeFilter is called a responder. Candidate nodes mus
t either be a | |
| 128 // responder or have an ancestor that is a responder. | |
| 129 // This iteration tests all ancestors at most once by caching earlier result
s. | |
| 130 for (unsigned i = 0; i < intersectedNodes.size(); ++i) { | |
| 131 Node* node = intersectedNodes[i].get(); | |
| 132 Vector<RawPtr<Node> > visitedNodes; | |
| 133 Node* respondingNode = 0; | |
| 134 for (Node* visitedNode = node; visitedNode; visitedNode = visitedNode->p
arentOrShadowHostNode()) { | |
| 135 // Check if we already have a result for a common ancestor from anot
her candidate. | |
| 136 respondingNode = responderMap.get(visitedNode); | |
| 137 if (respondingNode) | |
| 138 break; | |
| 139 visitedNodes.append(visitedNode); | |
| 140 // Check if the node filter applies, which would mean we have found
a responding node. | |
| 141 if (nodeFilter(visitedNode)) { | |
| 142 respondingNode = visitedNode; | |
| 143 // Continue the iteration to collect the ancestors of the respon
der, which we will need later. | |
| 144 for (visitedNode = visitedNode->parentOrShadowHostNode(); visite
dNode; visitedNode = visitedNode->parentOrShadowHostNode()) { | |
| 145 HashSet<RawPtr<Node> >::AddResult addResult = ancestorsToRes
pondersSet.add(visitedNode); | |
| 146 if (!addResult.isNewEntry) | |
| 147 break; | |
| 148 } | |
| 149 break; | |
| 150 } | |
| 151 } | |
| 152 // Insert the detected responder for all the visited nodes. | |
| 153 for (unsigned j = 0; j < visitedNodes.size(); j++) | |
| 154 responderMap.add(visitedNodes[j], respondingNode); | |
| 155 | |
| 156 if (respondingNode) | |
| 157 candidates.append(node); | |
| 158 } | |
| 159 | |
| 160 // We compile the list of component absolute quads instead of using the boun
ding rect | |
| 161 // to be able to perform better hit-testing on inline links on line-breaks. | |
| 162 for (unsigned i = 0; i < candidates.size(); i++) { | |
| 163 Node* candidate = candidates[i]; | |
| 164 // Skip nodes who's responders are ancestors of other responders. This g
ives preference to | |
| 165 // the inner-most event-handlers. So that a link is always preferred eve
n when contained | |
| 166 // in an element that monitors all click-events. | |
| 167 Node* respondingNode = responderMap.get(candidate); | |
| 168 ASSERT(respondingNode); | |
| 169 if (ancestorsToRespondersSet.contains(respondingNode)) | |
| 170 continue; | |
| 171 // Consolidate bounds for editable content. | |
| 172 if (editableAncestors.contains(candidate)) | |
| 173 continue; | |
| 174 if (candidate->isContentEditable()) { | |
| 175 Node* replacement = candidate; | |
| 176 Node* parent = candidate->parentOrShadowHostNode(); | |
| 177 while (parent && parent->isContentEditable()) { | |
| 178 replacement = parent; | |
| 179 if (editableAncestors.contains(replacement)) { | |
| 180 replacement = 0; | |
| 181 break; | |
| 182 } | |
| 183 editableAncestors.add(replacement); | |
| 184 parent = parent->parentOrShadowHostNode(); | |
| 185 } | |
| 186 candidate = replacement; | |
| 187 } | |
| 188 if (candidate) | |
| 189 appendSubtargetsForNode(candidate, subtargets); | |
| 190 } | |
| 191 } | |
| 192 | |
| 193 // Uses a hybrid of distance to adjust and intersect ratio, normalizing each sco
re between 0 and 1 | |
| 194 // and combining them. The distance to adjust works best for disambiguating clic
ks on targets such | |
| 195 // as links, where the width may be significantly larger than the touch width. U
sing area of overlap | |
| 196 // in such cases can lead to a bias towards shorter links. Conversely, percentag
e of overlap can | |
| 197 // provide strong confidence in tapping on a small target, where the overlap is
often quite high, | |
| 198 // and works well for tightly packed controls. | |
| 199 float hybridDistanceFunction(const IntPoint& touchHotspot, const IntRect& touchR
ect, const SubtargetGeometry& subtarget) | |
| 200 { | |
| 201 IntRect rect = subtarget.boundingBox(); | |
| 202 | |
| 203 // Convert from frame coordinates to window coordinates. | |
| 204 rect = subtarget.node()->document().view()->contentsToWindow(rect); | |
| 205 | |
| 206 float radiusSquared = 0.25f * (touchRect.size().diagonalLengthSquared()); | |
| 207 float distanceToAdjustScore = rect.distanceSquaredToPoint(touchHotspot) / ra
diusSquared; | |
| 208 | |
| 209 int maxOverlapWidth = std::min(touchRect.width(), rect.width()); | |
| 210 int maxOverlapHeight = std::min(touchRect.height(), rect.height()); | |
| 211 float maxOverlapArea = std::max(maxOverlapWidth * maxOverlapHeight, 1); | |
| 212 rect.intersect(touchRect); | |
| 213 float intersectArea = rect.size().area(); | |
| 214 float intersectionScore = 1 - intersectArea / maxOverlapArea; | |
| 215 | |
| 216 float hybridScore = intersectionScore + distanceToAdjustScore; | |
| 217 | |
| 218 return hybridScore; | |
| 219 } | |
| 220 | |
| 221 FloatPoint contentsToWindow(FrameView *view, FloatPoint pt) | |
| 222 { | |
| 223 int x = static_cast<int>(pt.x() + 0.5f); | |
| 224 int y = static_cast<int>(pt.y() + 0.5f); | |
| 225 IntPoint adjusted = view->contentsToWindow(IntPoint(x, y)); | |
| 226 return FloatPoint(adjusted.x(), adjusted.y()); | |
| 227 } | |
| 228 | |
| 229 // Adjusts 'point' to the nearest point inside rect, and leaves it unchanged if
already inside. | |
| 230 void adjustPointToRect(FloatPoint& point, const FloatRect& rect) | |
| 231 { | |
| 232 if (point.x() < rect.x()) | |
| 233 point.setX(rect.x()); | |
| 234 else if (point.x() > rect.maxX()) | |
| 235 point.setX(rect.maxX()); | |
| 236 | |
| 237 if (point.y() < rect.y()) | |
| 238 point.setY(rect.y()); | |
| 239 else if (point.y() > rect.maxY()) | |
| 240 point.setY(rect.maxY()); | |
| 241 } | |
| 242 | |
| 243 bool snapTo(const SubtargetGeometry& geom, const IntPoint& touchPoint, const Int
Rect& touchArea, IntPoint& adjustedPoint) | |
| 244 { | |
| 245 FrameView* view = geom.node()->document().view(); | |
| 246 FloatQuad quad = geom.quad(); | |
| 247 | |
| 248 if (quad.isRectilinear()) { | |
| 249 IntRect contentBounds = geom.boundingBox(); | |
| 250 // Convert from frame coordinates to window coordinates. | |
| 251 IntRect bounds = view->contentsToWindow(contentBounds); | |
| 252 if (bounds.contains(touchPoint)) { | |
| 253 adjustedPoint = touchPoint; | |
| 254 return true; | |
| 255 } | |
| 256 if (bounds.intersects(touchArea)) { | |
| 257 bounds.intersect(touchArea); | |
| 258 adjustedPoint = bounds.center(); | |
| 259 return true; | |
| 260 } | |
| 261 return false; | |
| 262 } | |
| 263 | |
| 264 // The following code tries to adjust the point to place inside a both the t
ouchArea and the non-rectilinear quad. | |
| 265 // FIXME: This will return the point inside the touch area that is the close
st to the quad center, but does not | |
| 266 // guarantee that the point will be inside the quad. Corner-cases exist wher
e the quad will intersect but this | |
| 267 // will fail to adjust the point to somewhere in the intersection. | |
| 268 | |
| 269 // Convert quad from content to window coordinates. | |
| 270 FloatPoint p1 = contentsToWindow(view, quad.p1()); | |
| 271 FloatPoint p2 = contentsToWindow(view, quad.p2()); | |
| 272 FloatPoint p3 = contentsToWindow(view, quad.p3()); | |
| 273 FloatPoint p4 = contentsToWindow(view, quad.p4()); | |
| 274 quad = FloatQuad(p1, p2, p3, p4); | |
| 275 | |
| 276 if (quad.containsPoint(touchPoint)) { | |
| 277 adjustedPoint = touchPoint; | |
| 278 return true; | |
| 279 } | |
| 280 | |
| 281 // Pull point towards the center of the element. | |
| 282 FloatPoint center = quad.center(); | |
| 283 | |
| 284 adjustPointToRect(center, touchArea); | |
| 285 adjustedPoint = roundedIntPoint(center); | |
| 286 | |
| 287 return quad.containsPoint(adjustedPoint); | |
| 288 } | |
| 289 | |
| 290 // A generic function for finding the target node with the lowest distance metri
c. A distance metric here is the result | |
| 291 // of a distance-like function, that computes how well the touch hits the node. | |
| 292 // Distance functions could for instance be distance squared or area of intersec
tion. | |
| 293 bool findNodeWithLowestDistanceMetric(Node*& targetNode, IntPoint& targetPoint,
IntRect& targetArea, const IntPoint& touchHotspot, const IntRect& touchArea, Sub
targetGeometryList& subtargets, DistanceFunction distanceFunction) | |
| 294 { | |
| 295 targetNode = 0; | |
| 296 float bestDistanceMetric = std::numeric_limits<float>::infinity(); | |
| 297 SubtargetGeometryList::const_iterator it = subtargets.begin(); | |
| 298 const SubtargetGeometryList::const_iterator end = subtargets.end(); | |
| 299 IntPoint adjustedPoint; | |
| 300 | |
| 301 for (; it != end; ++it) { | |
| 302 Node* node = it->node(); | |
| 303 float distanceMetric = distanceFunction(touchHotspot, touchArea, *it); | |
| 304 if (distanceMetric < bestDistanceMetric) { | |
| 305 if (snapTo(*it, touchHotspot, touchArea, adjustedPoint)) { | |
| 306 targetPoint = adjustedPoint; | |
| 307 targetArea = it->boundingBox(); | |
| 308 targetNode = node; | |
| 309 bestDistanceMetric = distanceMetric; | |
| 310 } | |
| 311 } else if (distanceMetric - bestDistanceMetric < zeroTolerance) { | |
| 312 if (snapTo(*it, touchHotspot, touchArea, adjustedPoint)) { | |
| 313 if (node->isDescendantOf(targetNode)) { | |
| 314 // Try to always return the inner-most element. | |
| 315 targetPoint = adjustedPoint; | |
| 316 targetNode = node; | |
| 317 targetArea = it->boundingBox(); | |
| 318 } | |
| 319 } | |
| 320 } | |
| 321 } | |
| 322 | |
| 323 if (targetNode) { | |
| 324 targetArea = targetNode->document().view()->contentsToWindow(targetArea)
; | |
| 325 } | |
| 326 return (targetNode); | |
| 327 } | |
| 328 | |
| 329 } // namespace TouchAdjustment | |
| 330 | |
| 331 bool findBestClickableCandidate(Node*& targetNode, IntPoint& targetPoint, const
IntPoint& touchHotspot, const IntRect& touchArea, const Vector<RefPtr<Node> >& n
odes) | |
| 332 { | |
| 333 IntRect targetArea; | |
| 334 TouchAdjustment::SubtargetGeometryList subtargets; | |
| 335 TouchAdjustment::compileSubtargetList(nodes, subtargets, TouchAdjustment::no
deRespondsToTapGesture, TouchAdjustment::appendBasicSubtargetsForNode); | |
| 336 return TouchAdjustment::findNodeWithLowestDistanceMetric(targetNode, targetP
oint, targetArea, touchHotspot, touchArea, subtargets, TouchAdjustment::hybridDi
stanceFunction); | |
| 337 } | |
| 338 | |
| 339 } // namespace blink | |
| OLD | NEW |