Select best target for tap gesture.
authorcommit-queue@webkit.org <commit-queue@webkit.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Mon, 19 Mar 2012 15:33:26 +0000 (15:33 +0000)
committercommit-queue@webkit.org <commit-queue@webkit.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Mon, 19 Mar 2012 15:33:26 +0000 (15:33 +0000)
https://bugs.webkit.org/show_bug.cgi?id=78801

Source/WebCore:

Patch by Allan Sandfeld Jensen <allan.jensen@nokia.com> on 2012-03-19
Reviewed by Kenneth Rohde Christiansen.
IntRect changes reviewed by Dave Hyatt.

The new API is available through EventHandler::bestClickableNodeForTouchPoint, but
implementation details have been placed in page/TouchAdjustment.

The default hit detection is performed by measuring the distance to the center
lines of the absolute rects of the hit nodes. Absolute rects are used instead
of bounding rects to make hit-detecting against links over line breaks. Distance
to center line is used to make it easier to hit small links next to large links.
For line-rects the distance to the center-line is a better expression of the distance
to a rectangles center than the distance to the center-point.

Tests: touchadjustment/event-triggered-widgets.html
       touchadjustment/html-label.html
       touchadjustment/nested-touch.html
       touchadjustment/touch-inlines.html

* Target.pri:
* page/EventHandler.cpp:
(WebCore::EventHandler::handleGestureTap):
(WebCore::EventHandler::bestClickableNodeForTouchPoint):
* page/EventHandler.h:
* page/TouchAdjustment.cpp: Added.
(WebCore::TouchAdjustment::QuadForHitTest::QuadForHitTest):
(WebCore::TouchAdjustment::QuadForHitTest::node):
(WebCore::TouchAdjustment::QuadForHitTest::quad):
(WebCore::TouchAdjustment::QuadForHitTest::boundingBox):
(WebCore::TouchAdjustment::nodeRespondsToTapGesture):
(WebCore::TouchAdjustment::appendAbsoluteQuadsForNodeToHitTestList):
(WebCore::TouchAdjustment::compileQuadsForHitTesting):
(WebCore::TouchAdjustment::distanceSquaredToQuadCenterLine):
(WebCore::TouchAdjustment::findNodeWithLowestMetric):
(WebCore::findBestClickableCandidate):
* page/TouchAdjustment.h: Added.
* platform/graphics/FloatQuad.h:
(WebCore::FloatQuad::center):
* platform/graphics/IntPoint.h:
(WebCore::IntPoint::distanceSquaredToPoint):
* platform/graphics/IntRect.cpp:
(WebCore::distanceToInterval):
(WebCore::IntRect::differenceToPoint):
(WebCore::IntRect::differenceFromCenterLineToPoint):
* platform/graphics/IntRect.h:
(WebCore::IntRect::distanceSquaredToPoint):
(WebCore::IntRect::distanceSquaredFromCenterLineToPoint):
* platform/graphics/IntSize.h:
(WebCore::IntSize::diagonalLengthSquared):
* testing/Internals.cpp:
(WebCore::Internals::touchPositionAdjustedToBestClickableNode):
(WebCore::Internals::touchNodeAdjustedToBestClickableNode):
* testing/Internals.h:
* testing/Internals.idl:

Source/WebKit2:

Patch by Allan Sandfeld Jensen <allan.jensen@nokia.com> on 2012-03-19
Reviewed by Kenneth Rohde Christiansen.

Send radius to handlePotentialSingleTapEvent so it can do the same hit
detection the tap gesture later does.

* UIProcess/WebPageProxy.cpp:
(WebKit::WebPageProxy::handlePotentialActivation):
* UIProcess/WebPageProxy.h:
* UIProcess/qt/QtWebPageEventHandler.cpp:
(QtWebPageEventHandler::handlePotentialSingleTapEvent):
* WebProcess/WebPage/WebPage.cpp:
(WebKit::WebPage::highlightPotentialActivation):
* WebProcess/WebPage/WebPage.h:
* WebProcess/WebPage/WebPage.messages.in:

Tools:

Patch by Allan Sandfeld Jensen <allan.jensen@nokia.com> on 2012-03-19
Reviewed by Kenneth Rohde Christiansen.

Add TOUCH_ADJUSTMENT to enabled features.

* qmake/mkspecs/features/features.prf:

LayoutTests:

Patch by Allan Sandfeld Jensen <allan.jensen@nokia.com> on 2012-03-19
Reviewed by Kenneth Rohde Christiansen.

Test of touch adjustments. Tests several both normal and tricky cases.

* platform/chromium/test_expectations.txt:
* platform/efl/Skipped:
* platform/gtk/Skipped:
* platform/mac/Skipped:
* platform/win/Skipped:
* touchadjustment/event-triggered-widgets-expected.txt: Added.
* touchadjustment/event-triggered-widgets.html: Added.
* touchadjustment/html-label-expected.txt: Added.
* touchadjustment/html-label.html: Added.
* touchadjustment/nested-touch-expected.txt: Added.
* touchadjustment/nested-touch.html: Added.
* touchadjustment/touch-inlines-expected.txt: Added.
* touchadjustment/touch-inlines.html: Added.

git-svn-id: https://svn.webkit.org/repository/webkit/trunk@111185 268f45cc-cd09-0410-ab3c-d52691b4dbfc

37 files changed:
LayoutTests/ChangeLog
LayoutTests/platform/chromium/test_expectations.txt
LayoutTests/platform/efl/Skipped
LayoutTests/platform/gtk/Skipped
LayoutTests/platform/mac/Skipped
LayoutTests/platform/win/Skipped
LayoutTests/touchadjustment/event-triggered-widgets-expected.txt [new file with mode: 0644]
LayoutTests/touchadjustment/event-triggered-widgets.html [new file with mode: 0644]
LayoutTests/touchadjustment/html-label-expected.txt [new file with mode: 0644]
LayoutTests/touchadjustment/html-label.html [new file with mode: 0644]
LayoutTests/touchadjustment/nested-touch-expected.txt [new file with mode: 0644]
LayoutTests/touchadjustment/nested-touch.html [new file with mode: 0644]
LayoutTests/touchadjustment/touch-inlines-expected.txt [new file with mode: 0644]
LayoutTests/touchadjustment/touch-inlines.html [new file with mode: 0644]
Source/WebCore/ChangeLog
Source/WebCore/Target.pri
Source/WebCore/page/EventHandler.cpp
Source/WebCore/page/EventHandler.h
Source/WebCore/page/TouchAdjustment.cpp [new file with mode: 0644]
Source/WebCore/page/TouchAdjustment.h [new file with mode: 0644]
Source/WebCore/platform/graphics/FloatQuad.h
Source/WebCore/platform/graphics/IntPoint.h
Source/WebCore/platform/graphics/IntRect.cpp
Source/WebCore/platform/graphics/IntRect.h
Source/WebCore/platform/graphics/IntSize.h
Source/WebCore/testing/Internals.cpp
Source/WebCore/testing/Internals.h
Source/WebCore/testing/Internals.idl
Source/WebKit2/ChangeLog
Source/WebKit2/UIProcess/WebPageProxy.cpp
Source/WebKit2/UIProcess/WebPageProxy.h
Source/WebKit2/UIProcess/qt/QtWebPageEventHandler.cpp
Source/WebKit2/WebProcess/WebPage/WebPage.cpp
Source/WebKit2/WebProcess/WebPage/WebPage.h
Source/WebKit2/WebProcess/WebPage/WebPage.messages.in
Tools/ChangeLog
Tools/qmake/mkspecs/features/features.prf

index 97fd6ea..6be9b51 100644 (file)
@@ -1,3 +1,26 @@
+2012-03-19  Allan Sandfeld Jensen  <allan.jensen@nokia.com>
+
+        Select best target for tap gesture.
+        https://bugs.webkit.org/show_bug.cgi?id=78801
+
+        Reviewed by Kenneth Rohde Christiansen.
+
+        Test of touch adjustments. Tests several both normal and tricky cases.
+
+        * platform/chromium/test_expectations.txt:
+        * platform/efl/Skipped:
+        * platform/gtk/Skipped:
+        * platform/mac/Skipped:
+        * platform/win/Skipped:
+        * touchadjustment/event-triggered-widgets-expected.txt: Added.
+        * touchadjustment/event-triggered-widgets.html: Added.
+        * touchadjustment/html-label-expected.txt: Added.
+        * touchadjustment/html-label.html: Added.
+        * touchadjustment/nested-touch-expected.txt: Added.
+        * touchadjustment/nested-touch.html: Added.
+        * touchadjustment/touch-inlines-expected.txt: Added.
+        * touchadjustment/touch-inlines.html: Added.
+
 2012-03-19  Robert Kroeger  <rjkroege@chromium.org>
 
         [chromium] synthesize wheel events for fling on main thread
