diff --git a/mobile/chrome/content/BrowserView.js b/mobile/chrome/content/BrowserView.js index ffad437679cd..683940ef4fe0 100644 --- a/mobile/chrome/content/BrowserView.js +++ b/mobile/chrome/content/BrowserView.js @@ -169,6 +169,10 @@ BrowserView.Util = { 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) { @@ -244,7 +248,7 @@ BrowserView.prototype = { 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); this._idleService.removeIdleObserver(this._idleServiceObserver, kBrowserViewPrefetchBeginIdleWait); @@ -260,9 +264,13 @@ BrowserView.prototype = { if (!bvs) return; + 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 @@ -270,21 +278,33 @@ BrowserView.prototype = { // 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); + let newZoomLevel = BrowserView.Util.clampZoomLevel(zoomLevel); - if (newZL != bvs.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); @@ -395,19 +415,17 @@ BrowserView.prototype = { 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; @@ -421,11 +439,7 @@ BrowserView.prototype = { 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; @@ -478,29 +492,30 @@ BrowserView.prototype = { return; let { x: scrollX, y: scrollY } = BrowserView.Util.getContentScrollOffset(this._browser); - Browser.contentScrollboxScroller.scrollTo(this.browserToViewport(scrollX), + 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; - 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: scrollX, y: scrollY } = BrowserView.Util.getContentScrollOffset(this._browser); + + 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() { @@ -513,14 +528,16 @@ BrowserView.prototype = { .QueryInterface(Ci.nsIInterfaceRequestor) .getInterface(Ci.nsIDOMWindowUtils); var handheldFriendly = windowUtils.getDocumentMetadata("HandheldFriendly"); - + if (handheldFriendly == "true") { browser.className = "browser-handheld"; this.setZoomLevel(1); browser.markupDocumentViewer.textZoom = 1; } else { browser.className = "browser"; - 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); this.setZoomLevel(BrowserView.Util.pageZoomLevel(this.getVisibleRect(), w, h)); } }, @@ -541,6 +558,49 @@ BrowserView.prototype = { this.setZoomLevel(bvs.zoomLevel + zoomDelta); }, + // + // XXXrf This method is used as a workaround for the fact that + // 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 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 this workaround isn't necessary in almost all cases. + // 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). + // + // An example where the workaround is necessary is in going from the + // firstrun page to a Twitter feed. Condition (1) isn't satisfied + // because both pages have the same in-browser width. Condition (2) + // isn't satisfied for all of the page because of what appears to be + // some kind of overflowing container div. To see this, place some + // dumps of the rectangles incoming in the HandleMozAfterPaint + // method, and navigate to someone's Twitter feed. Only sidebar on + // the right-hand side of the feed is invalidated across the page, + // and otherwise, the 500x800 visible region is invalidated as well. + // + // See browser.js for where this is invoked. + // + invalidateEntireView: function invalidateEntireView() { + if (this._browserViewportState) + this._viewportChanged(false, true); + }, + /** * Render a rectangle within the browser viewport to the destination canvas * under the given scale. diff --git a/mobile/chrome/content/TileManager.js.in b/mobile/chrome/content/TileManager.js.in index 2f7dde6a3f6c..bc7186ff79b5 100644 --- a/mobile/chrome/content/TileManager.js.in +++ b/mobile/chrome/content/TileManager.js.in @@ -151,17 +151,92 @@ TileManager.prototype = { 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); } }, diff --git a/mobile/chrome/content/browser.js b/mobile/chrome/content/browser.js index 1494c2a66121..99319a6760b2 100644 --- a/mobile/chrome/content/browser.js +++ b/mobile/chrome/content/browser.js @@ -542,7 +542,7 @@ var Browser = { if (addons.length > 0) { let disabledStrings = document.getElementById("bundle_browser").getString("alertAddonsDisabled"); let label = PluralForm.get(addons.length, disabledStrings).replace("#1", addons.length); - + let alerts = Cc["@mozilla.org/alerts-service;1"].getService(Ci.nsIAlertsService); alerts.showAlertNotification(URI_GENERIC_ICON_XPINSTALL, strings.getString("alertAddons"), label, false, "", null); @@ -1274,7 +1274,7 @@ Browser.MainDragger.prototype = { doffset.subtract(newX.value - origX.value, newY.value - origY.value); } - + }; function nsBrowserAccess() @@ -1387,7 +1387,7 @@ const BrowserSearch = { get engines() { if (this._engines) return this._engines; - return this._engines = this.searchService.getVisibleEngines({ }); + return this._engines = this.searchService.getVisibleEngines({ }); }, addPageSearchEngine: function (aEngine, aDocument) { @@ -1425,7 +1425,7 @@ const BrowserSearch = { let button = document.createElement("button"); button.className = "search-engine-button button-dark"; button.setAttribute("oncommand", "BrowserSearch.addPermanentSearchEngine(this.engine);this.parentNode.hidden=true;"); - + let engine = newEngines[i]; button.engine = engine.engine; button.setAttribute("label", engine.engine.title); @@ -1592,7 +1592,7 @@ IdentityHandler.prototype = { let state = Browser.selectedTab.getIdentityState(); let location = getBrowser().contentWindow.location; let currentStatus = getBrowser().securityUI.QueryInterface(Ci.nsISSLStatusProvider).SSLStatus; - + this._lastStatus = currentStatus; this._lastLocation = {}; try { @@ -1766,7 +1766,7 @@ IdentityHandler.prototype = { hide: function ih_hide() { this._identityPopup.hidden = true; this._identityBox.removeAttribute("open"); - + BrowserUI.popPopup(); BrowserUI.unlockToolbar(); }, @@ -1997,7 +1997,7 @@ var MemoryObserver = { observe: function() { let memory = Cc["@mozilla.org/xpcom/memory-service;1"].getService(Ci.nsIMemory); do { - Browser.windowUtils.garbageCollect(); + Browser.windowUtils.garbageCollect(); } while (memory.isLowMemory() && Browser.sacrificeTab()); } }; @@ -2035,27 +2035,27 @@ function importDialog(parent, src, arguments) { xhr.send(null); if (!xhr.responseXML) return null; - + let doc = xhr.responseXML.documentElement; - + var dialog = null; - + // we need to insert before select-container if we want it to show correctly let selectContainer = document.getElementById("select-container"); let parent = selectContainer.parentNode; - + // emit DOMWillOpenModalDialog event let event = document.createEvent("Events"); event.initEvent("DOMWillOpenModalDialog", true, false); let dispatcher = parent || getBrowser(); dispatcher.dispatchEvent(event); - // create a full-screen semi-opaque box as a background + // create a full-screen semi-opaque box as a background let back = document.createElement("box"); back.setAttribute("class", "modal-block"); dialog = back.appendChild(document.importNode(doc, true)); parent.insertBefore(back, selectContainer); - + dialog.arguments = arguments; dialog.parent = parent; return dialog; @@ -2401,10 +2401,6 @@ Tab.prototype = { let bv = Browser._browserView; if (this == Browser.selectedTab) { - // !!! --- RESIZE HACK BEGIN ----- - bv.simulateMozAfterSizeChange(); - // !!! --- RESIZE HACK END ----- - let restoringPage = (this._state != null); if (!this._browserViewportState.zoomChanged && !restoringPage) { @@ -2440,6 +2436,10 @@ Tab.prototype = { if (!this._loadingTimeout) { if (this == Browser.selectedTab) { Browser._browserView.beginBatchOperation(); + + // XXXrf This is a workaround. See the comment at the top of + // this method's definition in BrowserView.js for details. + Browser._browserView.invalidateEntireView(); } this._loadingTimeout = setTimeout(Util.bind(this._resizeAndPaint, this), 2000); }