Bug 624931 - Use -moz-transform for Panorama zoom. r=iangilman a=sdwilsh
authorPatrick Walton <pwalton@mozilla.com>
Mon, 31 Jan 2011 16:41:05 -0800
changeset 61671 d94a56d7812335f29492a373a4e843865c2f9909
parent 61670 73f7643d522d8d167b35c87b071ddfeaff7b7863
child 61672 10c381463850b3970d9032bcd493bad6c16a9691
push id1
push userroot
push dateTue, 10 Dec 2013 15:46:25 +0000
reviewersiangilman, sdwilsh
bugs624931
milestone2.0b11pre
Bug 624931 - Use -moz-transform for Panorama zoom. r=iangilman a=sdwilsh
browser/base/content/tabview/groupitems.js
browser/base/content/tabview/tabitems.js
browser/base/content/tabview/tabview.css
browser/base/content/tabview/ui.js
browser/base/content/test/tabview/Makefile.in
browser/base/content/test/tabview/browser_tabview_bug624931.js
--- a/browser/base/content/tabview/groupitems.js
+++ b/browser/base/content/tabview/groupitems.js
@@ -2351,20 +2351,18 @@ let GroupItems = {
       box.width = 250;
       box.height = 200;
 
       new GroupItem([ tab._tabViewTabItem ], { bounds: box });
     }
 
     if (shouldUpdateTabBar)
       this._updateTabBar();
