Bug 480958. Update viewport dimensions as per MozScrolledAreaChanged. r=mfinkle
authorRoy Frostig <froystig@cs.stanford.edu>
Tue, 08 Sep 2009 15:02:49 -0700
changeset 65748 9d87332caf2424933b6fc2b9e57d942273c45b47
parent 65747 dad5c7928e5a12e27baee78ecba2c019f145a705
child 65749 4651482bda1de61aa85025deb54950360133069a
push id1
push userroot
push dateTue, 26 Apr 2011 22:38:44 +0000
treeherdermozilla-beta@bfdb6e623a36 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmfinkle
bugs480958
Bug 480958. Update viewport dimensions as per MozScrolledAreaChanged. r=mfinkle
mobile/chrome/content/BrowserView.js
mobile/chrome/content/TileManager.js.in
mobile/chrome/content/browser.js
--- 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;
 
--- a/mobile/chrome/content/browser.js
+++ b/mobile/chrome/content/browser.js
@@ -2439,20 +2439,16 @@ Tab.prototype = {
   /**
    * Throttles redraws to once every second while loading the page, zooming to fit page if
    * user hasn't started zooming.
    */
   _resizeAndPaint: function() {
     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) {
         // Only fit page if user hasn't started zooming around and this is a page that
         // isn't being restored.
         bv.zoomToPage();
         
         // zoomChanged gets set to true, but user did not change zooming
@@ -2480,16 +2476,17 @@ Tab.prototype = {
     //if (this._loading)
     //  dump("!!! Already loading this tab, please file a bug\n");
 
     this._loading = true;
     this._browserViewportState.zoomChanged = false;
 
     if (!this._loadingTimeout) {
       Browser._browserView.beginBatchOperation();
+      Browser._browserView.invalidateEntireView();
       this._loadingTimeout = setTimeout(Util.bind(this._resizeAndPaint, this), 2000);
     }
   },
 
   endLoading: function() {
     // Determine at what resolution the browser is rendered based on meta tag
     let browser = this._browser;
     if (Util.contentIsHandheld(browser)) {