[iOS] Add a version of viewport shrink-to-fit heuristics that preserves page layout
authorwenson_hsieh@apple.com <wenson_hsieh@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Wed, 1 May 2019 21:08:38 +0000 (21:08 +0000)
committerwenson_hsieh@apple.com <wenson_hsieh@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Wed, 1 May 2019 21:08:38 +0000 (21:08 +0000)
https://bugs.webkit.org/show_bug.cgi?id=197342
<rdar://problem/50063091>

Reviewed by Tim Horton.

Source/WebCore:

Adds support for a new shrink-to-fit heuristic that attempts to lay out the contents of the page at a larger
width in order to shrink content to fit the viewport. See WebKit ChangeLog for more details.

Tests: fast/viewport/ios/shrink-to-fit-content-constant-width.html
       fast/viewport/ios/shrink-to-fit-content-large-width-breakpoint.html
       fast/viewport/ios/shrink-to-fit-content-no-viewport.html
       fast/viewport/ios/shrink-to-fit-content-responsive-viewport-with-horizontal-overflow.html
       fast/viewport/ios/shrink-to-fit-content-temporary-overflow.html

* page/ViewportConfiguration.cpp:
(WebCore::ViewportConfiguration::setMinimumEffectiveDeviceWidth):
(WebCore::ViewportConfiguration::setIsKnownToLayOutWiderThanViewport):
(WebCore::ViewportConfiguration::description const):
* page/ViewportConfiguration.h:
(WebCore::ViewportConfiguration::canIgnoreScalingConstraints const):
(WebCore::ViewportConfiguration::minimumEffectiveDeviceWidth const):

Add several new getters and setters in ViewportConfiguration.

(WebCore::ViewportConfiguration::isKnownToLayOutWiderThanViewport const):
(WebCore::ViewportConfiguration::shouldIgnoreMinimumEffectiveDeviceWidth const):

Importantly, only allow ignoring the minimum effective device width in webpages with responsive viewports, if
they also have *not* laid out wider than the viewport.

(WebCore::ViewportConfiguration::setForceAlwaysUserScalable):

Source/WebKit:

This patch introduces a new shrink-to-fit heuristic that attempts to lay out the contents of the page at a
larger width in order to shrink content to fit the viewport. This is similar to existing shrink-to-fit behaviors
used for viewport sizing in multitasking mode, except that it not only scales the view, but additionally expands
the layout size, such that the overall layout of the page is preserved. In fact, the reason we ended up
reverting the existing flavor of shrink-to-fit in all cases except for multitasking was that page layout was not
preserved, which caused elements that poke out of the viewport to make the rest of the page look out of
proportion — see <rdar://problem/23818102> and related radars.

Covered by 5 new layout tests, and by adjusting a couple of existing layout tests. See comments below for more
details.

* Platform/Logging.h:

Add a new ViewportSizing logging channel. This will only log on pages that overflow the viewport and shrink to
fit as a result.

* Shared/WebPreferences.yaml:

Turn IgnoreViewportScalingConstraints off by default. This preference currently controls whether we allow
shrink-to-fit behaviors, and is only used by Safari when it is in multitasking mode. The value of this
preference is currenly *on* by default, and is turned off almost immediately during every page load after the
first visible content rect update, wherein visibleContentRectUpdateInfo.allowShrinkToFit() is false.

However, this sometimes causes a brief jitter during page load; to fix this, make the default value for
IgnoreViewportScalingConstraints false, and change the logic in WebPage::updateVisibleContentRects to
setCanIgnoreScalingConstraints to true if either the IgnoreViewportScalingConstraints preference (not only
affected by an internal debug switch) is true, or WKWebView SPI is used to enable the behavior.

* WebProcess/WebCoreSupport/WebFrameLoaderClient.cpp:
(WebKit::WebFrameLoaderClient::dispatchDidFinishDocumentLoad):
(WebKit::WebFrameLoaderClient::dispatchDidFinishLoad):

Add a new hook for WebFrameLoaderClient to call into WebPage when document load finishes. Also, tweak
dispatchDidFinishLoad to take a WebFrame& instead of a WebFrame* in a drive-by fix (the frame is assumed to be
non-null anyways).

* WebProcess/WebPage/WebPage.cpp:
(WebKit::WebPage::didCommitLoad):
(WebKit::WebPage::didFinishDocumentLoad):
(WebKit::WebPage::didFinishLoad):

When finishing document load or finishing the overall load, kick off the shrink-to-fit timer; when committing a
load, cancel the timer.

* WebProcess/WebPage/WebPage.h:
* WebProcess/WebPage/ios/WebPageIOS.mm:
(WebKit::WebPage::setViewportConfigurationViewLayoutSize):

Don't allow the minimum effective device width from the client to stomp over any minimum effective device width
set as a result of the new shrink-to-fit heuristic; on some pages that load quickly, this can result in a race
where the minimum effective device width (i.e. a value that lower-bounds the minimum layout width) is first set
by the shrink-to-fit heuristic, and then set to an incorrect value by the client.

In the near future, web view SPI used to set the minimum effective device width should actually be removed
altogether, since the new shrink-to-fit heuristic supersedes any need for the client to fiddle with the minimum
effective device width.

(WebKit::WebPage::dynamicViewportSizeUpdate):

When performing a dynamic viewport size update, additionally re-run the shrink-to-fit heuristic. This allows
the minimum layout size of the viewport to be updated, if necessary. An example of where this matters is when a
web page is *below* a tablet/desktop layout breakpoint in portrait device orientation, but then exceeds this
layout breakpoint in landscape orientation. In this scenario, rotating the device should swap between these two
page layouts.

(WebKit::WebPage::resetViewportDefaultConfiguration):
(WebKit::WebPage::scheduleShrinkToFitContent):
(WebKit::WebPage::shrinkToFitContentTimerFired):
(WebKit::WebPage::immediatelyShrinkToFitContent):

Leverage the existing capability for a viewport to have a "minimum effective device width" to grant the viewport
a larger layout size than it would normally have, and then scale down to fit within the bounds of the view. One
challenge with this overall approach is that laying out at a larger width may cause the page to lay out even
wider in response, which may actually worsen horizontal scrolling. To mitigate this, we only attempt to lay out
at the current content width once; if laying out at this width reduced the amount of horizontal scrolling by any
amount, then proceed with this layout width; otherwise, revert to the previous layout width.

(WebKit::WebPage::shouldIgnoreMetaViewport const):

Pull some common logic out into a readonly getter.

(WebKit::WebPage::updateVisibleContentRects):

See the comment below WebPreferences.yaml, above.

LayoutTests:

Introduces new layout tests, and adjusts some existing tests. See comments below.

* fast/viewport/ios/shrink-to-fit-content-constant-width-expected.txt: Added.
* fast/viewport/ios/shrink-to-fit-content-constant-width.html: Added.

Add a new layout test to exercise the scenario where a constant width viewport narrower than the view is used.

* fast/viewport/ios/shrink-to-fit-content-large-width-breakpoint-expected.txt: Added.
* fast/viewport/ios/shrink-to-fit-content-large-width-breakpoint.html: Added.