-    else if (shouldShowTabView) {
-      tab._tabViewTabItem.setZoomPrep(false);
+    else if (shouldShowTabView)
       UI.showTabView();
-    }
   },
 
   // ----------
   // Function: removeHiddenGroups
   // Removes all hidden groups' data and its browser tabs.
   removeHiddenGroups: function GroupItems_removeHiddenGroups() {
     if (this._removingHiddenGroups)
       return;
--- a/browser/base/content/tabview/tabitems.js
+++ b/browser/base/content/tabview/tabitems.js
@@ -89,17 +89,16 @@ function TabItem(tab, options) {
     .addClass('expander')
     .appendTo($div);
 
   this.tabCanvas = new TabCanvas(this.tab, this.$canvas[0]);
 
   this.defaultSize = new Point(TabItems.tabWidth, TabItems.tabHeight);
   this._hidden = false;
   this.isATabItem = true;
-  this._zoomPrep = false;
   this.sizeExtra = new Point();
   this.keepProportional = true;
   this._hasBeenDrawn = false;
   this._reconnected = false;
   this.isStacked = false;
 
   var self = this;
 
@@ -423,19 +422,16 @@ TabItem.prototype = Utils.extend(new Ite
 
     // force the input size to be valid
     let validSize = TabItems.calcValidSize(
       new Point(inRect.width, inRect.height), 
       {hideTitle: (this.isStacked || options.hideTitle === true)});
     let rect = new Rect(inRect.left, inRect.top, 
       validSize.x, validSize.y);
 
-    if (this._zoomPrep)
-      this.bounds.copy(rect);
-    else {
       var css = {};
 
       if (rect.left != this.bounds.left || options.force)
         css.left = rect.left;
 
       if (rect.top != this.bounds.top || options.force)
         css.top = rect.top;
 
@@ -515,17 +511,16 @@ TabItem.prototype = Utils.extend(new Ite
          "-moz-padding-end": pad + 2 + "px",
          "padding-top": pad + "px",
          "padding-bottom": pad + "px",
          "border-color": "rgba(0,0,0,"+ alphaRange.scale(proportion) +")",
         });
       }
 
       this._hasBeenDrawn = true;
-    }
 
     UI.clearShouldResizeItems();
 
     this._updateDebugBounds();
     rect = this.getBounds(); // ensure that it's a <Rect>
 
     if (!Utils.isRect(this.bounds))
       Utils.trace('TabItem.setBounds: this.bounds is not a real rectangle!', this.bounds);
@@ -620,57 +615,62 @@ TabItem.prototype = Utils.extend(new Ite
   // Parameters:
   //   isNewBlankTab - boolean indicates whether it is a newly opened blank tab.
   zoomIn: function TabItem_zoomIn(isNewBlankTab) {
     // don't allow zoom in if its group is hidden
     if (this.parent && this.parent.hidden)
       return;
 
     var self = this;
-    var $tabEl = this.$container;
+    var $tabEl = this.$container, $canvas = this.$canvas;
     var childHitResult = { shouldZoom: true };
     if (this.parent)
       childHitResult = this.parent.childHit(this);
 
+    this.shouldHideCachedData = true;
+    TabItems._update(this.tab);
+
     if (childHitResult.shouldZoom) {
       // Zoom in!
       var tab = this.tab;
-      var orig = $tabEl.bounds();
 
       function onZoomDone() {
+        $canvas.css({ '-moz-transform': null });
+        $tabEl.removeClass("front");
+
         UI.goToTab(tab);
 
         // tab might not be selected because hideTabView() is invoked after 
         // UI.goToTab() so we need to setup everything for the gBrowser.selectedTab
         if (tab != gBrowser.selectedTab) {
           UI.onTabSelect(gBrowser.selectedTab);
         } else { 
           if (isNewBlankTab)
             gWindow.gURLBar.focus();
         }
         if (childHitResult.callback)
           childHitResult.callback();
       }
 
       let animateZoom = gPrefBranch.getBoolPref("animate_zoom");
       if (animateZoom) {
+        let transform = this.getZoomTransform();
         TabItems.pausePainting();
-        $tabEl.addClass("front")
-        .animate(this.getZoomRect(), {
+
+        $tabEl.addClass("front");
+        $canvas
+        .css({ '-moz-transform-origin': transform.transformOrigin })
+        .animate({ '-moz-transform': transform.transform }, {
           duration: 230,
           easing: 'fast',
           complete: function() {
             onZoomDone();
 
             setTimeout(function() {
               TabItems.resumePainting();
-
-              $tabEl
-                .css(orig)
-                .removeClass("front");
             }, 0);
           }
         });
       } else {
         setTimeout(onZoomDone, 0);
       } 
     }
   },
@@ -678,116 +678,94 @@ TabItem.prototype = Utils.extend(new Ite
   // ----------
   // Function: zoomOut
   // Handles the zoom down animation after returning to TabView.
   // It is expected that this routine will be called from the chrome thread
   //
   // Parameters:
   //   complete - a function to call after the zoom down animation
   zoomOut: function TabItem_zoomOut(complete) {
-    var $tab = this.$container;
+    let $tab = this.$container, $canvas = this.$canvas;
     var self = this;
     
     let onZoomDone = function onZoomDone() {
-      self.setZoomPrep(false);
+      $tab.removeClass("front");
+      $canvas.css("-moz-transform", null);
 
       GroupItems.setActiveOrphanTab(null);
 
       if (typeof complete == "function")
         complete();
     };
-    
+
+    this.shouldHideCachedData = true;
+    TabItems._update(this.tab);
+
+    $tab.addClass("front");
+
     let animateZoom = gPrefBranch.getBoolPref("animate_zoom");
     if (animateZoom) {
-      let box = this.getBounds();
-      box.width -= this.sizeExtra.x;
-      if (!this.isStacked)
-        box.height -= this.sizeExtra.y + TabItems.fontSizeRange.max;
-      else
-        box.height -= this.sizeExtra.y;
-  
-      TabItems.pausePainting();
-      $tab.animate({
-        left: box.left,
-        top: box.top,
-        width: box.width,
-        height: box.height
-      }, {
+      // The scaleCheat of 2 here is a clever way to speed up the zoom-out
+      // code. See getZoomTransform() below.
+      let transform = this.getZoomTransform(2);
+      $canvas.css({
+        '-moz-transform': transform.transform,
+        '-moz-transform-origin': transform.transformOrigin
+      });
+
+      $canvas.animate({ "-moz-transform": "scale(1.0)" }, {
         duration: 300,
         easing: 'cubic-bezier', // note that this is legal easing, even without parameters
         complete: function() {
           TabItems.resumePainting();
           onZoomDone();
         }
       });
     } else {
       onZoomDone();
     }
   },
 
   // ----------
-  // Function: getZoomRect
-  // Returns a faux rect (just an object with top, left, width, height)
-  // which represents the maximum bounds of the tab thumbnail in the zoom
-  // animation. Note that this is not just the rect of the window itself,
-  // due to scaleCheat.
-  getZoomRect: function TabItem_getZoomRect(scaleCheat) {
-    let $tabEl = iQ(this.container);
-    let orig = $tabEl.bounds();
+  // Function: getZoomTransform
+  // Returns the transform function which represents the maximum bounds of the
+  // tab thumbnail in the zoom animation.
+  getZoomTransform: function TabItem_getZoomTransform(scaleCheat) {
+    // Taking the bounds of the container (as opposed to the canvas) makes us
+    // immune to any transformations applied to the canvas.
+    let { left, top, width, height, right, bottom } = this.$container.bounds();
+
+    let { innerWidth: windowWidth, innerHeight: windowHeight } = window;
+
     // The scaleCheat is a clever way to speed up the zoom-in code.
     // Because image scaling is slowest on big images, we cheat and stop
     // the image at scaled-down size and placed accordingly. Because the
     // animation is fast, you can't see the difference but it feels a lot
     // zippier. The only trick is choosing the right animation function so
-    // that you don't see a change in percieved animation speed.
+    // that you don't see a change in percieved animation speed from frame #1
+    // (the tab) to frame #2 (the half-size image) to frame #3 (the first frame
+    // of real animation). Choosing an animation that starts fast is key.
+
     if (!scaleCheat)
       scaleCheat = 1.7;
 
-    let zoomWidth = orig.width + (window.innerWidth - orig.width) / scaleCheat;
-    return {
-      top:    orig.top    * (1 - 1/scaleCheat),
-      left:   orig.left   * (1 - 1/scaleCheat),
-      width:  zoomWidth,
-      height: (orig.width ? orig.height * zoomWidth / orig.width : 0)
-    };
-  },
+    let zoomWidth = width + (window.innerWidth - width) / scaleCheat;
+    let zoomScaleFactor = zoomWidth / width;
 
-  // ----------
-  // Function: setZoomPrep
-  // Either go into or return from (depending on <value>) "zoom prep" mode,
-  // where the tab fills a large portion of the screen in anticipation of
-  // the zoom out animation.
-  setZoomPrep: function TabItem_setZoomPrep(value) {
-    let animateZoom = gPrefBranch.getBoolPref("animate_zoom");
-
-    var $div = this.$container;
+    let zoomHeight = height * zoomScaleFactor;
+    let zoomTop = top * (1 - 1/scaleCheat);
+    let zoomLeft = left * (1 - 1/scaleCheat);
 
-    if (value && animateZoom) {
-      this._zoomPrep = true;
-
-      // The scaleCheat of 2 here is a clever way to speed up the zoom-out code.
-      // Because image scaling is slowest on big images, we cheat and start the image
-      // at half-size and placed accordingly. Because the animation is fast, you can't
-      // see the difference but it feels a lot zippier. The only trick is choosing the
-      // right animation function so that you don't see a change in percieved
-      // animation speed from frame #1 (the tab) to frame #2 (the half-size image) to
-      // frame #3 (the first frame of real animation). Choosing an animation that starts
-      // fast is key.
+    let xOrigin = (left - zoomLeft) / ((left - zoomLeft) + (zoomLeft + zoomWidth - right)) * 100;
+    let yOrigin = (top - zoomTop) / ((top - zoomTop) + (zoomTop + zoomHeight - bottom)) * 100;
 
-      $div
-        .addClass('front')
-        .css(this.getZoomRect(2));
-    } else {
-      let box = this.getBounds();
-
-      this._zoomPrep = false;
-      $div.removeClass('front');
-
-      this.setBounds(box, true, {force: true});
-    }
+    return {
+      transformOrigin: xOrigin + "% " + yOrigin + "%",
+      transform: "scale(" + zoomScaleFactor + ")"
+    };
   }
 });
 
 // ##########
 // Class: TabItems
 // Singleton for managing <TabItem>s
 let TabItems = {
   minTabWidth: 40,
--- a/browser/base/content/tabview/tabview.css
+++ b/browser/base/content/tabview/tabview.css
@@ -81,22 +81,24 @@ body {
   display: block !important;
 }
 
 /* Tab: Zooming
 ----------------------------------*/
 
 .front {
   z-index: 999999 !important;
-  border-radius: 0 !important;
-  box-shadow: none !important;
-  -moz-transform: none !important;
   image-rendering: -moz-crisp-edges;
 }
 
+.front canvas {
+  border: none !important;
+  padding: 1px !important;
+}
+
 /* Groups
 ----------------------------------*/
 
 .groupItem {
   position: absolute;
 }
 
 .appTabTray {
--- a/browser/base/content/tabview/ui.js
+++ b/browser/base/content/tabview/ui.js
@@ -510,19 +510,16 @@ let UI = {
         dispatchEvent(event);
 
         // Flush pending updates
         GroupItems.flushAppTabUpdates();
 
         TabItems.resumePainting();
       });
     } else {
-      if (currentTab && currentTab._tabViewTabItem)
-        currentTab._tabViewTabItem.setZoomPrep(false);
-
       self.setActiveTab(null);
       dispatchEvent(event);
 
       // Flush pending updates
       GroupItems.flushAppTabUpdates();
 
       TabItems.resumePainting();
     }
@@ -739,19 +736,16 @@ let UI = {
           // an un-named groupItem, which means that the groupItem is gone (null) and
           // there are no visible tabs. 
           let closingUnnamedGroup = (groupItem == null &&
               gBrowser.visibleTabs.length <= 1); 
               
           if (closingLastOfGroup || closingUnnamedGroup) {
             // for the tab focus event to pick up.
             self._closedLastVisibleTab = true;
-            // remove the zoom prep.
-            if (tab && tab._tabViewTabItem)
-              tab._tabViewTabItem.setZoomPrep(false);
             self.showTabView();
           }
         }
       }
     };
 
     // TabMove
     this._eventListeners.move = function(tab) {
@@ -881,25 +875,16 @@ let UI = {
             break;
           }
         }
       }
 
       if (GroupItems.getActiveGroupItem() || GroupItems.getActiveOrphanTab())
         GroupItems._updateTabBar();
     }
-
-    // ___ prepare for when we return to TabView
-    if (newItem != oldItem) {
-      if (oldItem)
-        oldItem.setZoomPrep(false);
-      if (newItem)
-        newItem.setZoomPrep(true);
-    } else if (oldItem)
-      oldItem.setZoomPrep(true);
   },
 
   // ----------
   // Function: setReorderTabsOnHide
   // Sets the groupItem which the tab items' tabs should be re-ordered when
   // switching to the main browser UI.
   // Parameters:
   //   groupItem - the groupItem which would be used for re-ordering tabs.
--- a/browser/base/content/test/tabview/Makefile.in
+++ b/browser/base/content/test/tabview/Makefile.in
@@ -84,16 +84,17 @@ include $(topsrcdir)/config/rules.mk
                  browser_tabview_bug613541.js \
                  browser_tabview_bug616729.js \
                  browser_tabview_bug616967.js \
                  browser_tabview_bug618828.js \
                  browser_tabview_bug619937.js \
                  browser_tabview_bug622835.js \
                  browser_tabview_bug622872.js \
                  browser_tabview_bug624265.js \
+                 browser_tabview_bug624931.js \
                  browser_tabview_bug624727.js \
                  browser_tabview_bug624953.js \
                  browser_tabview_bug625269.js \
                  browser_tabview_bug625424.js \
                  browser_tabview_bug626368.js \
                  browser_tabview_bug627288.js \
                  browser_tabview_bug627736.js \
                  browser_tabview_bug628165.js \
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/tabview/browser_tabview_bug624931.js
@@ -0,0 +1,91 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests that there is a transform being applied to the tabs as they zoom in
+// and out.
+
+let tab, frontChanged, transformChanged;
+
+function test() {
+  waitForExplicitFinish();
+
+  window.addEventListener("tabviewshown", onTabViewWindowLoaded, false);
+  TabView.toggle();
+}
+
+function onTabViewWindowLoaded() {
+  window.removeEventListener("tabviewshown", onTabViewWindowLoaded, false);
+
+  let contentWindow = document.getElementById("tab-view").contentWindow;
+  tab = contentWindow.UI.getActiveTab();
+  ok(tab, "We have an active tab");
+
+  frontChanged = transformChanged = false;
+  tab.$container[0].addEventListener("DOMAttrModified", checkForFrontAddition,
+                                     false);
+  tab.$canvas[0].addEventListener("DOMAttrModified", checkForTransformAddition,
+                                  false);
+
+  window.addEventListener("tabviewhidden", onTabViewHidden, false);
+  TabView.toggle();
+}
+
+function checkForFrontAddition(aEvent) {
+  if (aEvent.attrName == "class" &&
+      aEvent.target.classList.contains("front")) {
+    frontChanged = true;
+  }
+}
+
+function checkForTransformAddition(aEvent) {
+  if (aEvent.attrName == "style" && aEvent.target.style.MozTransform) {
+    transformChanged = true;
+  }
+}
+
+function onTabViewHidden() {
+  window.removeEventListener("tabviewhidden", onTabViewHidden, false);
+
+  ok(frontChanged, "the CSS class 'front' was added while zooming in");
+  ok(transformChanged, "the CSS class '-moz-transform' was modified while " +
+     "zooming in");
+
+  frontChanged = transformChanged = false;
+  tab.$container[0].removeEventListener("DOMAttrModified",
+                                        checkForFrontAddition, false);
+  tab.$container[0].addEventListener("DOMAttrModified", checkForFrontRemoval,
+                                     false);
+
+  window.addEventListener("tabviewshown", onTabViewShownAgain, false);
+  TabView.toggle();
+}
+
+function checkForFrontRemoval(aEvent) {
+  if (aEvent.attrName == "class" &&
+      !aEvent.target.classList.contains("front")) {
+    frontChanged = true;
+  }
+}
+
+function onTabViewShownAgain() {
+  window.removeEventListener("tabviewshown", onTabViewShownAgain, false);
+
+  ok(frontChanged, "the CSS class 'front' was removed while zooming out");
+  ok(transformChanged, "the CSS class 'transform' was removed while zooming " +
+     "out");
+
+  tab.$container[0].removeEventListener("DOMAttrModified",
+                                        checkForFrontRemoval, false);
+  tab.$canvas[0].removeEventListener("DOMAttrModified",
+                                     checkForTransformAddition, false);
+
+  window.addEventListener("tabviewhidden", onTabViewHiddenAgain, false);
+  TabView.toggle();
+}
+
+function onTabViewHiddenAgain() {
+  window.removeEventListener("tabviewhidden", onTabViewHiddenAgain, false);
+
+  finish();
+}
+