[iOS] Content offset jumps erratically when autoscrolling near scroll view content...
authorwenson_hsieh@apple.com <wenson_hsieh@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Fri, 18 Jan 2019 04:15:24 +0000 (04:15 +0000)
committerwenson_hsieh@apple.com <wenson_hsieh@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Fri, 18 Jan 2019 04:15:24 +0000 (04:15 +0000)
https://bugs.webkit.org/show_bug.cgi?id=193494
<rdar://problem/46859627>

Reviewed by Simon Fraser and Tim Horton.

Source/WebCore:

When computing the content offset to scroll to when revealing a given rect in content coordinates, we currently
just use the unobscured content rect. As a result, when scrolling to reveal a rect, we'll clamp the final scroll
position such that only content is visible. For example, when asked to reveal the rect `(0, 0, 1, 1)`, we adjust
the scroll position to be the origin.

However, consider the case where a client (e.g. Mail on iOS) has added a content inset to the web view's scroll
view. If we're asked to reveal a rect that is outside the content area but within a content inset, we will still
end up clamping the scroll position to the unobscured rect. This manifests in a bug where selecting text and
autoscrolling in iOS Mail compose while the scroll view is scrolled all the way to the top to reveal the To/Cc/
Subject fields causes the content offset to jump to the origin, rather than staying at (0, -topContentInset).

To fix this, we teach `RenderLayer::scrollRectToVisible` about content insets that are visible. Rather than use
the content rects as-is, expand to encompass visible content insets as well. This ensures that revealing a
position which is already visible won't cause us to scroll away the content inset area and only show the
unobscured rect.

Tests:  editing/selection/ios/autoscroll-with-top-content-inset.html
        fast/scrolling/ios/scroll-into-view-with-top-content-inset.html

* page/FrameView.cpp:
(WebCore::FrameView::unobscuredContentRectExpandedByContentInsets const):

Introduce a helper method that expands the unobscured content rect to include surrounding content insets.

* page/FrameView.h:
* page/Page.h:
(WebCore::Page::contentInsets const):
(WebCore::Page::setContentInsets):
* rendering/RenderLayer.cpp:
(WebCore::RenderLayer::scrollRectToVisible):
(WebCore::RenderLayer::getRectToExpose const):

Source/WebKit:

Adds `contentInsets` to `VisibleContentRectUpdateInfo`. This keeps track of the visible content insets
surrounding the unobscured content rect. See WebCore ChangeLog for more details.

* Shared/VisibleContentRectUpdateInfo.cpp:
(WebKit::VisibleContentRectUpdateInfo::encode const):
(WebKit::VisibleContentRectUpdateInfo::decode):
(WebKit::operator<<):
* Shared/VisibleContentRectUpdateInfo.h:
(WebKit::VisibleContentRectUpdateInfo::VisibleContentRectUpdateInfo):
(WebKit::VisibleContentRectUpdateInfo::contentInsets const):
(WebKit::operator==):
* UIProcess/API/Cocoa/WKWebView.mm:
(-[WKWebView _computedObscuredInset]):
(-[WKWebView _computedContentInset]):
(-[WKWebView _computedUnobscuredSafeAreaInset]):

We don't care about source compatibility with iOS 10 and below anymore, so we should change these >= iOS 11
target checks to simply `PLATFORM(IOS)`.

(-[WKWebView _updateVisibleContentRects]):

Compute the visible content insets on all sides of the unobscured content rect. These insets are scaled to
content coordinates.

* UIProcess/ios/WKContentView.h:
* UIProcess/ios/WKContentView.mm:
(floatBoxExtent):

Add a helper to convert `UIEdgeInsets` to `WebCore::FloatBoxExtent`, and use it in a few places below.

(-[WKContentView didUpdateVisibleRect:unobscuredRect:contentInsets:unobscuredRectInScrollViewCoordinates:obscuredInsets:unobscuredSafeAreaInsets:inputViewBounds:scale:minimumScale:inStableState:isChangingObscuredInsetsInteractively:enclosedInScrollableAncestorView:]):
(-[WKContentView didUpdateVisibleRect:unobscuredRect:unobscuredRectInScrollViewCoordinates:obscuredInsets:unobscuredSafeAreaInsets:inputViewBounds:scale:minimumScale:inStableState:isChangingObscuredInsetsInteractively:enclosedInScrollableAncestorView:]): Deleted.
* WebProcess/WebPage/ios/WebPageIOS.mm:
(WebKit::WebPage::updateVisibleContentRects):

Update the Page's content insets.

Tools:

Add a new test option to add a top content inset to the test runner's WKWebView's scroll view, and automatically
scroll to reveal this top content inset area when beginning the test (i.e., scroll to (0, -topContentInset)).

* DumpRenderTree/ios/UIScriptControllerIOS.mm:
(WTR::UIScriptController::contentOffsetX const):
(WTR::UIScriptController::contentOffsetY const):
* TestRunnerShared/UIScriptContext/Bindings/UIScriptController.idl:
* TestRunnerShared/UIScriptContext/UIScriptController.cpp:
(WTR::UIScriptController::contentOffsetX const):
(WTR::UIScriptController::contentOffsetY const):
* TestRunnerShared/UIScriptContext/UIScriptController.h:

Also add new UIScriptController methods to ask for the content offset of the platform scroll view.

* WebKitTestRunner/TestController.cpp:
(WTR::updateTestOptionsFromTestHeader):
* WebKitTestRunner/TestOptions.h:
(WTR::TestOptions::hasSameInitializationOptions const):
* WebKitTestRunner/ios/TestControllerIOS.mm:
(WTR::TestController::platformResetStateToConsistentValues):
* WebKitTestRunner/ios/UIScriptControllerIOS.mm:
(WTR::UIScriptController::contentOffsetX const):
(WTR::UIScriptController::contentOffsetY const):

LayoutTests:

* editing/selection/ios/autoscroll-with-top-content-inset-expected.txt: Added.
* editing/selection/ios/autoscroll-with-top-content-inset.html: Added.

Add a new test to verify that moving the selection by autoscrolling near the top content inset area does not
cause the scroll view's content offset to jump.

* fast/scrolling/ios/scroll-into-view-with-top-content-inset-expected.txt: Added.
* fast/scrolling/ios/scroll-into-view-with-top-content-inset.html: Added.

Add a new test to verify that programmatically scrolling an element that's already visible into view does not
scroll away the scroll view's content inset.

* resources/ui-helper.js:
(window.UIHelper.contentOffset):
(window.UIHelper):

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

27 files changed:
LayoutTests/ChangeLog
LayoutTests/editing/selection/ios/autoscroll-with-top-content-inset-expected.txt [new file with mode: 0644]
LayoutTests/editing/selection/ios/autoscroll-with-top-content-inset.html [new file with mode: 0644]
LayoutTests/fast/scrolling/ios/scroll-into-view-with-top-content-inset-expected.txt [new file with mode: 0644]
LayoutTests/fast/scrolling/ios/scroll-into-view-with-top-content-inset.html [new file with mode: 0644]
LayoutTests/resources/ui-helper.js
Source/WebCore/ChangeLog
Source/WebCore/page/FrameView.cpp
Source/WebCore/page/FrameView.h
Source/WebCore/page/Page.h
Source/WebCore/rendering/RenderLayer.cpp
Source/WebKit/ChangeLog
Source/WebKit/Shared/VisibleContentRectUpdateInfo.cpp
Source/WebKit/Shared/VisibleContentRectUpdateInfo.h
Source/WebKit/UIProcess/API/Cocoa/WKWebView.mm
Source/WebKit/UIProcess/ios/WKContentView.h
Source/WebKit/UIProcess/ios/WKContentView.mm
Source/WebKit/WebProcess/WebPage/ios/WebPageIOS.mm
Tools/ChangeLog
Tools/DumpRenderTree/ios/UIScriptControllerIOS.mm
Tools/TestRunnerShared/UIScriptContext/Bindings/UIScriptController.idl
Tools/TestRunnerShared/UIScriptContext/UIScriptController.cpp
Tools/TestRunnerShared/UIScriptContext/UIScriptController.h
Tools/WebKitTestRunner/TestController.cpp
Tools/WebKitTestRunner/TestOptions.h
Tools/WebKitTestRunner/ios/TestControllerIOS.mm
Tools/WebKitTestRunner/ios/UIScriptControllerIOS.mm

index c8554d9..3c8324a 100644 (file)
@@ -1,3 +1,27 @@
+2019-01-17  Wenson Hsieh  <wenson_hsieh@apple.com>
+
+        [iOS] Content offset jumps erratically when autoscrolling near scroll view content inset areas
+        https://bugs.webkit.org/show_bug.cgi?id=193494
+        <rdar://problem/46859627>
+
+        Reviewed by Simon Fraser and Tim Horton.
+
+        * editing/selection/ios/autoscroll-with-top-content-inset-expected.txt: Added.
+        * editing/selection/ios/autoscroll-with-top-content-inset.html: Added.
+
+        Add a new test to verify that moving the selection by autoscrolling near the top content inset area does not
+        cause the scroll view's content offset to jump.
+
+        * fast/scrolling/ios/scroll-into-view-with-top-content-inset-expected.txt: Added.
+        * fast/scrolling/ios/scroll-into-view-with-top-content-inset.html: Added.
+
+        Add a new test to verify that programmatically scrolling an element that's already visible into view does not
+        scroll away the scroll view's content inset.
+
+        * resources/ui-helper.js:
+        (window.UIHelper.contentOffset):
+        (window.UIHelper):
+
 2019-01-17  John Wilander  <wilander@apple.com>
 
         Add infrastructure to enable/disable ITP Debug Mode through Preferences