Add a new layout test to exercise the scenario where a responsive website that lays out larger than the view
width ends up with even more horizontal scrolling when laying out at the initial content width. In this
scenario, we shouldn't try to expand the viewport to try and encompass the content width, since that would only
induce even worse horizontal scrolling.

* fast/viewport/ios/shrink-to-fit-content-no-viewport-expected.txt: Added.
* fast/viewport/ios/shrink-to-fit-content-no-viewport.html: Added.

Add a new layout test for the case where there is no viewport, but content lays out wider than the view.

* fast/viewport/ios/shrink-to-fit-content-responsive-viewport-with-horizontal-overflow-expected.txt: Added.
* fast/viewport/ios/shrink-to-fit-content-responsive-viewport-with-horizontal-overflow.html: Added.

Add a new layout test for the case where the page has opted for a responsive viewport (device-width, initial
scale 1), but has laid out wider than the viewport anyways. In this case, we want to shrink the contents down to
fit inside the view.

* fast/viewport/ios/shrink-to-fit-content-temporary-overflow-expected.txt: Added.
* fast/viewport/ios/shrink-to-fit-content-temporary-overflow.html: Added.

Add a new layout test to exercise the case where, during page load, content width temporarily increases, and
then decreases such that it once again fits within the viewport. In this case, we don't want to expand the
viewport to be as wide as the large temporary width of the page.

* fast/viewport/ios/width-is-device-width-overflowing-body-overflow-hidden-expected.txt:
* fast/viewport/ios/width-is-device-width-overflowing-body-overflow-hidden.html:
* fast/viewport/ios/width-is-device-width-overflowing-expected.txt:
* fast/viewport/ios/width-is-device-width-overflowing.html:

Tweak these 2 existing layout tests to include "shrink-to-fit=no", to prevent the new heuristics from shrinking
the page to fit on device classes that use native viewports by default.

* platform/ipad/fast/viewport/ios/width-is-device-width-overflowing-body-overflow-hidden-expected.txt:
* platform/ipad/fast/viewport/ios/width-is-device-width-overflowing-expected.txt:

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

27 files changed:
LayoutTests/ChangeLog
LayoutTests/fast/viewport/ios/shrink-to-fit-content-constant-width-expected.txt [new file with mode: 0644]
LayoutTests/fast/viewport/ios/shrink-to-fit-content-constant-width.html [new file with mode: 0644]
LayoutTests/fast/viewport/ios/shrink-to-fit-content-large-width-breakpoint-expected.txt [new file with mode: 0644]
LayoutTests/fast/viewport/ios/shrink-to-fit-content-large-width-breakpoint.html [new file with mode: 0644]
LayoutTests/fast/viewport/ios/shrink-to-fit-content-no-viewport-expected.txt [new file with mode: 0644]
LayoutTests/fast/viewport/ios/shrink-to-fit-content-no-viewport.html [new file with mode: 0644]
LayoutTests/fast/viewport/ios/shrink-to-fit-content-responsive-viewport-with-horizontal-overflow-expected.txt [new file with mode: 0644]
LayoutTests/fast/viewport/ios/shrink-to-fit-content-responsive-viewport-with-horizontal-overflow.html [new file with mode: 0644]
LayoutTests/fast/viewport/ios/shrink-to-fit-content-temporary-overflow-expected.txt [new file with mode: 0644]
LayoutTests/fast/viewport/ios/shrink-to-fit-content-temporary-overflow.html [new file with mode: 0644]
LayoutTests/fast/viewport/ios/width-is-device-width-overflowing-body-overflow-hidden-expected.txt
LayoutTests/fast/viewport/ios/width-is-device-width-overflowing-body-overflow-hidden.html
LayoutTests/fast/viewport/ios/width-is-device-width-overflowing-expected.txt
LayoutTests/fast/viewport/ios/width-is-device-width-overflowing.html
LayoutTests/platform/ipad/fast/viewport/ios/width-is-device-width-overflowing-body-overflow-hidden-expected.txt
LayoutTests/platform/ipad/fast/viewport/ios/width-is-device-width-overflowing-expected.txt
Source/WebCore/ChangeLog
Source/WebCore/page/ViewportConfiguration.cpp
Source/WebCore/page/ViewportConfiguration.h
Source/WebKit/ChangeLog
Source/WebKit/Platform/Logging.h
Source/WebKit/Shared/WebPreferences.yaml
Source/WebKit/WebProcess/WebCoreSupport/WebFrameLoaderClient.cpp
Source/WebKit/WebProcess/WebPage/WebPage.cpp
Source/WebKit/WebProcess/WebPage/WebPage.h
Source/WebKit/WebProcess/WebPage/ios/WebPageIOS.mm

index 28bcc7e..b650c26 100644 (file)
@@ -1,3 +1,56 @@
+2019-05-01  Wenson Hsieh  <wenson_hsieh@apple.com>
+
+        [iOS] Add a version of viewport shrink-to-fit heuristics that preserves page layout
+        https://bugs.webkit.org/show_bug.cgi?id=197342
+        <rdar://problem/50063091>
+
+        Reviewed by Tim Horton.
+
+        Introduces new layout tests, and adjusts some existing tests. See comments below.
+
+        * fast/viewport/ios/shrink-to-fit-content-constant-width-expected.txt: Added.
+        * fast/viewport/ios/shrink-to-fit-content-constant-width.html: Added.
+
+        Add a new layout test to exercise the scenario where a constant width viewport narrower than the view is used.
+
+        * fast/viewport/ios/shrink-to-fit-content-large-width-breakpoint-expected.txt: Added.
+        * fast/viewport/ios/shrink-to-fit-content-large-width-breakpoint.html: Added.
+
+        Add a new layout test to exercise the scenario where a responsive website that lays out larger than the view
+        width ends up with even more horizontal scrolling when laying out at the initial content width. In this
+        scenario, we shouldn't try to expand the viewport to try and encompass the content width, since that would only
+        induce even worse horizontal scrolling.
+
+        * fast/viewport/ios/shrink-to-fit-content-no-viewport-expected.txt: Added.
+        * fast/viewport/ios/shrink-to-fit-content-no-viewport.html: Added.
+
+        Add a new layout test for the case where there is no viewport, but content lays out wider than the view.
+
+        * fast/viewport/ios/shrink-to-fit-content-responsive-viewport-with-horizontal-overflow-expected.txt: Added.
+        * fast/viewport/ios/shrink-to-fit-content-responsive-viewport-with-horizontal-overflow.html: Added.
+
+        Add a new layout test for the case where the page has opted for a responsive viewport (device-width, initial
+        scale 1), but has laid out wider than the viewport anyways. In this case, we want to shrink the contents down to
+        fit inside the view.
+
+        * fast/viewport/ios/shrink-to-fit-content-temporary-overflow-expected.txt: Added.
+        * fast/viewport/ios/shrink-to-fit-content-temporary-overflow.html: Added.
+
+        Add a new layout test to exercise the case where, during page load, content width temporarily increases, and
+        then decreases such that it once again fits within the viewport. In this case, we don't want to expand the
+        viewport to be as wide as the large temporary width of the page.
+
+        * fast/viewport/ios/width-is-device-width-overflowing-body-overflow-hidden-expected.txt:
+        * fast/viewport/ios/width-is-device-width-overflowing-body-overflow-hidden.html:
+        * fast/viewport/ios/width-is-device-width-overflowing-expected.txt:
+        * fast/viewport/ios/width-is-device-width-overflowing.html:
+
+        Tweak these 2 existing layout tests to include "shrink-to-fit=no", to prevent the new heuristics from shrinking
+        the page to fit on device classes that use native viewports by default.
+
+        * platform/ipad/fast/viewport/ios/width-is-device-width-overflowing-body-overflow-hidden-expected.txt:
+        * platform/ipad/fast/viewport/ios/width-is-device-width-overflowing-expected.txt:
+
 2019-05-01  Zalan Bujtas  <zalan@apple.com>
 
         [iOS] Star rating is covered with a black circle when writing a review on Yelp