index f2e8c48..67d27f8 100644 (file)
@@ -116,6 +116,9 @@ BUGWK72010 SKIP : fast/dom/navigator-vibration.html = FAIL
 // Battery Status API is not supported yet in the chromium port.
 BUGWK62698 SKIP : batterystatus = PASS FAIL
 
+// Touch Adjustment is not supported yet in the chromium port.
+BUGWK78801 SKIP : touchadjustment/ = FAIL
+
 // -----------------------------------------------------------------
 // WONTFIX TESTS
 // -----------------------------------------------------------------
index 4d1615c..8145496 100644 (file)
@@ -2626,6 +2626,10 @@ fast/events/page-visibility-iframe-delete-test.html
 fast/events/page-visibility-iframe-propagation-test.html
 fast/events/page-visibility-transition-test.html
 
+# Touch adjustment not enabled
+# https://bugs.webkit.org/show_bug.cgi?id=78801
+touchadjustment
+
 # There are no expected result set yet.
 animations/additive-transform-animations.html
 animations/animation-border-overflow.html
index 961ccda..a832094 100644 (file)
@@ -1576,6 +1576,10 @@ plugins/netscape-plugin-page-cache-works.html
 # https://bugs.webkit.org/show_bug.cgi?id=80534
 fast/events/autoscroll-in-textfield.html
 
+# Touch adjustment not enabled
+# https://bugs.webkit.org/show_bug.cgi?id=78801
+touchadjustment
+
 # https://bugs.webkit.org/show_bug.cgi?id=81089
 inspector/debugger/snippets-model.html
 
index c90e1d9..a2bbc04 100644 (file)
@@ -573,6 +573,10 @@ fast/mutation/end-of-task-delivery.html
 # Needs PageClients::vibrationClient() implementation.
 fast/dom/navigator-vibration.html
 
+# Touch adjustment not enabled
+# https://bugs.webkit.org/show_bug.cgi?id=78801
+touchadjustment
+
 # http:///webkit.org/b/80531
 # REGRESSION(r110072): fast/forms/textfield-overflow.html is failing
 fast/forms/textfield-overflow.html
index b87ef24..3fe7686 100644 (file)
@@ -1667,6 +1667,10 @@ fast/dom/navigator-vibration.html
 #Battery Status API is not implemented.
 batterystatus
 
+# Touch adjustment not enabled
+# https://bugs.webkit.org/show_bug.cgi?id=78801
+touchadjustment
+
 # Those tests need a text baseline after lazily allocating layers.
 # The change should only be layer removal.
 animations/combo-transform-translate+scale.html