diff --git a/LayoutTests/editing/selection/ios/autoscroll-with-top-content-inset-expected.txt b/LayoutTests/editing/selection/ios/autoscroll-with-top-content-inset-expected.txt
new file mode 100644 (file)
index 0000000..3ee5cff
--- /dev/null
@@ -0,0 +1,13 @@
+Select me and drag up.
+
+Verifies that triggering autoscroll near the top of a web view with a top content inset does not cause the scroll view's content offset to jump to 0. This test requires WebKitTestRunner. To verify manually, load this page in a web view that has a scroll view top content inset, and scroll such that the full top content inset area is visible. Check that starting a text selection loupe gesture near the top of the top of the document and dragging upwards does not thrash the scroll view's content offset.
+
+On success, you will see a series of "PASS" messages, followed by "TEST COMPLETE".
+
+
+PASS finalContentOffset.x is 0
+PASS verticalMovementDuringDrag < 1 is true
+PASS successfullyParsed is true
+
+TEST COMPLETE
+
diff --git a/LayoutTests/editing/selection/ios/autoscroll-with-top-content-inset.html b/LayoutTests/editing/selection/ios/autoscroll-with-top-content-inset.html
new file mode 100644 (file)
index 0000000..f4337ef
--- /dev/null
@@ -0,0 +1,51 @@
+<!DOCTYPE html> <!-- webkit-test-runner [ useFlexibleViewport=true, contentInset.top=100 ] -->
+<html>
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<head>
+    <script src="../../../resources/basic-gestures.js"></script>
+    <script src="../../../resources/ui-helper.js"></script>
+    <script src="../../../resources/js-test.js"></script>
+    <style>
+    body {
+        margin: 0;
+        box-sizing: border-box;
+        border: red 1px solid;
+    }
+
+    #text {
+        font-size: 30px;
+    }
+
+    #console {
+        overflow: scroll;
+        height: 100px;
+    }
+    </style>
+    <script>
+    jsTestIsAsync = true;
+
+    async function run()
+    {
+        await UIHelper.activateAndWaitForInputSessionAt(110, 40);
+        originalContentOffset = await UIHelper.contentOffset();
+        await longPressAndHoldAtPoint(110, 40);
+        await touchAndDragFromPointToPoint(110, 40, 210, 40);
+        await liftUpAtPoint(210, 40);
+        finalContentOffset = await UIHelper.contentOffset();
+        verticalMovementDuringDrag = Math.abs(finalContentOffset.y - originalContentOffset.y);
+
+        shouldBe("finalContentOffset.x", "0");
+        shouldBeTrue("verticalMovementDuringDrag < 1");
+        finishJSTest();
+    }
+    </script>
+</head>
+<body contenteditable onload="run()">
+    <p id="text"><strong>Select me and drag up.</strong></p>
+    <p id="description"></p>
+    <p id="console"></p>
+</body>
+<script>
+    description("Verifies that triggering autoscroll near the top of a web view with a top content inset does not cause the scroll view's content offset to jump to 0. This test requires WebKitTestRunner. To verify manually, load this page in a web view that has a scroll view top content inset, and scroll such that the full top content inset area is visible. Check that starting a text selection loupe gesture near the top of the top of the document and dragging upwards does not thrash the scroll view's content offset.");
+</script>
+</html>
diff --git a/LayoutTests/fast/scrolling/ios/scroll-into-view-with-top-content-inset-expected.txt b/LayoutTests/fast/scrolling/ios/scroll-into-view-with-top-content-inset-expected.txt
new file mode 100644 (file)
index 0000000..f7f479c
--- /dev/null
@@ -0,0 +1,13 @@
+Verifies that Element.scrollIntoView() does not scroll away the top content inset if the element is already visible. This test requires WebKitTestRunner.
+
+On success, you will see a series of "PASS" messages, followed by "TEST COMPLETE".
+
+
+PASS originalContentOffset.x is 0
+PASS originalContentOffset.y is -100
+PASS finalContentOffset.x is 0
+PASS finalContentOffset.y is -100
+PASS successfullyParsed is true
+
+TEST COMPLETE
+
diff --git a/LayoutTests/fast/scrolling/ios/scroll-into-view-with-top-content-inset.html b/LayoutTests/fast/scrolling/ios/scroll-into-view-with-top-content-inset.html
new file mode 100644 (file)
index 0000000..fd77ef6
--- /dev/null
@@ -0,0 +1,47 @@
+<!DOCTYPE html> <!-- webkit-test-runner [ useFlexibleViewport=true, contentInset.top=100 ] -->
+<html>
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<head>
+    <script src="../../../resources/ui-helper.js"></script>
+    <script src="../../../resources/js-test.js"></script>
+    <style>
+    body {
+        margin: 0;
+        border: red 1px solid;
+    }
+
+    #target {
+        position: absolute;
+        width: 4px;
+        height: 4px;
+        top: 0px;
+        left: 0px;
+        background-color: silver;
+    }
+    </style>
+    <script>
+    jsTestIsAsync = true;
+
+    async function run()
+    {
+        originalContentOffset = await UIHelper.contentOffset();
+        target.scrollIntoView({ block: "nearest", inline: "nearest" });
+        finalContentOffset = await UIHelper.contentOffset();
+
+        shouldBe("originalContentOffset.x", "0");
+        shouldBe("originalContentOffset.y", "-100");
+        shouldBe("finalContentOffset.x", "0");
+        shouldBe("finalContentOffset.y", "-100");
+        finishJSTest();
+    }
+    </script>
+</head>
+<body contenteditable onload="run()">
+    <div id="target"></div>
+    <p id="description"></p>
+    <p id="console"></p>
+</body>
+<script>
+    description("Verifies that Element.scrollIntoView() does not scroll away the top content inset if the element is already visible. This test requires WebKitTestRunner.");
+</script>
+</html>
index 1269759..1884db7 100644 (file)
@@ -554,4 +554,16 @@ window.UIHelper = class UIHelper {
         const escapedIdentifier = identifier.replace(/`/g, "\\`");
         return new Promise(resolve => testRunner.runUIScript(`uiController.setKeyboardInputModeIdentifier(\`${escapedIdentifier}\`)`, resolve));
     }
+
+    static contentOffset()
+    {
+        if (!this.isIOS())
+            return Promise.resolve();
+
+        const uiScript = "JSON.stringify([uiController.contentOffsetX, uiController.contentOffsetY])";
+        return new Promise(resolve => testRunner.runUIScript(uiScript, result => {
+            const [offsetX, offsetY] = JSON.parse(result)
+            resolve({ x: offsetX, y: offsetY });
+        }));
+    }
 }
