--- a/mobile/chrome/content/BrowserView.js
+++ b/mobile/chrome/content/BrowserView.js
@@ -166,16 +166,20 @@ BrowserView.Util = {
createBrowserViewportState: function createBrowserViewportState() {
return new BrowserView.BrowserViewportState(new Rect(0, 0, 1, 1), 0, 0, 1);
},
getViewportStateFromBrowser: function getViewportStateFromBrowser(browser) {
return browser.__BrowserView__vps;
},
+ /**
+ * Calling this is likely to cause a reflow of the browser's document. Use
+ * wisely.
+ */
getBrowserDimensions: function getBrowserDimensions(browser) {
let cdoc = browser.contentDocument;
if (cdoc instanceof SVGDocument) {
let rect = cdoc.rootElement.getBoundingClientRect();
return [Math.ceil(rect.width), Math.ceil(rect.height)];
}
// These might not exist yet depending on page load state
@@ -230,17 +234,17 @@ BrowserView.prototype = {
init: function init(container, visibleRectFactory) {
this._batchOps = [];
this._container = container;
this._browser = null;
this._browserViewportState = null;
this._contentWindow = null;
this._renderMode = 0;
this._offscreenDepth = 0;
-
+
let cacheSize = kBrowserViewCacheSize;
try {
cacheSize = gPrefService.getIntPref("tile.cache.size");
} catch(e) {}
if (cacheSize == -1) {
let sysInfo = Cc["@mozilla.org/system-info;1"].getService(Ci.nsIPropertyBag2);
let device = sysInfo.get("device");
@@ -257,16 +261,17 @@ BrowserView.prototype = {
default:
// Use a minimum number of tiles sice we don't know the device
cacheSize = 6;
}
}
this._tileManager = new TileManager(this._appendTile, this._removeTile, this, cacheSize);
this._visibleRectFactory = visibleRectFactory;
+ this._suppressZoomToPage = false;
this._idleServiceObserver = new BrowserView.IdleServiceObserver(this);
this._idleService = Cc["@mozilla.org/widget/idleservice;1"].getService(Ci.nsIIdleService);
this._idleService.addIdleObserver(this._idleServiceObserver, kBrowserViewPrefetchBeginIdleWait);
},
uninit: function uninit() {
this.setBrowser(null, null, false);
@@ -277,39 +282,60 @@ BrowserView.prototype = {
return this._visibleRectFactory();
},
setViewportDimensions: function setViewportDimensions(width, height, causedByZoom) {
let bvs = this._browserViewportState;
if (!bvs)
return;
+ if (!causedByZoom)
+ this._suppressZoomToPage = false;
+
+ let oldwidth = bvs.viewportRect.right;
+ let oldheight = bvs.viewportRect.bottom;
bvs.viewportRect.right = width;
bvs.viewportRect.bottom = height;
+ let sizeChanged = (oldwidth != width || oldheight != height);
+
// XXX we might not want the user's page to disappear from under them
// at this point, which could happen if the container gets resized such
// that visible rect becomes entirely outside of viewport rect. might
// be wise to define what UX should be in this case, like a move occurs.
// then again, we could also argue this is the responsibility of the
// caller who would do such a thing...
- this._viewportChanged(true, !!causedByZoom);
+ this._viewportChanged(sizeChanged, sizeChanged && !!causedByZoom);
},
- setZoomLevel: function setZoomLevel(zl) {
+ /**
+ * @return [width, height]
+ */
+ getViewportDimensions: function getViewportDimensions() {
let bvs = this._browserViewportState;
+
+ if (!bvs)
+ throw "Cannot get viewport dimensions when no browser is set";
+
+ return [bvs.viewportRect.right, bvs.viewportRect.bottom];
+ },
+
+ setZoomLevel: function setZoomLevel(zoomLevel) {
+ let bvs = this._browserViewportState;
+
if (!bvs)
return;
- let newZL = BrowserView.Util.clampZoomLevel(zl);
- if (newZL != bvs.zoomLevel) {
+ let newZoomLevel = BrowserView.Util.clampZoomLevel(zoomLevel);
+
+ if (newZoomLevel != bvs.zoomLevel) {
let browserW = this.viewportToBrowser(bvs.viewportRect.right);
let browserH = this.viewportToBrowser(bvs.viewportRect.bottom);
- bvs.zoomLevel = newZL; // side-effect: now scale factor in transformations is newZL
+ bvs.zoomLevel = newZoomLevel; // side-effect: now scale factor in transformations is newZoomLevel
this.setViewportDimensions(this.browserToViewport(browserW),
this.browserToViewport(browserH),
true);
bvs.zoomChanged = true;
}
},
getZoomLevel: function getZoomLevel() {
@@ -434,47 +460,41 @@ BrowserView.prototype = {
/**
* Swap out the current browser and browser viewport state with a new pair.
*/
setBrowser: function setBrowser(browser, browserViewportState, doZoom) {
if (browser && !browserViewportState) {
throw "Cannot set non-null browser with null BrowserViewportState";
}
- let browserChanged = (this._browser !== browser);
+ let oldBrowser = this._browser;
- if (this._browser) {
- this._browser.removeEventListener("MozAfterPaint", this.handleMozAfterPaint, false);
- this._browser.removeEventListener("scroll", this.handlePageScroll, false);
+ let browserChanged = (oldBrowser !== browser);
- // !!! --- RESIZE HACK BEGIN -----
- // change to the real event type and perhaps refactor the handler function name
- this._browser.removeEventListener("FakeMozAfterSizeChange", this.handleMozAfterSizeChange, false);
- // !!! --- RESIZE HACK END -------
+ if (oldBrowser) {
+ oldBrowser.removeEventListener("MozAfterPaint", this.handleMozAfterPaint, false);
+ oldBrowser.removeEventListener("scroll", this.handlePageScroll, false);
+ oldBrowser.removeEventListener("MozScrolledAreaChanged", this.handleMozScrolledAreaChanged, false);
- this._browser.setAttribute("type", "content");
- this._browser.docShell.isOffScreenBrowser = false;
+ oldBrowser.setAttribute("type", "content");
+ oldBrowser.docShell.isOffScreenBrowser = false;
}
this._browser = browser;
this._contentWindow = (browser) ? browser.contentWindow : null;
this._browserViewportState = browserViewportState;
if (browser) {
browser.setAttribute("type", "content-primary");
this.beginBatchOperation();
browser.addEventListener("MozAfterPaint", this.handleMozAfterPaint, false);
browser.addEventListener("scroll", this.handlePageScroll, false);
-
- // !!! --- RESIZE HACK BEGIN -----
- // change to the real event type and perhaps refactor the handler function name
- browser.addEventListener("FakeMozAfterSizeChange", this.handleMozAfterSizeChange, false);
- // !!! --- RESIZE HACK END -------
+ browser.addEventListener("MozScrolledAreaChanged", this.handleMozScrolledAreaChanged, false);
if (doZoom) {
browser.docShell.isOffScreenBrowser = true;
this.zoomToPage();
}
this._viewportChanged(browserChanged, browserChanged);
@@ -522,49 +542,54 @@ BrowserView.prototype = {
return;
let { x: scrollX, y: scrollY } = BrowserView.Util.getContentScrollOffset(this._browser);
Browser.contentScrollboxScroller.scrollTo(this.browserToViewport(scrollX),
this.browserToViewport(scrollY));
this.onAfterVisibleMove();
},
- // !!! --- RESIZE HACK BEGIN -----
- simulateMozAfterSizeChange: function simulateMozAfterSizeChange() {
- let [w, h] = BrowserView.Util.getBrowserDimensions(this._browser);
- let ev = document.createEvent("MouseEvents");
- ev.initMouseEvent("FakeMozAfterSizeChange", false, false, window, 0, w, h, 0, 0, false, false, false, false, 0, null);
- this._browser.dispatchEvent(ev);
- },
- // !!! --- RESIZE HACK END -------
+ handleMozScrolledAreaChanged: function handleMozScrolledAreaChanged(ev) {
+ if (ev.target != this._browser.contentDocument)
+ return;
+
+ let { x: scrollX, y: scrollY } = BrowserView.Util.getContentScrollOffset(this._browser);
- handleMozAfterSizeChange: function handleMozAfterSizeChange(ev) {
- // !!! --- RESIZE HACK BEGIN -----
- // get the correct properties off of the event, these are wrong because
- // we're using a MouseEvent, as it has an X and Y prop of some sort and
- // we piggyback on that.
- let w = ev.screenX;
- let h = ev.screenY;
- // !!! --- RESIZE HACK END -------
- this.setViewportDimensions(this.browserToViewport(w), this.browserToViewport(h));
+ let x = ev.x + scrollX;
+ let y = ev.y + scrollY;
+ let w = ev.width;
+ let h = ev.height;
+
+ // Adjust width and height from the incoming event properties so that we
+ // ignore changes to width and height contributed by growth in page
+ // quadrants other than x > 0 && y > 0.
+ if (x < 0) w += x;
+ if (y < 0) h += y;
+
+ this.setViewportDimensions(this.browserToViewport(w),
+ this.browserToViewport(h));
},
zoomToPage: function zoomToPage() {
- this.setZoomLevel(this.getZoomForPage());
+ // See invalidateEntireView() for why we might be suppressing this zoom.
+ if (!this._suppressZoomToPage)
+ this.setZoomLevel(this.getZoomForPage());
},
getZoomForPage: function getZoomForPage() {
let browser = this._browser;
if (!browser)
return 0;
if (Util.contentIsHandheld(browser))
return 1;
- let [w, h] = BrowserView.Util.getBrowserDimensions(browser);
+ let bvs = this._browserViewportState; // browser exists, so bvs must as well
+ let w = this.viewportToBrowser(bvs.viewportRect.right);
+ let h = this.viewportToBrowser(bvs.viewportRect.bottom);
return BrowserView.Util.pageZoomLevel(this.getVisibleRect(), w, h);
},
zoom: function zoom(aDirection) {
let bvs = this._browserViewportState;
if (!bvs)
throw "No browser is set";
@@ -573,16 +598,65 @@ BrowserView.prototype = {
var zoomDelta = 0.05; // 1/20
if (aDirection >= 0)
zoomDelta *= -1;
this.setZoomLevel(bvs.zoomLevel + zoomDelta);
},
+ //
+ // MozAfterPaint events do not guarantee to inform us of all
+ // invalidated paints (See
+ // https://developer.mozilla.org/en/Gecko-Specific_DOM_Events#Important_notes
+ // for details on what the event *does* guarantee). This is only an
+ // issue when the same current <browser> is used to navigate to a
+ // new page. Unless a zoom was issued during the page transition
+ // (e.g. a call to zoomToPage() or something of that nature), we
+ // aren't guaranteed that we've actually invalidated the entire
+ // page. We don't want to leave bits of the previous page in the
+ // view of the new one, so this method exists as a way for Browser
+ // to inform us that the page is changing, and that we really ought
+ // to invalidate everything. Ideally, we wouldn't have to rely on
+ // this being called, and we would get proper invalidates for the
+ // whole page no matter what is or is not visible.
+ //
+ // Note that calling this function isn't necessary in almost all
+ // cases, but should be done for correctness. Most of the time, one
+ // of the following two conditions is satisfied. Either
+ //
+ // (1) Pages have different widths so the Browser calls a
+ // zoomToPage() which forces a dirtyAll, or
+ // (2) MozAfterPaint does indeed inform us of dirtyRects covering
+ // the entire page (everything that could possibly become
+ // visible).
+ //
+ // Since calling this method means "everything is wrong and the
+ // <browser> is about to start giving you new data via MozAfterPaint
+ // and MozScrolledAreaChanged", we also supress any zoomToPage()
+ // that might be called until the next time setViewportDimensions()
+ // is called (which will probably be caused by an incoming
+ // MozScrolledAreaChanged event, or via someone very eagerly setting
+ // it manually so that they can zoom to that manual "page" width).
+ //
+ /**
+ * Invalidates the entire page by throwing away any cached graphical
+ * portions of the view and refusing to allow a zoomToPage() until
+ * the next explicit update of the viewport dimensions.
+ *
+ * This method should be called when the <browser> last set by
+ * setBrowser() is about to navigate to a new page.
+ */
+ invalidateEntireView: function invalidateEntireView() {
+ if (this._browserViewportState) {
+ this._viewportChanged(false, true, true);
+ this._suppressZoomToPage = true;
+ }
+ },
+
/**
* Render a rectangle within the browser viewport to the destination canvas
* under the given scale.
*
* @param destCanvas The destination canvas into which the image is rendered.
* @param destWidth Destination width
* @param destHeight Destination height
* @param srcRect [optional] The source rectangle in BrowserView coordinates.
--- a/mobile/chrome/content/TileManager.js.in
+++ b/mobile/chrome/content/TileManager.js.in
@@ -146,28 +146,105 @@ TileManager.prototype = {
* critical rect.
*/
viewportChangeHandler: function viewportChangeHandler(viewportRect,
criticalRect,
boundsSizeChanged,
dirtyAll) {
let tc = this._tileCache;
- tc.iBound = Math.ceil(viewportRect.right / kTileWidth);
- tc.jBound = Math.ceil(viewportRect.bottom / kTileHeight);
+ let iBoundOld = tc.iBound;
+ let jBoundOld = tc.jBound;
+ let iBound = tc.iBound = Math.ceil(viewportRect.right / kTileWidth) - 1;
+ let jBound = tc.jBound = Math.ceil(viewportRect.bottom / kTileHeight) - 1;
if (criticalRect.isEmpty() || !criticalRect.equals(this._criticalRect)) {
this.beginCriticalMove(criticalRect);
- this.endCriticalMove(criticalRect, !boundsSizeChanged);
+ this.endCriticalMove(criticalRect, !(dirtyAll || boundsSizeChanged));
}
- if (boundsSizeChanged) {
- // TODO fastpath if !dirtyAll
+
+ if (dirtyAll) {
this.dirtyRects([viewportRect.clone()], true);
+ } else if (boundsSizeChanged) {
+
+ //
+ // This is a special case. The bounds size changed, but we are
+ // told that not everything is dirty (so mayhap content grew or
+ // shrank vertically or horizontally). We might have old tiles
+ // around in those areas just due to the fact that they haven't
+ // been yet evicted, so we patrol the new regions in search of
+ // any such leftover tiles and mark those we find as dirty.
+ //
+ // The two dirty rects below mark dirty any renegade tiles in
+ // the newly annexed grid regions as per the following diagram
+ // of the "new" viewport.
+ //
+ // +------------+------+
+ // |old | A |
+ // |viewport | |
+ // | | |
+ // | | |
+ // | | |
+ // +------------+ |
+ // | B | |
+ // | | |
+ // +------------+------+
+ //
+ // The first rectangle covers annexed region A, the second
+ // rectangle covers annexed region B.
+ //
+ // XXXrf If the tiles are large, then we are creating some
+ // redundant work here by invalidating the entire tile that
+ // the old viewport boundary crossed (note markDirty() being
+ // called with no rectangle parameter). The rectangular area
+ // within the tile lying beyond the old boundary is certainly
+ // dirty, but not the area before. Moreover, since we mark
+ // dirty entire tiles that may cross into the old viewport,
+ // they might, in particular, cross into the critical rect
+ // (which is anyhwere in the old viewport), so we call a
+ // criticalRectPaint() for such cleanup. We do all this more
+ // or less because we don't have much of a notion of "the old
+ // viewport" here except for in the sense that we know the
+ // index bounds on the tilecache grid from before (and the new
+ // index bounds now).
+ //
+
+ let t, l, b, r, rect;
+ let rects = [];
+
+ if (iBoundOld <= iBound) {
+ l = iBoundOld * kTileWidth;
+ t = 0;
+ r = (iBound + 1) * kTileWidth;
+ b = (jBound + 1) * kTileHeight;
+
+ rect = new Rect(l, t, r - l, b - t);
+ rect.restrictTo(viewportRect);
+
+ if (!rect.isEmpty())
+ rects.push(rect);
+ }
+
+ if (jBoundOld <= jBound) {
+ l = 0;
+ t = jBoundOld * kTileHeight;
+ r = (iBound + 1) * kTileWidth;
+ b = (jBound + 1) * kTileHeight;
+
+ rect = new Rect(l, t, r - l, b - t);
+ rect.restrictTo(viewportRect);
+
+ if (!rect.isEmpty())
+ rects.push(rect);
+ }
+
+ this.dirtyRects(rects, true);
}
+
},
dirtyRects: function dirtyRects(rects, doCriticalRender) {
let criticalIsDirty = false;
let criticalRect = this._criticalRect;
let tc = this._tileCache;
let crawler = this._crawler;