diff --git a/LayoutTests/fast/viewport/ios/shrink-to-fit-content-constant-width-expected.txt b/LayoutTests/fast/viewport/ios/shrink-to-fit-content-constant-width-expected.txt
new file mode 100644 (file)
index 0000000..4c18735
--- /dev/null
@@ -0,0 +1,11 @@
+This test verifies that a page with a constant width viewport smaller than the actual view width is scaled to fit the view. To test manually, load the page and verify that the bar spans the full width of the page.
+
+On success, you will see a series of "PASS" messages, followed by "TEST COMPLETE".
+
+
+PASS minScale is expectedScale
+PASS innerWidth is 300
+PASS successfullyParsed is true
+
+TEST COMPLETE
+
diff --git a/LayoutTests/fast/viewport/ios/shrink-to-fit-content-constant-width.html b/LayoutTests/fast/viewport/ios/shrink-to-fit-content-constant-width.html
new file mode 100644 (file)
index 0000000..1785e64
--- /dev/null
@@ -0,0 +1,45 @@
+<!DOCTYPE html> <!-- webkit-test-runner [ shouldIgnoreMetaViewport=true ] -->
+<html>
+<head>
+<meta name="viewport" content="width=300">
+<style>
+body, html {
+    margin: 0;
+}
+
+#bar {
+    width: 100%;
+    height: 100px;
+    background: linear-gradient(to right, red 0%, green 50%, blue 100%);
+}
+
+#description {
+    width: 300px;
+    overflow: scroll;
+}
+</style>
+<script src="../../../resources/ui-helper.js"></script>
+<script src="../../../resources/js-test.js"></script>
+<script>
+jsTestIsAsync = true;
+
+description("This test verifies that a page with a constant width viewport smaller than the actual view width is scaled to fit the view. To test manually, load the page and verify that the bar spans the full width of the page.");
+
+addEventListener("load", async () => {
+    if (!window.testRunner)
+        return;
+
+    await UIHelper.ensurePresentationUpdate();
+    minScale = (await UIHelper.minimumZoomScale()).toFixed(2);
+    expectedScale = (screen.width / 300).toFixed(2);
+    shouldBe("minScale", "expectedScale");
+    shouldBe("innerWidth", "300");
+    finishJSTest();
+});
+</script>
+</head>
+<body>
+<div id="bar"></div>
+<div id="description"></div>
+</body>
+</html>
diff --git a/LayoutTests/fast/viewport/ios/shrink-to-fit-content-large-width-breakpoint-expected.txt b/LayoutTests/fast/viewport/ios/shrink-to-fit-content-large-width-breakpoint-expected.txt
new file mode 100644 (file)
index 0000000..274bdf0
--- /dev/null
@@ -0,0 +1,11 @@
+This test verifies that the shrink-to-fit-content heuristic doesn't induce more horizontal scrolling than we would otherwise have. To run the test manually, load the page and check that the bar almost entirely fits within the viewport, with no more than 480px of horizontal scrolling (on a 320px-wide device) and 20px of scrolling (on a 768px-wide device).
+
+On success, you will see a series of "PASS" messages, followed by "TEST COMPLETE".
+
+
+PASS minScale is 1
+PASS 800 is >= document.scrollingElement.scrollWidth
+PASS successfullyParsed is true
+
+TEST COMPLETE
+
diff --git a/LayoutTests/fast/viewport/ios/shrink-to-fit-content-large-width-breakpoint.html b/LayoutTests/fast/viewport/ios/shrink-to-fit-content-large-width-breakpoint.html
new file mode 100644 (file)
index 0000000..cd3fbc1
--- /dev/null
@@ -0,0 +1,52 @@
+<!DOCTYPE html> <!-- webkit-test-runner [ shouldIgnoreMetaViewport=true ] -->
+<html>
+<head>
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<style>
+body, html {
+    margin: 0;
+    width: 100%;
+    height: 100%;
+}
+
+@media screen and (min-width: 780px) {
+    #bar {
+        min-width: 10000px;
+    }
+}
+
+.bar {
+    width: 800px;
+    height: 100px;
+    background: linear-gradient(to right, red 0%, green 50%, blue 100%);
+}
+
+#description {
+    width: 300px;
+    overflow: scroll;
+}
+</style>
+<script src="../../../resources/ui-helper.js"></script>
+<script src="../../../resources/js-test.js"></script>
+<script>
+jsTestIsAsync = true;
+
+description("This test verifies that the shrink-to-fit-content heuristic doesn't induce more horizontal scrolling than we would otherwise have. To run the test manually, load the page and check that the bar almost entirely fits within the viewport, with no more than 480px of horizontal scrolling (on a 320px-wide device) and 20px of scrolling (on a 768px-wide device).");
+
+addEventListener("load", async () => {
+    if (!window.testRunner)
+        return;
+
+    await UIHelper.ensurePresentationUpdate();
+    minScale = await UIHelper.minimumZoomScale();
+    shouldBe("minScale", "1");
+    shouldBeGreaterThanOrEqual("800", "document.scrollingElement.scrollWidth");
+    finishJSTest();
+});
+</script>
+</head>
+<body>
+<div id="description"></div>
+<div id="bar" class="bar"></div>
+</body>
+</html>
diff --git a/LayoutTests/fast/viewport/ios/shrink-to-fit-content-no-viewport-expected.txt b/LayoutTests/fast/viewport/ios/shrink-to-fit-content-no-viewport-expected.txt
new file mode 100644 (file)
index 0000000..04d20dd
--- /dev/null
@@ -0,0 +1,11 @@
+This test verifies that a page with a no viewport but with content larger than the actual view width is scaled to fit the view. To test manually, load the page and verify that the bar spans the full width of the page.
+
+On success, you will see a series of "PASS" messages, followed by "TEST COMPLETE".
+
+
+PASS minScale is expectedScale
+PASS innerWidth became 1000
+PASS successfullyParsed is true
+
+TEST COMPLETE
+
diff --git a/LayoutTests/fast/viewport/ios/shrink-to-fit-content-no-viewport.html b/LayoutTests/fast/viewport/ios/shrink-to-fit-content-no-viewport.html
new file mode 100644 (file)
index 0000000..92898b2
--- /dev/null
@@ -0,0 +1,46 @@
+<!DOCTYPE html> <!-- webkit-test-runner [ shouldIgnoreMetaViewport=true ] -->
+<html>
+<head>
+<meta name="viewport">
+<style>
+body, html {
+    margin: 0;
+    width: 100%;
+    height: 100%;
+}
+
+#bar {
+    width: 1000px;
+    height: 100px;
+    background: linear-gradient(to right, red 0%, green 50%, blue 100%);
+}
+
+#description {
+    width: 300px;
+    overflow: scroll;
+}
+</style>
+<script src="../../../resources/ui-helper.js"></script>
+<script src="../../../resources/js-test.js"></script>
+<script>
+jsTestIsAsync = true;
+
+description("This test verifies that a page with a no viewport but with content larger than the actual view width is scaled to fit the view. To test manually, load the page and verify that the bar spans the full width of the page.");
+
+addEventListener("load", async () => {
+    if (!window.testRunner)
+        return;
+
+    await UIHelper.ensurePresentationUpdate();
+    minScale = (await UIHelper.minimumZoomScale()).toFixed(2);
+    expectedScale = (screen.width / 1000).toFixed(2);
+    shouldBe("minScale", "expectedScale");
+    shouldBecomeEqual("innerWidth", "1000", finishJSTest);
+});
+</script>
+</head>
+<body>
+<div id="bar"></div>
+<div id="description"></div>
+</body>
+</html>
diff --git a/LayoutTests/fast/viewport/ios/shrink-to-fit-content-responsive-viewport-with-horizontal-overflow-expected.txt b/LayoutTests/fast/viewport/ios/shrink-to-fit-content-responsive-viewport-with-horizontal-overflow-expected.txt
new file mode 100644 (file)
index 0000000..e721437
--- /dev/null
@@ -0,0 +1,11 @@
+This test verifies that the shrink-to-fit-content heuristic prevents horizontal scrolling by shrinking the page, even when a page specifies a responsive viewport. To run the test manually, load the page and check that the bar entirely fits within the viewport, and the page is not horizontally scrollable.
+
+On success, you will see a series of "PASS" messages, followed by "TEST COMPLETE".
+
+
+PASS minScale is expectedScale
+PASS innerWidth became 960
+PASS successfullyParsed is true
+
+TEST COMPLETE
+
diff --git a/LayoutTests/fast/viewport/ios/shrink-to-fit-content-responsive-viewport-with-horizontal-overflow.html b/LayoutTests/fast/viewport/ios/shrink-to-fit-content-responsive-viewport-with-horizontal-overflow.html
new file mode 100644 (file)
index 0000000..8579a2a
--- /dev/null
@@ -0,0 +1,46 @@
+<!DOCTYPE html> <!-- webkit-test-runner [ shouldIgnoreMetaViewport=true ] -->
+<html>
+<head>
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<style>
+body, html {
+    margin: 0;
+    width: 100%;
+    height: 100%;
+}
+
+.bar {
+    width: 960px;
+    height: 100px;
+    background: linear-gradient(to right, red 0%, green 50%, blue 100%);
+}
+
+#description {
+    width: 300px;
+    overflow: scroll;
+}
+</style>
+<script src="../../../resources/ui-helper.js"></script>
+<script src="../../../resources/js-test.js"></script>
+<script>
+jsTestIsAsync = true;
+
+description("This test verifies that the shrink-to-fit-content heuristic prevents horizontal scrolling by shrinking the page, even when a page specifies a responsive viewport. To run the test manually, load the page and check that the bar entirely fits within the viewport, and the page is not horizontally scrollable.");
+
+addEventListener("load", async () => {
+    if (!window.testRunner)
+        return;
+
+    await UIHelper.ensurePresentationUpdate();
+    minScale = (await UIHelper.minimumZoomScale()).toFixed(2);
+    expectedScale = (screen.width / 960).toFixed(2);
+    shouldBe("minScale", "expectedScale");
+    shouldBecomeEqual("innerWidth", "960", finishJSTest);
+});
+</script>
+</head>
+<body>
+<div id="description"></div>
+<div class="bar"></div>
+</body>
+</html>
diff --git a/LayoutTests/fast/viewport/ios/shrink-to-fit-content-temporary-overflow-expected.txt b/LayoutTests/fast/viewport/ios/shrink-to-fit-content-temporary-overflow-expected.txt
new file mode 100644 (file)
index 0000000..10cdea3
--- /dev/null
@@ -0,0 +1,10 @@
+This test verifies that a temporary change in content width does not cause the viewport width to permanently expand to try and accomodate the content. To test manually, load the page and check that the box below reads 'PASS'. This test is only intended to run on devices with less than 1200px screen width.
+
+On success, you will see a series of "PASS" messages, followed by "TEST COMPLETE".
+
+
+PASS minScale is 1
+PASS successfullyParsed is true
+
+TEST COMPLETE
+
diff --git a/LayoutTests/fast/viewport/ios/shrink-to-fit-content-temporary-overflow.html b/LayoutTests/fast/viewport/ios/shrink-to-fit-content-temporary-overflow.html
new file mode 100644 (file)
index 0000000..c86ee1d
--- /dev/null
@@ -0,0 +1,75 @@
+<!DOCTYPE html> <!-- webkit-test-runner [ shouldIgnoreMetaViewport=true ] -->
+<html>
+<head>
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<style>
+body, html {
+    margin: 0;
+    width: 100%;
+    height: 100%;
+}
+
+.square {
+    background-color: green;
+    width: 100px;
+    height: 100px;
+}
+
+.square::before {
+    color: white;
+    content: "PASS";
+}
+
+@media screen and (min-width: 1200px) {
+    .square::before {
+        content: "FAIL";
+    }
+
+    .square {
+        background-color: red;
+    }
+}
+
+.bar {
+    width: 1500px;
+    height: 100px;
+    background: linear-gradient(to right, red 0%, green 50%, blue 100%);
+}
+
+#description {
+    width: 300px;
+    overflow: scroll;
+}
+</style>
+<script src="../../../resources/ui-helper.js"></script>
+<script src="../../../resources/js-test.js"></script>
+<script>
+jsTestIsAsync = true;
+
+description("This test verifies that a temporary change in content width does not cause the viewport width to permanently expand to try and accomodate the content. To test manually, load the page and check that the box below reads 'PASS'. This test is only intended to run on devices with less than 1200px screen width.");
+
+addEventListener("load", async () => {
+    if (!window.testRunner)
+        return;
+
+    await UIHelper.ensurePresentationUpdate();
+    minScale = await UIHelper.minimumZoomScale();
+    shouldBe("minScale", "1");
+    finishJSTest();
+});
+</script>
+</head>
+<body>
+<div id="description"></div>
+<div class="square"></div>
+<script>
+const bar = document.createElement("div");
+bar.classList.add("bar");
+document.body.appendChild(bar);
+document.scrollingElement.scrollTo(0, 1);
+document.scrollingElement.scrollTo(0, 0);
+document.scrollingElement.scrollTop;
+bar.remove();
+</script>
+</body>
+</html>
index 831c9a6..b02d52b 100644 (file)
@@ -1,8 +1,8 @@
-<!DOCTYPE html>
+<!DOCTYPE html> <!-- webkit-test-runner [ useFlexibleViewport=true ] -->
 
 <html>
 <head>
