[iOS] Basic hit testing for content overlapping fast-scrollable overflow
authorantti@apple.com <antti@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Wed, 6 Mar 2019 21:05:07 +0000 (21:05 +0000)
committerantti@apple.com <antti@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Wed, 6 Mar 2019 21:05:07 +0000 (21:05 +0000)
https://bugs.webkit.org/show_bug.cgi?id=195360

Reviewed by Simon Fraser.

Source/WebKit:

* UIProcess/RemoteLayerTree/ios/RemoteLayerTreeHostIOS.mm:
(WebKit::RemoteLayerTreeHost::makeNode):

Use new UIView subclass for tiled layer hosting (so we know they have content even when contents property is nil).

* UIProcess/RemoteLayerTree/ios/RemoteLayerTreeViews.h:
* UIProcess/RemoteLayerTree/ios/RemoteLayerTreeViews.mm:
(collectDescendantViewsAtPoint):

Factor collection step into a function.
Do basic skipping of event-transparent layers.

(-[UIView _web_findDescendantViewAtPoint:withEvent:]):

To handle overlap cases, find the scroll view that has the deepest non-transparent view hit as its descendant.

(-[UIView _web_recursiveFindDescendantInteractibleViewAtPoint:withEvent:]): Deleted.

LayoutTests:

* fast/scrolling/ios/overflow-scroll-overlap-expected.txt: Added.
* fast/scrolling/ios/overflow-scroll-overlap.html: Added.

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

LayoutTests/ChangeLog
LayoutTests/fast/scrolling/ios/overflow-scroll-overlap-expected.txt [new file with mode: 0644]
LayoutTests/fast/scrolling/ios/overflow-scroll-overlap.html [new file with mode: 0644]
Source/WebKit/ChangeLog
Source/WebKit/UIProcess/RemoteLayerTree/ios/RemoteLayerTreeHostIOS.mm
Source/WebKit/UIProcess/RemoteLayerTree/ios/RemoteLayerTreeViews.h
Source/WebKit/UIProcess/RemoteLayerTree/ios/RemoteLayerTreeViews.mm

index 159c726..880de8c 100644 (file)
@@ -1,3 +1,13 @@
+2019-03-06  Antti Koivisto  <antti@apple.com>
+
+        [iOS] Basic hit testing for content overlapping fast-scrollable overflow
+        https://bugs.webkit.org/show_bug.cgi?id=195360
+
+        Reviewed by Simon Fraser.
+
+        * fast/scrolling/ios/overflow-scroll-overlap-expected.txt: Added.
+        * fast/scrolling/ios/overflow-scroll-overlap.html: Added.
+
 2019-03-06  Joseph Pecoraro  <pecoraro@apple.com>
 
         Web Inspector: CPU Usage Timeline - Statistics and Sources sections