index 225edcf..609aaaf 100644 (file)
@@ -1,3 +1,43 @@
+2019-01-17  Wenson Hsieh  <wenson_hsieh@apple.com>
+
+        [iOS] Content offset jumps erratically when autoscrolling near scroll view content inset areas
+        https://bugs.webkit.org/show_bug.cgi?id=193494
+        <rdar://problem/46859627>
+
+        Reviewed by Simon Fraser and Tim Horton.
+
+        When computing the content offset to scroll to when revealing a given rect in content coordinates, we currently
+        just use the unobscured content rect. As a result, when scrolling to reveal a rect, we'll clamp the final scroll
+        position such that only content is visible. For example, when asked to reveal the rect `(0, 0, 1, 1)`, we adjust
+        the scroll position to be the origin.
+
+        However, consider the case where a client (e.g. Mail on iOS) has added a content inset to the web view's scroll
+        view. If we're asked to reveal a rect that is outside the content area but within a content inset, we will still
+        end up clamping the scroll position to the unobscured rect. This manifests in a bug where selecting text and
+        autoscrolling in iOS Mail compose while the scroll view is scrolled all the way to the top to reveal the To/Cc/
+        Subject fields causes the content offset to jump to the origin, rather than staying at (0, -topContentInset).
+
+        To fix this, we teach `RenderLayer::scrollRectToVisible` about content insets that are visible. Rather than use
+        the content rects as-is, expand to encompass visible content insets as well. This ensures that revealing a
+        position which is already visible won't cause us to scroll away the content inset area and only show the
+        unobscured rect.
+
+        Tests:  editing/selection/ios/autoscroll-with-top-content-inset.html
+                fast/scrolling/ios/scroll-into-view-with-top-content-inset.html
+
+        * page/FrameView.cpp:
+        (WebCore::FrameView::unobscuredContentRectExpandedByContentInsets const):
+
+        Introduce a helper method that expands the unobscured content rect to include surrounding content insets.
+
+        * page/FrameView.h:
+        * page/Page.h:
+        (WebCore::Page::contentInsets const):
+        (WebCore::Page::setContentInsets):
+        * rendering/RenderLayer.cpp:
+        (WebCore::RenderLayer::scrollRectToVisible):
+        (WebCore::RenderLayer::getRectToExpose const):
+
 2019-01-17  Truitt Savell  <tsavell@apple.com>
 
         Unreviewed, rolling out r240124.
index 8dd924d..0669815 100644 (file)
@@ -2014,6 +2014,14 @@ void FrameView::viewportContentsChanged()
 #endif
 }
 
+IntRect FrameView::unobscuredContentRectExpandedByContentInsets() const
+{
+    FloatRect unobscuredContentRect = this->unobscuredContentRect();
+    if (auto* page = frame().page())
+        unobscuredContentRect.expand(page->contentInsets());
+    return IntRect(unobscuredContentRect);
+}
+
 bool FrameView::fixedElementsLayoutRelativeToFrame() const
 {
     return frame().settings().fixedElementsLayoutRelativeToFrame();
index 4429846..0c781ee 100644 (file)
@@ -326,6 +326,8 @@ public:
     // Static function can be called from another thread.
     WEBCORE_EXPORT static LayoutRect rectForViewportConstrainedObjects(const LayoutRect& visibleContentRect, const LayoutSize& totalContentsSize, float frameScaleFactor, bool fixedElementsLayoutRelativeToFrame, ScrollBehaviorForFixedElements);
 #endif
+
+    IntRect unobscuredContentRectExpandedByContentInsets() const;
     
     bool fixedElementsLayoutRelativeToFrame() const;
 
index b6df80a..7f7b370 100644 (file)
@@ -348,6 +348,9 @@ public:
     const FloatBoxExtent& obscuredInsets() const { return m_obscuredInsets; }
     void setObscuredInsets(const FloatBoxExtent& obscuredInsets) { m_obscuredInsets = obscuredInsets; }
 
+    const FloatBoxExtent& contentInsets() const { return m_contentInsets; }
+    void setContentInsets(const FloatBoxExtent& insets) { m_contentInsets = insets; }
+
     const FloatBoxExtent& unobscuredSafeAreaInsets() const { return m_unobscuredSafeAreaInsets; }
     WEBCORE_EXPORT void setUnobscuredSafeAreaInsets(const FloatBoxExtent&);
 
@@ -792,6 +795,7 @@ private:
 
     float m_topContentInset { 0 };
     FloatBoxExtent m_obscuredInsets;
+    FloatBoxExtent m_contentInsets;
     FloatBoxExtent m_unobscuredSafeAreaInsets;
     FloatBoxExtent m_fullscreenInsets;
     Seconds m_fullscreenAutoHideDuration { 0_s };
index 072e937..ccef363 100644 (file)
@@ -2551,7 +2551,7 @@ void RenderLayer::scrollRectToVisible(const LayoutRect& absoluteRect, bool insid
 #if !PLATFORM(IOS_FAMILY)
             LayoutRect viewRect = frameView.visibleContentRect();
 #else
-            LayoutRect viewRect = frameView.unobscuredContentRect();
+            LayoutRect viewRect = frameView.unobscuredContentRectExpandedByContentInsets();
 #endif
             // Move the target rect into "scrollView contents" coordinates.
             LayoutRect targetRect = absoluteRect;
@@ -2591,7 +2591,7 @@ void RenderLayer::updateCompositingLayersAfterScroll()
     }
 }
 
