Bug 594958 - Tab thumbnails sometimes have gray (now black) bars at the bottom [r=ian, a=blocking2.0:final+]
authorTim Taubert <tim.taubert@gmx.de>
Thu, 03 Feb 2011 02:53:13 +0100
changeset 61858 54da8f75df7f9d2a91d9a16019c850f25994c6a3
parent 61857 490e39e1d9569f606e3ff5675c9569b4c7b8529d
child 61859 fd7962a458688e92cc5edd857d30da928438ad03
push id1
push userroot
push dateTue, 10 Dec 2013 15:46:25 +0000
reviewersian, blocking2
bugs594958
milestone2.0b12pre
Bug 594958 - Tab thumbnails sometimes have gray (now black) bars at the bottom [r=ian, a=blocking2.0:final+]
browser/base/content/tabview/tabitems.js
browser/base/content/test/tabview/Makefile.in
browser/base/content/test/tabview/browser_tabview_bug594958.js
--- a/browser/base/content/tabview/tabitems.js
+++ b/browser/base/content/tabview/tabitems.js
@@ -803,17 +803,17 @@ let TabItems = {
     let $canvas = iQ("<canvas>")
       .attr('moz-opaque', '');
     $canvas.appendTo(iQ("body"));
     $canvas.hide();
     this.tempCanvas = $canvas[0];
     // 150 pixels is an empirical size, below which FF's drawWindow()
     // algorithm breaks down
     this.tempCanvas.width = 150;
-    this.tempCanvas.height = 150;
+    this.tempCanvas.height = 112;
 
     this.tabsProgressListener = {
       onStateChange: function(browser, webProgress, request, stateFlags, status) {
         if ((stateFlags & Ci.nsIWebProgressListener.STATE_STOP) &&
             (stateFlags & Ci.nsIWebProgressListener.STATE_IS_WINDOW)) {
           // browser would only has _tabViewTabItemWithCachedData if 
           // it's showing cached data.
           if (browser._tabViewTabItemWithCachedData)
@@ -1276,77 +1276,109 @@ TabCanvas.prototype = {
   // ----------
   // Function: paint
   paint: function TabCanvas_paint(evt) {
     var w = this.canvas.width;
     var h = this.canvas.height;
     if (!w || !h)
       return;
 
-    let fromWin = this.tab.linkedBrowser.contentWindow;
-    if (fromWin == null) {
-      Utils.log('null fromWin in paint');
+    if (!this.tab.linkedBrowser.contentWindow) {
+      Utils.log('no tab.linkedBrowser.contentWindow in TabCanvas.paint()');
       return;
     }
 
+    let ctx = this.canvas.getContext("2d");
     let tempCanvas = TabItems.tempCanvas;
+    let bgColor = '#fff';
+
     if (w < tempCanvas.width) {
       // Small draw case where nearest-neighbor algorithm breaks down in Windows
       // First draw to a larger canvas (150px wide), and then draw that image
       // to the destination canvas.
-      
-      var tempCtx = tempCanvas.getContext("2d");
-      
-      let canvW = tempCanvas.width;
-      let canvH = (h/w) * canvW;
-      
-      var scaler = canvW/fromWin.innerWidth;
-  
-      tempCtx.save();
-      tempCtx.clearRect(0,0,tempCanvas.width,tempCanvas.height);
-      tempCtx.scale(scaler, scaler);
-      try{
-        tempCtx.drawWindow(fromWin, fromWin.scrollX, fromWin.scrollY, 
-          canvW/scaler, canvH/scaler, "#fff");
-      } catch(e) {
+      let tempCtx = tempCanvas.getContext("2d");
+      this._drawWindow(tempCtx, tempCanvas.width, tempCanvas.height, bgColor);
+
+      // Now copy to tabitem canvas.
+      try {
+        this._fillCanvasBackground(ctx, w, h, bgColor);
+        ctx.drawImage(tempCanvas, 0, 0, w, h);
+      } catch (e) {
         Utils.error('paint', e);
-      }  
-      tempCtx.restore();
-      
-      // Now copy to tabitem canvas. No save/restore necessary.      
-      var destCtx = this.canvas.getContext("2d");      
-      try{
-        // the tempcanvas is square, so draw it as a square.
-        destCtx.drawImage(tempCanvas, 0, 0, w, w);
-      } catch(e) {
-        Utils.error('paint', e);
-      }  
-      
+      }
     } else {
       // General case where nearest neighbor algorithm looks good
       // Draw directly to the destination canvas
-      
-      var ctx = this.canvas.getContext("2d");
-      
-      var scaler = w/fromWin.innerWidth;
-  
-      // TODO: Potentially only redraw the dirty rect? (Is it worth it?)
-  
-      ctx.save();
-      ctx.scale(scaler, scaler);
-      try{
-        ctx.drawWindow(fromWin, fromWin.scrollX, fromWin.scrollY, 
-          w/scaler, h/scaler, "#fff",
-          Ci.nsIDOMCanvasRenderingContext2D.DRAWWINDOW_DO_NOT_FLUSH);
-      } catch(e) {
-        Utils.error('paint', e);
-      }
-  
-      ctx.restore();
+      this._drawWindow(ctx, w, h, bgColor);
+    }
+  },
+
+  // ----------
+  // Function: _fillCanvasBackground
+  // Draws a rectangle of <width>x<height> with color <bgColor> to the given
+  // canvas context.
+  _fillCanvasBackground: function TabCanvas__fillCanvasBackground(ctx, width, height, bgColor) {
+    ctx.fillStyle = bgColor;
+    ctx.fillRect(0, 0, width, height);
+  },
+
+  // ----------
+  // Function: _drawWindow
+  // Draws contents of the tabs' browser window to the given canvas context.
+  _drawWindow: function TabCanvas__drawWindow(ctx, width, height, bgColor) {
+    this._fillCanvasBackground(ctx, width, height, bgColor);
+
+    let rect = this._calculateClippingRect(width, height);
+    let scaler = width / rect.width;
+
+    ctx.save();
+    ctx.scale(scaler, scaler);
+
+    try {
+      let win = this.tab.linkedBrowser.contentWindow;
+      ctx.drawWindow(win, rect.left, rect.top, rect.width, rect.height,
+                     bgColor, ctx.DRAWWINDOW_DO_NOT_FLUSH);
+    } catch (e) {
+      Utils.error('paint', e);
     }
+
+    ctx.restore();
+  },
+
+  // ----------
+  // Function: _calculateClippingRect
+  // Calculate the clipping rect that will be projected to the tab's
+  // thumbnail canvas.
+  _calculateClippingRect: function TabCanvas__calculateClippingRect(origWidth, origHeight) {
+    let win = this.tab.linkedBrowser.contentWindow;
+    let body = win.document.body;
+
+    let maxWidth = win.innerWidth - 25;
+    let maxHeight = win.innerHeight;
+
+    if (body) {
+      maxWidth = Math.max(maxWidth, body.scrollWidth - win.scrollX);
+      maxHeight = Math.max(maxHeight, body.scrollHeight - win.scrollY);
+    }
+
+    let height = Math.min(maxHeight, Math.floor(origHeight * maxWidth / origWidth));
+    let width = Math.floor(origWidth * height / origHeight);
+
+    // very short pages in combination with a very wide browser window force us
+    // to extend the clipping rect and add some empty space around the thumb
+    let factor = 0.7;
+    if (width < maxWidth * factor) {
+      width = maxWidth * factor;
+      height = Math.floor(origHeight * width / origWidth);
+    }
+
+    let left = win.scrollX + Math.max(0, Math.round((maxWidth - width) / 2));
+    let top = win.scrollY;
+
+    return new Rect(left, top, width, height);
   },
 
   // ----------
   // Function: toImageData
   toImageData: function TabCanvas_toImageData() {
     return this.canvas.toDataURL("image/png", "");
   }
 };
--- a/browser/base/content/test/tabview/Makefile.in
+++ b/browser/base/content/test/tabview/Makefile.in
@@ -52,16 +52,17 @@ include $(topsrcdir)/config/rules.mk
                  browser_tabview_bug587231.js \
                  browser_tabview_bug587351.js \
                  browser_tabview_bug587503.js \
                  browser_tabview_bug587990.js \
                  browser_tabview_bug588265.js \
                  browser_tabview_bug589324.js \
                  browser_tabview_bug590606.js \
                  browser_tabview_bug591706.js \
+                 browser_tabview_bug594958.js \
                  browser_tabview_bug595191.js \
                  browser_tabview_bug595436.js \
                  browser_tabview_bug595518.js \
                  browser_tabview_bug595521.js \
                  browser_tabview_bug595560.js \
                  browser_tabview_bug595804.js \
                  browser_tabview_bug595930.js \
                  browser_tabview_bug595943.js \
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/tabview/browser_tabview_bug594958.js
@@ -0,0 +1,149 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function test() {
+  let win;
+  let cw;
+
+  let getGroupItem = function (index) {
+    return cw.GroupItems.groupItems[index];
+  }
+
+  let newWindow = function (callback) {
+    newWindowWithTabView(function (tvwin) {
+      registerCleanupFunction(function () {
+        if (!tvwin.closed)
+          tvwin.close();
+      });
+
+      win = tvwin;
+      cw = win.TabView.getContentWindow();
+
+      callback();
+    });
+  }
+
+  let finishTest = function () {
+    win.close();
+    finish();
+  }
+
+  // very long page that produces black bars at the right
+  let html1 = '<html><body style="background-color: #00f;">' +
+              '<div style="background: #fff; width: 95%; height: 10000px; ' +
+              ' margin: 0 auto;"></div></body></html>';
+
+  // very short page that produces black bars at the bottom
+  let html2 = '<html><body style="background-color: #00f;"></body></html>';
+
+  let tests = [{
+    url: 'data:text/html,' + html1,
+    colors: [
+      {right: [0, 0, 255], bottom: [255, 255, 255]},
+      {right: [0, 0, 255], bottom: [255, 255, 255]},
+      {right: [0, 0, 255], bottom: [255, 255, 255]}
+    ]
+  }, {
+    url: 'about:blank',
+    colors: [
+      {right: [255, 255, 255], bottom: [255, 255, 255]},
+      {right: [255, 255, 255], bottom: [255, 255, 255]},
+      {right: [255, 255, 255], bottom: [255, 255, 255]}
+    ]
+  }, {
+    url: 'data:text/html,' + html2,
+    colors: [
+      {right: [0, 0, 255], bottom: [0, 0, 255]},
+      {right: [0, 0, 255], bottom: [255, 255, 255]},
+      {right: [0, 0, 255], bottom: [0, 0, 255]}
+    ]
+  }];
+
+  let next = function () {
+    if (!tests.length) {
+      finishTest();
+      return;
+    }
+
+    let test = tests.shift();
+    let tab = win.gBrowser.tabs[0];
+
+    tab.linkedBrowser.addEventListener('load', function onLoad() {
+      tab.linkedBrowser.removeEventListener('load', onLoad, true);
+      checkUrl(test);
+    }, true);
+
+    tab.linkedBrowser.loadURI(test.url);
+  }
+
+  let checkUrl = function (test) {
+    let sizes = [[-400, 0], [800, -200], [-400, 200]];
+
+    let nextSize = function () {
+      if (!sizes.length) {
+        next();
+        return;
+      }
+
+      let size = sizes.shift();
+      let colors = test.colors.shift();
+      let groupItem = getGroupItem(0);
+
+      let checkWithSmallItemSize = function () {
+        groupItem.setSize(150, 150, true);
+        groupItem.setUserSize();
+
+        afterAllTabItemsUpdated(function () {
+          checkPixelColors(test.url, colors, nextSize);
+        }, win);
+      }
+
+      let checkWithNormalItemSize = function () {
+        groupItem.setSize(300, 300, true);
+        groupItem.setUserSize();
+
+        afterAllTabItemsUpdated(function () {
+          checkPixelColors(test.url, colors, checkWithSmallItemSize);
+        }, win);
+      }
+
+      win.resizeBy.apply(win, size);
+      checkWithNormalItemSize();
+    }
+
+    nextSize();
+  }
+
+  let checkPixelColors = function (url, colors, callback) {
+    let tab = win.gBrowser.tabs[0];
+    let $canvas = tab._tabViewTabItem.$canvas;
+    let width = $canvas.width();
+    let height = $canvas.height();
+    let ctx = $canvas[0].getContext("2d");
+
+    afterAllTabItemsUpdated(function () {
+      checkPixelColor(ctx, url, colors.bottom, Math.floor(width / 4), height - 1, 'bottom');
+      checkPixelColor(ctx, url, colors.right, width - 1, Math.floor(height / 4), 'right');
+      callback();
+    }, win);
+  }
+
+  let checkPixelColor = function (ctx, url, color, x, y, edge) {
+    let data = ctx.getImageData(x, y, 1, 1).data;
+    let check = (data[0] == color[0] && data[1] == color[1] && data[2] == color[2]);
+    ok(check, url + ': ' + edge + ' edge color matches pixel value');
+  }
+
+  waitForExplicitFinish();
+  newWindow(next);
+}
+
+// ---------
+function newWindowWithTabView(callback) {
+  let win = window.openDialog(getBrowserURL(), "_blank", 
+                              "chrome,all,dialog=no,height=600,width=800");
+  win.addEventListener("load", function onLoad() {
+    win.removeEventListener("load", onLoad, false);
+    showTabView(function () callback(win), win);
+  }, false);
+}