-    <meta name="viewport" content="width=device-width">
+    <meta name="viewport" content="width=device-width, shrink-to-fit=no">
     <script src="resources/viewport-test-utils.js"></script>
     <style>
         body {
index d68648b..f6d9715 100644 (file)
@@ -1,8 +1,8 @@
-<!DOCTYPE html>
+<!DOCTYPE html> <!-- webkit-test-runner [ useFlexibleViewport=true ] -->
 
 <html>
 <head>
-    <meta name="viewport" content="width=device-width">
+    <meta name="viewport" content="width=device-width, shrink-to-fit=no">
     <script src="resources/viewport-test-utils.js"></script>
     <style>
         .wide {
index 1eddcd7..a202328 100644 (file)
@@ -1,3 +1,38 @@
+2019-05-01  Wenson Hsieh  <wenson_hsieh@apple.com>
+
+        [iOS] Add a version of viewport shrink-to-fit heuristics that preserves page layout
+        https://bugs.webkit.org/show_bug.cgi?id=197342
+        <rdar://problem/50063091>
+
+        Reviewed by Tim Horton.
+
+        Adds support for a new shrink-to-fit heuristic that attempts to lay out the contents of the page at a larger
+        width in order to shrink content to fit the viewport. See WebKit ChangeLog for more details.
+
+        Tests: fast/viewport/ios/shrink-to-fit-content-constant-width.html
+               fast/viewport/ios/shrink-to-fit-content-large-width-breakpoint.html
+               fast/viewport/ios/shrink-to-fit-content-no-viewport.html
+               fast/viewport/ios/shrink-to-fit-content-responsive-viewport-with-horizontal-overflow.html
+               fast/viewport/ios/shrink-to-fit-content-temporary-overflow.html
+
+        * page/ViewportConfiguration.cpp:
+        (WebCore::ViewportConfiguration::setMinimumEffectiveDeviceWidth):
+        (WebCore::ViewportConfiguration::setIsKnownToLayOutWiderThanViewport):
+        (WebCore::ViewportConfiguration::description const):
+        * page/ViewportConfiguration.h:
+        (WebCore::ViewportConfiguration::canIgnoreScalingConstraints const):
+        (WebCore::ViewportConfiguration::minimumEffectiveDeviceWidth const):
+
+        Add several new getters and setters in ViewportConfiguration.
+
+        (WebCore::ViewportConfiguration::isKnownToLayOutWiderThanViewport const):
+        (WebCore::ViewportConfiguration::shouldIgnoreMinimumEffectiveDeviceWidth const):
+
+        Importantly, only allow ignoring the minimum effective device width in webpages with responsive viewports, if
+        they also have *not* laid out wider than the viewport.
+
+        (WebCore::ViewportConfiguration::setForceAlwaysUserScalable):
+
 2019-05-01  Zalan Bujtas  <zalan@apple.com>
 
         [iOS] Star rating is covered with a black circle when writing a review on Yelp
index 23e3609..031e034 100644 (file)
@@ -586,6 +586,32 @@ int ViewportConfiguration::layoutHeight() const
     return minimumLayoutSize.height();
 }
 
+bool ViewportConfiguration::setMinimumEffectiveDeviceWidth(double width)
+{
+    if (WTF::areEssentiallyEqual(m_minimumEffectiveDeviceWidth, width))
+        return false;
+
+    m_minimumEffectiveDeviceWidth = width;
+
+    if (shouldIgnoreMinimumEffectiveDeviceWidth())
+        return false;
+
+    updateMinimumLayoutSize();
+    updateConfiguration();
+    return true;
+}
+
+bool ViewportConfiguration::setIsKnownToLayOutWiderThanViewport(bool value)
+{
+    if (m_isKnownToLayOutWiderThanViewport == value)
+        return false;
+
+    m_isKnownToLayOutWiderThanViewport = value;
+    updateMinimumLayoutSize();
+    updateConfiguration();
+    return true;
+}
+
 #ifndef NDEBUG
 
 TextStream& operator<<(TextStream& ts, const ViewportConfiguration::Parameters& parameters)
@@ -649,6 +675,7 @@ String ViewportConfiguration::description() const
     ts.dumpProperty("ignoring vertical scaling constraints", shouldIgnoreVerticalScalingConstraints() ? "true" : "false");
     ts.dumpProperty("avoids unsafe area", avoidsUnsafeArea() ? "true" : "false");
     ts.dumpProperty("minimum effective device width", m_minimumEffectiveDeviceWidth);
+    ts.dumpProperty("known to lay out wider than viewport", m_isKnownToLayOutWiderThanViewport ? "true" : "false");
     
     ts.endGroup();
 
index 66283a9..8d71a65 100644 (file)
@@ -86,11 +86,38 @@ public:
     WEBCORE_EXPORT bool setViewportArguments(const ViewportArguments&);
 
     WEBCORE_EXPORT bool setCanIgnoreScalingConstraints(bool);
-    void setForceAlwaysUserScalable(bool forceAlwaysUserScalable) { m_forceAlwaysUserScalable = forceAlwaysUserScalable; }
+    constexpr bool canIgnoreScalingConstraints() const { return m_canIgnoreScalingConstraints; }
+
+    WEBCORE_EXPORT bool setMinimumEffectiveDeviceWidth(double);
+    constexpr double minimumEffectiveDeviceWidth() const
+    {
+        if (shouldIgnoreMinimumEffectiveDeviceWidth())
+            return 0;
+        return m_minimumEffectiveDeviceWidth;
+    }
+
+    constexpr bool isKnownToLayOutWiderThanViewport() const { return m_isKnownToLayOutWiderThanViewport; }
+    WEBCORE_EXPORT bool setIsKnownToLayOutWiderThanViewport(bool value);
+
+    constexpr bool shouldIgnoreMinimumEffectiveDeviceWidth() const
+    {
+        if (m_canIgnoreScalingConstraints)
+            return true;
+
+        if (m_viewportArguments == ViewportArguments())
+            return false;
+
+        if ((m_viewportArguments.zoom == 1. || m_viewportArguments.width == ViewportArguments::ValueDeviceWidth) && !m_isKnownToLayOutWiderThanViewport)
+            return true;
+
+        return false;
+    }
 
+    void setForceAlwaysUserScalable(bool forceAlwaysUserScalable) { m_forceAlwaysUserScalable = forceAlwaysUserScalable; }
     double layoutSizeScaleFactor() const { return m_layoutSizeScaleFactor; }
 
     WEBCORE_EXPORT IntSize layoutSize() const;
+    WEBCORE_EXPORT int layoutWidth() const;
     WEBCORE_EXPORT double initialScale() const;
     WEBCORE_EXPORT double initialScaleIgnoringContentSize() const;
     WEBCORE_EXPORT double minimumScale() const;
@@ -98,7 +125,6 @@ public:
     double maximumScaleIgnoringAlwaysScalable() const { return m_configuration.maximumScale; }
     WEBCORE_EXPORT bool allowsUserScaling() const;
     WEBCORE_EXPORT bool allowsUserScalingIgnoringAlwaysScalable() const;
-    bool allowsShrinkToFit() const;
     bool avoidsUnsafeArea() const { return m_configuration.avoidsUnsafeArea; }
 
     // Matches a width=device-width, initial-scale=1 viewport.
@@ -120,7 +146,6 @@ private:
     void updateConfiguration();
     double viewportArgumentsLength(double length) const;
     double initialScaleFromSize(double width, double height, bool shouldIgnoreScalingConstraints) const;
-    int layoutWidth() const;
     int layoutHeight() const;
 
     bool shouldOverrideDeviceWidthAndShrinkToFit() const;
@@ -131,27 +156,6 @@ private:
     void updateDefaultConfiguration();
     bool canOverrideConfigurationParameters() const;
 
-    constexpr bool shouldIgnoreMinimumEffectiveDeviceWidth() const
-    {
-        if (m_canIgnoreScalingConstraints)
-            return true;
-
-        if (m_viewportArguments == ViewportArguments())
-            return false;
-
-        if (m_viewportArguments.width == ViewportArguments::ValueDeviceWidth || m_viewportArguments.zoom == 1.)
-            return true;
-
-        return false;
-    }
-
-    constexpr double minimumEffectiveDeviceWidth() const
-    {
-        if (shouldIgnoreMinimumEffectiveDeviceWidth())
-            return 0;
-        return m_minimumEffectiveDeviceWidth;
-    }
-
     constexpr double forceAlwaysUserScalableMaximumScale() const
     {
         const double forceAlwaysUserScalableMaximumScaleIgnoringLayoutScaleFactor = 5;
@@ -185,6 +189,7 @@ private:
     double m_minimumEffectiveDeviceWidth { 0 };
     bool m_canIgnoreScalingConstraints;
     bool m_forceAlwaysUserScalable;
+    bool m_isKnownToLayOutWiderThanViewport { false };
 };
 
 WTF::TextStream& operator<<(WTF::TextStream&, const ViewportConfiguration::Parameters&);
index d9934a1..9562720 100644 (file)
@@ -1,3 +1,96 @@
+2019-05-01  Wenson Hsieh  <wenson_hsieh@apple.com>
+
+        [iOS] Add a version of viewport shrink-to-fit heuristics that preserves page layout
+        https://bugs.webkit.org/show_bug.cgi?id=197342
+        <rdar://problem/50063091>
+
+        Reviewed by Tim Horton.
+
+        This patch introduces a new shrink-to-fit heuristic that attempts to lay out the contents of the page at a
+        larger width in order to shrink content to fit the viewport. This is similar to existing shrink-to-fit behaviors
+        used for viewport sizing in multitasking mode, except that it not only scales the view, but additionally expands
+        the layout size, such that the overall layout of the page is preserved. In fact, the reason we ended up
+        reverting the existing flavor of shrink-to-fit in all cases except for multitasking was that page layout was not
+        preserved, which caused elements that poke out of the viewport to make the rest of the page look out of
+        proportion — see <rdar://problem/23818102> and related radars.
+
+        Covered by 5 new layout tests, and by adjusting a couple of existing layout tests. See comments below for more
+        details.
+
+        * Platform/Logging.h:
+
+        Add a new ViewportSizing logging channel. This will only log on pages that overflow the viewport and shrink to
+        fit as a result.
+
+        * Shared/WebPreferences.yaml:
+
+        Turn IgnoreViewportScalingConstraints off by default. This preference currently controls whether we allow
+        shrink-to-fit behaviors, and is only used by Safari when it is in multitasking mode. The value of this
+        preference is currenly *on* by default, and is turned off almost immediately during every page load after the
+        first visible content rect update, wherein visibleContentRectUpdateInfo.allowShrinkToFit() is false.
+
+        However, this sometimes causes a brief jitter during page load; to fix this, make the default value for
+        IgnoreViewportScalingConstraints false, and change the logic in WebPage::updateVisibleContentRects to
+        setCanIgnoreScalingConstraints to true if either the IgnoreViewportScalingConstraints preference (not only
+        affected by an internal debug switch) is true, or WKWebView SPI is used to enable the behavior.
+
+        * WebProcess/WebCoreSupport/WebFrameLoaderClient.cpp:
+        (WebKit::WebFrameLoaderClient::dispatchDidFinishDocumentLoad):
+        (WebKit::WebFrameLoaderClient::dispatchDidFinishLoad):
+
+        Add a new hook for WebFrameLoaderClient to call into WebPage when document load finishes. Also, tweak
+        dispatchDidFinishLoad to take a WebFrame& instead of a WebFrame* in a drive-by fix (the frame is assumed to be
+        non-null anyways).
+
+        * WebProcess/WebPage/WebPage.cpp:
+        (WebKit::WebPage::didCommitLoad):
+        (WebKit::WebPage::didFinishDocumentLoad):
+        (WebKit::WebPage::didFinishLoad):
+
+        When finishing document load or finishing the overall load, kick off the shrink-to-fit timer; when committing a
+        load, cancel the timer.
+
+        * WebProcess/WebPage/WebPage.h:
+        * WebProcess/WebPage/ios/WebPageIOS.mm:
+        (WebKit::WebPage::setViewportConfigurationViewLayoutSize):
+
+        Don't allow the minimum effective device width from the client to stomp over any minimum effective device width
+        set as a result of the new shrink-to-fit heuristic; on some pages that load quickly, this can result in a race
+        where the minimum effective device width (i.e. a value that lower-bounds the minimum layout width) is first set
+        by the shrink-to-fit heuristic, and then set to an incorrect value by the client.
+
+        In the near future, web view SPI used to set the minimum effective device width should actually be removed
+        altogether, since the new shrink-to-fit heuristic supersedes any need for the client to fiddle with the minimum
+        effective device width.
+
+        (WebKit::WebPage::dynamicViewportSizeUpdate):
+
+        When performing a dynamic viewport size update, additionally re-run the shrink-to-fit heuristic. This allows
+        the minimum layout size of the viewport to be updated, if necessary. An example of where this matters is when a
+        web page is *below* a tablet/desktop layout breakpoint in portrait device orientation, but then exceeds this
+        layout breakpoint in landscape orientation. In this scenario, rotating the device should swap between these two
+        page layouts.
+
+        (WebKit::WebPage::resetViewportDefaultConfiguration):
+        (WebKit::WebPage::scheduleShrinkToFitContent):
+        (WebKit::WebPage::shrinkToFitContentTimerFired):
+        (WebKit::WebPage::immediatelyShrinkToFitContent):
+
+        Leverage the existing capability for a viewport to have a "minimum effective device width" to grant the viewport
+        a larger layout size than it would normally have, and then scale down to fit within the bounds of the view. One
+        challenge with this overall approach is that laying out at a larger width may cause the page to lay out even
+        wider in response, which may actually worsen horizontal scrolling. To mitigate this, we only attempt to lay out
+        at the current content width once; if laying out at this width reduced the amount of horizontal scrolling by any
+        amount, then proceed with this layout width; otherwise, revert to the previous layout width.
+
+        (WebKit::WebPage::shouldIgnoreMetaViewport const):
+
+        Pull some common logic out into a readonly getter.
+
+        (WebKit::WebPage::updateVisibleContentRects):
+
+        See the comment below WebPreferences.yaml, above.
+
 2019-05-01  Dean Jackson  <dino@apple.com>
 
         Link Previews that use WKImagePreviewViewController are not always scaled correctly
index 9d5a42d..1f773a7 100644 (file)
@@ -85,6 +85,7 @@ extern "C" {
     M(TextInput) \
     M(ViewGestures) \
     M(ViewState) \
+    M(ViewportSizing) \
     M(VirtualMemory) \
     M(VisibleRects) \
     M(WebGL) \
index c4fccfc..cd9326e 100644 (file)
@@ -1089,7 +1089,7 @@ LogsPageMessagesToSystemConsoleEnabled:
 
 IgnoreViewportScalingConstraints:
   type: bool
-  defaultValue: true
+  defaultValue: false
   category: debug
   webcoreBinding: none
   condition: PLATFORM(IOS_FAMILY)
index c49bb1b..529c046 100644 (file)
@@ -609,6 +609,8 @@ void WebFrameLoaderClient::dispatchDidFinishDocumentLoad()
 
     // Notify the UIProcess.
     webPage->send(Messages::WebPageProxy::DidFinishDocumentLoadForFrame(m_frame->frameID(), navigationID, UserData(WebProcess::singleton().transformObjectsToHandles(userData.get()).get())));
+
+    webPage->didFinishDocumentLoad(*m_frame);
 }
 
 void WebFrameLoaderClient::dispatchDidFinishLoad()
@@ -631,7 +633,7 @@ void WebFrameLoaderClient::dispatchDidFinishLoad()
     if (WebFrame::LoadListener* loadListener = m_frame->loadListener())
         loadListener->didFinishLoad(m_frame);
 
-    webPage->didFinishLoad(m_frame);
+    webPage->didFinishLoad(*m_frame);
 }
 
 void WebFrameLoaderClient::forcePageTransitionIfNeeded()