diff --git a/LayoutTests/fast/scrolling/ios/overflow-scroll-overlap-expected.txt b/LayoutTests/fast/scrolling/ios/overflow-scroll-overlap-expected.txt
new file mode 100644 (file)
index 0000000..7ccd3a1
--- /dev/null
@@ -0,0 +1,14 @@
+Test that overlapped, nested and clipped scrollable areas are correctly targeted.
+
+case 1: Scrollable 1 
+case 2: 
+case 3: Scrollable 3 
+case 4: Scrollable 6 
+case 5: Scrollable 8 
+case 6: Scrollable 9 
+case 7: Scrollable 12 
+case 8: Scrollable 13 
+case 9: Scrollable 15 
+case 10: 
+case 11: 
+
diff --git a/LayoutTests/fast/scrolling/ios/overflow-scroll-overlap.html b/LayoutTests/fast/scrolling/ios/overflow-scroll-overlap.html
new file mode 100644 (file)
index 0000000..209246e
--- /dev/null
@@ -0,0 +1,230 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<style>
+body {
+    touch-action: none;
+}
+.case {
+    width: 150px;
+    height: 150px;
+    display: inline-block;
+    position: relative;
+}
+.scrollcontent {
+    width: 500px;
+    height: 500px;
+    background: green;
+}
+
+.overflowscroll {
+    overflow: scroll;
+    height: 100px;
+    width: 100px;
+    position: absolute;
+    border: 2px solid black;
+}
+.overlapping {
+    position:absolute;
+    left: 25px;
+    top: 25px;
+    width: 100px;
+    height: 100px;
+    background: red;
+}
+.clip {
+    position:absolute;
+    width: 100px;
+    height: 100px;
+    overflow:hidden;
+}
+.large {
+    width: 3000px;
+    height: 150px;
+}
+#log {
+    position:relative;
+    white-space: pre;
+}
+</style>
+<script src="../../../resources/basic-gestures.js"></script>
+<script type="text/javascript">
+if (window.testRunner) {
+    testRunner.dumpAsText();
+    testRunner.waitUntilDone();
+    internals.settings.setAsyncFrameScrollingEnabled(true);
+    internals.settings.setAsyncOverflowScrollingEnabled(true);
+}
+
+function sleep(delay)
+{
+    return new Promise((resolve) => { setTimeout(resolve, delay); });
+}
+
+async function runTest() {
+    for (const scrollable of document.querySelectorAll('.overflowscroll')) {
+        scrollable.addEventListener('scroll', function(e) {
+            logScroll(e.target);
+        });
+    }
+
+    {
+        let i = 0;
+        for (const scrollcontent of document.querySelectorAll('.scrollcontent'))
+            scrollcontent.innerText = "Scrollable " + ++i;
+    }
+    {
+        let i = 0;
+        for (const overlapping of document.querySelectorAll('.overlapping'))
+            overlapping.innerText = "Overlapping " + ++i;
+    }
+
+
+    if (!window.testRunner || !testRunner.runUIScript)
+        return;
+
+    for (const testcase of document.querySelectorAll('.case'))
+        testcase.style.display = 'none';
+
+    {
+        let i = 0;
+        for (const testcase of document.querySelectorAll('.case')) {
+            ++i;
+            testcase.style.display = 'inline-block';
+            
+            const target = testcase.querySelector('.target');
+            const rect = target.getBoundingClientRect();
+            const centerX = (rect.left + rect.right) / 2;
+            const centerY = (rect.top + rect.bottom) / 2;
+            await touchAndDragFromPointToPoint(centerX, centerY, centerX, centerY - 30);
+            await liftUpAtPoint(centerX, centerY - 30);
+            await sleep(500);
+
+            testcase.style.display = 'none';
+            outputCase(i);
+        }
+   }
+
+    for (const testcase of document.querySelectorAll('.case'))
+        testcase.style.display = 'none';
+
+    testRunner.notifyDone();
+}
+
+const scrolledElements = new Set();
+
+function logScroll(element) {
+    if (scrolledElements.has(element))
+        return;
+    scrolledElements.add(element);
+}
+
+function outputCase(i) {
+    log.innerText += "case " + i + ": ";
+    for (const scrolled of scrolledElements)
+        log.innerText += scrolled.getElementsByClassName("scrollcontent")[0].innerText + " ";
+    log.innerText += "\n";
+    scrolledElements.clear();
+}
+</script>
+</head>
+<body onload="runTest()">
+<p>
+Test that overlapped, nested and clipped scrollable areas are correctly targeted.
+</p>
+<div class="case">
+    <div class="overflowscroll target" style="z-index:1">
+        <div class="scrollcontent"></div>
+    </div>
+    <div class="overlapping"></div>
+</div>
+
+<div class="case">
+    <div class="overflowscroll target">
+        <div class="scrollcontent"></div>
+    </div>
+    <div class="overlapping" style="z-index:1"></div>
+</div>
+
+<div class="case ">
+    <div class="overflowscroll target" style="z-index:1">
+        <div class="scrollcontent"></div>
+    </div>
+    <div class="overflowscroll" style="left:20px; top:20px; z-index:0;">
+        <div class="scrollcontent"></div>
+    </div>
+</div>
+
+<div class="case">
+    <div class="overflowscroll target">
+        <div class="scrollcontent"></div>
+    </div>
+    <div class="overflowscroll" style="left:20px; top:20px; z-index:1;">
+        <div class="scrollcontent"></div>
+    </div>
+</div>
+
+<div class="case">
+    <div class="overflowscroll target">
+        <div class="scrollcontent" style="z-index:1"></div>
+        <div class="overflowscroll" style="left:20px; top:20px;">
+            <div class="scrollcontent"></div>
+        </div>
+    </div>
+</div>
+
+<div class="case">
+    <div class="overflowscroll target">
+        <div class="scrollcontent" style="z-index:1"></div>
+        <div class="overflowscroll" style="left:60px; top:60px;">
+            <div class="scrollcontent"></div>
+        </div>
+    </div>
+</div>
+
+<div class="case">
+    <div class="overflowscroll target">
+        <div class="scrollcontent large" style="z-index:1"></div>
+        <div class="overflowscroll" style="left:20px; top:20px;">
+            <div class="scrollcontent large"></div>
+        </div>
+    </div>
+</div>
+
+<div class="case">
+    <div class="overflowscroll target">
+        <div class="scrollcontent large" style="z-index:1"></div>
+        <div class="overflowscroll" style="left:60px; top:60px;">
+            <div class="scrollcontent large"></div>
+        </div>
+    </div>
+</div>
+
+<div class="case">
+    <div class="clip" style="left:20px; top:20px;">
+        <div class="overflowscroll target" style="left:-20px; top:-20px;">
+            <div class="scrollcontent"></div>
+        </div>
+    </div>
+</div>
+
+<div class="case">
+    <div class="clip" style="left:60px; top:60px;">
+        <div class="overflowscroll target" style="left:-60px; top:-60px;">
+            <div class="scrollcontent"></div>
+        </div>
+    </div>
+</div>
+
+<div class="case">
+    <div class="overflowscroll target">
+        <div class="scrollcontent"></div>
+    </div>
+    <div class="overlapping large" style="z-index:1"></div>
+</div>
+
+<div id=log></div>
+
+</body>
+</html>
index 88dfe43..d7692aa 100644 (file)
@@ -1,3 +1,28 @@
+2019-03-06  Antti Koivisto  <antti@apple.com>
+
+        [iOS] Basic hit testing for content overlapping fast-scrollable overflow
+        https://bugs.webkit.org/show_bug.cgi?id=195360
+
+        Reviewed by Simon Fraser.
+
+        * UIProcess/RemoteLayerTree/ios/RemoteLayerTreeHostIOS.mm:
+        (WebKit::RemoteLayerTreeHost::makeNode):
+
+        Use new UIView subclass for tiled layer hosting (so we know they have content even when contents property is nil).
+
+        * UIProcess/RemoteLayerTree/ios/RemoteLayerTreeViews.h:
+        * UIProcess/RemoteLayerTree/ios/RemoteLayerTreeViews.mm:
+        (collectDescendantViewsAtPoint):
+
+        Factor collection step into a function.
+        Do basic skipping of event-transparent layers.
+
+        (-[UIView _web_findDescendantViewAtPoint:withEvent:]):
+
+        To handle overlap cases, find the scroll view that has the deepest non-transparent view hit as its descendant.
+
+        (-[UIView _web_recursiveFindDescendantInteractibleViewAtPoint:withEvent:]): Deleted.
+
 2019-03-06  Wenson Hsieh  <wenson_hsieh@apple.com>
 
         Move RenderObject::isTransparentOrFullyClippedRespectingParentFrames() to RenderLayer