-LayoutRect RenderLayer::getRectToExpose(const LayoutRect &visibleRect, const LayoutRect &exposeRect, bool insideFixed, const ScrollAlignment& alignX, const ScrollAlignment& alignY) const
+LayoutRect RenderLayer::getRectToExpose(const LayoutRect& visibleRect, const LayoutRect& exposeRect, bool insideFixed, const ScrollAlignment& alignX, const ScrollAlignment& alignY) const
 {
     FrameView& frameView = renderer().view().frameView();
     if (renderer().isRenderView() && insideFixed) {
index 89b471d..1f3e471 100644 (file)
@@ -1,3 +1,48 @@
+2019-01-17  Wenson Hsieh  <wenson_hsieh@apple.com>
+
+        [iOS] Content offset jumps erratically when autoscrolling near scroll view content inset areas
+        https://bugs.webkit.org/show_bug.cgi?id=193494
+        <rdar://problem/46859627>
+
+        Reviewed by Simon Fraser and Tim Horton.
+
+        Adds `contentInsets` to `VisibleContentRectUpdateInfo`. This keeps track of the visible content insets
+        surrounding the unobscured content rect. See WebCore ChangeLog for more details.
+
+        * Shared/VisibleContentRectUpdateInfo.cpp:
+        (WebKit::VisibleContentRectUpdateInfo::encode const):
+        (WebKit::VisibleContentRectUpdateInfo::decode):
+        (WebKit::operator<<):
+        * Shared/VisibleContentRectUpdateInfo.h:
+        (WebKit::VisibleContentRectUpdateInfo::VisibleContentRectUpdateInfo):
+        (WebKit::VisibleContentRectUpdateInfo::contentInsets const):
+        (WebKit::operator==):
+        * UIProcess/API/Cocoa/WKWebView.mm:
+        (-[WKWebView _computedObscuredInset]):
+        (-[WKWebView _computedContentInset]):
+        (-[WKWebView _computedUnobscuredSafeAreaInset]):
+
+        We don't care about source compatibility with iOS 10 and below anymore, so we should change these >= iOS 11
+        target checks to simply `PLATFORM(IOS)`.
+
+        (-[WKWebView _updateVisibleContentRects]):
+
+        Compute the visible content insets on all sides of the unobscured content rect. These insets are scaled to
+        content coordinates.
+
+        * UIProcess/ios/WKContentView.h:
+        * UIProcess/ios/WKContentView.mm:
+        (floatBoxExtent):
+
+        Add a helper to convert `UIEdgeInsets` to `WebCore::FloatBoxExtent`, and use it in a few places below.
+
+        (-[WKContentView didUpdateVisibleRect:unobscuredRect:contentInsets:unobscuredRectInScrollViewCoordinates:obscuredInsets:unobscuredSafeAreaInsets:inputViewBounds:scale:minimumScale:inStableState:isChangingObscuredInsetsInteractively:enclosedInScrollableAncestorView:]):
+        (-[WKContentView didUpdateVisibleRect:unobscuredRect:unobscuredRectInScrollViewCoordinates:obscuredInsets:unobscuredSafeAreaInsets:inputViewBounds:scale:minimumScale:inStableState:isChangingObscuredInsetsInteractively:enclosedInScrollableAncestorView:]): Deleted.
+        * WebProcess/WebPage/ios/WebPageIOS.mm:
+        (WebKit::WebPage::updateVisibleContentRects):
+
+        Update the Page's content insets.
+
 2019-01-17  Truitt Savell  <tsavell@apple.com>
 
         Unreviewed, rolling out r240124.
index 2ee8d96..e9a883d 100644 (file)
@@ -37,6 +37,7 @@ void VisibleContentRectUpdateInfo::encode(IPC::Encoder& encoder) const
 {
     encoder << m_exposedContentRect;
     encoder << m_unobscuredContentRect;
+    encoder << m_contentInsets;
     encoder << m_unobscuredContentRectRespectingInputViewBounds;
     encoder << m_unobscuredRectInScrollViewCoordinates;
     encoder << m_customFixedPositionRect;
@@ -61,6 +62,8 @@ bool VisibleContentRectUpdateInfo::decode(IPC::Decoder& decoder, VisibleContentR
         return false;
     if (!decoder.decode(result.m_unobscuredContentRect))
         return false;
+    if (!decoder.decode(result.m_contentInsets))
+        return false;
     if (!decoder.decode(result.m_unobscuredContentRectRespectingInputViewBounds))
         return false;
     if (!decoder.decode(result.m_unobscuredRectInScrollViewCoordinates))
@@ -114,6 +117,7 @@ TextStream& operator<<(TextStream& ts, const VisibleContentRectUpdateInfo& info)
 
     ts.dumpProperty("exposedContentRect", info.exposedContentRect());
     ts.dumpProperty("unobscuredContentRect", info.unobscuredContentRect());
+    ts.dumpProperty("contentInsets", info.contentInsets());
     ts.dumpProperty("unobscuredContentRectRespectingInputViewBounds", info.unobscuredContentRectRespectingInputViewBounds());
     ts.dumpProperty("unobscuredRectInScrollViewCoordinates", info.unobscuredRectInScrollViewCoordinates());
     ts.dumpProperty("customFixedPositionRect", info.customFixedPositionRect());
index 91c1822..814ca54 100644 (file)
@@ -45,9 +45,10 @@ class VisibleContentRectUpdateInfo {
 public:
     VisibleContentRectUpdateInfo() = default;
 
-    VisibleContentRectUpdateInfo(const WebCore::FloatRect& exposedContentRect, const WebCore::FloatRect& unobscuredContentRect, const WebCore::FloatRect& unobscuredRectInScrollViewCoordinates, const WebCore::FloatRect& unobscuredContentRectRespectingInputViewBounds, const WebCore::FloatRect& customFixedPositionRect, const WebCore::FloatBoxExtent& obscuredInsets, const WebCore::FloatBoxExtent& unobscuredSafeAreaInsets, double scale, bool inStableState, bool isFirstUpdateForNewViewSize, bool isChangingObscuredInsetsInteractively, bool allowShrinkToFit, bool enclosedInScrollableAncestorView, MonotonicTime timestamp, double horizontalVelocity, double verticalVelocity, double scaleChangeRate, uint64_t lastLayerTreeTransactionId)
+    VisibleContentRectUpdateInfo(const WebCore::FloatRect& exposedContentRect, const WebCore::FloatRect& unobscuredContentRect, const WebCore::FloatBoxExtent& contentInsets, const WebCore::FloatRect& unobscuredRectInScrollViewCoordinates, const WebCore::FloatRect& unobscuredContentRectRespectingInputViewBounds, const WebCore::FloatRect& customFixedPositionRect, const WebCore::FloatBoxExtent& obscuredInsets, const WebCore::FloatBoxExtent& unobscuredSafeAreaInsets, double scale, bool inStableState, bool isFirstUpdateForNewViewSize, bool isChangingObscuredInsetsInteractively, bool allowShrinkToFit, bool enclosedInScrollableAncestorView, MonotonicTime timestamp, double horizontalVelocity, double verticalVelocity, double scaleChangeRate, uint64_t lastLayerTreeTransactionId)
         : m_exposedContentRect(exposedContentRect)
         , m_unobscuredContentRect(unobscuredContentRect)
+        , m_contentInsets(contentInsets)
         , m_unobscuredContentRectRespectingInputViewBounds(unobscuredContentRectRespectingInputViewBounds)
         , m_unobscuredRectInScrollViewCoordinates(unobscuredRectInScrollViewCoordinates)
         , m_customFixedPositionRect(customFixedPositionRect)
@@ -69,6 +70,7 @@ public:
 
     const WebCore::FloatRect& exposedContentRect() const { return m_exposedContentRect; }
     const WebCore::FloatRect& unobscuredContentRect() const { return m_unobscuredContentRect; }
+    const WebCore::FloatBoxExtent& contentInsets() const { return m_contentInsets; }
     const WebCore::FloatRect& unobscuredRectInScrollViewCoordinates() const { return m_unobscuredRectInScrollViewCoordinates; }
     const WebCore::FloatRect& unobscuredContentRectRespectingInputViewBounds() const { return m_unobscuredContentRectRespectingInputViewBounds; }
     const WebCore::FloatRect& customFixedPositionRect() const { return m_customFixedPositionRect; }
@@ -97,6 +99,7 @@ public:
 private:
     WebCore::FloatRect m_exposedContentRect;
     WebCore::FloatRect m_unobscuredContentRect;
+    WebCore::FloatBoxExtent m_contentInsets;
     WebCore::FloatRect m_unobscuredContentRectRespectingInputViewBounds;
     WebCore::FloatRect m_unobscuredRectInScrollViewCoordinates;
     WebCore::FloatRect m_customFixedPositionRect; // When visual viewports are enabled, this is the layout viewport.
@@ -121,6 +124,7 @@ inline bool operator==(const VisibleContentRectUpdateInfo& a, const VisibleConte
     return a.scale() == b.scale()
         && a.exposedContentRect() == b.exposedContentRect()
         && a.unobscuredContentRect() == b.unobscuredContentRect()
+        && a.contentInsets() == b.contentInsets()
         && a.unobscuredContentRectRespectingInputViewBounds() == b.unobscuredContentRectRespectingInputViewBounds()
         && a.customFixedPositionRect() == b.customFixedPositionRect()
         && a.obscuredInsets() == b.obscuredInsets()
index 7c45a7d..265e6fe 100644 (file)
@@ -1734,7 +1734,7 @@ static WebCore::Color scrollViewBackgroundColor(WKWebView *webView)
     if (_haveSetObscuredInsets)
         return _obscuredInsets;
 
-#if __IPHONE_OS_VERSION_MIN_REQUIRED >= 110000
+#if PLATFORM(IOS)
     if (self._safeAreaShouldAffectObscuredInsets)
         return UIEdgeInsetsAdd(UIEdgeInsetsZero, self._scrollViewSystemContentInset, self._effectiveObscuredInsetEdgesAffectedBySafeArea);
 #endif
@@ -1749,7 +1749,7 @@ static WebCore::Color scrollViewBackgroundColor(WKWebView *webView)
 
     UIEdgeInsets insets = [_scrollView contentInset];
 
-#if __IPHONE_OS_VERSION_MIN_REQUIRED >= 110000
+#if PLATFORM(IOS)
     if (self._safeAreaShouldAffectObscuredInsets)
         insets = UIEdgeInsetsAdd(insets, self._scrollViewSystemContentInset, self._effectiveObscuredInsetEdgesAffectedBySafeArea);
 #endif
@@ -1762,7 +1762,7 @@ static WebCore::Color scrollViewBackgroundColor(WKWebView *webView)
     if (_haveSetUnobscuredSafeAreaInsets)
         return _unobscuredSafeAreaInsets;
 
-#if __IPHONE_OS_VERSION_MIN_REQUIRED >= 110000
+#if PLATFORM(IOS)
     if (!self._safeAreaShouldAffectObscuredInsets)
         return self.safeAreaInsets;
 #endif
@@ -2974,6 +2974,26 @@ static bool scrollViewCanScroll(UIScrollView *scrollView)
     CGRect unobscuredRectInContentCoordinates = _frozenUnobscuredContentRect ? _frozenUnobscuredContentRect.value() : [self convertRect:unobscuredRect toView:_contentView.get()];
     unobscuredRectInContentCoordinates = CGRectIntersection(unobscuredRectInContentCoordinates, [self _contentBoundsExtendedForRubberbandingWithScale:scaleFactor]);
 
+    // The following logic computes the extent to which the bottom, top, left and right content insets are visible.
+    auto scrollViewInsets = [_scrollView contentInset];
+    auto scrollViewBounds = [_scrollView bounds];
+    auto scrollViewContentSize = [_scrollView contentSize];
+    auto scrollViewOriginIncludingInset = UIEdgeInsetsInsetRect(scrollViewBounds, computedContentInsetUnadjustedForKeyboard).origin;
+    auto maximumVerticalScrollExtentWithoutRevealingBottomContentInset = scrollViewContentSize.height - CGRectGetHeight(scrollViewBounds);
+    auto maximumHorizontalScrollExtentWithoutRevealingRightContentInset = scrollViewContentSize.width - CGRectGetWidth(scrollViewBounds);
+    auto contentInsets = UIEdgeInsetsZero;
+    if (scrollViewInsets.left > 0 && scrollViewOriginIncludingInset.x < 0)
+        contentInsets.left = std::min(-scrollViewOriginIncludingInset.x, scrollViewInsets.left) / scaleFactor;
+
+    if (scrollViewInsets.top > 0 && scrollViewOriginIncludingInset.y < 0)
+        contentInsets.top = std::min(-scrollViewOriginIncludingInset.y, scrollViewInsets.top) / scaleFactor;
+
+    if (scrollViewInsets.right > 0 && scrollViewOriginIncludingInset.x > maximumHorizontalScrollExtentWithoutRevealingRightContentInset)
+        contentInsets.right = std::min(scrollViewOriginIncludingInset.x - maximumHorizontalScrollExtentWithoutRevealingRightContentInset, scrollViewInsets.right) / scaleFactor;
+
+    if (scrollViewInsets.bottom > 0 && scrollViewOriginIncludingInset.y > maximumVerticalScrollExtentWithoutRevealingBottomContentInset)
+        contentInsets.bottom = std::min(scrollViewOriginIncludingInset.y - maximumVerticalScrollExtentWithoutRevealingBottomContentInset, scrollViewInsets.bottom) / scaleFactor;
+
 #if ENABLE(CSS_SCROLL_SNAP) && ENABLE(ASYNC_SCROLLING)
     if (inStableState) {
         WebKit::RemoteScrollingCoordinatorProxy* coordinator = _page->scrollingCoordinatorProxy();
@@ -2993,6 +3013,7 @@ static bool scrollViewCanScroll(UIScrollView *scrollView)
 
     [_contentView didUpdateVisibleRect:visibleRectInContentCoordinates
         unobscuredRect:unobscuredRectInContentCoordinates
+        contentInsets:contentInsets
         unobscuredRectInScrollViewCoordinates:unobscuredRect
         obscuredInsets:_obscuredInsets
         unobscuredSafeAreaInsets:[self _computedUnobscuredSafeAreaInset]
index a0607e3..522aca1 100644 (file)
@@ -71,6 +71,7 @@ class WebProcessPool;
 
 - (void)didUpdateVisibleRect:(CGRect)visibleRect
     unobscuredRect:(CGRect)unobscuredRect
+    contentInsets:(UIEdgeInsets)contentInsets
     unobscuredRectInScrollViewCoordinates:(CGRect)unobscuredRectInScrollViewCoordinates
     obscuredInsets:(UIEdgeInsets)obscuredInsets
     unobscuredSafeAreaInsets:(UIEdgeInsets)unobscuredSafeAreaInsets
index ced85e8..304c23f 100644 (file)
@@ -365,6 +365,11 @@ ALLOW_DEPRECATED_DECLARATIONS_END
     [_textSelectionAssistant deactivateSelection];
 }
 
+static WebCore::FloatBoxExtent floatBoxExtent(UIEdgeInsets insets)
+{
+    return WebCore::FloatBoxExtent(insets.top, insets.right, insets.bottom, insets.left);
+}
+
 - (CGRect)_computeUnobscuredContentRectRespectingInputViewBounds:(CGRect)unobscuredContentRect inputViewBounds:(CGRect)inputViewBounds
 {
     // The input view bounds are in window coordinates, but the unobscured rect is in content coordinates. Account for this by converting input view bounds to content coordinates.
@@ -376,6 +381,7 @@ ALLOW_DEPRECATED_DECLARATIONS_END
 
 - (void)didUpdateVisibleRect:(CGRect)visibleContentRect
     unobscuredRect:(CGRect)unobscuredContentRect
+    contentInsets:(UIEdgeInsets)contentInsets
     unobscuredRectInScrollViewCoordinates:(CGRect)unobscuredRectInScrollViewCoordinates
     obscuredInsets:(UIEdgeInsets)obscuredInsets
     unobscuredSafeAreaInsets:(UIEdgeInsets)unobscuredSafeAreaInsets
@@ -404,11 +410,12 @@ ALLOW_DEPRECATED_DECLARATIONS_END
     WebKit::VisibleContentRectUpdateInfo visibleContentRectUpdateInfo(
         visibleContentRect,
         unobscuredContentRect,
+        floatBoxExtent(contentInsets),
         unobscuredRectInScrollViewCoordinates,
         unobscuredContentRectRespectingInputViewBounds,
         fixedPositionRectForLayout,
-        WebCore::FloatBoxExtent(obscuredInsets.top, obscuredInsets.right, obscuredInsets.bottom, obscuredInsets.left),
-        WebCore::FloatBoxExtent(unobscuredSafeAreaInsets.top, unobscuredSafeAreaInsets.right, unobscuredSafeAreaInsets.bottom, unobscuredSafeAreaInsets.left),
+        floatBoxExtent(obscuredInsets),
+        floatBoxExtent(unobscuredSafeAreaInsets),
         zoomScale,
         isStableState,
         _sizeChangedSinceLastVisibleContentRectUpdate,
index 06696c0..9b09a0a 100644 (file)
@@ -3016,6 +3016,7 @@ void WebPage::updateVisibleContentRects(const VisibleContentRectUpdateInfo& visi
         viewportConfigurationChanged();
 
     frameView.setUnobscuredContentSize(visibleContentRectUpdateInfo.unobscuredContentRect().size());
+    m_page->setContentInsets(visibleContentRectUpdateInfo.contentInsets());
     m_page->setObscuredInsets(visibleContentRectUpdateInfo.obscuredInsets());
     m_page->setUnobscuredSafeAreaInsets(visibleContentRectUpdateInfo.unobscuredSafeAreaInsets());
     m_page->setEnclosedInScrollableAncestorView(visibleContentRectUpdateInfo.enclosedInScrollableAncestorView());
index 5bd97e6..6bb4100 100644 (file)
@@ -1,3 +1,35 @@
+2019-01-17  Wenson Hsieh  <wenson_hsieh@apple.com>
+
+        [iOS] Content offset jumps erratically when autoscrolling near scroll view content inset areas
+        https://bugs.webkit.org/show_bug.cgi?id=193494
+        <rdar://problem/46859627>
+
+        Reviewed by Simon Fraser and Tim Horton.
+
+        Add a new test option to add a top content inset to the test runner's WKWebView's scroll view, and automatically
+        scroll to reveal this top content inset area when beginning the test (i.e., scroll to (0, -topContentInset)).
+
+        * DumpRenderTree/ios/UIScriptControllerIOS.mm:
+        (WTR::UIScriptController::contentOffsetX const):
+        (WTR::UIScriptController::contentOffsetY const):
+        * TestRunnerShared/UIScriptContext/Bindings/UIScriptController.idl:
+        * TestRunnerShared/UIScriptContext/UIScriptController.cpp:
+        (WTR::UIScriptController::contentOffsetX const):
+        (WTR::UIScriptController::contentOffsetY const):
+        * TestRunnerShared/UIScriptContext/UIScriptController.h:
+
+        Also add new UIScriptController methods to ask for the content offset of the platform scroll view.
+
+        * WebKitTestRunner/TestController.cpp:
+        (WTR::updateTestOptionsFromTestHeader):
+        * WebKitTestRunner/TestOptions.h:
+        (WTR::TestOptions::hasSameInitializationOptions const):
+        * WebKitTestRunner/ios/TestControllerIOS.mm:
+        (WTR::TestController::platformResetStateToConsistentValues):
+        * WebKitTestRunner/ios/UIScriptControllerIOS.mm:
+        (WTR::UIScriptController::contentOffsetX const):
+        (WTR::UIScriptController::contentOffsetY const):
+
 2019-01-17  Truitt Savell  <tsavell@apple.com>
 
         Unreviewed, rolling out r240124.
index 398034a..8a1125f 100644 (file)
@@ -211,6 +211,16 @@ static CGPoint contentOffsetBoundedInValidRange(UIScrollView *scrollView, CGPoin
     return contentOffset;
 }
 
+double UIScriptController::contentOffsetX() const
+{
+    return [gWebScrollView contentOffset].x;
+}
+
+double UIScriptController::contentOffsetY() const
+{
+    return [gWebScrollView contentOffset].y;
+}
+
 void UIScriptController::scrollToOffset(long x, long y)
 {
     [gWebScrollView setContentOffset:contentOffsetBoundedInValidRange(gWebScrollView, CGPointMake(x, y)) animated:YES];
index 7d204e8..5424ba8 100644 (file)
@@ -231,6 +231,9 @@ interface UIScriptController {
 
     void resignFirstResponder();
 
+    readonly attribute double contentOffsetX;
+    readonly attribute double contentOffsetY;
+
     void scrollToOffset(long x, long y); // Initiate an animated scroll in the UI process.
     attribute object didEndScrollingCallback;
 
index c9587c4..a5baa06 100644 (file)
@@ -336,6 +336,16 @@ JSRetainPtr<JSStringRef> UIScriptController::formInputLabel() const
     return nullptr;
 }
 
+double UIScriptController::contentOffsetX() const
+{
+    return 0;
+}
+
+double UIScriptController::contentOffsetY() const
+{
+    return 0;
+}
+
 void UIScriptController::scrollToOffset(long x, long y)
 {
 }
index a6482c9..d3fb1cb 100644 (file)
@@ -113,6 +113,9 @@ public:
     JSObjectRef contentsOfUserInterfaceItem(JSStringRef) const;
     void overridePreference(JSStringRef preference, JSStringRef value);
     
+    double contentOffsetX() const;
+    double contentOffsetY() const;
+
     void scrollToOffset(long x, long y);
 
     void immediateScrollToOffset(long x, long y);
index 7c88e00..926177e 100644 (file)
@@ -1266,6 +1266,8 @@ static void updateTestOptionsFromTestHeader(TestOptions& testOptions, const std:
             testOptions.editable = parseBooleanTestHeaderValue(value);
         else if (key == "enableUndoManagerAPI")
             testOptions.enableUndoManagerAPI = parseBooleanTestHeaderValue(value);
+        else if (key == "contentInset.top")
+            testOptions.contentInsetTop = std::stod(value);
         pairStart = pairEnd + 1;
     }
 }
index 10cd4e4..614ce7b 100644 (file)
@@ -69,6 +69,8 @@ struct TestOptions {
     bool editable { false };
     bool enableUndoManagerAPI { false };
 
+    double contentInsetTop { 0 };
+
     float deviceScaleFactor { 1 };
     Vector<String> overrideLanguages;
     std::string applicationManifest;
@@ -113,7 +115,8 @@ struct TestOptions {
             || shouldIgnoreMetaViewport != options.shouldIgnoreMetaViewport
             || enableEditableImages != options.enableEditableImages
             || editable != options.editable
-            || enableUndoManagerAPI != options.enableUndoManagerAPI)
+            || enableUndoManagerAPI != options.enableUndoManagerAPI
+            || contentInsetTop != options.contentInsetTop)
             return false;
 
         if (experimentalFeatures != options.experimentalFeatures)
index 4a1080c..855d2d5 100644 (file)
@@ -132,7 +132,8 @@ void TestController::platformResetStateToConsistentValues(const TestOptions& opt
         UIScrollView *scrollView = webView.scrollView;
         [scrollView _removeAllAnimations:YES];
         [scrollView setZoomScale:1 animated:NO];
-        [scrollView setContentOffset:CGPointZero];
+        scrollView.contentInset = UIEdgeInsetsMake(options.contentInsetTop, 0, 0, 0);
+        scrollView.contentOffset = CGPointMake(0, -options.contentInsetTop);
 
         if (webView.interactingWithFormControl)
             shouldRestoreFirstResponder = [webView resignFirstResponder];
index 9e20477..4aa9429 100644 (file)
@@ -487,6 +487,18 @@ static CGPoint contentOffsetBoundedInValidRange(UIScrollView *scrollView, CGPoin
     return contentOffset;
 }
 
+double UIScriptController::contentOffsetX() const
+{
+    TestRunnerWKWebView *webView = TestController::singleton().mainWebView()->platformView();
+    return webView.scrollView.contentOffset.x;
+}
+
+double UIScriptController::contentOffsetY() const
+{
+    TestRunnerWKWebView *webView = TestController::singleton().mainWebView()->platformView();
+    return webView.scrollView.contentOffset.y;
+}
+
 void UIScriptController::scrollToOffset(long x, long y)
 {
     TestRunnerWKWebView *webView = TestController::singleton().mainWebView()->platformView();