diff --git a/LayoutTests/touchadjustment/event-triggered-widgets-expected.txt b/LayoutTests/touchadjustment/event-triggered-widgets-expected.txt
new file mode 100644 (file)
index 0000000..f94e20e
--- /dev/null
@@ -0,0 +1,31 @@
+Test various ways to trigger input-widgets. On a touch interface, all the actions should be triggerable with either a touch down or a touch tap.
+
+On success, you will see a series of "PASS" messages, followed by "TEST COMPLETE".
+
+
+Testing small direct hits
+PASS adjustedNode.id is element.id
+PASS adjustedNode.id is element.id
+PASS adjustedNode.id is element.id
+PASS adjustedNode.id is not element.id
+PASS adjustedNode.id is element.id
+Testing large direct hits
+PASS adjustedNode.id is element.id
+PASS adjustedNode.id is element.id
+PASS adjustedNode.id is element.id
+PASS adjustedNode.id is not element.id
+PASS adjustedNode.id is element.id
+Testing large direct hits
+PASS adjustedNode.id is element.id
+PASS adjustedNode.id is element.id
+PASS adjustedNode.id is element.id
+PASS adjustedNode.id is not element.id
+PASS adjustedNode.id is element.id
+PASS successfullyParsed is true
+
+TEST COMPLETE
+Focus here should give a text input-field.
+Mouse-over here should give a text input-field.
+Hovering here should give a text input-field. 
+Focusing here should only give focus outline.
+Focusing here should give a text input-field.
diff --git a/LayoutTests/touchadjustment/event-triggered-widgets.html b/LayoutTests/touchadjustment/event-triggered-widgets.html
new file mode 100644 (file)
index 0000000..1279b77
--- /dev/null
@@ -0,0 +1,169 @@
+<html>
+<head>
+<style>
+    .box { border: 1px solid black; border-radius: 5px 5px; margin: 1em; max-width: 40em; }
+</style>
+
+<script src="../fast/js/resources/js-test-pre.js"></script>
+
+</head>
+
+<body onload="runTests()">
+
+<p id='description'></p>
+
+<div id='console'></div>
+
+<script>
+    var element;
+    var adjustedNode;
+    function findAbsolutePosition(node) {
+        var pos = new Object();
+        pos.left = 0; pos.top = 0;
+        do {
+            pos.left += node.offsetLeft;
+            pos.top += node.offsetTop;
+        } while (node = node.offsetParent);
+        return pos;
+    }
+
+    function testDirectTouch(element)
+    {
+        var pos = findAbsolutePosition(element);
+        var x = pos.left + element.clientWidth / 2 - 1;
+        var y = pos.top + element.clientHeight / 2 - 1;
+        var width = 3;
+        var height = 3;
+        adjustedNode = internals.touchNodeAdjustedToBestClickableNode(x, y, width, height, document);
+
+        if (adjustedNode.nodeType == 3) // TEXT node
+            adjustedNode = adjustedNode.parentNode;
+    }
+
+    function testDirectFatFinger(element)
+    {
+        var pos = findAbsolutePosition(element);
+        var x = pos.left + element.clientWidth / 2 - 1;
+        var y = pos.top - 5 ;
+        var width = element.clientHeight;
+        var height = element.clientHeight + 10;
+        adjustedNode = internals.touchNodeAdjustedToBestClickableNode(x, y, width, height, document);
+        if (adjustedNode.nodeType == 3) // TEXT node
+            adjustedNode = adjustedNode.parentNode;
+    }
+
+    function testIndirectFatFinger(element)
+    {
+        var pos = findAbsolutePosition(element);
+        var x = pos.left + element.clientWidth / 2 - 1;
+        var y = pos.top - 7;
+        var width = 10;
+        var height = 10;
+        adjustedNode = internals.touchNodeAdjustedToBestClickableNode(x, y, width, height, document);
+        if (adjustedNode.nodeType == 3) // TEXT node
+            adjustedNode = adjustedNode.parentNode;
+    }
+
+    function testTouchHit(elementid, touchType) {
+        element = document.getElementById(elementid);
+        touchType(element);
+        shouldBe('adjustedNode.id', 'element.id');
+    }
+
+    function testTouchMiss(elementid, touchType) {
+        element = document.getElementById(elementid);
+        touchType(element);
+        shouldNotBe('adjustedNode.id', 'element.id');
+    }
+
+    function testDirectTouches()
+    {
+        debug('Testing small direct hits');
+        testTouchHit('test1', testDirectTouch);
+        testTouchHit('test2', testDirectTouch);
+        testTouchHit('test3', testDirectTouch);
+        testTouchMiss('test4', testDirectTouch);
+        testTouchHit('test5', testDirectTouch);
+    }
+
+    function testDirectFatFingers()
+    {
+        debug('Testing large direct hits');
+        testTouchHit('test1', testDirectFatFinger);
+        testTouchHit('test2', testDirectFatFinger);
+        testTouchHit('test3', testDirectFatFinger);
+        testTouchMiss('test4', testDirectFatFinger);
+        testTouchHit('test5', testDirectFatFinger);
+    }
+
+    function testIndirectFatFingers()
+    {
+        debug('Testing large direct hits');
+        testTouchHit('test1', testIndirectFatFinger);
+        testTouchHit('test2', testIndirectFatFinger);
+        testTouchHit('test3', testIndirectFatFinger);
+        testTouchMiss('test4', testIndirectFatFinger);
+        testTouchHit('test5', testIndirectFatFinger);
+    }
+
+    function runTests()
+    {
+        if (window.layoutTestController && window.internals && internals.touchNodeAdjustedToBestClickableNode) {
+            description('Test various ways to trigger input-widgets. On a touch interface, all the actions should be triggerable with either a touch down or a touch tap.');
+            layoutTestController.dumpAsText();
+            layoutTestController.waitUntilDone();
+            testDirectTouches();
+            testDirectFatFingers();
+            testIndirectFatFingers();
+            isSuccessfullyParsed();
+            layoutTestController.notifyDone();
+        }
+    }
+</script>
+
+<script>
+    function triggerInput() {
+        var element = event.srcElement;
+        if (!element.open) {
+            element.innerHTML = '<input type=text style="width: 100%"></input>'
+            element.open = true;
+        }
+        element.firstChild.focus();
+    }
+</script>
+
+<div id=test1 class=box tabindex=1 onfocus='triggerInput()'>
+Focus here should give a text input-field.
+</div>
+
+<div id=test2 class=box onmouseover='triggerInput()'> 
+Mouse-over here should give a text input-field.
+</div>
+
+<style>
+    .box:not(:hover) .hovertriggered { visibility: hidden;}
+    .box:hover .hoverfallback { display: none; }  
+</style>
+
+<div id=test3 class=box> 
+    <span class=hoverfallback>Hovering here should give a text input-field.</span>
+    <input type=text class=hovertriggered></input>
+</div>
+
+<div id=test4 class=box onfocus='triggerInput()'>
+    <span tabindex=1> Focusing here should only give focus outline.
+    </span>
+</div>
+
+<div id=test5 class=box>
+    <span tabindex=1> Focusing here should give a text input-field.
+    </span>
+</div>
+
+<script>
+    var element = document.getElementById('test5');
+    element.addEventListener('DOMFocusIn', triggerInput, false);
+</script>
+
+</body>
+</html>
diff --git a/LayoutTests/touchadjustment/html-label-expected.txt b/LayoutTests/touchadjustment/html-label-expected.txt
new file mode 100644 (file)
index 0000000..f969fdb
--- /dev/null
@@ -0,0 +1,22 @@
+Do not click here
+Click here, but not here. 
+
+
+
+Tests if labels are treated as clickable if the input they control is.
+
+On success, you will see a series of "PASS" messages, followed by "TEST COMPLETE".
+
+
+Testing small direct hits.
+PASS adjustedNode.id is "mylink"
+PASS adjustedNode.id is "mylabel"
+PASS adjustedNode.id is "myinput"
+Testing indirect hits.
+PASS adjustedNode.id is "mylink"
+PASS adjustedNode.id is "mylabel"
+PASS adjustedNode.id is "myinput"
+PASS successfullyParsed is true
+
+TEST COMPLETE
+
diff --git a/LayoutTests/touchadjustment/html-label.html b/LayoutTests/touchadjustment/html-label.html
new file mode 100644 (file)
index 0000000..1c0ce7d
--- /dev/null
@@ -0,0 +1,108 @@
+<html>
+<head>
+<script src="../fast/js/resources/js-test-pre.js"></script>
+
+</head>
+
+<body id="mybody" onload="runTests()">
+
+<a href="#myform" id="mylink">Do not click here</a><br>
+<form id="myform">
+<label for="myinput" id="mylabel">Click here,</label>
+<span id="myspan">but not here.</span>
+<input type="text" id="myinput" value="To focus this."></input>
+</form>
+<br><br><br>
+
+<p id='description'></p>
+<div id='console'></div>
+
+<script>
+    var element;
+    var adjustedNode;
+    function findAbsolutePosition(node) {
+        var pos = new Object();
+        pos.left = 0; pos.top = 0;
+        do {
+            pos.left += node.offsetLeft;
+            pos.top += node.offsetTop;
+        } while (node = node.offsetParent);
+        return pos;
+    }
+
+    function testDirectTouch(element)
+    {
+        var pos = findAbsolutePosition(element);
+        var x = pos.left + element.clientWidth / 2 - 20;
+        var y = pos.top + element.clientHeight / 2 - 30;
+        var width = 40;
+        var height = 60;
+        var adjustedNode = internals.touchNodeAdjustedToBestClickableNode(x, y, width, height, document);
+        while (adjustedNode.nodeType != 1) // Not an element.
+            adjustedNode = adjustedNode.parentNode;
+        return adjustedNode;
+    }
+
+    function testIndirectTouch(element)
+    {
+        // Touch just right of the element.
+        var pos = findAbsolutePosition(element);
+        var x = pos.left + element.clientWidth + 10 - 30;
+        var y = pos.top + element.clientHeight / 2 - 20;
+        var width = 60;
+        var height = 40;
+        var adjustedNode = internals.touchNodeAdjustedToBestClickableNode(x, y, width, height, document);
+        while (adjustedNode.nodeType != 1 ) // Not an element.
+            adjustedNode = adjustedNode.parentNode;
+        return adjustedNode;
+    }
+
+    function testDirectTouches()
+    {
+        debug('Testing small direct hits.');
+
+        element = document.getElementById('mylink');
+        adjustedNode = testDirectTouch(element);
+        shouldBeEqualToString('adjustedNode.id', 'mylink');
+
+        element = document.getElementById('mylabel');
+        adjustedNode = testDirectTouch(element);
+        shouldBeEqualToString('adjustedNode.id', 'mylabel');
+
+        element = document.getElementById('myinput');
+        adjustedNode = testDirectTouch(element);
+        shouldBeEqualToString('adjustedNode.id', 'myinput');
+    }
+
+    function testIndirectTouches()
+    {
+        debug('Testing indirect hits.');
+
+        element = document.getElementById('mylink');
+        adjustedNode = testIndirectTouch(element);
+        shouldBeEqualToString('adjustedNode.id', 'mylink');
+
+        element = document.getElementById('mylabel');
+        adjustedNode = testIndirectTouch(element);
+        shouldBeEqualToString('adjustedNode.id', 'mylabel');
+
+        element = document.getElementById('myinput');
+        adjustedNode = testIndirectTouch(element);
+        shouldBeEqualToString('adjustedNode.id', 'myinput');
+    }
+
+    function runTests()
+    {
+        if (window.layoutTestController && window.internals && internals.touchNodeAdjustedToBestClickableNode) {
+            description('Tests if labels are treated as clickable if the input they control is.');
+            layoutTestController.dumpAsText();
+            layoutTestController.waitUntilDone();
+            testDirectTouches();
+            testIndirectTouches();
+            isSuccessfullyParsed();
+            layoutTestController.notifyDone();
+        }
+    }
+</script>
+</body>
+</html>
diff --git a/LayoutTests/touchadjustment/nested-touch-expected.txt b/LayoutTests/touchadjustment/nested-touch-expected.txt
new file mode 100644 (file)
index 0000000..73d7c40
--- /dev/null
@@ -0,0 +1,18 @@
+Box with a local click handler.
+Box without a local click handler.
+Test the case where a clickable target is nested inside a document that is monitoring clicks. The target with the local event-handler should be chosen if multiple targets are touched.
+
+On success, you will see a series of "PASS" messages, followed by "TEST COMPLETE".
+
+
+Testing small direct hits.
+PASS adjustedNode.id is element.id
+PASS adjustedNode.id is element.id
+Testing prefered hits.
+PASS adjustedNode.id is element1.id
+PASS adjustedNode.id is element1.id
+PASS adjustedNode.id is element1.id
+PASS successfullyParsed is true
+
+TEST COMPLETE
+
diff --git a/LayoutTests/touchadjustment/nested-touch.html b/LayoutTests/touchadjustment/nested-touch.html
new file mode 100644 (file)
index 0000000..8f6bdcd
--- /dev/null
@@ -0,0 +1,120 @@
+<html>
+<head>
+<style>
+    .box { border: 1px solid black; border-radius: 5px 5px; margin: 1em; max-width: 40em; }
+</style>
+<script src="../fast/js/resources/js-test-pre.js"></script>
+
+</head>
+
+<body onload="runTests()">
+
+<div class=box id=mybox1>
+Box with a local click handler.
+</div>
+
+<div class=box id=mybox2> 
+Box without a local click handler.
+</div>
+
+<script>
+  function monitor(e) { alert(e.target +( e.target.id ? ( ' #' + e.target.id) : '')); };
+  function doSomething(e) {};
+
+  var element = document.getElementById('mybox1');
+  element.addEventListener('click', doSomething, false);
+  element = document.body;
+  element.addEventListener('click', monitor, false);
+</script>
+
+
+<p id='description'></p>
+<div id='console'></div>
+
+<script>
+    var element;
+    var adjustedNode;
+    function findAbsolutePosition(node) {
+        var pos = new Object();
+        pos.left = 0; pos.top = 0;
+        do {
+            pos.left += node.offsetLeft;
+            pos.top += node.offsetTop;
+        } while (node = node.offsetParent);
+        return pos;
+    }
+
+    function testDirectTouch(element)
+    {
+        var pos = findAbsolutePosition(element);
+        var x = pos.left + element.clientWidth / 2 - 1;
+        var y = pos.top + element.clientHeight / 2 - 1;
+        var width = 3;
+        var height = 3;
+        var adjustedNode = internals.touchNodeAdjustedToBestClickableNode(x, y, width, height, document);
+        if (adjustedNode.nodeType == 3) // TEXT node
+            adjustedNode = adjustedNode.parentNode;
+        return adjustedNode;
+    }
+
+    function testDoubleTouch(element1, element2, offset)
+    {
+        var pos1 = findAbsolutePosition(element1);
+        var pos2 = findAbsolutePosition(element2);
+        // We assume the elements have the same x coord and width.
+        var x = pos1.left + element1.clientWidth/2 - 1;
+        var y1 = pos1.top + element1.clientHeight/2 + 1;
+        var y2 = pos2.top + element2.clientHeight/2 - 1;
+        var width = y2 - y1;
+        var height = y2 - y1;
+        var adjustedNode = internals.touchNodeAdjustedToBestClickableNode(x, y1, width, height, document);
+        if (adjustedNode.nodeType == 3) // TEXT node
+            adjustedNode = adjustedNode.parentNode;
+        return adjustedNode;
+    }
+
+    function testDirectTouches()
+    {
+        debug('Testing small direct hits.');
+
+        element = document.getElementById('mybox1');
+        adjustedNode = testDirectTouch(element);
+        shouldBe('adjustedNode.id', 'element.id');
+
+        element = document.getElementById('mybox2');
+        adjustedNode = testDirectTouch(element);
+        shouldBe('adjustedNode.id', 'element.id');
+    }
+
+    function testPreferedTouch()
+    {
+        debug('Testing prefered hits.');
+
+        element1 = document.getElementById('mybox1');
+        element2 = document.getElementById('mybox2');
+        adjustedNode = testDoubleTouch(element1, element2, 0);
+        shouldBe('adjustedNode.id', 'element1.id');
+
+        // First test was centered, now move the test closer to the wrong node, and ensure we still get the prefered node.
+        adjustedNode = testDoubleTouch(element1, element2, 5);
+        shouldBe('adjustedNode.id', 'element1.id');
+
+        adjustedNode = testDoubleTouch(element1, element2, 10);
+        shouldBe('adjustedNode.id', 'element1.id');
+    }
+
+    function runTests()
+    {
+        if (window.layoutTestController && window.internals && internals.touchNodeAdjustedToBestClickableNode) {
+            description('Test the case where a clickable target is nested inside a document that is monitoring clicks. The target with the local event-handler should be chosen if multiple targets are touched.');
+            layoutTestController.dumpAsText();
+            layoutTestController.waitUntilDone();
+            testDirectTouches();
+            testPreferedTouch();
+            isSuccessfullyParsed();
+            layoutTestController.notifyDone();
+        }
+    }
+</script>
+</body>
+</html>
diff --git a/LayoutTests/touchadjustment/touch-inlines-expected.txt b/LayoutTests/touchadjustment/touch-inlines-expected.txt
new file mode 100644 (file)
index 0000000..974ad82
--- /dev/null
@@ -0,0 +1,29 @@
+some link
+some link breaking lines and link
+hola mundo! a split up link
+hello world some link also breaking
+hi there some link that is breaking multiple lines just for the very fun of it
+
+Test touch-adjustment on inline links. Making sure we can hit over line-breaks, and can miss when tapping between parts of a line-broken link.
+
+On success, you will see a series of "PASS" messages, followed by "TEST COMPLETE".
+
+
+Test some direct hits.
+PASS adjustedNode.id is "1"
+PASS adjustedNode.id is "2"
+PASS adjustedNode.id is "4"
+PASS adjustedNode.id is "6"
+Test a few direct misses.
+PASS adjustedNode.id is ""
+PASS adjustedNode.id is ""
+Test some in-direct hits.
+PASS adjustedNode.id is "2"
+PASS adjustedNode.id is "3"
+PASS adjustedNode.id is "4"
+PASS adjustedNode.id is "4"
+PASS adjustedNode.id is "6"
+PASS successfullyParsed is true
+
+TEST COMPLETE
+
diff --git a/LayoutTests/touchadjustment/touch-inlines.html b/LayoutTests/touchadjustment/touch-inlines.html
new file mode 100644 (file)
index 0000000..e99804b
--- /dev/null
@@ -0,0 +1,99 @@
+<html>
+<head>
+    <script src="../fast/js/resources/js-test-pre.js"></script>
+</head>
+<body onload="runTests()">
+
+<p style="width: 10em;">
+<a id="1" href="#1">some link</a><br>
+<a id="2" href="#2">some link breaking lines</a> and <a id="3" href="#3">link</a><br>
+hola mundo! <a id="4" href="#4">a split up link</a><br>
+hello world <a id="5" href="#5">some link also breaking</a><br>
+hi there <a id="6" href="#6">some link that is breaking multiple lines just for the very fun of it</a><br><br>
+</p>
+
+
+<p id='description'></p>
+<div id='console'></div>
+
+<script>
+    var element;
+    var adjustedNode;
+    function testRoundTouch(x, y, radius)
+    {
+        var x = x - radius;
+        var y = y - radius;
+        var width = radius * 2;
+        var height = radius * 2;
+        var adjustedNode = internals.touchNodeAdjustedToBestClickableNode(x, y, width, height, document);
+        if (adjustedNode.nodeType == 3) // TEXT node
+            adjustedNode = adjustedNode.parentNode;
+        return adjustedNode;
+    }
+
+    function testDirectTouches()
+    {
+        debug('Test some direct hits.');
+
+        adjustedNode = testRoundTouch(30, 20, 8)
+        shouldBeEqualToString('adjustedNode.id', '1');
+
+        adjustedNode = testRoundTouch(30, 60, 8)
+        shouldBeEqualToString('adjustedNode.id', '2');
+
+        adjustedNode = testRoundTouch(120, 80, 8)
+        shouldBeEqualToString('adjustedNode.id', '4');
+
+        adjustedNode = testRoundTouch(80, 180, 8)
+        shouldBeEqualToString('adjustedNode.id', '6');
+    }
+
+    function testDirectMisses()
+    {
+        debug('Test a few direct misses.');
+
+        adjustedNode = testRoundTouch(56, 60, 8)
+        shouldBeEqualToString('adjustedNode.id', '');
+
+        adjustedNode = testRoundTouch(20, 160, 4)
+        shouldBeEqualToString('adjustedNode.id', '');
+
+    }
+
+    function testIndirectTouches()
+    {
+        debug('Test some in-direct hits.');
+
+        adjustedNode = testRoundTouch(56, 60, 20)
+        shouldBeEqualToString('adjustedNode.id', '2');
+
+        adjustedNode = testRoundTouch(85, 80, 20)
+        shouldBeEqualToString('adjustedNode.id', '3');
+
+        adjustedNode = testRoundTouch(120, 60, 20)
+        shouldBeEqualToString('adjustedNode.id', '4');
+
+        adjustedNode = testRoundTouch(40, 100, 20)
+        shouldBeEqualToString('adjustedNode.id', '4');
+
+        adjustedNode = testRoundTouch(20, 165, 20)
+        shouldBeEqualToString('adjustedNode.id', '6');
+
+    }
+
+    function runTests()
+    {
+        if (window.layoutTestController && window.internals && internals.touchNodeAdjustedToBestClickableNode) {
+            description('Test touch-adjustment on inline links. Making sure we can hit over line-breaks, and can miss when tapping between parts of a line-broken link.');
+            layoutTestController.dumpAsText();
+            layoutTestController.waitUntilDone();
+            testDirectTouches();
+            testDirectMisses();
+            testIndirectTouches();
+            isSuccessfullyParsed();
+            layoutTestController.notifyDone();
+        }
+    }
+</script>
+</body>
+</html>
index f1a4260..58baed3 100644 (file)
@@ -1,3 +1,62 @@
+2012-03-19  Allan Sandfeld Jensen  <allan.jensen@nokia.com>
+
+        Select best target for tap gesture.
+        https://bugs.webkit.org/show_bug.cgi?id=78801
+
+        Reviewed by Kenneth Rohde Christiansen.
+        IntRect changes reviewed by Dave Hyatt.
+
+        The new API is available through EventHandler::bestClickableNodeForTouchPoint, but
+        implementation details have been placed in page/TouchAdjustment.
+
+        The default hit detection is performed by measuring the distance to the center
+        lines of the absolute rects of the hit nodes. Absolute rects are used instead
+        of bounding rects to make hit-detecting against links over line breaks. Distance
+        to center line is used to make it easier to hit small links next to large links.
+        For line-rects the distance to the center-line is a better expression of the distance
+        to a rectangles center than the distance to the center-point.
+
+        Tests: touchadjustment/event-triggered-widgets.html
+               touchadjustment/html-label.html
+               touchadjustment/nested-touch.html
+               touchadjustment/touch-inlines.html
+
+        * Target.pri:
+        * page/EventHandler.cpp:
+        (WebCore::EventHandler::handleGestureTap):
+        (WebCore::EventHandler::bestClickableNodeForTouchPoint):
+        * page/EventHandler.h:
+        * page/TouchAdjustment.cpp: Added.
+        (WebCore::TouchAdjustment::QuadForHitTest::QuadForHitTest):
+        (WebCore::TouchAdjustment::QuadForHitTest::node):
+        (WebCore::TouchAdjustment::QuadForHitTest::quad):
+        (WebCore::TouchAdjustment::QuadForHitTest::boundingBox):
+        (WebCore::TouchAdjustment::nodeRespondsToTapGesture):
+        (WebCore::TouchAdjustment::appendAbsoluteQuadsForNodeToHitTestList):
+        (WebCore::TouchAdjustment::compileQuadsForHitTesting):
+        (WebCore::TouchAdjustment::distanceSquaredToQuadCenterLine):
+        (WebCore::TouchAdjustment::findNodeWithLowestMetric):
+        (WebCore::findBestClickableCandidate):
+        * page/TouchAdjustment.h: Added.
+        * platform/graphics/FloatQuad.h:
+        (WebCore::FloatQuad::center):
+        * platform/graphics/IntPoint.h:
+        (WebCore::IntPoint::distanceSquaredToPoint):
+        * platform/graphics/IntRect.cpp:
+        (WebCore::distanceToInterval):
+        (WebCore::IntRect::differenceToPoint):
+        (WebCore::IntRect::differenceFromCenterLineToPoint):
+        * platform/graphics/IntRect.h:
+        (WebCore::IntRect::distanceSquaredToPoint):
+        (WebCore::IntRect::distanceSquaredFromCenterLineToPoint):
+        * platform/graphics/IntSize.h:
+        (WebCore::IntSize::diagonalLengthSquared):
+        * testing/Internals.cpp:
+        (WebCore::Internals::touchPositionAdjustedToBestClickableNode):
+        (WebCore::Internals::touchNodeAdjustedToBestClickableNode):
+        * testing/Internals.h:
+        * testing/Internals.idl:
+
 2012-03-19  Mark Pilgrim  <pilgrim@chromium.org>
 
         Add ENABLED(FILE_SYSTEM) to DOMFilePath.h
