tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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:
Mgfx/layers/apz/src/AsyncPanZoomController.cpp | 15+++++++++++++++
Amobile/android/geckoview/src/androidTest/assets/www/bug1994311.html | 25+++++++++++++++++++++++++
Mmobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/BaseSessionTest.kt | 1+
Mmobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/InteractiveWidgetTest.kt | 85+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
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), + ) + } }