index 629a9c6..305918f 100644 (file)
@@ -72,9 +72,11 @@ std::unique_ptr<RemoteLayerTreeNode> RemoteLayerTreeHost::makeNode(const RemoteL
     case PlatformCALayer::LayerTypeWebLayer:
     case PlatformCALayer::LayerTypeRootLayer:
     case PlatformCALayer::LayerTypeSimpleLayer:
+        return makeAdoptingView([[WKCompositingView alloc] init]);
+        
     case PlatformCALayer::LayerTypeTiledBackingLayer:
     case PlatformCALayer::LayerTypePageTiledBackingLayer:
-        return makeAdoptingView([[WKCompositingView alloc] init]);
+        return makeAdoptingView([[WKTiledBackingView alloc] init]);
 
     case PlatformCALayer::LayerTypeTiledBackingTileLayer:
         return RemoteLayerTreeNode::createWithPlainLayer(properties.layerID);
index 9a58a02..cd47abb 100644 (file)
@@ -40,6 +40,9 @@ class WebPageProxy;
 @interface WKCompositingView : UIView
 @end
 
+@interface WKTiledBackingView : WKCompositingView
+@end
+
 @interface WKTransformView : WKCompositingView
 @end
 
index 2f359b0..606350c 100644 (file)
 #import <pal/spi/cocoa/QuartzCoreSPI.h>
 #import <wtf/SoftLinking.h>
 
+static void collectDescendantViewsAtPoint(Vector<UIView *, 16>& viewsAtPoint, UIView *parent, CGPoint point, UIEvent *event)
+{
+    if (parent.clipsToBounds && ![parent pointInside:point withEvent:event])
+        return;
+
+    for (UIView *view in [parent subviews]) {
+        CGPoint subviewPoint = [view convertPoint:point fromView:parent];
+
+        // FIXME: This doesn't cover all possible cases yet.
+        auto isTransparent = [&] {
+            if ([view isKindOfClass:[WKTiledBackingView class]])
+                return false;
+            if (![view isKindOfClass:[WKCompositingView class]])
+                return false;
+            if (view.layer.contents)
+                return false;
+            return true;
+        }();
+
+        if (!isTransparent && [view pointInside:subviewPoint withEvent:event])
+            viewsAtPoint.append(view);
+
+        if (![view subviews])
+            return;
+
+        collectDescendantViewsAtPoint(viewsAtPoint, view, subviewPoint, event);
+    };
+}
+
 @interface UIView (WKHitTesting)
 - (UIView *)_web_findDescendantViewAtPoint:(CGPoint)point withEvent:(UIEvent *)event;
 @end
 
 @implementation UIView (WKHitTesting)
 
-// UIView hit testing assumes that views should only hit test subviews that are entirely contained
-// in the view. This is not true of web content.
-// We only want to find views that allow native interaction here. Other views are ignored.
-- (UIView *)_web_recursiveFindDescendantInteractibleViewAtPoint:(CGPoint)point withEvent:(UIEvent *)event
+- (UIView *)_web_findDescendantViewAtPoint:(CGPoint)point withEvent:(UIEvent *)event
 {
-    if (self.clipsToBounds && ![self pointInside:point withEvent:event])
-        return nil;
+    Vector<UIView *, 16> viewsAtPoint;
+    collectDescendantViewsAtPoint(viewsAtPoint, self, point, event);
 
-    __block UIView *foundView = nil;
-    [[self subviews] enumerateObjectsUsingBlock:^(UIView *view, NSUInteger idx, BOOL *stop) {
-        CGPoint subviewPoint = [view convertPoint:point fromView:self];
+    for (auto i = viewsAtPoint.size(); i--;) {
+        auto *view = viewsAtPoint[i];
+        if (!view.isUserInteractionEnabled)
+            continue;
 
-        if (view.isUserInteractionEnabled && [view pointInside:subviewPoint withEvent:event]) {
-            if ([view conformsToProtocol:@protocol(WKNativelyInteractible)]) {
-                foundView = view;
-
-                if (![view subviews])
-                    return;
-
-                if (UIView *hitView = [view hitTest:subviewPoint withEvent:event])
-                    foundView = hitView;
-            } else if ([view isKindOfClass:[WKChildScrollView class]])
-                foundView = view;
+        if ([view conformsToProtocol:@protocol(WKNativelyInteractible)]) {
+            CGPoint subviewPoint = [view convertPoint:point fromView:self];
+            return [view hitTest:subviewPoint withEvent:event];
         }
 
-        if (![view subviews])
-            return;
-
-        if (UIView *hitView = [view _web_recursiveFindDescendantInteractibleViewAtPoint:subviewPoint withEvent:event])
-            foundView = hitView;
-    }];
-
-    return foundView;
-}
-
-- (UIView *)_web_findDescendantViewAtPoint:(CGPoint)point withEvent:(UIEvent *)event
-{
-    return [self _web_recursiveFindDescendantInteractibleViewAtPoint:point withEvent:event];
+        if ([view isKindOfClass:[WKChildScrollView class]]) {
+            // See if the deepest view hit is actually a child of the scrollview.
+            if ([viewsAtPoint.last() isDescendantOfView:view])
+                return view;
+        }
+    }
+    return nil;
 }
 
 @end
 
 @end
 
+@implementation WKTiledBackingView
+@end
+
 @implementation WKTransformView
 
 + (Class)layerClass