index d9f9486..db4f184 100644 (file)
@@ -1033,6 +1033,7 @@ SOURCES += \
     page/SecurityPolicy.cpp \
     page/Settings.cpp \
     page/SpatialNavigation.cpp \
+    page/TouchAdjustment.cpp \
     page/SuspendableTimer.cpp \
     page/UserContentURLPattern.cpp \
     page/WindowFeatures.cpp \
@@ -2126,6 +2127,7 @@ HEADERS += \
     page/SpeechInputListener.h \
     page/SpeechInputResult.h \
     page/SpeechInputResultList.h \
+    page/TouchAdjustment.h \
     page/WebKitAnimation.h \
     page/WebKitAnimationList.h \
     page/WindowFeatures.h \
index dc5602b..b7a20ab 100644 (file)
@@ -73,6 +73,7 @@
 #include "Scrollbar.h"
 #include "Settings.h"
 #include "SpatialNavigation.h"
+#include "StaticHashSetNodeList.h"
 #include "StyleCachedImage.h"
 #include "TextEvent.h"
 #include "TextIterator.h"
 #include "PlatformGestureEvent.h"
 #endif
 
+#if ENABLE(TOUCH_ADJUSTMENT)
+#include "TouchAdjustment.h"
+#endif
+
 #if ENABLE(SVG)
 #include "SVGDocument.h"
 #include "SVGElementInstance.h"
