commit d5857d6a53cc6df60b0019a3ca484ae4da069351
parent c48c536f3fe5619d1dbd025aa9acb5b6495720fc
Author: Hiroyuki Ikezoe <hikezoe.birchill@mozilla.com>
Date: Tue, 4 Nov 2025 04:16:06 +0000
Bug 1994311 - Explicitly invoke a content repaint request with eVisualUpdate if the composition size is changed. r=botond,geckoview-reviewers
Differential Revision: https://phabricator.services.mozilla.com/D268819
Diffstat:
4 files changed, 126 insertions(+), 0 deletions(-)
diff --git a/gfx/layers/apz/src/AsyncPanZoomController.cpp b/gfx/layers/apz/src/AsyncPanZoomController.cpp
@@ -5765,6 +5765,21 @@ void AsyncPanZoomController::NotifyLayersUpdated(
aLayerMetrics.GetCompositionSizeWithoutDynamicToolbar());
needToReclampScroll = true;
}
+ if (Metrics().IsRootContent()) {
+ // If the composition size changed, the compositor's layout viewport
+ // offset may have changed (to keep the layout viewport enclosing the
+ // visual viewport) in a way the main thread doesn't know about until it
+ // gets a repaint request. An example scenario where this can occur is if
+ // the software keyboard is hidden. In such cases we need to trigger a
+ // content repaint request with `eVisualUpdate` otherwise any visual
+ // scroll offset changes triggered on the main-thread will never reflect
+ // to APZ.
+ if (Metrics().GetBoundingCompositionSize() !=
+ aLayerMetrics.GetBoundingCompositionSize()) {
+ needContentRepaint = true;
+ contentRepaintType = RepaintUpdateType::eVisualUpdate;
+ }
+ }
Metrics().SetBoundingCompositionSize(
aLayerMetrics.GetBoundingCompositionSize());
Metrics().SetPresShellResolution(aLayerMetrics.GetPresShellResolution());
diff --git a/mobile/android/geckoview/src/androidTest/assets/www/bug1994311.html b/mobile/android/geckoview/src/androidTest/assets/www/bug1994311.html
@@ -0,0 +1,25 @@
+<!doctype html>
+<html>
+ <head>
+ <meta charset="utf-8" />
+ <meta
+ name="viewport"
+ content="width=device-width,initial-scale=1,interactive-widget=resizes-visual"
+ />
+ <style>
+ body {
+ margin: 0px;
+ padding: 0px;
+ }
+ textarea {
+ width: 100%;
+ height: 4vh;
+ position: fixed;
+ bottom: 100px;
+ }
+ </style>
+ </head>
+ <body>
+ <div style="width: 100vw; height: 200vh; background: blue"></div>
+ </body>
+</html>
diff --git a/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/BaseSessionTest.kt b/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/BaseSessionTest.kt
@@ -156,6 +156,7 @@ open class BaseSessionTest(
const val BUG1909181_HTML_PATH = "/assets/www/bug1909181.html"
const val BUG1912358_HTML_PATH = "/assets/www/bug1912358.html"
const val BUG1985669_HTML_PATH = "/assets/www/bug1985669.html"
+ const val BUG1994311_HTML_PATH = "/assets/www/bug1994311.html"
const val POSITION_STICKY_HTML_PATH = "/assets/www/position-sticky.html"
const val POSITION_STICKY_ON_MAIN_THREAD_HTML_PATH = "/assets/www/position-sticky-on-main-thread.html"
const val INTERACTIVE_WIDGET_HTML_PATH = "/assets/www/interactive-widget.html"
diff --git a/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/InteractiveWidgetTest.kt b/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/InteractiveWidgetTest.kt
@@ -16,6 +16,7 @@ import androidx.test.ext.junit.rules.ActivityScenarioRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
import org.hamcrest.Matchers.equalTo
+import org.hamcrest.Matchers.not
import org.junit.After
import org.junit.Before
import org.junit.Rule
@@ -245,4 +246,88 @@ class InteractiveWidgetTest : BaseSessionTest() {
// Close the software keyboard.
imm.hideSoftInputFromWindow(view.getWindowToken(), 0)
}
+
+ @GeckoSessionTestRule.NullDelegate(Autofill.Delegate::class)
+ @Test
+ fun bug1994311() {
+ mainSession.setActive(true)
+ sessionRule.display?.run { setDynamicToolbarMaxHeight(0) }
+
+ mainSession.loadTestPath(BaseSessionTest.BUG1994311_HTML_PATH)
+ mainSession.waitForPageStop()
+ mainSession.promiseAllPaintsDone()
+ mainSession.flushApzRepaints()
+
+ val viewportHeight = mainSession.evaluateJS("window.visualViewport.height") as Double
+
+ // Open the software keyboard.
+ ensureKeyboardOpen()
+
+ mainSession.flushApzRepaints()
+ mainSession.promiseAllPaintsDone()
+
+ // Scroll down visually.
+ mainSession.panZoomController.scrollBy(
+ ScreenLength.zero(),
+ ScreenLength.fromPixels(viewportHeight),
+ PanZoomController.SCROLL_BEHAVIOR_AUTO,
+ )
+
+ mainSession.flushApzRepaints()
+ mainSession.promiseAllPaintsDone()
+
+ var scrollY = mainSession.evaluateJS("window.scrollY") as Double
+
+ // Now the layout scroll offset is different from the visual scroll offset.
+ assertThat(
+ "The layout scroll offset hasn't reached the destination",
+ scrollY,
+ not(equalTo(viewportHeight)),
+ )
+
+ var resizeEventPromise = mainSession.evaluatePromiseJS(
+ """
+ new Promise(resolve => {
+ visualViewport.addEventListener('resize', () => {
+ resolve(true);
+ }, { once: true });
+ });
+ """.trimIndent(),
+ )
+ // Explicitly call `waitForRoundTrip()` to make sure the above event listener
+ // has set up in the content.
+ mainSession.waitForRoundTrip()
+
+ // Dismiss the software keyboard.
+ imm.hideSoftInputFromWindow(view.getWindowToken(), 0)
+
+ assertThat(
+ "The visual viewport height should be changed",
+ resizeEventPromise.value as Boolean,
+ equalTo(true),
+ )
+
+ val currentViewportHeight = mainSession.evaluateJS("window.visualViewport.height") as Double
+ assertThat(
+ "The visual viewport height is restored to the original one",
+ currentViewportHeight,
+ equalTo(viewportHeight),
+ )
+
+ // Dismissing the software keyboard changes the root composition size,
+ // and the new composition size is propagated to APZ and the root
+ // content APZC notifies to the main-thread that there's a pending visual
+ // scroll offset change which needs to be reflected to the main-thread.
+ // Because of this round trip of the information we need to wait for it.
+ mainSession.flushApzRepaints()
+ mainSession.promiseAllPaintsDone()
+
+ scrollY = mainSession.evaluateJS("window.scrollY") as Double
+
+ assertThat(
+ "Now the layout scroll offset is equal to the visual scroll destination",
+ scrollY,
+ equalTo(viewportHeight),
+ )
+ }
}