index a3eac9c..0d8a0d3 100644 (file)
@@ -424,6 +424,9 @@ WebPage::WebPage(uint64_t pageID, WebPageCreationParameters&& parameters)
 #if PLATFORM(WPE)
     , m_hostFileDescriptor(WTFMove(parameters.hostFileDescriptor))
 #endif
+#if ENABLE(VIEWPORT_RESIZING)
+    , m_shrinkToFitContentTimer(*this, &WebPage::shrinkToFitContentTimerFired, 0_s)
+#endif
 {
     ASSERT(m_pageID);
 
@@ -5714,6 +5717,10 @@ void WebPage::didCommitLoad(WebFrame* frame)
         viewportConfigurationChanged();
 #endif
 
+#if ENABLE(VIEWPORT_RESIZING)
+    m_shrinkToFitContentTimer.stop();
+#endif
+
 #if ENABLE(PRIMARY_SNAPSHOTTED_PLUGIN_HEURISTIC)
     resetPrimarySnapshottedPlugIn();
 #endif
@@ -5727,12 +5734,22 @@ void WebPage::didCommitLoad(WebFrame* frame)
     updateMainFrameScrollOffsetPinning();
 }
 
-void WebPage::didFinishLoad(WebFrame* frame)
+void WebPage::didFinishDocumentLoad(WebFrame& frame)
 {
-    if (!frame->isMainFrame())
+    if (!frame.isMainFrame())
+        return;
+
+#if ENABLE(VIEWPORT_RESIZING)
+    scheduleShrinkToFitContent();
+#endif
+}
+
+void WebPage::didFinishLoad(WebFrame& frame)
+{
+    if (!frame.isMainFrame())
         return;
 
-    WebProcess::singleton().sendPrewarmInformation(frame->url());
+    WebProcess::singleton().sendPrewarmInformation(frame.url());
 
 #if ENABLE(PRIMARY_SNAPSHOTTED_PLUGIN_HEURISTIC)
     m_readyToFindPrimarySnapshottedPlugin = true;
@@ -5741,6 +5758,10 @@ void WebPage::didFinishLoad(WebFrame* frame)
 #else
     UNUSED_PARAM(frame);
 #endif
+
+#if ENABLE(VIEWPORT_RESIZING)
+    scheduleShrinkToFitContent();
+#endif
 }
 
 void WebPage::didInsertMenuElement(HTMLMenuElement& element)