@@ -2412,10 +2417,20 @@ bool EventHandler::handleGestureEvent(const PlatformGestureEvent& gestureEvent)
 bool EventHandler::handleGestureTap(const PlatformGestureEvent& gestureEvent)
 {
     // FIXME: Refactor this code to not hit test multiple times.
+    IntPoint adjustedPoint = gestureEvent.position();
+#if ENABLE(TOUCH_ADJUSTMENT)
+    if (!gestureEvent.area().isEmpty()) {
+        Node* targetNode = 0;
+        // For now we use the adjusted position to ensure the later redundant hit-tests hits the right node.
+        bestClickableNodeForTouchPoint(gestureEvent.position(), IntSize(gestureEvent.area().width() / 2, gestureEvent.area().height() / 2), adjustedPoint, targetNode);
+        if (!targetNode)
+            return false;
+    }
+#endif
     bool defaultPrevented = false;
-    PlatformMouseEvent fakeMouseMove(gestureEvent.position(), gestureEvent.globalPosition(), NoButton, PlatformEvent::MouseMoved, /* clickCount */ 1, gestureEvent.shiftKey(), gestureEvent.ctrlKey(), gestureEvent.altKey(), gestureEvent.metaKey(), gestureEvent.timestamp());
-    PlatformMouseEvent fakeMouseDown(gestureEvent.position(), gestureEvent.globalPosition(), LeftButton, PlatformEvent::MousePressed, /* clickCount */ 1, gestureEvent.shiftKey(), gestureEvent.ctrlKey(), gestureEvent.altKey(), gestureEvent.metaKey(), gestureEvent.timestamp());
-    PlatformMouseEvent fakeMouseUp(gestureEvent.position(), gestureEvent.globalPosition(), LeftButton, PlatformEvent::MouseReleased, /* clickCount */ 1, gestureEvent.shiftKey(), gestureEvent.ctrlKey(), gestureEvent.altKey(), gestureEvent.metaKey(), gestureEvent.timestamp());
+    PlatformMouseEvent fakeMouseMove(adjustedPoint, gestureEvent.globalPosition(), NoButton, PlatformEvent::MouseMoved, /* clickCount */ 1, gestureEvent.shiftKey(), gestureEvent.ctrlKey(), gestureEvent.altKey(), gestureEvent.metaKey(), gestureEvent.timestamp());
+    PlatformMouseEvent fakeMouseDown(adjustedPoint, gestureEvent.globalPosition(), LeftButton, PlatformEvent::MousePressed, /* clickCount */ 1, gestureEvent.shiftKey(), gestureEvent.ctrlKey(), gestureEvent.altKey(), gestureEvent.metaKey(), gestureEvent.timestamp());
+    PlatformMouseEvent fakeMouseUp(adjustedPoint, gestureEvent.globalPosition(), LeftButton, PlatformEvent::MouseReleased, /* clickCount */ 1, gestureEvent.shiftKey(), gestureEvent.ctrlKey(), gestureEvent.altKey(), gestureEvent.metaKey(), gestureEvent.timestamp());
     mouseMoved(fakeMouseMove);
     defaultPrevented |= handleMousePressEvent(fakeMouseDown);
     defaultPrevented |= handleMouseReleaseEvent(fakeMouseUp);
@@ -2441,6 +2456,22 @@ bool EventHandler::handleGestureScrollCore(const PlatformGestureEvent& gestureEv
 }
 #endif
 
+#if ENABLE(TOUCH_ADJUSTMENT)
+void EventHandler::bestClickableNodeForTouchPoint(const IntPoint& touchCenter, const IntSize& touchRadius, IntPoint& targetPoint, Node*& targetNode)
+{
+    HitTestRequest::HitTestRequestType hitType = HitTestRequest::ReadOnly | HitTestRequest::Active;
+    HitTestResult result = hitTestResultAtPoint(touchCenter, /*allowShadowContent*/ false, /*ignoreClipping*/ false, DontHitTestScrollbars, hitType, touchRadius);
+
+    IntRect touchRect = result.rectForPoint(touchCenter);
+    RefPtr<StaticHashSetNodeList> nodeList = StaticHashSetNodeList::adopt(result.rectBasedTestResult());
+    if (!findBestClickableCandidate(targetNode, targetPoint, touchCenter, touchRect, *nodeList.get())) {
+        // Default to just returning innerNonSharedNode.
+        targetPoint = touchCenter;
+        targetNode = result.innerNonSharedNode();
+    }
+}
+#endif
+
 #if ENABLE(CONTEXT_MENUS)
 bool EventHandler::sendContextMenuEvent(const PlatformMouseEvent& event)
 {
index 1cac075..701fa2b 100644 (file)
@@ -167,6 +167,10 @@ public:
     bool handleGestureScrollUpdate(const PlatformGestureEvent&);
 #endif
 
+#if ENABLE(TOUCH_ADJUSTMENT)
+    void bestClickableNodeForTouchPoint(const IntPoint& touchCenter, const IntSize& touchRadius, IntPoint& targetPoint, Node*& targetNode);
+#endif
+
 #if ENABLE(CONTEXT_MENUS)
     bool sendContextMenuEvent(const PlatformMouseEvent&);
     bool sendContextMenuEventForKey();
diff --git a/Source/WebCore/page/TouchAdjustment.cpp b/Source/WebCore/page/TouchAdjustment.cpp
new file mode 100644 (file)
index 0000000..fa34130
--- /dev/null
@@ -0,0 +1,212 @@
+/*
+ * Copyright (C) 2012 Nokia Corporation and/or its subsidiary(-ies)
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Library General Public
+ * License as published by the Free Software Foundation; either
+ * version 2 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Library General Public License for more details.
+ *
+ * You should have received a copy of the GNU Library General Public License
+ * along with this library; see the file COPYING.LIB.  If not, write to
+ * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ */
+
+#include "config.h"
+
+#include "TouchAdjustment.h"
+
+#include "ContainerNode.h"
+#include "FloatPoint.h"
+#include "FloatQuad.h"
+#include "HTMLLabelElement.h"
+#include "HTMLNames.h"
+#include "IntPoint.h"
+#include "IntSize.h"
+#include "Node.h"
+#include "RenderBox.h"
+#include "RenderObject.h"
+#include "RenderStyle.h"
+
+namespace WebCore {
+
+namespace TouchAdjustment {
+
+// Class for remembering absolute quads of a target node and what node they represent.
+class SubtargetGeometry {
+public:
+    SubtargetGeometry(Node* node, const FloatQuad& quad)
+        : m_node(node)
+        , m_quad(quad)
+    { }
+
+    Node* node() const { return m_node; }
+    FloatQuad quad() const { return m_quad; }
+    IntRect boundingBox() const { return m_quad.enclosingBoundingBox(); }
+
+private:
+    Node* m_node;
+    FloatQuad m_quad;
+};
+
+typedef Vector<SubtargetGeometry> SubtargetGeometryList;
+typedef bool (*NodeFilter)(Node*);
+typedef float (*DistanceFunction)(const IntPoint&, const IntRect&, const SubtargetGeometry&);
+
+// Takes non-const Node* because isContentEditable is a non-const function.
+bool nodeRespondsToTapGesture(Node* node)
+{
+    if (node->isLink()
+        || node->isContentEditable()
+        || node->isMouseFocusable())
+        return true;
+    if (node->isElementNode()) {
+        Element* element =  static_cast<Element*>(node);
+        if (element->hasTagName(HTMLNames::labelTag) && static_cast<HTMLLabelElement*>(element)->control())
+            return true;
+    }
+    // FIXME: Implement hasDefaultEventHandler and use that instead of all of the above checks.
+    if (node->hasEventListeners()
+        && (node->hasEventListeners(eventNames().clickEvent)
+            || node->hasEventListeners(eventNames().DOMActivateEvent)
+            || node->hasEventListeners(eventNames().mousedownEvent)
+            || node->hasEventListeners(eventNames().mouseupEvent)
+            || node->hasEventListeners(eventNames().mousemoveEvent)
+            // Checking for focus events is not necessary since they can only fire on
+            // focusable elements which have already been captured above.
+        ))
+        return true;
+    if (node->renderStyle()) {
+        // Accept nodes that has a CSS effect when touched.
+        if (node->renderStyle()->affectedByActiveRules() || node->renderStyle()->affectedByHoverRules())
+            return true;
+    }
+    return false;
+}
+
+static inline void appendSubtargetsForNodeToList(Node* node, SubtargetGeometryList& subtargets)
+{
+    // Since the node is a result of a hit test, we are already ensured it has a renderer.
+    ASSERT(node->renderer());
+
+    Vector<FloatQuad> quads;
+    node->renderer()->absoluteQuads(quads);
+
+    Vector<FloatQuad>::const_iterator it = quads.begin();
+    const Vector<FloatQuad>::const_iterator end = quads.end();
+    for (; it != end; ++it)
+        subtargets.append(SubtargetGeometry(node, *it));
+}
+
+// Compiles a list of subtargets of all the relevant target nodes.
+void compileSubtargetList(const NodeList& intersectedNodes, SubtargetGeometryList& subtargets, NodeFilter nodeFilter)
+{
+    // Find candidates responding to tap gesture events in O(n) time.
+    HashMap<Node*, Node*> responderMap;
+    HashSet<Node*> ancestorsToRespondersSet;
+    Vector<Node*> candidates;
+
+    // A node matching the NodeFilter is called a responder. Candidate nodes must either be a
+    // responder or have an ancestor that is a responder.
+    // This iteration tests all ancestors at most once by caching earlier results.
+    unsigned length = intersectedNodes.length();
+    for (unsigned i = 0; i < length; ++i) {
+        Node* const node = intersectedNodes.item(i);
+        if (responderMap.contains(node))
+            // Skip nodes that are direct ancestors of other candidates. They would hit-test
+            // against the same absolute quads.
+            continue;
+        Vector<Node*> visitedNodes;
+        Node* respondingNode = 0;
+        for (Node* visitedNode = node; visitedNode; visitedNode = visitedNode->parentOrHostNode()) {
+            // Check if we already have a result for a common ancestor from another candidate.
+            respondingNode = responderMap.get(visitedNode);
+            if (respondingNode)
+                break;
+            visitedNodes.append(visitedNode);
+            // Check if the node filter applies, which would mean we have found a responding node.
+            if (nodeFilter(visitedNode)) {
+                respondingNode = visitedNode;
+                // Continue the iteration to collect the ancestors of the responder, which we will need later.
+                for (visitedNode = visitedNode->parentOrHostNode(); visitedNode; visitedNode = visitedNode->parentOrHostNode()) {
+                    pair<HashSet<Node*>::iterator, bool> addResult = ancestorsToRespondersSet.add(visitedNode);
+                    if (!addResult.second)
+                        break;
+                }
+                break;
+            }
+        }
+        // Insert the detected responder for all the visited nodes.
+        for (unsigned j = 0; j < visitedNodes.size(); j++)
+            responderMap.add(visitedNodes[j], respondingNode);
+
+        if (respondingNode)
+            candidates.append(node);
+    }
+
+    // We compile the list of component absolute quads instead of using the bounding rect
+    // to be able to perform better hit-testing on inline links on line-breaks.
+    length = candidates.size();
+    for (unsigned i = 0; i < length; i++) {
+        Node* candidate = candidates[i];
+        // Skip nodes who's responders are ancestors of other responders. This gives preference to
+        // the inner-most event-handlers. So that a link is always preferred even when contained
+        // in an element that monitors all click-events.
+        Node* respondingNode = responderMap.get(candidate);
+        ASSERT(respondingNode);
+        if (ancestorsToRespondersSet.contains(respondingNode))
+            continue;
+        appendSubtargetsForNodeToList(candidate, subtargets);
+    }
+}
+
+float distanceSquaredToTargetCenterLine(const IntPoint& touchHotspot, const IntRect& touchArea, const SubtargetGeometry& subtarget)
+{
+    UNUSED_PARAM(touchArea);
+    // For a better center of a line-box we use the center-line instead of the center-point.
+    // We use the center-line of the bounding box of the quad though, since it is much faster
+    // and gives the same result in all untransformed cases, and in transformed cases still
+    // gives a better distance-function than the distance to the center-point.
+    IntRect rect = subtarget.boundingBox();
+
+    return rect.distanceSquaredFromCenterLineToPoint(touchHotspot);
+}
+
+// A generic function for finding the target node with the lowest distance metric. A distance metric here is the result
+// of a distance-like function, that computes how well the touch hits the node.
+// Distance functions could for instance be distance squared or area of intersection.
+bool findNodeWithLowestDistanceMetric(Node*& targetNode, IntPoint& targetPoint, const IntPoint& touchHotspot, const IntRect& touchArea, SubtargetGeometryList& subtargets, DistanceFunction distanceFunction)
+{
+    targetNode = 0;
+
+    float bestDistanceMetric = INFINITY;
+    SubtargetGeometryList::const_iterator it = subtargets.begin();
+    const SubtargetGeometryList::const_iterator end = subtargets.end();
+    for (; it != end; ++it) {
+        Node* node = it->node();
+        float distanceMetric = distanceFunction(touchHotspot, touchArea, *it);
+        if (distanceMetric < bestDistanceMetric) {
+            targetPoint = roundedIntPoint(it->quad().center());
+            targetNode = node;
+            bestDistanceMetric = distanceMetric;
+        }
+    }
+
+    return (targetNode);
+}
+
+} // namespace TouchAdjustment
+
+bool findBestClickableCandidate(Node*& targetNode, IntPoint &targetPoint, const IntPoint &touchHotspot, const IntRect &touchArea, const NodeList& nodeList)
+{
+    TouchAdjustment::SubtargetGeometryList subtargets;
+    TouchAdjustment::compileSubtargetList(nodeList, subtargets, TouchAdjustment::nodeRespondsToTapGesture);
+    return TouchAdjustment::findNodeWithLowestDistanceMetric(targetNode, targetPoint, touchHotspot, touchArea, subtargets, TouchAdjustment::distanceSquaredToTargetCenterLine);
+}
+
+} // namespace WebCore
diff --git a/Source/WebCore/page/TouchAdjustment.h b/Source/WebCore/page/TouchAdjustment.h
new file mode 100644 (file)
index 0000000..d5dfc10
--- /dev/null
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2012 Nokia Corporation and/or its subsidiary(-ies)
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Library General Public
+ * License as published by the Free Software Foundation; either
+ * version 2 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Library General Public License for more details.
+ *
+ * You should have received a copy of the GNU Library General Public License
+ * along with this library; see the file COPYING.LIB.  If not, write to
+ * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ */
+
+#ifndef TouchAdjustment_h
+#define TouchAdjustment_h
+
+#include "IntPoint.h"
+#include "IntRect.h"
+#include "Node.h"
+#include "NodeList.h"
+#include <wtf/Vector.h>
+
+namespace WebCore {
+
+bool findBestClickableCandidate(Node*& targetNode, IntPoint& targetPoint, const IntPoint& touchHotspot, const IntRect& touchArea, const NodeList&);
+// FIXME: Implement the similar functions for other gestures here as well.
+
+} // namespace WebCore
+
+#endif
index b5e2a8c..bfb794d 100644 (file)
@@ -88,6 +88,13 @@ public:
     // from transformed rects.
     bool containsQuad(const FloatQuad&) const;
 
+    // The center of the quad. If the quad is the result of a affine-transformed rectangle this is the same as the original center transformed.
+    FloatPoint center() const
+    {
+        return FloatPoint((m_p1.x() + m_p2.x() + m_p3.x() + m_p4.x()) / 4.0,
+                          (m_p1.y() + m_p2.y() + m_p3.y() + m_p4.y()) / 4.0);
+    }
+
     FloatRect boundingBox() const;
     IntRect enclosingBoundingBox() const
     {
index ddc41b4..0af6e85 100644 (file)
@@ -111,6 +111,8 @@ public:
             m_y < other.m_y ? m_y : other.m_y);
     }
 
+    int distanceSquaredToPoint(const IntPoint&) const;
+
     void clampNegativeToZero()
     {
         *this = expandedTo(zero());
@@ -222,6 +224,11 @@ inline IntSize toSize(const IntPoint& a)
     return IntSize(a.x(), a.y());
 }
 
+inline int IntPoint::distanceSquaredToPoint(const IntPoint& point) const
+{
+    return ((*this) - point).diagonalLengthSquared();
+}
+
 #if PLATFORM(QT)
 inline QDataStream& operator<<(QDataStream& stream, const IntPoint& point)
 {
index 03c35e8..dfa2f91 100644 (file)
@@ -132,6 +132,35 @@ void IntRect::scale(float s)
     m_size.setHeight((int)(height() * s));
 }
 
+static inline int distanceToInterval(int pos, int start, int end)
+{
+    if (pos < start)
+        return start - pos;
+    if (pos > end)
+        return end - pos;
+    return 0;
+}
+
+IntSize IntRect::differenceToPoint(const IntPoint& point) const
+{
+    int xdistance = distanceToInterval(point.x(), x(), maxX());
+    int ydistance = distanceToInterval(point.y(), y(), maxY());
+    return IntSize(xdistance, ydistance);
+}
+
+IntSize IntRect::differenceFromCenterLineToPoint(const IntPoint& point) const
+{
+    // The center-line is the natural center of a rectangle. It has an equal distance to all sides of the rectangle.
+    IntPoint centerPoint = center();
+    int xdistance = centerPoint.x() - point.x();
+    int ydistance = centerPoint.y() - point.y();
+    if (width() > height())
+        xdistance = distanceToInterval(point.x(), x() + (height() / 2), maxX() - (height() / 2));
+    else
+        ydistance = distanceToInterval(point.y(), y() + (width() / 2), maxY() - (width() / 2));
+    return IntSize(xdistance, ydistance);
+}
+
 IntRect unionRect(const Vector<IntRect>& rects)
 {
     IntRect result;
index 600c21d..ca7fea4 100644 (file)
@@ -186,6 +186,11 @@ public:
     void inflate(int d) { inflateX(d); inflateY(d); }
     void scale(float s);
 
+    IntSize differenceToPoint(const IntPoint&) const;
+    IntSize differenceFromCenterLineToPoint(const IntPoint&) const;
+    int distanceSquaredToPoint(const IntPoint& p) const { return differenceToPoint(p).diagonalLengthSquared(); }
+    int distanceSquaredFromCenterLineToPoint(const IntPoint& p) const { return differenceFromCenterLineToPoint(p).diagonalLengthSquared(); }
+
     IntRect transposedRect() const { return IntRect(m_location.transposedPoint(), m_size.transposedSize()); }
 
 #if PLATFORM(WX)
index 1639847..f11f815 100644 (file)
@@ -104,6 +104,11 @@ public:
         *this = expandedTo(IntSize());
     }
 
+    int diagonalLengthSquared() const
+    {
+        return m_width * m_width + m_height * m_height;
+    }
+
     IntSize transposedSize() const
     {
         return IntSize(m_height, m_width);
index 3a7dd1b..6207265 100644 (file)
 #include "BatteryController.h"
 #endif
 
+#if ENABLE(TOUCH_ADJUSTMENT)
+#include "EventHandler.h"
+#include "WebKitPoint.h"
+#endif
+
 namespace WebCore {
 
 static bool markerTypesFrom(const String& markerType, DocumentMarker::MarkerTypes& result)
@@ -569,6 +574,42 @@ String Internals::rangeAsText(const Range* range, ExceptionCode& ec)
     return range->text();
 }
 
+
+#if ENABLE(TOUCH_ADJUSTMENT)
+PassRefPtr<WebKitPoint> Internals::touchPositionAdjustedToBestClickableNode(long x, long y, long width, long height, Document* document, ExceptionCode& ec)
+{
+    if (!document || !document->frame()) {
+        ec = INVALID_ACCESS_ERR;
+        return 0;
+    }
+
+    IntSize radius(width / 2, height / 2);
+    IntPoint point(x + radius.width(), y + radius.height());
+
+    Node* targetNode;
+    IntPoint adjustedPoint;
+    document->frame()->eventHandler()->bestClickableNodeForTouchPoint(point, radius, adjustedPoint, targetNode);
+    return WebKitPoint::create(adjustedPoint.x(), adjustedPoint.y());
+}
+
+Node* Internals::touchNodeAdjustedToBestClickableNode(long x, long y, long width, long height, Document* document, ExceptionCode& ec)
+{
+    if (!document || !document->frame()) {
+        ec = INVALID_ACCESS_ERR;
+        return 0;
+    }
+
+    IntSize radius(width / 2, height / 2);
+    IntPoint point(x + radius.width(), y + radius.height());
+
+    Node* targetNode;
+    IntPoint adjustedPoint;
+    document->frame()->eventHandler()->bestClickableNodeForTouchPoint(point, radius, adjustedPoint, targetNode);
+    return targetNode;
+}
+#endif
+
+
 int Internals::lastSpellCheckRequestSequence(Document* document, ExceptionCode& ec)
 {
     SpellChecker* checker = spellchecker(document);
index 1a52246..c19c299 100644 (file)
@@ -44,6 +44,7 @@ class InternalSettings;
 class Node;
 class Range;
 class ShadowRoot;
+class WebKitPoint;
 
 typedef int ExceptionCode;
 
@@ -111,6 +112,11 @@ public:
     unsigned lengthFromRange(Element* scope, const Range*, ExceptionCode&);
     String rangeAsText(const Range*, ExceptionCode&);
 
+#if ENABLE(TOUCH_ADJUSTMENT)
+    PassRefPtr<WebKitPoint> touchPositionAdjustedToBestClickableNode(long x, long y, long width, long height, Document*, ExceptionCode&);
+    Node* touchNodeAdjustedToBestClickableNode(long x, long y, long width, long height, Document*, ExceptionCode&);
+#endif
+
     int lastSpellCheckRequestSequence(Document*, ExceptionCode&);
     int lastSpellCheckProcessedSequence(Document*, ExceptionCode&);
     
index 3610757..d21a2c6 100644 (file)
@@ -86,6 +86,11 @@ module window {
         unsigned long lengthFromRange(in Element scope, in Range range) raises (DOMException);
         DOMString rangeAsText(in Range range) raises (DOMException);
 
+#if defined(ENABLE_TOUCH_ADJUSTMENT) && ENABLE_TOUCH_ADJUSTMENT
+        WebKitPoint touchPositionAdjustedToBestClickableNode(in long x, in long y, in long width, in long height, in Document document) raises (DOMException);
+        Node touchNodeAdjustedToBestClickableNode(in long x, in long y, in long width, in long height, in Document document) raises (DOMException);
+#endif
+
         long lastSpellCheckRequestSequence(in Document document) raises (DOMException);
         long lastSpellCheckProcessedSequence(in Document document) raises (DOMException);
 
index bc8ae22..8fb4a97 100644 (file)
@@ -1,3 +1,23 @@
+2012-03-19  Allan Sandfeld Jensen  <allan.jensen@nokia.com>
+
+        Select best target for tap gesture.
+        https://bugs.webkit.org/show_bug.cgi?id=78801
+
+        Reviewed by Kenneth Rohde Christiansen.
+
+        Send radius to handlePotentialSingleTapEvent so it can do the same hit
+        detection the tap gesture later does.
+
+        * UIProcess/WebPageProxy.cpp:
+        (WebKit::WebPageProxy::handlePotentialActivation):
+        * UIProcess/WebPageProxy.h:
+        * UIProcess/qt/QtWebPageEventHandler.cpp:
+        (QtWebPageEventHandler::handlePotentialSingleTapEvent):
+        * WebProcess/WebPage/WebPage.cpp:
+        (WebKit::WebPage::highlightPotentialActivation):
+        * WebProcess/WebPage/WebPage.h:
+        * WebProcess/WebPage/WebPage.messages.in:
+
 2012-03-19  Alexander Færøy  <alexander.faeroy@nokia.com>
 
         [Qt] Add experimental API for dynamically changing the UA string
index 45360fc..a86dfdc 100644 (file)
@@ -1012,9 +1012,9 @@ void WebPageProxy::handleGestureEvent(const WebGestureEvent& event)
 
 #if ENABLE(TOUCH_EVENTS)
 #if PLATFORM(QT)
-void WebPageProxy::handlePotentialActivation(const IntPoint& layoutPoint)
+void WebPageProxy::handlePotentialActivation(const IntPoint& touchPoint, const IntSize& touchArea)
 {
-    process()->send(Messages::WebPage::HighlightPotentialActivation(layoutPoint), m_pageID);
+    process()->send(Messages::WebPage::HighlightPotentialActivation(touchPoint, touchArea), m_pageID);
 }
 #endif
 
index 35ea3fc..dc98554 100644 (file)
@@ -391,7 +391,7 @@ public:
 #if ENABLE(TOUCH_EVENTS)
     void handleTouchEvent(const NativeWebTouchEvent&);
 #if PLATFORM(QT)
-    void handlePotentialActivation(const WebCore::IntPoint&);
+    void handlePotentialActivation(const WebCore::IntPoint& touchPoint, const WebCore::IntSize& touchArea);
 #endif
 #endif
 
index 51cd327..96bc7cf 100644 (file)
@@ -232,7 +232,7 @@ void QtWebPageEventHandler::handlePotentialSingleTapEvent(const QTouchEvent::Tou
 {
 #if ENABLE(TOUCH_EVENTS)
     QTransform fromItemTransform = m_webPage->transformFromItem();
-    m_webPageProxy->handlePotentialActivation(fromItemTransform.map(point.pos()).toPoint());
+    m_webPageProxy->handlePotentialActivation(fromItemTransform.map(point.pos()).toPoint(), IntSize(point.rect().size().toSize()));
 #else
     Q_UNUSED(point);
 #endif
index 647f2fc..e7872e1 100644 (file)
@@ -1448,16 +1448,20 @@ void WebPage::restoreSessionAndNavigateToCurrentItem(const SessionState& session
 
 #if ENABLE(TOUCH_EVENTS)
 #if PLATFORM(QT)
-void WebPage::highlightPotentialActivation(const IntPoint& point)
+void WebPage::highlightPotentialActivation(const IntPoint& point, const IntSize& area)
 {
     Node* activationNode = 0;
     Frame* mainframe = m_page->mainFrame();
+    IntPoint adjustedPoint;
 
     if (point != IntPoint::zero()) {
+#if ENABLE(TOUCH_ADJUSTMENT)
+        mainframe->eventHandler()->bestClickableNodeForTouchPoint(point, IntSize(area.width() / 2, area.height() / 2), adjustedPoint, activationNode);
+#else
         HitTestResult result = mainframe->eventHandler()->hitTestResultAtPoint(mainframe->view()->windowToContents(point), /*allowShadowContent*/ false, /*ignoreClipping*/ true);
         activationNode = result.innerNode();
-
-        if (!activationNode->isFocusable())
+#endif
+        if (activationNode && !activationNode->isFocusable())
             activationNode = activationNode->enclosingLinkEventParentOrSelf();
     }
 
index 1a3d771..034f1c5 100644 (file)
@@ -564,7 +564,7 @@ private:
     void touchEvent(const WebTouchEvent&);
     void touchEventSyncForTesting(const WebTouchEvent&, bool& handled);
 #if PLATFORM(QT)
-    void highlightPotentialActivation(const WebCore::IntPoint&);
+    void highlightPotentialActivation(const WebCore::IntPoint&, const WebCore::IntSize& area);
 #endif
 #endif
     void contextMenuHidden() { m_isShowingContextMenu = false; }
index ff4be82..2055a51 100644 (file)
@@ -40,9 +40,8 @@ messages -> WebPage {
 #if ENABLE(TOUCH_EVENTS)
     TouchEvent(WebKit::WebTouchEvent event)
     TouchEventSyncForTesting(WebKit::WebTouchEvent event) -> (bool handled)
-#endif
 #if ENABLE(TOUCH_EVENTS) && PLATFORM(QT)
-    HighlightPotentialActivation(WebCore::IntPoint point)
+    HighlightPotentialActivation(WebCore::IntPoint point, WebCore::IntSize area)
 #endif
 
     ContextMenuHidden()
index e70546c..02e59b0 100644 (file)
@@ -1,3 +1,14 @@
+2012-03-19  Allan Sandfeld Jensen  <allan.jensen@nokia.com>
+
+        Select best target for tap gesture.
+        https://bugs.webkit.org/show_bug.cgi?id=78801
+
+        Reviewed by Kenneth Rohde Christiansen.
+
+        Add TOUCH_ADJUSTMENT to enabled features.
+
+        * qmake/mkspecs/features/features.prf:
+
 2012-03-19  Robert Kroeger  <rjkroege@chromium.org>
 
         [chromium] synthesize wheel events for fling on main thread
index 06da1af..06168c7 100644 (file)
@@ -89,6 +89,7 @@ haveQt(5) {
 !contains(DEFINES, ENABLE_VIDEO_TRACK=.): DEFINES += ENABLE_VIDEO_TRACK=0
 !contains(DEFINES, ENABLE_TOUCH_ICON_LOADING=.): DEFINES += ENABLE_TOUCH_ICON_LOADING=0
 !contains(DEFINES, ENABLE_ANIMATION_API=.): DEFINES += ENABLE_ANIMATION_API=0
+!contains(DEFINES, ENABLE_TOUCH_ADJUSTMENT=.): DEFINES += ENABLE_TOUCH_ADJUSTMENT=1
 
 # Policy decisions: for using a particular third-party library or optional OS service
 !contains(DEFINES, WTF_USE_QT_IMAGE_DECODER=.): DEFINES += WTF_USE_QT_IMAGE_DECODER=1
@@ -265,6 +266,7 @@ contains(DEFINES, ENABLE_VIDEO_TRACK=1): FEATURE_DEFINES_JAVASCRIPT += ENABLE_VI
 contains(DEFINES, ENABLE_DATA_TRANSFER_ITEMS=1): FEATURE_DEFINES_JAVASCRIPT += ENABLE_DATA_TRANSFER_ITEMS=1
 contains(DEFINES, ENABLE_FULLSCREEN_API=1): FEATURE_DEFINES_JAVASCRIPT += ENABLE_FULLSCREEN_API=1
 contains(DEFINES, ENABLE_REQUEST_ANIMATION_FRAME=1): FEATURE_DEFINES_JAVASCRIPT += ENABLE_REQUEST_ANIMATION_FRAME=1
+contains(DEFINES, ENABLE_TOUCH_ADJUSTMENT=1): FEATURE_DEFINES_JAVASCRIPT += ENABLE_TOUCH_ADJUSTMENT=1
 
 # Used to compute defaults for the build-webkit script
 # Don't place anything after this!