Bug 878935: Pause painting while reflow-on-zoom is in progress to provide a better user experience. [r=kats]

This commit is contained in:
Scott Johnson 2013-10-01 14:52:13 -05:00
parent 44e3459f45
commit b0b7d88ca3
6 changed files with 186 additions and 37 deletions

View File

@ -137,6 +137,18 @@ interface nsIMarkupDocumentViewer : nsISupports
*/
void changeMaxLineBoxWidth(in int32_t maxLineBoxWidth);
/**
* Instruct the refresh driver to discontinue painting until further
* notice.
*/
void pausePainting();
/**
* Instruct the refresh driver to resume painting after a previous call to
* pausePainting().
*/
void resumePainting();
/*
* Render the document as if being viewed on a device with the specified
* media type. This will cause a reflow.

View File

@ -2657,6 +2657,17 @@ struct LineBoxInfo
nscoord mMaxLineBoxWidth;
};
static void
ChangeChildPaintingEnabled(nsIMarkupDocumentViewer* aChild, void* aClosure)
{
bool* enablePainting = (bool*) aClosure;
if (*enablePainting) {
aChild->ResumePainting();
} else {
aChild->PausePainting();
}
}
static void
ChangeChildMaxLineBoxWidth(nsIMarkupDocumentViewer* aChild, void* aClosure)
{
@ -3264,7 +3275,36 @@ NS_IMETHODIMP nsDocumentViewer::AppendSubtree(nsTArray<nsCOMPtr<nsIMarkupDocumen
return NS_OK;
}
NS_IMETHODIMP nsDocumentViewer::ChangeMaxLineBoxWidth(int32_t aMaxLineBoxWidth)
nsresult
nsDocumentViewer::PausePainting()
{
bool enablePaint = false;
CallChildren(ChangeChildPaintingEnabled, &enablePaint);
nsIPresShell* presShell = GetPresShell();
if (presShell) {
presShell->FreezePainting();
}
return NS_OK;
}
nsresult
nsDocumentViewer::ResumePainting()
{
bool enablePaint = true;
CallChildren(ChangeChildPaintingEnabled, &enablePaint);
nsIPresShell* presShell = GetPresShell();
if (presShell) {
presShell->ThawPainting();
}
return NS_OK;
}
nsresult
nsDocumentViewer::ChangeMaxLineBoxWidth(int32_t aMaxLineBoxWidth)
{
// Change the max line box width for all children.
struct LineBoxInfo lbi = { aMaxLineBoxWidth };

View File

@ -820,6 +820,18 @@ public:
*/
bool IsPaintingSuppressed() const { return mPaintingSuppressed; }
/**
* Resume painting by thawing the refresh driver of this and all parent
* presentations.
*/
virtual void FreezePainting() = 0;
/**
* Resume painting by thawing the refresh driver of this and all parent
* presentations.
*/
virtual void ThawPainting() = 0;
/**
* Unsuppress painting.
*/

View File

@ -9709,3 +9709,27 @@ nsIPresShell::SetMaxLineBoxWidth(nscoord aMaxLineBoxWidth)
FrameNeedsReflow(GetRootFrame(), eResize, NS_FRAME_HAS_DIRTY_CHILDREN);
}
}
void
PresShell::FreezePainting()
{
// We want to freeze painting all the way up the presentation hierarchy.
nsCOMPtr<nsIPresShell> parent = GetParentPresShell();
if (parent) {
parent->FreezePainting();
}
GetPresContext()->RefreshDriver()->Freeze();
}
void
PresShell::ThawPainting()
{
// We want to thaw painting all the way up the presentation hierarchy.
nsCOMPtr<nsIPresShell> parent = GetParentPresShell();
if (parent) {
parent->ThawPainting();
}
GetPresContext()->RefreshDriver()->Thaw();
}

View File

@ -710,6 +710,9 @@ protected:
virtual void ThemeChanged() MOZ_OVERRIDE { mPresContext->ThemeChanged(); }
virtual void BackingScaleFactorChanged() MOZ_OVERRIDE { mPresContext->UIResolutionChanged(); }
virtual void FreezePainting() MOZ_OVERRIDE;
virtual void ThawPainting() MOZ_OVERRIDE;
void UpdateImageVisibility();
nsRevocableEventPtr<nsRunnableMethod<PresShell> > mUpdateImageVisibilityEvent;

View File