index 0605bde..8fbee41 100644 (file)
@@ -131,6 +131,8 @@ OBJC_CLASS NSObject;
 OBJC_CLASS WKAccessibilityWebPageObject;
 #endif
 
+#define ENABLE_VIEWPORT_RESIZING PLATFORM(IOS_FAMILY)
+
 namespace API {
 class Array;
 }
@@ -349,7 +351,8 @@ public:
     void didCommitLoad(WebFrame*);
     void willReplaceMultipartContent(const WebFrame&);
     void didReplaceMultipartContent(const WebFrame&);
-    void didFinishLoad(WebFrame*);
+    void didFinishDocumentLoad(WebFrame&);
+    void didFinishLoad(WebFrame&);
     void show();
     String userAgent(const URL&) const;
     String platformUserAgent(const URL&) const;
@@ -1228,6 +1231,13 @@ private:
     InteractionInformationAtPosition positionInformation(const InteractionInformationRequest&);
     WebAutocorrectionContext autocorrectionContext();
     bool applyAutocorrectionInternal(const String& correction, const String& originalText);
+    bool shouldIgnoreMetaViewport() const;
+#endif
+
+#if ENABLE(VIEWPORT_RESIZING)
+    void scheduleShrinkToFitContent();
+    void shrinkToFitContentTimerFired();
+    bool immediatelyShrinkToFitContent();
 #endif
 
 #if PLATFORM(IOS_FAMILY) && ENABLE(DATA_INTERACTION)
