Skip to content

Commit

Permalink
iOS 17: History back sometimes scrolls to top of the page (with app b…
Browse files Browse the repository at this point in the history
…anner)

https://bugs.webkit.org/show_bug.cgi?id=231563
rdar://117107620

Reviewed by Tim Horton.

Sometimes, navigating back to a page on iOS would fail to restore the scroll position to the correct
location. This happened when `WebPage::restorePageState()` hit the second mismatched
`minimumLayoutSizeInScrollViewCoordinates` clause, which made use of
`historyItem.unobscuredContentRect()`. This could happen when MobileSafari app banners were hidden
or shown. `historyItem.unobscuredContentRect()` had the wrong value in this case, containing a rect
that had a 0,0 location which triggered a scroll back to the top.

`historyItem.unobscuredContentRect()` was wrong because it was set in
`HistoryController::saveScrollPositionAndViewStateToItem()` which runs after we've reset the content
size in preparation for the navigation; note that this function uses
`frameView->cachedScrollPosition()`. So the fix is to cache `unobscuredContentRect()` and
`exposedContentRect()` in the same way.

This is messy; ScrollView shouldn't be in the business of storing caches related to page navigation,
but fixing that is out of scope for this change.

The API test navigates and goes back with a view size change, which triggers the bug.

Also add some release logging to help diagnose scroll restoration bugs.

* Source/WebCore/dom/Document.cpp:
(WebCore::Document::setBackForwardCacheState):
* Source/WebCore/loader/HistoryController.cpp:
(WebCore::FrameLoader::HistoryController::saveScrollPositionAndViewStateToItem):
* Source/WebCore/platform/ScrollView.cpp:
(WebCore::ScrollView::cacheCurrentScrollState):
* Source/WebCore/platform/ScrollView.h:
(WebCore::ScrollView::cachedUnobscuredContentRect const):
(WebCore::ScrollView::cachedExposedContentRect const):
(WebCore::ScrollView::cacheCurrentScrollPosition): Deleted.
* Tools/TestWebKitAPI/TestWebKitAPI.xcodeproj/project.pbxproj:
* Tools/TestWebKitAPI/Tests/ios/ScrollViewInsetTests.mm:
(TestWebKitAPI::TEST):
* Tools/TestWebKitAPI/ios/scrollable-page.html: Added.

Canonical link: https://commits.webkit.org/269611@main
  • Loading branch information