@ -172,6 +172,11 @@ function doChangeMaxLineBoxWidth(aWidth) {
if (range) {
BrowserEventHandler._zoomInAndSnapToRange(range);
} else {
// In this case, we actually didn't zoom into a specific range. It probably
// happened from a page load reflow-on-zoom event, so we need to make sure
// painting is re-enabled.
BrowserApp.selectedTab.clearReflowOnZoomPendingActions();
}
}
@ -2690,6 +2695,7 @@ Tab.prototype = {
this.browser.addEventListener("MozApplicationManifest", this, true);
Services.obs.addObserver(this, "before-first-paint", false);
Services.obs.addObserver(this, "after-viewport-change", false);
Services.prefs.addObserver("browser.ui.zoom.force-user-scalable", this, false);
if (aParams.delayLoad) {
@ -2752,6 +2758,43 @@ Tab.prototype = {
return minFontSize / this.getInflatedFontSizeFor(aElement);
},
clearReflowOnZoomPendingActions: function() {
// Reflow was completed, so now re-enable painting.
let webNav = BrowserApp.selectedTab.window.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIWebNavigation);
let docShell = webNav.QueryInterface(Ci.nsIDocShell);
let docViewer = docShell.contentViewer.QueryInterface(Ci.nsIMarkupDocumentViewer);
docViewer.resumePainting();
BrowserApp.selectedTab._mReflozPositioned = false;
},
/**
* Reflow on zoom consists of a few different sub-operations:
*
* 1. When a double-tap event is seen, we verify that the correct preferences
* are enabled and perform the pre-position handling calculation. We also
* signal that reflow-on-zoom should be performed at this time, and pause
* painting.
* 2. During the next call to setViewport(), which is in the Tab prototype,
* we detect that a call to changeMaxLineBoxWidth should be performed. If
* we're zooming out, then the max line box width should be reset at this
* time. Otherwise, we call performReflowOnZoom.
* 2a. PerformReflowOnZoom() and resetMaxLineBoxWidth() schedule a call to
* doChangeMaxLineBoxWidth, based on a timeout specified in preferences.
* 3. doChangeMaxLineBoxWidth changes the line box width (which also
* schedules a reflow event), and then calls _zoomInAndSnapToRange.
* 4. _zoomInAndSnapToRange performs the positioning of reflow-on-zoom and
* then re-enables painting.
*
* Some of the events happen synchronously, while others happen asynchronously.
* The following is a rough sketch of the progression of events:
*
* double tap event seen -> onDoubleTap() -> ... asynchronous ...
* -> setViewport() -> performReflowOnZoom() -> ... asynchronous ...
* -> doChangeMaxLineBoxWidth() -> _zoomInAndSnapToRange()
* -> ... asynchronous ... -> setViewport() -> Observe('after-viewport-change')
* -> resumePainting()
*/
performReflowOnZoom: function(aViewport) {
let zoom = this._drawZoom ? this._drawZoom : aViewport.zoom;
@ -2838,6 +2881,7 @@ Tab.prototype = {
this.browser.removeEventListener("MozApplicationManifest", this, true);
Services.obs.removeObserver(this, "before-first-paint");
Services.obs.removeObserver(this, "after-viewport-change");
Services.prefs.removeObserver("browser.ui.zoom.force-user-scalable", this);
// Make sure the previously selected panel remains selected. The selected panel of a deck is
@ -3135,6 +3179,7 @@ Tab.prototype = {
// In this case, the user pinch-zoomed in, so we don't want to
// preserve position as we would with reflow-on-zoom.
BrowserApp.selectedTab.probablyNeedRefloz = false;
BrowserApp.selectedTab.clearReflowOnZoomPendingActions();
BrowserApp.selectedTab._mReflozPoint = null;
}
@ -4045,6 +4090,11 @@ Tab.prototype = {
BrowserApp.selectedTab.performReflowOnZoom(vp);
}
break;
case "after-viewport-change":
if (BrowserApp.selectedTab._mReflozPositioned) {
BrowserApp.selectedTab.clearReflowOnZoomPendingActions();
}
break;
case "nsPref:changed":
if (aData == "browser.ui.zoom.force-user-scalable")
ViewportHandler.updateMetadata(this, false);
@ -4355,12 +4405,22 @@ var BrowserEventHandler = {
if (BrowserEventHandler.mReflozPref &&
!BrowserApp.selectedTab._mReflozPoint &&
!this._shouldSuppressReflowOnZoom(element)) {
// See comment above performReflowOnZoom() for a detailed description of
// the events happening in the reflow-on-zoom operation.
let data = JSON.parse(aData);
let zoomPointX = data.x;
let zoomPointY = data.y;
BrowserApp.selectedTab._mReflozPoint = { x: zoomPointX, y: zoomPointY,
range: BrowserApp.selectedBrowser.contentDocument.caretPositionFromPoint(zoomPointX, zoomPointY) };
// Before we perform a reflow on zoom, let's disable painting.
let webNav = BrowserApp.selectedTab.window.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIWebNavigation);
let docShell = webNav.QueryInterface(Ci.nsIDocShell);
let docViewer = docShell.contentViewer.QueryInterface(Ci.nsIMarkupDocumentViewer);
docViewer.pausePainting();
BrowserApp.selectedTab.probablyNeedRefloz = true;
}
@ -4461,11 +4521,7 @@ var BrowserEventHandler = {
},
_zoomInAndSnapToRange: function(aRange) {
if (!aRange) {
Cu.reportError("aRange is null in zoomInAndSnapToRange. Unable to maintain position.");
return;
}
// aRange is always non-null here, since a check happened previously.
let viewport = BrowserApp.selectedTab.getViewport();
let fudge = 15; // Add a bit of fudge.
let boundingElement = aRange.offsetNode;
@ -4488,6 +4544,8 @@ var BrowserEventHandler = {
let leftAdjustment = parseInt(boundingStyle.paddingLeft) +
parseInt(boundingStyle.borderLeftWidth);
BrowserApp.selectedTab._mReflozPositioned = true;
rect.type = "Browser:ZoomToRect";
rect.x = Math.max(viewport.cssPageLeft, rect.x - fudge + leftAdjustment);
rect.y = Math.max(topPos, viewport.cssPageTop);