@@ -1894,6 +1904,9 @@ private:
     WeakPtr<RemoteObjectRegistry> m_remoteObjectRegistry;
 #endif
     WebCore::IntSize m_lastSentIntrinsicContentSize;
+#if ENABLE(VIEWPORT_RESIZING)
+    WebCore::DeferrableOneShotTimer m_shrinkToFitContentTimer;
+#endif
 };
 
 } // namespace WebKit
index 6c55244..83f3126 100644 (file)
@@ -2791,7 +2791,8 @@ void WebPage::setViewportConfigurationViewLayoutSize(const FloatSize& size, doub
     LOG_WITH_STREAM(VisibleRects, stream << "WebPage " << m_pageID << " setViewportConfigurationViewLayoutSize " << size << " scaleFactor " << scaleFactor << " minimumEffectiveDeviceWidth " << minimumEffectiveDeviceWidth);
 
     auto previousLayoutSizeScaleFactor = m_viewportConfiguration.layoutSizeScaleFactor();
-    if (!m_viewportConfiguration.setViewLayoutSize(size, scaleFactor, minimumEffectiveDeviceWidth))
+    auto clampedMinimumEffectiveDevice = m_viewportConfiguration.isKnownToLayOutWiderThanViewport() ? WTF::nullopt : Optional<double>(minimumEffectiveDeviceWidth);
+    if (!m_viewportConfiguration.setViewLayoutSize(size, scaleFactor, WTFMove(clampedMinimumEffectiveDevice)))
         return;
 
     auto zoomToInitialScale = ZoomToInitialScale::No;
@@ -2873,8 +2874,16 @@ void WebPage::dynamicViewportSizeUpdate(const FloatSize& viewLayoutSize, const W
     }
 
     LOG_WITH_STREAM(VisibleRects, stream << "WebPage::dynamicViewportSizeUpdate setting view layout size to " << viewLayoutSize);
-    if (m_viewportConfiguration.setViewLayoutSize(viewLayoutSize))
+    bool viewportChanged = m_viewportConfiguration.setIsKnownToLayOutWiderThanViewport(false);
+    viewportChanged |= m_viewportConfiguration.setViewLayoutSize(viewLayoutSize);
+    if (viewportChanged)
         viewportConfigurationChanged();
+
+#if ENABLE(VIEWPORT_RESIZING)
+    if (immediatelyShrinkToFitContent())
+        viewportConfigurationChanged();
+#endif
+
     IntSize newLayoutSize = m_viewportConfiguration.layoutSize();
 
     if (setFixedLayoutSize(newLayoutSize))
@@ -3020,16 +3029,9 @@ void WebPage::resetViewportDefaultConfiguration(WebFrame* frame, bool hasMobileD
     }
 
     auto parametersForStandardFrame = [&] {
-        bool shouldIgnoreMetaViewport = false;
-        if (auto* mainDocument = m_page->mainFrame().document()) {
-            auto* loader = mainDocument->loader();
-            shouldIgnoreMetaViewport = loader && loader->metaViewportPolicy() == WebCore::MetaViewportPolicy::Ignore;
-        }
-
-        if (m_page->settings().shouldIgnoreMetaViewport())
-            shouldIgnoreMetaViewport = true;
-
-        return shouldIgnoreMetaViewport ? m_viewportConfiguration.nativeWebpageParameters() : ViewportConfiguration::webpageParameters();
+        if (shouldIgnoreMetaViewport())
+            return m_viewportConfiguration.nativeWebpageParameters();
+        return ViewportConfiguration::webpageParameters();
     };
 
     if (!frame) {
@@ -3049,6 +3051,83 @@ void WebPage::resetViewportDefaultConfiguration(WebFrame* frame, bool hasMobileD
         m_viewportConfiguration.setDefaultConfiguration(ViewportConfiguration::textDocumentParameters());
     else
         m_viewportConfiguration.setDefaultConfiguration(parametersForStandardFrame());
+    m_viewportConfiguration.setIsKnownToLayOutWiderThanViewport(false);
+}
+
+#if ENABLE(VIEWPORT_RESIZING)
+
+void WebPage::scheduleShrinkToFitContent()
+{
+    m_shrinkToFitContentTimer.restart();
+}
+
+void WebPage::shrinkToFitContentTimerFired()
+{
+    if (immediatelyShrinkToFitContent())
+        viewportConfigurationChanged(ZoomToInitialScale::Yes);
+}
+
+bool WebPage::immediatelyShrinkToFitContent()
+{
+    if (!shouldIgnoreMetaViewport())
+        return false;
+
+    if (!m_viewportConfiguration.viewportArguments().shrinkToFit)
+        return false;
+
+    if (m_viewportConfiguration.canIgnoreScalingConstraints())
+        return false;
+
+    auto mainFrame = makeRefPtr(m_mainFrame->coreFrame());
+    if (!mainFrame)
+        return false;
+
+    auto view = makeRefPtr(mainFrame->view());
+    auto mainDocument = makeRefPtr(mainFrame->document());
+    if (!view || !mainDocument)
+        return false;
+
+    mainDocument->updateLayout();
+
+    static const int toleratedHorizontalScrollingDistance = 20;
+    static const int maximumExpandedLayoutWidth = 1280;
+    int originalContentWidth = view->contentsWidth();
+    int originalLayoutWidth = m_viewportConfiguration.layoutWidth();
+    int originalHorizontalOverflowAmount = originalContentWidth - originalLayoutWidth;
+    if (originalHorizontalOverflowAmount <= toleratedHorizontalScrollingDistance || originalLayoutWidth >= maximumExpandedLayoutWidth || originalContentWidth <= m_viewportConfiguration.viewLayoutSize().width())
+        return false;
+
+    auto changeMinimumEffectiveDeviceWidth = [this, mainDocument] (int targetLayoutWidth) -> bool {
+        if (m_viewportConfiguration.setMinimumEffectiveDeviceWidth(targetLayoutWidth)) {
+            viewportConfigurationChanged();
+            mainDocument->updateLayout();
+            return true;
+        }
+        return false;
+    };
+
+    m_viewportConfiguration.setIsKnownToLayOutWiderThanViewport(true);
+    double originalMinimumDeviceWidth = m_viewportConfiguration.minimumEffectiveDeviceWidth();
+    if (changeMinimumEffectiveDeviceWidth(std::min(maximumExpandedLayoutWidth, originalContentWidth)) && view->contentsWidth() - m_viewportConfiguration.layoutWidth() > originalHorizontalOverflowAmount) {
+        changeMinimumEffectiveDeviceWidth(originalMinimumDeviceWidth);
+        m_viewportConfiguration.setIsKnownToLayOutWiderThanViewport(false);
+    }
+
+    // FIXME (197429): Consider additionally logging an error message to the console if a responsive meta viewport tag was used.
+    RELEASE_LOG(ViewportSizing, "Shrink-to-fit: content width %d => %d; layout width %d => %d", originalContentWidth, view->contentsWidth(), originalLayoutWidth, m_viewportConfiguration.layoutWidth());
+    return true;
+}
+
+#endif // ENABLE(VIEWPORT_RESIZING)
+
+bool WebPage::shouldIgnoreMetaViewport() const
+{
+    if (auto* mainDocument = m_page->mainFrame().document()) {
+        auto* loader = mainDocument->loader();
+        if (loader && loader->metaViewportPolicy() == WebCore::MetaViewportPolicy::Ignore)
+            return true;
+    }
+    return m_page->settings().shouldIgnoreMetaViewport();
 }
 
 void WebPage::viewportConfigurationChanged(ZoomToInitialScale zoomToInitialScale)
@@ -3256,7 +3335,7 @@ void WebPage::updateVisibleContentRects(const VisibleContentRectUpdateInfo& visi
     if (scrollPosition != frameView.scrollPosition())
         m_dynamicSizeUpdateHistory.clear();
 
-    if (m_viewportConfiguration.setCanIgnoreScalingConstraints(m_ignoreViewportScalingConstraints && visibleContentRectUpdateInfo.allowShrinkToFit()))
+    if (m_viewportConfiguration.setCanIgnoreScalingConstraints(m_ignoreViewportScalingConstraints || visibleContentRectUpdateInfo.allowShrinkToFit()))
         viewportConfigurationChanged();
 
     frameView.setUnobscuredContentSize(visibleContentRectUpdateInfo.unobscuredContentRect().size());