smfr committed Oct 21, 2023
1 parent 7b3eb7e commit 2bafc1b
Show file tree
Hide file tree
Showing 8 changed files with 80 additions and 8 deletions.
2 changes: 1 addition & 1 deletion Source/WebCore/dom/Document.cpp
Expand Up @@ -6099,7 +6099,7 @@ void Document::setBackForwardCacheState(BackForwardCacheState state)
// called too early on in the process of a page exiting the cache for that work to be possible in this
// function. It would be nice if there was more symmetry here.
// https://bugs.webkit.org/show_bug.cgi?id=98698
frameView->cacheCurrentScrollPosition();
frameView->cacheCurrentScrollState();
if (page && m_frame->isMainFrame()) {
frameView->resetScrollbarsAndClearContentsSize();
if (RefPtr scrollingCoordinator = page->scrollingCoordinator())
Expand Down
16 changes: 10 additions & 6 deletions Source/WebCore/loader/HistoryController.cpp
Expand Up @@ -75,17 +75,21 @@ void FrameLoader::HistoryController::saveScrollPositionAndViewStateToItem(Histor
if (!item || !frameView)
return;

if (m_frame.document()->backForwardCacheState() != Document::NotInBackForwardCache)
if (m_frame.document()->backForwardCacheState() != Document::NotInBackForwardCache) {
item->setScrollPosition(frameView->cachedScrollPosition());
else
#if PLATFORM(IOS_FAMILY)
item->setUnobscuredContentRect(frameView->cachedUnobscuredContentRect());
item->setExposedContentRect(frameView->cachedExposedContentRect());
#endif
} else {
item->setScrollPosition(frameView->scrollPosition());

#if PLATFORM(IOS_FAMILY)
item->setExposedContentRect(frameView->exposedContentRect());
item->setUnobscuredContentRect(frameView->unobscuredContentRect());
item->setUnobscuredContentRect(frameView->unobscuredContentRect());
item->setExposedContentRect(frameView->exposedContentRect());
#endif
}

Page* page = m_frame.page();
auto* page = m_frame.page();
if (page && m_frame.isMainFrame()) {
item->setPageScaleFactor(page->pageScaleFactor() / page->viewScaleFactor());
#if PLATFORM(IOS_FAMILY)
Expand Down
9 changes: 9 additions & 0 deletions Source/WebCore/platform/ScrollView.cpp
Expand Up @@ -429,6 +429,15 @@ ScrollPosition ScrollView::adjustScrollPositionWithinRange(const ScrollPosition&
return scrollPosition.constrainedBetween(minimumScrollPosition(), maximumScrollPosition());
}

void ScrollView::cacheCurrentScrollState()
{
m_cachedScrollPosition = scrollPosition();
#if PLATFORM(IOS_FAMILY)
m_cachedUnobscuredContentRect = unobscuredContentRect();
m_cachedExposedContentRect = exposedContentRect();
#endif
}

ScrollPosition ScrollView::documentScrollPositionRelativeToViewOrigin() const
{
return scrollPosition() - IntSize(
Expand Down
10 changes: 9 additions & 1 deletion Source/WebCore/platform/ScrollView.h
Expand Up @@ -277,8 +277,12 @@ class ScrollView : public Widget, public ScrollableArea {

IntSize overhangAmount() const final;

void cacheCurrentScrollPosition() { m_cachedScrollPosition = scrollPosition(); }
void cacheCurrentScrollState();
ScrollPosition cachedScrollPosition() const { return m_cachedScrollPosition; }
#if PLATFORM(IOS_FAMILY)
IntRect cachedUnobscuredContentRect() const { return m_cachedUnobscuredContentRect; }
FloatRect cachedExposedContentRect() const { return m_cachedExposedContentRect; }
#endif

// Functions for scrolling the view.
virtual void setScrollPosition(const ScrollPosition&, const ScrollPositionChangeOptions& = ScrollPositionChangeOptions::createProgrammatic());
Expand Down Expand Up @@ -537,6 +541,10 @@ class ScrollView : public Widget, public ScrollableArea {
#endif
ScrollPosition m_scrollPosition;
IntPoint m_cachedScrollPosition;
#if PLATFORM(IOS_FAMILY)
IntRect m_cachedUnobscuredContentRect;
FloatRect m_cachedExposedContentRect;
#endif
IntSize m_fixedLayoutSize;
IntSize m_contentsSize;

Expand Down
2 changes: 2 additions & 0 deletions Source/WebKit/WebProcess/WebPage/ios/WebPageIOS.mm
Expand Up @@ -527,6 +527,7 @@ static inline FloatRect adjustExposedRectForNewScale(const FloatRect& exposedRec
scrollPosition = FloatPoint(historyItem.scrollPosition());
}

RELEASE_LOG(Scrolling, "WebPage::restorePageState with matching minimumLayoutSize; historyItem.shouldRestoreScrollPosition %d, scrollPosition.y %d", historyItem.shouldRestoreScrollPosition(), historyItem.scrollPosition().y());
send(Messages::WebPageProxy::RestorePageState(scrollPosition, frameView.scrollOrigin(), historyItem.obscuredInsets(), boundedScale));
} else {
IntSize oldContentSize = historyItem.contentSize();
Expand All @@ -543,6 +544,7 @@ static inline FloatRect adjustExposedRectForNewScale(const FloatRect& exposedRec
newCenter = FloatRect(historyItem.unobscuredContentRect()).center();
}

RELEASE_LOG(Scrolling, "WebPage::restorePageState with mismatched minimumLayoutSize; historyItem.shouldRestoreScrollPosition %d, unobscured rect top %d, scale %.2f", historyItem.shouldRestoreScrollPosition(), historyItem.unobscuredContentRect().y(), newScale);
scalePage(newScale, IntPoint());
send(Messages::WebPageProxy::RestorePageCenterAndScale(newCenter, newScale));
}
Expand Down
4 changes: 4 additions & 0 deletions Tools/TestWebKitAPI/TestWebKitAPI.xcodeproj/project.pbxproj
Expand Up @@ -87,6 +87,7 @@
0F5651F91FCE513500310FBC /* scroll-to-anchor.html in Copy Resources */ = {isa = PBXBuildFile; fileRef = 0F5651F81FCE50E800310FBC /* scroll-to-anchor.html */; };
0FEFAF64271FC2CD005704D7 /* ScrollingCoordinatorTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 0FEFAF63271FC2CD005704D7 /* ScrollingCoordinatorTests.mm */; };
0FF1134E22D68679009A81DA /* ScrollViewScrollabilityTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 0FF1134D22D68679009A81DA /* ScrollViewScrollabilityTests.mm */; };
0FF1F0CD2AE205F5006ECC4F /* scrollable-page.html in Copy Resources */ = {isa = PBXBuildFile; fileRef = 0FF1F0C52AE204C0006ECC4F /* scrollable-page.html */; };
0FF332932A0EFCCF00C048D8 /* HysteresisActivityTests.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 0FF3328B2A0EFCCF00C048D8 /* HysteresisActivityTests.cpp */; };
115EB3431EE0BA03003C2C0A /* ViewportSizeForViewportUnits.mm in Sources */ = {isa = PBXBuildFile; fileRef = 115EB3421EE0B720003C2C0A /* ViewportSizeForViewportUnits.mm */; };
1171B24F219F49CD00CB897D /* FirstMeaningfulPaintMilestone_Bundle.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 11B7FD21219F46DD0069B27F /* FirstMeaningfulPaintMilestone_Bundle.cpp */; };
Expand Down Expand Up @@ -1836,6 +1837,7 @@
1CE6FAC32320267C00E48F6E /* rich-color-filtered.html in Copy Resources */,
2E92B8F7216490D4005B64F0 /* rich-text-attributes.html in Copy Resources */,
0F5651F91FCE513500310FBC /* scroll-to-anchor.html in Copy Resources */,
0FF1F0CD2AE205F5006ECC4F /* scrollable-page.html in Copy Resources */,
F4E0A296211FC5FB00AF7C7F /* selected-text-and-textarea.html in Copy Resources */,
F4D65DA81F5E4704009D8C27 /* selected-text-image-link-and-editable.html in Copy Resources */,
7A66BDB81EAF18D500CCC924 /* set-long-title.html in Copy Resources */,
Expand Down Expand Up @@ -2042,6 +2044,7 @@
0FEAE3671B7D19CB00CE17F2 /* Condition.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = Condition.cpp; sourceTree = "<group>"; };
0FEFAF63271FC2CD005704D7 /* ScrollingCoordinatorTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ScrollingCoordinatorTests.mm; sourceTree = "<group>"; };
0FF1134D22D68679009A81DA /* ScrollViewScrollabilityTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ScrollViewScrollabilityTests.mm; sourceTree = "<group>"; };
0FF1F0C52AE204C0006ECC4F /* scrollable-page.html */ = {isa = PBXFileReference; lastKnownFileType = text.html; name = "scrollable-page.html"; path = "ios/scrollable-page.html"; sourceTree = SOURCE_ROOT; };
0FF3328B2A0EFCCF00C048D8 /* HysteresisActivityTests.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = HysteresisActivityTests.cpp; sourceTree = "<group>"; };
0FFC45A41B73EBE20085BD62 /* Lock.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = Lock.cpp; sourceTree = "<group>"; };
115EB3421EE0B720003C2C0A /* ViewportSizeForViewportUnits.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ViewportSizeForViewportUnits.mm; sourceTree = "<group>"; };
Expand Down Expand Up @@ -4999,6 +5002,7 @@
A11E7D9F24A169E200026745 /* link-with-hover-menu.html */,
0F340777230382540060A1A0 /* overflow-scroll.html */,
A1C4FB721BACD1B7003742D0 /* pages.pages */,
0FF1F0C52AE204C0006ECC4F /* scrollable-page.html */,
CE6E81A320A933B800E2C80F /* set-timeout-function.html */,
);
name = Resources;
Expand Down
32 changes: 32 additions & 0 deletions Tools/TestWebKitAPI/Tests/ios/ScrollViewInsetTests.mm
Expand Up @@ -223,6 +223,38 @@ static void waitUntilInnerHeightIs(TestWKWebView *webView, CGFloat expectedValue
EXPECT_EQ(-400, initialContentOffset.y);
}

TEST(ScrollViewInsetTests, RestoreContentOffsetAfterBackWithInsetChange)
{
auto webView = adoptNS([[TestWKWebView alloc] initWithFrame:CGRectMake(0, 0, 320, viewHeight)]);
[webView scrollView].contentInset = UIEdgeInsetsMake(100, 0, 0, 0);

RetainPtr<NSURL> testURL = [[NSBundle mainBundle] URLForResource:@"scrollable-page" withExtension:@"html" subdirectory:@"TestWebKitAPI.resources"];
[webView loadRequest:[NSURLRequest requestWithURL:testURL.get()]];
[webView _test_waitForDidFinishNavigation];
[webView waitForNextPresentationUpdate];

[webView stringByEvaluatingJavaScript:@"scrollTo(0, 1000)"];
[webView waitForNextPresentationUpdate];

CGPoint contentOffsetAfterScrolling = [webView scrollView].contentOffset;
EXPECT_EQ(0, contentOffsetAfterScrolling.x);
EXPECT_EQ(1000, contentOffsetAfterScrolling.y);

RetainPtr<NSURL> secondPageURL = [[NSBundle mainBundle] URLForResource:@"composited" withExtension:@"html" subdirectory:@"TestWebKitAPI.resources"];
[webView loadRequest:[NSURLRequest requestWithURL:secondPageURL.get()]];
[webView _test_waitForDidFinishNavigation];
[webView waitForNextPresentationUpdate];

constexpr float additionalViewHeight = 20;
[webView goBack];
[webView setFrame:CGRectMake(0, 0, 320, viewHeight + additionalViewHeight)];
[webView _test_waitForDidFinishNavigation];

CGPoint contentOffsetAfterNavigation = [webView scrollView].contentOffset;
EXPECT_EQ(0, contentOffsetAfterNavigation.x);
EXPECT_EQ(1000 - additionalViewHeight / 2, contentOffsetAfterNavigation.y);
}

TEST(ScrollViewInsetTests, ChangeInsetWithoutAutomaticAdjustmentWhileWebProcessIsUnresponsive)
{
constexpr CGFloat initialTopInset = 100;
Expand Down
13 changes: 13 additions & 0 deletions Tools/TestWebKitAPI/ios/scrollable-page.html
@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width">
<style>
body {
height: 5000px;
}
</style>
</head>
<body>
</body>
</html>

0 comments on commit 2bafc1b

Please sign in to comment.