Bug 1586986 - Fire visual viewport resize events and flush position:fixed elements' layout in the same way what Chrome does. r=botond

On Chrome, visual viewport resize event is fired repeatedly during dynamic
toolbar transitions and visual viewport height obtained by the VisualViewport
API is also changed, but in terms of layout the height value is never used
until the dynamic toolbar height reaches to zero or is changed from zero.
The height used at the time is the height for vh units when the toolbar height
reaches to zero and the ICB height when the toolbar height is changed from zero.
To do so, we need to have another visual viewport size in parallel to the
original one and use them depending on situations.

Differential Revision: https://phabricator.services.mozilla.com/D52338

--HG--
extra : moz-landing-system : lando
This commit is contained in:
Hiroyuki Ikezoe 2019-11-21 21:36:59 +00:00
Родитель 1737c375d1
Коммит 7afdb8487c
17 изменённых файлов: 266 добавлений и 11 удалений

Просмотреть файл

@ -71,7 +71,12 @@ CSSSize VisualViewport::VisualViewportSize() const {
// Fetch the pres shell after the layout flush, as it might have destroyed it.
if (PresShell* presShell = GetPresShell()) {
if (presShell->IsVisualViewportSizeSet()) {
size = CSSRect::FromAppUnits(presShell->GetVisualViewportSize());
DynamicToolbarState state = presShell->GetDynamicToolbarState();
size = CSSRect::FromAppUnits(
(state == DynamicToolbarState::InTransition ||
state == DynamicToolbarState::Collapsed)
? presShell->GetVisualViewportSizeUpdatedByDynamicToolbar()
: presShell->GetVisualViewportSize());
} else {
nsIScrollableFrame* sf = presShell->GetRootScrollFrameAsScrollable();
if (sf) {

Просмотреть файл

@ -39,6 +39,7 @@ class MockMVMContext : public MVMContext {
MOCK_METHOD0(Destroy, void());
MOCK_METHOD1(SetVisualViewportSize, void(const CSSSize& aSize));
MOCK_METHOD0(PostVisualViewportResizeEventByDynamicToolbar, void());
MOCK_METHOD0(UpdateDisplayPortMargins, void());
void SetMVM(MobileViewportManager* aMVM) { mMVM = aMVM; }

Просмотреть файл

@ -150,6 +150,17 @@ void GeckoMVMContext::SetVisualViewportSize(const CSSSize& aSize) {
nsLayoutUtils::SetVisualViewportSize(mPresShell, aSize);
}
void GeckoMVMContext::PostVisualViewportResizeEventByDynamicToolbar() {
MOZ_ASSERT(mDocument);
// We only fire visual viewport events and don't want to cause any explicit
// reflows here since in general we don't use the up-to-date visual viewport
// size for layout.
if (auto* window = nsGlobalWindowInner::Cast(mDocument->GetInnerWindow())) {
window->VisualViewport()->PostResizeEvent();
}
}
void GeckoMVMContext::UpdateDisplayPortMargins() {
MOZ_ASSERT(mPresShell);
if (nsIFrame* root = mPresShell->GetRootScrollFrame()) {

Просмотреть файл

@ -53,6 +53,7 @@ class GeckoMVMContext : public MVMContext {
void SetResolutionAndScaleTo(float aResolution,
ResolutionChangeOrigin aOrigin) override;
void SetVisualViewportSize(const CSSSize& aSize) override;
void PostVisualViewportResizeEventByDynamicToolbar() override;
void UpdateDisplayPortMargins() override;
MOZ_CAN_RUN_SCRIPT_BOUNDARY void Reflow(const CSSSize& aNewSize) override;

Просмотреть файл

@ -57,6 +57,7 @@ class MVMContext {
virtual void SetResolutionAndScaleTo(float aResolution,
ResolutionChangeOrigin aOrigin) = 0;
virtual void SetVisualViewportSize(const CSSSize& aSize) = 0;
virtual void PostVisualViewportResizeEventByDynamicToolbar() = 0;
virtual void UpdateDisplayPortMargins() = 0;
virtual void Reflow(const CSSSize& aNewSize) = 0;

Просмотреть файл

@ -491,6 +491,30 @@ void MobileViewportManager::UpdateVisualViewportSize(
mContext->SetVisualViewportSize(compSize);
}
CSSToScreenScale MobileViewportManager::GetZoom() const {
CSSToLayoutDeviceScale cssToDev = mContext->CSSToDevPixelScale();
LayoutDeviceToLayerScale res(mContext->GetResolution());
return ResolutionToZoom(res, cssToDev);
}
void MobileViewportManager::UpdateVisualViewportSizeByDynamicToolbar(
ScreenIntCoord aToolbarHeight) {
if (!mContext) {
return;
}
ScreenIntSize displaySize = ViewAs<ScreenPixel>(
mDisplaySize, PixelCastJustification::LayoutDeviceIsScreenForBounds);
displaySize.height += aToolbarHeight;
CSSSize compSize = ScreenSize(GetCompositionSize(displaySize)) / GetZoom();
mVisualViewportSizeUpdatedByDynamicToolbar =
nsSize(nsPresContext::CSSPixelsToAppUnits(compSize.width),
nsPresContext::CSSPixelsToAppUnits(compSize.height));
mContext->PostVisualViewportResizeEventByDynamicToolbar();
}
void MobileViewportManager::UpdateDisplayPortMargins() {
if (!mContext) {
return;
@ -509,13 +533,7 @@ void MobileViewportManager::RefreshVisualViewportSize() {
ScreenIntSize displaySize = ViewAs<ScreenPixel>(
mDisplaySize, PixelCastJustification::LayoutDeviceIsScreenForBounds);
CSSToLayoutDeviceScale cssToDev = mContext->CSSToDevPixelScale();
LayoutDeviceToLayerScale res(mContext->GetResolution());
CSSToScreenScale zoom = ViewTargetAs<ScreenPixel>(
cssToDev * res / ParentLayerToLayerScale(1),
PixelCastJustification::ScreenIsParentLayerForRoot);
UpdateVisualViewportSize(displaySize, zoom);
UpdateVisualViewportSize(displaySize, GetZoom());
}
void MobileViewportManager::RefreshViewportSize(bool aForceAdjustResolution) {

Просмотреть файл

@ -87,6 +87,20 @@ class MobileViewportManager final : public nsIDOMEventListener,
void ShrinkToDisplaySizeIfNeeded(nsViewportInfo& aViewportInfo,
const mozilla::ScreenIntSize& aDisplaySize);
/*
* Similar to UpdateVisualViewportSize but this should be called only when we
* need to update visual viewport size in response to dynamic toolbar
* transitions.
* This function doesn't cause any reflows, just fires a visual viewport
* resize event.
*/
void UpdateVisualViewportSizeByDynamicToolbar(
mozilla::ScreenIntCoord aToolbarHeight);
nsSize GetVisualViewportSizeUpdatedByDynamicToolbar() const {
return mVisualViewportSizeUpdatedByDynamicToolbar;
}
private:
~MobileViewportManager();
@ -145,6 +159,8 @@ class MobileViewportManager final : public nsIDOMEventListener,
mozilla::ScreenIntSize GetCompositionSize(
const mozilla::ScreenIntSize& aDisplaySize) const;
mozilla::CSSToScreenScale GetZoom() const;
RefPtr<mozilla::MVMContext> mContext;
bool mIsFirstPaint;
bool mPainted;
@ -152,6 +168,14 @@ class MobileViewportManager final : public nsIDOMEventListener,
mozilla::CSSSize mMobileViewportSize;
mozilla::Maybe<float> mRestoreResolution;
mozilla::Maybe<mozilla::ScreenIntSize> mRestoreDisplaySize;
/*
* The visual viewport size updated by the dynamic toolbar transitions. This
* is typically used for the VisualViewport width/height APIs.
* NOTE: If you want to use this value, you should make sure to flush
* position:fixed elements layout and update
* FrameMetrics.mFixedLayerMargins to conform with this value.
*/
nsSize mVisualViewportSizeUpdatedByDynamicToolbar;
};
#endif

Просмотреть файл

@ -10738,6 +10738,19 @@ nsSize PresShell::GetLayoutViewportSize() const {
return result;
}
nsSize PresShell::GetVisualViewportSizeUpdatedByDynamicToolbar() const {
NS_ASSERTION(mVisualViewportSizeSet,
"asking for visual viewport size when its not set?");
if (!mMobileViewportManager) {
return mVisualViewportSize;
}
MOZ_ASSERT(GetDynamicToolbarState() == DynamicToolbarState::InTransition ||
GetDynamicToolbarState() == DynamicToolbarState::Collapsed);
return mMobileViewportManager->GetVisualViewportSizeUpdatedByDynamicToolbar();
}
void PresShell::RecomputeFontSizeInflationEnabled() {
mFontSizeInflationEnabled = DetermineFontSizeInflationState();

Просмотреть файл

@ -1500,6 +1500,18 @@ class PresShell final : public nsStubDocumentObserver,
nsPoint GetVisualViewportOffsetRelativeToLayoutViewport() const;
// Returns state of the dynamic toolbar.
DynamicToolbarState GetDynamicToolbarState() const {
if (!mPresContext) {
return DynamicToolbarState::None;
}
return mPresContext->GetDynamicToolbarState();
}
// Returns the visual viewport size during the dynamic toolbar is being
// shown/hidden.
nsSize GetVisualViewportSizeUpdatedByDynamicToolbar() const;
/* Enable/disable author style level. Disabling author style disables the
* entire author level of the cascade, including the HTML preshint level.
*/

Просмотреть файл

@ -218,6 +218,15 @@ enum class RenderingStateFlags : uint8_t {
MOZ_MAKE_ENUM_CLASS_BITWISE_OPERATORS(RenderingStateFlags)
// The state of the dynamic toolbar on Mobile.
enum class DynamicToolbarState {
None, // No dynamic toolbar, i.e. the toolbar is static or there is
// no available toolbar.
Expanded, // The dynamic toolbar is expanded to the maximum height.
InTransition, // The dynamic toolbar is being shown/hidden.
Collapsed, // The dynamic toolbar is collapsed to zero height.
};
#ifdef DEBUG
enum class VerifyReflowFlags {

Просмотреть файл

@ -9033,6 +9033,21 @@ ScrollMetadata nsLayoutUtils::ComputeScrollMetadata(
viewport.SizeTo(nsLayoutUtils::ExpandHeightForViewportUnits(
presContext, viewport.Size()));
metrics.SetLayoutViewport(viewport);
// We need to set 'fixed margins' to adjust 'fixed margins' value on the
// composiutor in the case where the dynamic toolbar is completely
// hidden because the margin value on the compositor is offset from the
// position where the dynamic toolbar is completely VISIBLE but now the
// toolbar is completely HIDDEN we need to adjust the difference on the
// compositor.
if (presContext->GetDynamicToolbarState() ==
DynamicToolbarState::Collapsed) {
metrics.SetFixedLayerMargins(
ScreenMargin(0, 0,
presContext->GetDynamicToolbarHeight() -
presContext->GetDynamicToolbarMaxHeight(),
0));
}
}
}

Просмотреть файл

@ -169,6 +169,7 @@ nsPresContext::nsPresContext(dom::Document* aDocument, nsPresContextType aType)
mCurAppUnitsPerDevPixel(0),
mAutoQualityMinFontSizePixelsPref(0),
mDynamicToolbarMaxHeight(0),
mDynamicToolbarHeight(0),
mPageSize(-1, -1),
mPageScale(0.0),
mPPScale(1.0f),
@ -675,6 +676,7 @@ nsresult nsPresContext::Init(nsDeviceContext* aDeviceContext) {
if (BrowserChild* browserChild =
BrowserChild::GetFrom(mDocument->GetDocShell())) {
mDynamicToolbarMaxHeight = browserChild->GetDynamicToolbarMaxHeight();
mDynamicToolbarHeight = mDynamicToolbarMaxHeight;
}
}
#endif
@ -2546,6 +2548,37 @@ void nsPresContext::UpdateDynamicToolbarOffset(ScreenIntCoord aOffset) {
}
MOZ_ASSERT(-mDynamicToolbarMaxHeight <= aOffset && aOffset <= 0);
if (mDynamicToolbarHeight == mDynamicToolbarMaxHeight + aOffset) {
return;
}
// Forcibly flush position:fixed elements in the case where the dynamic
// toolbar is going to be completely hidden or starts to be visible so that
// %-based style values will be recomputed with the visual viewport size which
// is including the area covered by the dynamic toolbar.
if (mDynamicToolbarHeight == 0 || aOffset == -mDynamicToolbarMaxHeight) {
mPresShell->MarkFixedFramesForReflow(IntrinsicDirty::Resize);
}
mDynamicToolbarHeight = mDynamicToolbarMaxHeight + aOffset;
if (RefPtr<MobileViewportManager> mvm =
mPresShell->GetMobileViewportManager()) {
mvm->UpdateVisualViewportSizeByDynamicToolbar(-aOffset);
}
}
DynamicToolbarState nsPresContext::GetDynamicToolbarState() const {
if (!IsRootContentDocumentCrossProcess() || !HasDynamicToolbar()) {
return DynamicToolbarState::None;
}
if (mDynamicToolbarMaxHeight == mDynamicToolbarHeight) {
return DynamicToolbarState::Expanded;
} else if (mDynamicToolbarHeight == 0) {
return DynamicToolbarState::Collapsed;
}
return DynamicToolbarState::InTransition;
}
#ifdef DEBUG

Просмотреть файл

@ -38,6 +38,7 @@
#include "mozilla/TimeStamp.h"
#include "mozilla/AppUnits.h"
#include "mozilla/MediaEmulationData.h"
#include "mozilla/PresShellForwards.h"
#include "prclist.h"
#include "nsThreadUtils.h"
#include "nsIMessageManager.h"
@ -396,6 +397,15 @@ class nsPresContext : public nsISupports,
* |aOffset| must be offset from the bottom edge of the ICB and it's negative.
*/
void UpdateDynamicToolbarOffset(mozilla::ScreenIntCoord aOffset);
mozilla::ScreenIntCoord GetDynamicToolbarHeight() const {
MOZ_ASSERT(IsRootContentDocumentCrossProcess());
return mDynamicToolbarHeight;
}
/**
* Returns the state of the dynamic toolbar.
*/
mozilla::DynamicToolbarState GetDynamicToolbarState() const;
/**
* Return true if this presentation context is a paginated
@ -1195,6 +1205,7 @@ class nsPresContext : public nsISupports,
nsSize mSizeForViewportUnits;
// The maximum height of the dynamic toolbar on mobile.
mozilla::ScreenIntCoord mDynamicToolbarMaxHeight;
mozilla::ScreenIntCoord mDynamicToolbarHeight;
nsSize mPageSize;
float mPageScale;
float mPPScale;

Просмотреть файл

@ -387,9 +387,15 @@ nsSize ViewportFrame::AdjustViewportSizeForFixedPosition(
// Layout fixed position elements to the visual viewport size if and only if
// it has been set and it is larger than the computed size, otherwise use the
// computed size.
if (presShell->IsVisualViewportSizeSet() &&
result < presShell->GetVisualViewportSize()) {
result = presShell->GetVisualViewportSize();
if (presShell->IsVisualViewportSizeSet()) {
if (presShell->GetDynamicToolbarState() == DynamicToolbarState::Collapsed &&
result < presShell->GetVisualViewportSizeUpdatedByDynamicToolbar()) {
// We need to use the viewport size updated by the dynamic toolbar in the
// case where the dynamic toolbar is completely hidden.
result = presShell->GetVisualViewportSizeUpdatedByDynamicToolbar();
} else if (result < presShell->GetVisualViewportSize()) {
result = presShell->GetVisualViewportSize();
}
}
// Expand the size to the layout viewport size if necessary.
const nsSize layoutViewportSize = presShell->GetLayoutViewportSize();

Просмотреть файл

@ -0,0 +1,24 @@
<!DOCTYPE html>
<meta name="viewport" content="width=device-width, minimum-scale=0.5">
<style>
html {
width: 100%;
height: 100%;
scrollbar-width: none;
}
body {
width: 200%;
height: 2000px;
margin: 0;
padding: 0;
}
#fixed-element {
width: 100%;
height: 200%;
position: fixed;
top: 0px;
background-color: green;
}
</style>
<div id="fixed-element"></div>

Просмотреть файл

@ -60,6 +60,7 @@ open class BaseSessionTest(noErrorCollector: Boolean = false) {
const val COLORS_HTML_PATH = "/assets/www/colors.html"
const val FIXED_BOTTOM = "/assets/www/fixedbottom.html"
const val FIXED_VH = "/assets/www/fixedvh.html"
const val FIXED_PERCENT = "/assets/www/fixedpercent.html"
const val STORAGE_TITLE_HTML_PATH = "/assets/www/reflect_local_storage_into_title.html"
const val HUNG_SCRIPT = "/assets/www/hungScript.html"
const val PUSH_HTML_PATH = "/assets/www/push/push.html"

Просмотреть файл

@ -16,6 +16,7 @@ import org.junit.Test
import org.junit.runner.RunWith
import org.mozilla.geckoview.GeckoResult
import org.mozilla.geckoview.test.rule.GeckoSessionTestRule.WithDisplay
import org.hamcrest.Matchers.closeTo
import org.hamcrest.Matchers.equalTo
private const val SCREEN_WIDTH = 100
@ -133,4 +134,73 @@ class DynamicToolbarTest : BaseSessionTest() {
}
}
@WithDisplay(height = SCREEN_HEIGHT, width = SCREEN_WIDTH)
@Test
fun visualViewportEvents() {
val dynamicToolbarMaxHeight = SCREEN_HEIGHT / 2
sessionRule.display?.run { setDynamicToolbarMaxHeight(dynamicToolbarMaxHeight) }
// Set active since setVerticalClipping call affects only for forground tab.
mainSession.setActive(true)
mainSession.loadTestPath(BaseSessionTest.FIXED_VH)
mainSession.waitForPageStop()
val pixelRatio = sessionRule.session.evaluateJS("window.devicePixelRatio") as Double
val scale = sessionRule.session.evaluateJS("window.visualViewport.scale") as Double
for (i in 1..dynamicToolbarMaxHeight) {
// Simulate the dynamic toolbar is going to be hidden.
sessionRule.display?.run { setVerticalClipping(-i) }
val expectedViewportHeight = (SCREEN_HEIGHT - dynamicToolbarMaxHeight + i) / scale / pixelRatio
val promise = sessionRule.session.evaluatePromiseJS("""
new Promise(resolve => {
window.visualViewport.addEventListener('resize', resolve(window.visualViewport.height));
});
""".trimIndent())
assertThat("The visual viewport height should be changed in response to the dynamc toolbar transition",
promise.value as Double, closeTo(expectedViewportHeight, .01))
}
}
@WithDisplay(height = SCREEN_HEIGHT, width = SCREEN_WIDTH)
@Test
fun percentBaseValueOnPositionFixedElement() {
val dynamicToolbarMaxHeight = SCREEN_HEIGHT / 2
sessionRule.display?.run { setDynamicToolbarMaxHeight(dynamicToolbarMaxHeight) }
// Set active since setVerticalClipping call affects only for forground tab.
mainSession.setActive(true)
mainSession.loadTestPath(BaseSessionTest.FIXED_PERCENT)
mainSession.waitForPageStop()
val originalHeight = mainSession.evaluateJS("""
getComputedStyle(document.querySelector('#fixed-element')).height
""".trimIndent()) as String
// Set the vertical clipping value to the middle of toolbar transition.
sessionRule.display?.run { setVerticalClipping(-dynamicToolbarMaxHeight / 2) }
var height = mainSession.evaluateJS("""
getComputedStyle(document.querySelector('#fixed-element')).height
""".trimIndent()) as String
assertThat("The %-based height should be the static in the middle of toolbar tansition",
height, equalTo(originalHeight))
// Set the vertical clipping value to hide the toolbar completely.
sessionRule.display?.run { setVerticalClipping(-dynamicToolbarMaxHeight) }
height = mainSession.evaluateJS("""
getComputedStyle(document.querySelector('#fixed-element')).height
""".trimIndent()) as String
val scale = sessionRule.session.evaluateJS("window.visualViewport.scale") as Double
val expectedHeight = (SCREEN_HEIGHT / scale).toInt()
assertThat("The %-based height should be now recomputed based on the screen height",
height, equalTo(expectedHeight.toString() + "px"))
}
}