Bug 462425: Convert Fennec to use WidgetStack framework for panning, r=stuart
authorMark Finkle <mfinkle@mozilla.com>
Sat, 22 Nov 2008 00:12:25 -0500
changeset 266 cdff44cd6045ea9580d0f17bcaf73635f49db56b
parent 265 858dc712ea4b6afbc2cce80738e823eb0a94adb4
child 267 1a0809c364163c9b5a7f5952380c1bb1f03abd0d
push id213
push usermfinkle@mozilla.com
push dateSat, 22 Nov 2008 05:14:18 +0000
reviewersstuart
bugs462425
Bug 462425: Convert Fennec to use WidgetStack framework for panning, r=stuart
chrome/content/CanvasBrowser.js
chrome/content/InputHandler.js
chrome/content/WidgetStack.js
chrome/content/browser-ui.js
chrome/content/browser.css
chrome/content/browser.js
chrome/content/browser.xul
chrome/content/deckbrowser.xml
chrome/content/tabs.xml
chrome/jar.mn
chrome/skin/browser.css
locales/en-US/chrome/shortcuts.properties
tests/wsTests.js
tests/wsTests.xul
new file mode 100644
--- /dev/null
+++ b/chrome/content/CanvasBrowser.js
@@ -0,0 +1,573 @@
+// -*- Mode: js2; tab-width: 2; indent-tabs-mode: nil; js2-basic-offset: 2; js2-skip-preprocessor-directives: t; -*-
+/*
+ * ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Mozilla Mobile Browser.
+ *
+ * The Initial Developer of the Original Code is
+ * Mozilla Corporation.
+ * Portions created by the Initial Developer are Copyright (C) 2008
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *   Stuart Parmenter <stuart@mozilla.com>
+ *   Brad Lassey <blassey@mozilla.com>
+ *   Mark Finkle <mfinkle@mozilla.com>
+ *   Gavin Sharp <gavin.sharp@gmail.com>
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+function CanvasBrowser(canvas) {
+  this._canvas = canvas;
+  this._zoomLevel = 1;
+  this._browser = null;
+  this._pageX = 0;
+  this._pageY = 0;
+}
+
+CanvasBrowser.prototype = {
+  _canvas: null,
+  _zoomLevel: 1,
+  _browser: null,
+  _pageX: 0,
+  _pageY: 0,
+  _screenX: 0,
+  _screenY: 0,
+
+  get viewportDimensions() {
+    var rect = this._canvas.getBoundingClientRect();
+    return [rect.width, rect.height];
+  },
+
+  get _effectiveViewportDimensions() {
+    var [w, h] = this.viewportDimensions;
+    return [this._screenToPage(w), this._screenToPage(h)];
+  },
+
+  get _effectiveCanvasDimensions() {
+          let canvasRect = this._canvas.getBoundingClientRect();
+          return [this._screenToPage(canvasRect.width),
+                  this._screenToPage(canvasRect.height)];
+  },
+
+  setCurrentBrowser: function(browser) {
+    let currentBrowser = this._browser;
+    if (currentBrowser) {
+      // stop monitor paint events for this browser
+      currentBrowser.removeEventListener("MozAfterPaint", this._paintHandler, false);
+      currentBrowser.setAttribute("type", "content");
+    }
+
+    browser.setAttribute("type", "content-primary");
+
+    // start monitoring paint events for this browser
+    var self = this;
+    this._paintHandler = function(ev) { self._handleMozAfterPaint(ev); }
+
+    browser.addEventListener("MozAfterPaint", this._paintHandler, false);
+
+    this._browser = browser;
+
+    self.zoomToPage();
+  },
+
+  startLoading: function() {
+    // Clear the whole canvas
+    // we clear the whole canvas because the browser's width or height
+    // could be less than the area we end up actually drawing.
+
+    var ctx = this._canvas.getContext("2d");
+    ctx.fillStyle = "rgb(255,255,255)";
+    ctx.fillRect(0, 0, this._canvas.width, this._canvas.height);
+    ctx.fillStyle = "rgb(0,0,0)";
+    ctx.fillText("Loading...", 20, 20);
+
+    this._resizeInterval = setInterval(function(self) { self.zoomToPage(); }, 1000, this);
+  },
+
+  endLoading: function() {
+    clearInterval(this._resizeInterval);
+    this.zoomToPage();
+  },
+
+  viewportHandler: function(bounds, oldBounds) {
+    let pageBounds = bounds.clone();
+    pageBounds.top = Math.floor(this._screenToPage(bounds.top));
+    pageBounds.left = Math.floor(this._screenToPage(bounds.left));
+    pageBounds.bottom = Math.ceil(this._screenToPage(bounds.bottom));
+    pageBounds.right = Math.ceil(this._screenToPage(bounds.right));
+
+    if (0) {
+      if (true /*!oldBounds*/) {
+        this._pageX = pageBounds.x;
+        this._pageY = pageBounds.y;
+
+        var ctx = this._canvas.getContext("2d");
+
+        ctx.save();
+        ctx.scale(this._zoomLevel, this._zoomLevel);
+
+        try {
+          dump("drawWindow: " + pageBounds.x + " " + pageBounds.y + " " + pageBounds.width + " " + pageBounds.height + "\n");
+          ctx.drawWindow(this._browser.contentWindow,
+                         pageBounds.x, pageBounds.y, pageBounds.width, pageBounds.height,
+                         "white",
+                         ctx.DRAWWINDOW_DO_NOT_FLUSH);
+        } catch (e) {
+          dump("DRAWWINDOW FAILED\n");
+        }
+
+        ctx.restore();
+        return;
+      }
+    }
+
+    if (!oldBounds) {
+      // no old bounds means we resized the viewport, so redraw everything
+      this._screenX = bounds.x;
+      this._screenY = bounds.y;
+      this._pageX = pageBounds.x;
+      this._pageY = pageBounds.y;
+
+      return this._redrawRect(pageBounds.x, pageBounds.y,
+                              pageBounds.width, pageBounds.height);
+    }
+
+    let dx = this._screenX - bounds.x;
+    let dy = this._screenY - bounds.y;
+
+    let [soffX, soffY] = this._canvasPageOffset;
+
+    this._screenX = bounds.x;
+    this._screenY = bounds.y;
+    this._pageX = pageBounds.x;
+    this._pageY = pageBounds.y;
+
+    let [offX, offY] = this._drawOffset;
+
+    // take in to account the canvas offset when we blit
+    let [eoffX, eoffY] = this._canvasPageOffset;
+    let [coffX, coffY] = [Math.floor(this._pageToScreen(eoffX - soffX)),
+                          Math.floor(this._pageToScreen(eoffY - soffY))];
+
+    //dump("viewportHandler: " + bounds.toSource() + " " + oldBounds.toSource() + "\n");
+
+    //dump("page offset " + eoffX + " " + eoffY + "\n");
+
+    // deal with repainting
+    let srcRect = { x: 0, y: 0,
+                    width: this._canvas.width, height: this._canvas.height };
+    let dstRect = { x: dx - coffX, y: dy - coffY,
+                    width: this._canvas.width, height: this._canvas.height };
+
+    // we don't need to do anything if the source and destination are the same
+    if (srcRect.x == dstRect.x && srcRect.y == dstRect.y &&
+        srcRect.width == dstRect.width && srcRect.height == dstRect.height) {
+      dump("avoiding dumb paint\n");
+      return;
+    }
+
+    // blit what we can
+    var ctx = this._canvas.getContext("2d");
+    ctx.drawImage(this._canvas,
+                  srcRect.x, srcRect.y,
+                  srcRect.width, srcRect.height,
+                  dstRect.x, dstRect.y,
+                  dstRect.width, dstRect.height);
+
+    //dump("blitting " + srcRect.toSource() + " to " + dstRect.toSource() + "\n");
+
+    // redraw the rest
+    var rgn = Cc["@mozilla.org/gfx/region;1"].createInstance(Ci.nsIScriptableRegion);
+    rgn.setToRect(srcRect.x, srcRect.y, srcRect.width, srcRect.height);
+    rgn.subtractRect(dstRect.x, dstRect.y, dstRect.width, dstRect.height);
+
+    let outX = {}; let outY = {}; let outW = {}; let outH = {};
+    rgn.getBoundingBox(outX, outY, outW, outH);
+    dstRect = { x: outX.value, y: outY.value, width: outW.value, height: outH.value };
+
+    if (dstRect.width > 0 && dstRect.height > 0) {
+      dstRect.width += 1;
+      dstRect.height += 1;
+
+
+      //dump("redrawing: offset " + dstRect.x + " " + dstRect.y + "\n");
+
+      ctx.save();
+      ctx.translate(dstRect.x, dstRect.y);
+      ctx.scale(this._zoomLevel, this._zoomLevel);
+
+      var [offX, offY] = this._drawOffset;
+      let scaledRect = { x: offX + this._screenToPage(dstRect.x),
+                         y: offY + this._screenToPage(dstRect.y),
+                         width: this._screenToPage(dstRect.width),
+                         height: this._screenToPage(dstRect.height) };
+
+      //dump("            rect " + scaledRect.toSource() + "\n");
+
+      ctx.drawWindow(this._browser.contentWindow,
+                     scaledRect.x, scaledRect.y,
+                     scaledRect.width, scaledRect.height,
+                     "white",
+                     ctx.DRAWWINDOW_DO_NOT_FLUSH);
+
+      // for testing
+      //ctx.fillStyle = "rgba(255,0,0,0.5)";
+      //ctx.fillRect(0, 0, scaledRect.width, scaledRect.height);
+
+      ctx.restore();
+    }
+  },
+
+  get _canvasPageOffset() {
+    /*
+    let [canvasW, canvasH] = this._effectiveCanvasDimensions;
+    let [viewportW, viewportH] = this._effectiveViewportDimensions;
+    let offscreenCanvasW = (canvasW - viewportW);
+    let offscreenCanvasH = (canvasH - viewportH);
+    let [contentWidth, contentHeight] = this._contentAreaDimensions;
+
+    let [pageX, pageY] = this._pageOffset();
+
+    let left = Math.max(-pageX, -(offscreenCanvasW / 2));
+    let rightMost = (contentWidth - canvasW);
+    if (left > rightMost && rightMost > 0)
+      left = rightMost;
+
+    let top = Math.max(-pageY, -(offscreenCanvasH / 2));
+    let bottomMost = (contentHeight - canvasH);
+    if (top > bottomMost && bottomMost > 0)
+      top = bottomMost;
+
+    return [left, top];
+    */
+    return [0, 0];
+  },
+
+  get _drawOffset() {
+    let [offX, offY] = this._canvasPageOffset;
+    let [pageX, pageY] = this._pageOffset();
+
+    //dump(offX + " " + offY + " " + pageX + " " + pageY + "\n");
+    return [pageX + offX, pageY + offY];
+  },
+
+  _handleMozAfterPaint: function(aEvent) {
+    let cwin = this._browser.contentWindow;
+
+    for (let i = 0; i < aEvent.clientRects.length; i++) {
+      let e = aEvent.clientRects.item(i);
+      //dump(Math.floor(e.left + cwin.scrollX),
+      //     Math.floor(e.top + cwin.scrollY),
+      //     Math.ceil(e.width), Math.ceil(e.height));
+      this._redrawRect(Math.floor(e.left + cwin.scrollX),
+                       Math.floor(e.top + cwin.scrollY),
+                       Math.ceil(e.width), Math.ceil(e.height));
+    }
+  },
+
+  _redrawRect: function(x, y, width, height) {
+    function intersect(r1, r2) {
+      let xmost1 = r1.x + r1.width;
+      let ymost1 = r1.y + r1.height;
+      let xmost2 = r2.x + r2.width;
+      let ymost2 = r2.y + r2.height;
+
+      let x = Math.max(r1.x, r2.x);
+      let y = Math.max(r1.y, r2.y);
+
+      let temp = Math.min(xmost1, xmost2);
+      if (temp <= x)
+        return null;
+
+      let width = temp - x;
+
+      temp = Math.min(ymost1, ymost2);
+      if (temp <= y)
+        return null;
+
+      let height = temp - y;
+
+      return { x: x,
+               y: y,
+               width: width,
+               height: height };
+    }
+
+    let r1 = { x : x,
+               y : y,
+               width : width,
+               height: height };
+
+    // check to see if the input coordinates are inside the visiable destination
+    let [canvasW, canvasH] = this._effectiveCanvasDimensions;
+    let r2 = { x : this._pageX,
+               y : this._pageY,
+               width : canvasW,
+               height: canvasH };
+
+    let dest = intersect(r1, r2);
+
+    if (!dest)
+      return;
+
+    //dump(dest.toSource() + "\n");
+
+    var ctx = this._canvas.getContext("2d");
+
+    ctx.save();
+    ctx.scale(this._zoomLevel, this._zoomLevel);
+
+    var [offX, offY] = this._drawOffset;
+    //dump("offx, offy: " + offX + " " + offY + "\n");
+    ctx.translate(dest.x - offX, dest.y - offY);
+
+    //dump("drawWindow#2: " + dest.x + " " + dest.y + " " + dest.width + " " + dest.height + " @ " + (dest.x - offX) + " " + (dest.y - offY) + "\n");
+    ctx.drawWindow(this._browser.contentWindow,
+                   dest.x, dest.y,
+                   dest.width, dest.height,
+                   "white",
+                   ctx.DRAWWINDOW_DO_NOT_FLUSH);
+
+    ctx.restore();
+  },
+
+  _clampZoomLevel: function(aZoomLevel) {
+    const min = 0.2;
+    const max = 2.0;
+
+    return Math.min(Math.max(min, aZoomLevel), max);
+  },
+
+  set zoomLevel(val) {
+    this._zoomLevel = this._clampZoomLevel(val);
+    Browser.updateViewportSize();
+  },
+
+  get zoomLevel() {
+    return this._zoomLevel;
+  },
+
+  zoom: function(aDirection) {
+    if (aDirection == 0)
+      return;
+
+    var zoomDelta = 0.05; // 1/20
+    if (aDirection >= 0)
+      zoomDelta *= -1;
+
+    this.zoomLevel = this._zoomLevel + zoomDelta;
+  },
+
+  zoomToPage: function() {
+    //dump("zoom to page\n");
+    // Adjust the zoomLevel to fit the page contents in our window
+    // width
+    let [contentW, ] = this._contentAreaDimensions;
+    let [viewportW, ] = this.viewportDimensions;
+
+    if (contentW > viewportW)
+      this.zoomLevel = viewportW / contentW;
+  },
+
+  zoomToElement: function(aElement) {
+    const margin = 15;
+
+    // XXX The widget stack code doesn't do what we want when you change
+    // the viewport bounds to something smaller than your current position
+    // so pan back to 0,0 before we resize and then pan to our destination
+    ws.panTo(0, 0);
+
+    // scale to the element's width
+    let [viewportW, ] = this.viewportDimensions;
+
+    let elRect = this._getPagePosition(aElement);
+    let zoomLevel = viewportW / (elRect.width + (2 * margin));
+    this.zoomLevel = Math.min(zoomLevel, 10);
+
+    // pan to the element
+    ws.panTo(Math.floor(Math.max(this._pageToScreen(elRect.x) - margin, 0)),
+             Math.floor(Math.max(this._pageToScreen(elRect.y) - margin, 0)));
+  },
+
+  zoomFromElement: function(aElement) {
+    let elRect = this._getPagePosition(aElement);
+
+    // XXX The widget stack code doesn't do what we want when you change
+    // the viewport bounds to something smaller than your current position
+    // so pan back to 0,0 before we resize and then pan to our destination
+    ws.panTo(0, 0);
+
+    // pan to the element
+    // don't bother with x since we're zooming all the way out
+    this.zoomToPage();
+
+    // XXX have this center the element on the page
+    ws.panTo(0, Math.floor(Math.max(0, this._pageToScreen(elRect.y))));
+  },
+
+  /**
+   * Retrieve the content element for a given point in client coordinates
+   * (relative to the top left corner of the chrome window).
+   */
+  elementFromPoint: function(aX, aY) {
+    let [x, y] = this._clientToContentCoords(aX, aY);
+    let cwu = this._browser.contentWindow
+                  .QueryInterface(Components.interfaces.nsIInterfaceRequestor)
+                  .getInterface(Components.interfaces.nsIDOMWindowUtils);
+
+    let element = cwu.elementFromPoint(x, y,
+                                       true,   /* ignore root scroll frame*/
+                                       false); /* don't flush layout */
+
+    return element;
+  },
+
+  /**
+   * Retrieve the page position for a given element
+   * (relative to the document origin).
+   */
+  _getPagePosition: function(aElement) {
+    let r = aElement.getBoundingClientRect();
+    let cwin = this._browser.contentWindow;
+    let retVal = {
+      width: r.width,
+      height: r.height,
+      x: r.left + cwin.scrollX,
+      y: r.top + cwin.scrollY
+    };
+
+    return retVal;
+  },
+
+  _pageOffset: function() {
+    //    return [this._screenToPage(ws._viewport.viewportInnerBounds.x),
+    //            this._screenToPage(ws._viewport.viewportInnerBounds.y)];
+    return [this._pageX, this._pageY];
+  },
+
+  /* Given a set of client coordinates (relative to the app window),
+   * returns the content coordinates relative to the viewport.
+   */
+  _clientToContentCoords: function(aClientX, aClientY) {
+    // Determine position relative to the document origin
+    // Need to adjust for the deckbrowser not being at 0,0
+    // (e.g. due to other browser UI)
+
+    let browserRect = this._canvas.getBoundingClientRect();
+    let clickOffsetX = this._screenToPage(aClientX - browserRect.left) + this._pageX;
+    let clickOffsetY = this._screenToPage(aClientY - browserRect.top) + this._pageY;
+
+    // Take scroll offset into account to return coordinates relative to the viewport
+    let cwin = this._browser.contentWindow;
+    return [clickOffsetX - cwin.scrollX,
+            clickOffsetY - cwin.scrollY];
+  },
+
+  get _contentAreaDimensions() {
+    var cdoc = this._browser.contentDocument;
+
+    // Return the document width/height for XUL documents (which is
+    // essentially the same as the viewport width/height).
+    if (cdoc instanceof XULDocument)
+      return [cdoc.width, cdoc.height];
+
+    if (cdoc instanceof SVGDocument) {
+      let rect = cdoc.rootElement.getBoundingClientRect();
+      return [rect.width, rect.height];
+    }
+
+    // These might not exist yet depending on page load state
+    var body = cdoc.body || {};
+    var html = cdoc.documentElement || {};
+
+    var w = Math.max(body.scrollWidth, html.scrollWidth);
+    var h = Math.max(body.scrollHeight, html.scrollHeight);
+
+    if (isNaN(w) || isNaN(h) || w == 0 || h == 0)
+      return [this._canvas.width, this._canvas.height];
+
+    return [w, h];
+  },
+
+  _screenToPage: function(aValue) {
+    return aValue / this._zoomLevel;
+  },
+
+  _pageToScreen: function(aValue) {
+    return aValue * this._zoomLevel;
+  },
+
+  /* ensures that a given content element is visible */
+  ensureElementIsVisible: function(aElement) {
+    let elRect = this._getPagePosition(aElement);
+    let [viewportW, viewportH] = this._effectiveViewportDimensions;
+    let curRect = {
+      x: this._pageX,
+      y: this._pageY,
+      width: viewportW,
+      height: viewportH
+    };
+
+    // Adjust for part of our viewport being offscreen
+    // XXX this assumes that the browser is meant to be fullscreen
+    let browserRect = this._currentBrowser.getBoundingClientRect();
+    curRect.height -= this._screenToPage(Math.abs(browserRect.top));
+    if (browserRect.top < 0)
+      curRect.y -= this._screenToPage(browserRect.top);
+    curRect.width -= this._screenToPage(Math.abs(browserRect.left));
+    if (browserRect.left < 0)
+      curRect.x -= this._screenToPage(browserRect.left);
+
+    let newx = curRect.x;
+    let newy = curRect.y;
+
+    if (elRect.x + elRect.width > curRect.x + curRect.width) {
+      newx = curRect.x + ((elRect.x + elRect.width)-(curRect.x + curRect.width));
+    } else if (elRect.x < curRect.x) {
+      newx = elRect.x;
+    }
+
+    if (elRect.y + elRect.height > curRect.y + curRect.height) {
+      newy = curRect.y + ((elRect.y + elRect.height)-(curRect.y + curRect.height));
+    } else if (elRect.y < curRect.y) {
+      newy = elRect.y;
+    }
+
+    this.panTo(newx, newy);
+  },
+
+  /* Pans directly to a given content element */
+  panToElement: function(aElement) {
+    var elRect = this._getPagePosition(aElement);
+
+    this.panTo(elRect.x, elRect.y);
+  },
+
+  panTo: function(x, y) {
+    ws.panTo(x, y);
+  }
+};
new file mode 100644
--- /dev/null
+++ b/chrome/content/InputHandler.js
@@ -0,0 +1,646 @@
+// -*- Mode: js2; tab-width: 2; indent-tabs-mode: nil; js2-basic-offset: 2; js2-skip-preprocessor-directives: t; -*-
+/*
+ * ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Mozilla Mobile Browser.
+ *
+ * The Initial Developer of the Original Code is
+ * Mozilla Corporation.
+ * Portions created by the Initial Developer are Copyright (C) 2008
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *   Stuart Parmenter <stuart@mozilla.com>
+ *   Brad Lassey <blassey@mozilla.com>
+ *   Mark Finkle <mfinkle@mozilla.com>
+ *   Gavin Sharp <gavin.sharp@gmail.com>
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+
+/**
+ * Everything that is registed in _modules gets called with each event that the
+ * InputHandler is registered to listen for.
+ *
+ * When one of the handlers decides it wants to handle the event, it should call
+ * grab() on its owner which will cause it to receive all of the events until it
+ * calls ungrab().  Calling grab will notify the other handlers via a
+ * cancelPending() notification.  This tells them to stop what they're doing and
+ * give up hope for being the one to process the events.
+ */
+
+function InputHandler() {
+  let stack = document.getElementById("browser-container");
+  stack.addEventListener("DOMMouseScroll", this, true);
+
+  let content = document.getElementById("canvas");
+  content.addEventListener("mouseout", this, true);
+  content.addEventListener("mousedown", this, true);
+  content.addEventListener("mouseup", this, true);
+  content.addEventListener("mousemove", this, true);
+  content.addEventListener("keydown", this, true);
+  content.addEventListener("keyup", this, true);
+
+  let prefsvc = Components.classes["@mozilla.org/preferences-service;1"].
+    getService(Components.interfaces.nsIPrefBranch2);
+  let allowKinetic = prefsvc.getBoolPref("browser.ui.panning.kinetic");
+  //let allowKinetic = false;
+
+  if (allowKinetic)
+    this._modules.push(new KineticPanningModule(this));
+  else
+    this._modules.push(new PanningModule(this));
+
+  this._modules.push(new ClickingModule(this));
+  this._modules.push(new ScrollwheelModule(this));
+}
+
+InputHandler.prototype = {
+  _modules : [],
+  _grabbed : null,
+
+  grab: function(obj) {
+    //dump("grabbing\n");
+    this._grabbed = obj;
+
+    for each(mod in this._modules) {
+      if (mod != obj)
+        mod.cancelPending();
+    }
+    // only send events to this object
+    // call cancel on all modules
+  },
+
+  ungrab: function(obj) {
+    //dump("unggrabbing\n");
+    this._grabbed = null;
+    // only send events to this object
+    // call cancel on all modules
+  },
+
+  handleEvent: function (aEvent) {
+    if (this._grabbed) {
+      this._grabbed.handleEvent(aEvent);
+    } else {
+      for each(mod in this._modules)
+        mod.handleEvent(aEvent);
+    }
+  }
+};
+
+
+/**
+ * Kinetic panning code
+ */
+
+function KineticPanningModule(owner) {
+  this._owner = owner;
+}
+
+KineticPanningModule.prototype = {
+  _owner: null,
+  _dragData: {
+    dragging: false,
+    sX: 0,
+    sY: 0,
+    dragStartTimeout: -1,
+
+    reset: function() {
+      this.dragging = false;
+      this.sX = 0;
+      this.sY = 0;
+      if (this.dragStartTimeout != -1)
+        clearTimeout(this.dragStartTimeout);
+      this.dragStartTimeout = -1;
+    }
+  },
+
+  _kineticData: {
+    // const
+    kineticStepSize: 15,
+    kineticDecelloration: 0.004,
+    momentumBufferSize: 3,
+
+    momentumBuffer: [],
+    momentumBufferIndex: 0,
+    lastTime: 0,
+    kineticDuration: 0,
+    kineticDirX: 0,
+    kineticDirY: 0,
+    kineticHandle : -1,
+    kineticStep : 0,
+    kineticStartX : 0,
+    kineticStartY : 0,
+    kineticInitialVel: 0,
+
+    reset: function() {
+      if (this.kineticHandle != -1) {
+        window.clearInterval(this.kineticHandle);
+	this.kineticHandle = -1;
+      }
+
+      this.momentumBuffer = [];
+      this.momentumBufferIndex = 0;
+      this.lastTime = 0;
+      this.kineticDuration = 0;
+      this.kineticDirX = 0;
+      this.kineticDirY = 0;
+      this.kineticStep  = 0;
+      this.kineticStartX  = 0;
+      this.kineticStartY  = 0;
+      this.kineticInitialVel = 0;
+    }
+  },
+
+  handleEvent: function(aEvent) {
+    switch (aEvent.type) {
+      case "mousedown":
+        return this._onMouseDown(aEvent);
+        break;
+      case "mousemove":
+        return this._onMouseMove(aEvent);
+      case "mouseout":
+      case "mouseup":
+        return this._onMouseUp(aEvent);
+    }
+  },
+
+  /* If someone else grabs events ahead of us, cancel any pending
+   * timeouts we may have.
+   */
+  cancelPending: function() {
+    this._dragData.reset();
+    // XXX we should cancel kinetic here as well
+    //dump("canceling drag\n");
+  },
+
+  _dragStart: function(sX, sY) {
+    this._dragData.dragging = true;
+    this._dragData.dragStartTimeout = -1;
+
+    // grab all events until we stop the drag
+    this._owner.grab(this);
+
+    ws.dragStart(sX, sY);
+
+    // set the kinetic start time
+    this._kineticData.lastTime = Date.now();
+  },
+
+  _dragStop: function(sX, sY) {
+    // start kinetic scrolling here.
+    if (!this._startKinetic(sX, sY)) {
+      this._endKinetic(sX, sY);
+    }
+  },
+
+  _dragMove: function(sX, sY) {
+    ws.dragMove(sX, sY);
+  },
+
+  _onMouseDown: function(aEvent) {
+    // if we're in the process of kineticly scrolling, stop and start over
+    if (this.kineticHandle != -1)
+      this._endKinetic(aEvent.screenX, aEvent.screenY);
+
+    let dragData = this._dragData;
+
+    dragData.sX = aEvent.screenX;
+    dragData.sY = aEvent.screenY;
+
+    dragData.dragStartTimeout = setTimeout(function(self, sX, sY) { self._dragStart(sX, sY) },
+                                           200, this, aEvent.screenX, aEvent.screenY);
+  },
+  _onMouseUp: function(aEvent) {
+    let dragData = this._dragData;
+
+    if (dragData.dragging)
+      this._dragStop(aEvent.screenX, aEvent.screenY);
+    else
+      this._dragData.reset(); // be sure to reset the timer
+  },
+
+  _onMouseMove: function(aEvent) {
+    // don't do anything if we're in the process of kineticly scrolling
+    if (this._kineticData.kineticHandle != -1)
+      return;
+
+    let dragData = this._dragData;
+
+    let dx = dragData.sX - aEvent.screenX;
+    let dy = dragData.sY - aEvent.screenY;
+
+    if (!dragData.dragging && dragData.dragStartTimeout != -1) {
+      if ((Math.abs(dx*dx) + Math.abs(dy*dy)) > 100) {
+        clearTimeout(dragData.dragStartTimeout);
+        this._dragStart(aEvent.screenX, aEvent.screenY);
+      }
+    }
+    if (!dragData.dragging)
+      return;
+
+    this._dragMove(aEvent.screenX, aEvent.screenY);
+
+    dragData.sX = aEvent.screenX;
+    dragData.sY = aEvent.screenY;
+
+    // update our kinetic data
+    let kineticData = this._kineticData;
+    let t = Date.now();
+    let dt = t - kineticData.lastTime;
+    kineticData.lastTime = t;
+    let momentumBuffer = { dx: -dx, dy: -dy, dt: dt }
+
+    kineticData.momentumBuffer[kineticData.momentumBufferIndex] = momentumBuffer;
+    kineticData.momentumBufferIndex++;
+    kineticData.momentumBufferIndex %= kineticData.momentumBufferSize;
+  },
+
+  _startKinetic: function(sX, sY) {
+    let kineticData = this._kineticData;
+
+    let dx = 0;
+    let dy = 0;
+    let dt = 0;
+    if (kineticData.kineticInitialVel)
+      return true;
+
+    if (!kineticData.momentumBuffer)
+      return false;
+
+    for (let i = 0; i < kineticData.momentumBufferSize; i++) {
+      let me = kineticData.momentumBuffer[(kineticData.momentumBufferIndex + i) % kineticData.momentumBufferSize];
+      if (!me)
+	return false;
+
+      dx += me.dx;
+      dy += me.dy;
+      dt += me.dt;
+    }
+    if (dt <= 0)
+      return false;
+
+    let dist = Math.sqrt(dx*dx+dy*dy);
+    let vel  = dist/dt;
+    if (vel < 1)
+      return false;
+
+    kineticData.kineticDirX = dx/dist;
+    kineticData.kineticDirY = dy/dist;
+    if (kineticData.kineticDirX > 0.9) {
+      kineticData.kineticDirX = 1;
+      kineticData.kineticDirY = 0;
+    } else if (kineticData.kineticDirY < -0.9) {
+      kineticData.kineticDirX = 0;
+      kineticData.kineticDirY = -1;
+    } else if (kineticData.kineticDirX < -0.9) {
+      kineticData.kineticDirX = -1;
+      kineticData.kineticDirY = 0;
+    } else if (kineticData.kineticDirY > 0.9) {
+      kineticData.kineticDirX = 0;
+      kineticData.kineticDirY = 1;
+    }
+    
+    kineticData.kineticDuration = vel/(2 * kineticData.kineticDecelloration);
+    kineticData.kineticStep = 0;
+    kineticData.kineticStartX =  sX;
+    kineticData.kineticStartY =  sY;
+    kineticData.kineticInitialVel = vel;
+    kineticData.kineticHandle = window.setInterval(this._doKinetic, kineticData.kineticStepSize, this);
+    return true;
+  },
+  
+  _doKinetic: function(self) {
+    let kineticData = self._kineticData;
+
+    let t = kineticData.kineticStep * kineticData.kineticStepSize;
+    kineticData.kineticStep++;
+    if (t > kineticData.kineticDuration)
+      t = kineticData.kineticDuration;
+    let dist = kineticData.kineticInitialVel * t -
+               kineticData.kineticDecelloration * t * t; 
+    let newX = Math.floor(kineticData.kineticDirX * dist + kineticData.kineticStartX);
+    let newY = Math.floor(kineticData.kineticDirY * dist + kineticData.kineticStartY);
+
+    self._dragMove(newX, newY);
+
+    if(t >= kineticData.kineticDuration)
+      self._endKinetic(newX, newY);
+  },
+
+  _endKinetic: function(sX, sY) {
+    ws.dragStop(sX, sY);
+    this._owner.ungrab(this);
+    this._dragData.reset();
+    this._kineticData.reset();
+  },
+};
+
+
+/**
+ * Regular non-kinetic panning code
+ */
+
+function PanningModule(owner) {
+  this._owner = owner;
+}
+
+PanningModule.prototype = {
+  _owner: null,
+  _dragData: {
+    dragging: false,
+    sX: 0,
+    sY: 0,
+    dragStartTimeout: -1,
+
+    reset: function() {
+      this.dragging = false;
+      this.sX = 0;
+      this.sY = 0;
+      if (this.dragStartTimeout != -1)
+        clearTimeout(this.dragStartTimeout);
+      this.dragStartTimeout = -1;
+    }
+  },
+
+  handleEvent: function(aEvent) {
+    switch (aEvent.type) {
+      case "mousedown":
+        return this._onMouseDown(aEvent);
+        break;
+      case "mousemove":
+        return this._onMouseMove(aEvent);
+      case "mouseout":
+      case "mouseup":
+        return this._onMouseUp(aEvent);
+    }
+  },
+
+  /* If someone else grabs events ahead of us, cancel any pending
+   * timeouts we may have.
+   */
+  cancelPending: function() {
+    this._dragData.reset();
+    //dump("canceling drag\n");
+  },
+
+  _dragStart: function(sX, sY) {
+    //dump("starting drag\n");
+    this._dragData.dragging = true;
+    this._dragData.dragStartTimeout = -1;
+
+    // grab all events until we stop the drag
+    this._owner.grab(this);
+
+    ws.dragStart(sX, sY);
+  },
+
+  _dragStop: function(sX, sY) {
+    //dump("ending drag\n");
+    this._dragData.reset();
+
+    ws.dragStop(sX, sY);
+
+    this._owner.ungrab(this);
+  },
+
+  _dragMove: function(sX, sY) {
+    //dump("moving drag" + sX + " " + sY + "\n");
+    ws.dragMove(sX, sY);
+  },
+
+  _onMouseDown: function(aEvent) {
+    let dragData = this._dragData;
+
+    dragData.sX = aEvent.screenX;
+    dragData.sY = aEvent.screenY;
+
+    dragData.dragStartTimeout = setTimeout(function(self, sX, sY) { self._dragStart(sX, sY) },
+                                           200, this, aEvent.screenX, aEvent.screenY);
+  },
+
+  _onMouseUp: function(aEvent) {
+    let dragData = this._dragData;
+
+    if (dragData.dragging)
+      this._dragStop(aEvent.screenX, aEvent.screenY);
+    else
+      this._dragData.reset(); // be sure to reset the timer
+  },
+
+  _onMouseMove: function(aEvent) {
+    let dragData = this._dragData;
+
+    let dx = dragData.sX - aEvent.screenX;
+    let dy = dragData.sY - aEvent.screenY;
+
+    if (!dragData.dragging && dragData.dragStartTimeout != -1) {
+      if ((Math.abs(dx*dx) + Math.abs(dy*dy)) > 100) {
+        clearTimeout(dragData.dragStartTimeout);
+        this._dragStart(aEvent.screenX, aEvent.screenY);
+      }
+    }
+    if (!dragData.dragging)
+      return;
+
+    this._dragMove(aEvent.screenX, aEvent.screenY);
+
+    dragData.sX = aEvent.screenX;
+    dragData.sY = aEvent.screenY;
+  }
+};
+
+/**
+ * Mouse click handlers
+ */
+
+function ClickingModule(owner) {
+  this._owner = owner;
+}
+
+
+ClickingModule.prototype = {
+  _clickTimeout : -1,
+  _events : [],
+  _zoomed : false,
+
+  handleEvent: function (aEvent) {
+    switch (aEvent.type) {
+      // UI panning events
+      case "mousedown":
+        //dump("mousedown\n");
+        this._events.push({event: aEvent, time: Date.now()});
+
+        // we're waiting for a click
+        if (this._clickTimeout != -1) {
+          // if we just got another mousedown, don't send anything until we get another mousedown
+          clearTimeout(this._clickTimeout);
+          this.clickTimeout = -1
+        }
+        break;
+      case "mouseup":
+        // keep an eye out for mouseups that didn't start with a mousedown
+        if (!(this._events.length % 2)) {
+          this._reset();
+          break;
+        }
+        
+        //dump("mouseup\n");
+        this._events.push({event: aEvent, time: Date.now()});
+
+        if (this._clickTimeout == -1) {
+          this._clickTimeout = setTimeout(function(self) { self._sendSingleClick() }, 400, this);
+        } else {
+          clearTimeout(this._clickTimeout);
+          this._sendDoubleClick();
+        }
+        break;
+      case "mouseout":
+        this._reset();
+        break;
+    }
+  },
+
+  /* If someone else grabs events ahead of us, cancel any pending
+   * timeouts we may have.
+   */
+  cancelPending: function() {
+    //dump("canceling click\n");
+
+    this._reset();
+  },
+
+  _reset: function() {
+    if (this._clickTimeout != -1)
+      clearTimeout(this._clickTimeout);
+    this._clickTimeout = -1;
+
+    this._events = [];
+  },
+
+  _sendSingleClick: function() {
+    this._owner.grab(this);
+    this._redispatchMouseEvent(this._events[0].event);
+    this._redispatchMouseEvent(this._events[1].event);
+    this._owner.ungrab(this);
+
+    this._reset();
+  },
+
+  _sendDoubleClick: function() {
+    this._owner.grab(this);
+
+    // XXX disable zooming until it works properly.
+
+    function optimalElementForPoint(cX, cY) {
+      var element = Browser.content.elementFromPoint(cX, cY);
+      if (!element)
+	return null;
+
+      // Find the nearest non-inline ancestor
+      while (element.parentNode) {
+        let display = window.getComputedStyle(element, "").getPropertyValue("display");
+        let zoomable = /table/.test(display) || /block/.test(display);
+        if (zoomable)
+          break;
+
+        element = element.parentNode;
+      }
+      return element;
+    }
+
+    let firstEvent = this._events[0].event;
+    let zoomElement = optimalElementForPoint(firstEvent.clientX, firstEvent.clientY);
+
+    if (zoomElement) {
+      if (this._zoomed) {
+	// zoom out
+	this._zoomed = false;
+	Browser.content.zoomFromElement(zoomElement);
+      } else {
+	// zoom in
+	this._zoomed = true;
+	Browser.content.zoomToElement(zoomElement);
+      }
+
+    }
+
+    this._owner.ungrab(this);
+
+    this._reset();
+  },
+
+
+  _redispatchMouseEvent: function(aEvent, aType) {
+    if (!(aEvent instanceof MouseEvent)) {
+      Components.utils.reportError("_redispatchMouseEvent called with a non-mouse event");
+      return;
+    }
+
+    var [x, y] = Browser.content._clientToContentCoords(aEvent.clientX, aEvent.clientY);
+    //dump("sending mouse event to: " + x + " " + y + "\n");
+
+    var cwin = Browser.currentBrowser.contentWindow;
+    var cwu = cwin.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
+                  .getInterface(Components.interfaces.nsIDOMWindowUtils);
+
+    // Redispatch the mouse event, ignoring the root scroll frame
+    cwu.sendMouseEvent(aType || aEvent.type,
+                       x, y,
+                       aEvent.button || 0,
+                       aEvent.detail || 1,
+                       0, true);
+  }
+};
+
+
+/**
+ * Scrollwheel zooming handler
+ */
+
+function ScrollwheelModule(owner) {
+  this._owner = owner;
+}
+
+ScrollwheelModule.prototype = {
+  handleEvent: function (aEvent) {
+    switch (aEvent.type) {
+      // UI panning events
+      case "DOMMouseScroll":
+        this._owner.grab(this);
+        this.deckbrowser.zoom(aEvent.detail);
+        this._owner.ungrab(this);
+        break;
+    }
+  },
+
+  /* If someone else grabs events ahead of us, cancel any pending
+   * timeouts we may have.
+   */
+  cancelPending: function() {
+  }
+};
+
new file mode 100644
--- /dev/null
+++ b/chrome/content/WidgetStack.js
@@ -0,0 +1,1464 @@
+/* -*- Mode: js2; tab-width: 40; indent-tabs-mode: nil; js2-basic-offset: 2; js2-skip-preprocessor-directives: t; -*- */
+/*
+ * ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Mozilla Mobile Browser.
+ *
+ * The Initial Developer of the Original Code is
+ * Mozilla Corporation.
+ * Portions created by the Initial Developer are Copyright (C) 2008
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *   Vladimir Vukicevic <vladimir@pobox.com>
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+var gWsDoLog = false;
+var gWsLogDiv = null;
+
+function logbase() {
+  if (!gWsDoLog)
+    return;
+
+  if (gWsLogDiv == null && "console" in window) {
+    console.log.apply(console, arguments);
+  } else {
+    var s = "";
+    for (var i = 0; i < arguments.length; i++) {
+      s += arguments[i] + " ";
+    }
+    s += "\n";
+    if (gWsLogDiv) {
+      gWsLogDiv.appendChild(document.createElementNS("http://www.w3.org/1999/xhtml", "br"));
+      gWsLogDiv.appendChild(document.createTextNode(s));
+    }
+
+    dump(s);
+  }
+}
+
+function log() {
+  return;
+  logbase.apply(window, arguments);
+}
+
+function log2() {
+  return;
+  logbase.apply(window, arguments);
+}
+
+let reportError = log;
+
+/*
+ * wsBorder class
+ *
+ * Simple container for top,left,bottom,right "border" values
+ */
+function wsBorder(t, l, b, r) {
+  this.setBorder(t, l, b, r);
+}
+
+wsBorder.prototype = {
+  _t: 0, _l: 0, _b: 0, _r: 0,
+
+  get left() { return this._l; },
+  get right() { return this._r; },
+  get top() { return this._t; },
+  get bottom() { return this._b; },
+
+  set left(v) { this._l = v; },
+  set right(v) { this._r = v; },
+  set top(v) { this._t = v; },
+  set bottom(v) { this._b = v; },
+
+  setBorder: function (t, l, b, r) {
+    this._t = t;
+    this._l = l;
+    this._b = b;
+    this._r = r;
+  },
+
+  toString: function () {
+    return "[l:" + this._l + ",t:" + this._t + ",r:" + this._r + ",b:" + this._b + "]";
+  }
+};
+
+/*
+ * wsRect class
+ *
+ * Rectangle class, with both x/y/w/h and t/l/b/r accessors.
+ */
+function wsRect(x, y, w, h) {
+  this.setRect(x, y, w, h);
+}
+
+wsRect.prototype = {
+  _l: 0, _t: 0, _b: 0, _r: 0,
+
+  get x() { return this._l; },
+  get y() { return this._t; },
+  get width() { return this._r - this._l; },
+  get height() { return this._b - this._t; },
+  get w() { return this.width; },
+  get h() { return this.height; },
+
+  set x(v) { let w = this.w; this._l = v; this._r = v + w; },
+  set y(v) { let h = this.h; this._t = v; this._b = v + h; },
+  set width(v) { this._r = this._l + v; },
+  set height(v) { this._b = this._t + v; },
+  set w(v) { this.w = v; },
+  set h(v) { this.h = v; },
+
+  get left() { return this._l; },
+  get right() { return this._r; },
+  get top() { return this._t; },
+  get bottom() { return this._b; },
+
+  set left(v) { this._l = v; },
+  set right(v) { this._r = v; },
+  set top(v) { this._t = v; },
+  set bottom(v) { this._b = v; },
+
+  setRect: function(x, y, w, h) {
+    this._l = x;
+    this._t = y;
+    this._r = x+w;
+    this._b = y+h;
+
+    return this;
+  },
+
+  setBounds: function(t, l, b, r) {
+    this._t = t;
+    this._l = l;
+    this._b = b;
+    this._r = r;
+
+    return this;
+  },
+
+  clone: function() {
+    return new wsRect(this.x, this.y, this.width, this.height);
+  },
+
+  copyFrom: function(r) {
+    this._t = r._t;
+    this._l = r._l;
+    this._b = r._b;
+    this._r = r._r;
+
+    return this;
+  },
+
+  copyFromTLBR: function(r) {
+    this._l = r.left;
+    this._t = r.top;
+    this._r = r.right;
+    this._b = r.bottom;
+
+    return this;
+  },
+
+  translate: function(x, y) {
+    this._l += x;
+    this._r += x;
+    this._t += y;
+    this._b += y;
+
+    return this;
+  },
+
+  // return a new wsRect that is the union of that one and this one
+  union: function(rect) {
+    let l = Math.min(this._l, rect._l);
+    let r = Math.max(this._r, rect._r);
+    let t = Math.min(this._t, rect._t);
+    let b = Math.max(this._b, rect._b);
+
+    return new wsRect(l, t, r-l, b-t);
+  },
+
+  toString: function() {
+    return "[" + this.x + "," + this.y + "," + this.width + "," + this.height + "]";
+  },
+
+  expandBy: function(b) {
+    this._l += b.left;
+    this._r += b.right;
+    this._t += b.top;
+    this._b += b.bottom;
+    return this;
+  },
+
+  contains: function(other) {
+    return !!(other._l >= this._l &&
+              other._r <= this._r &&
+              other._t >= this._t &&
+              other._b <= this._b);
+  },
+
+  intersects: function(other) {
+    let xok = (other._l > this._l && other._l < this._r) ||
+      (other._r > this._l && other._r < this._r) ||
+      (other._l <= this._l && other._r >= this._r);
+    let yok = (other._t > this._t && other._t < this._b) ||
+      (other._b > this._t && other._b < this._b) ||
+      (other._t <= this._t && other._t >= this._b);
+    return xok && yok;
+  }
+};
+
+/*
+ * The "Widget Stack"
+ *
+ * Manages a <xul:stack>'s children, allowing them to be dragged around
+ * the stack, subject to specified constraints.  Optionally supports
+ * one widget designated as the viewport, which can be panned over a virtual
+ * area without needing to draw that area entirely.  The viewport widget
+ * is designated by a 'viewport' attribute on the child element.
+ *
+ * Widgets are subject to various constraints, specified in xul via the
+ * 'constraint' attribute.  Current constraints are:
+ *   ignore-x: When panning, ignore any changes to the widget's x position
+ *   ignore-y: When panning, ignore any changes to the widget's y position
+ *   vp-relative: This widget's position should be claculated relative to
+ *     the viewport widget.  It will always keep the same offset from that
+ *     widget as initially laid out, regardless of changes to the viewport
+ *     bounds.
+ *   frozen: This widget is in a fixed position and should never pan.
+ */
+function WidgetStack(el, ew, eh) {
+  this.init(el, ew, eh);
+}
+
+WidgetStack.prototype = {
+  // the <stack> element
+  _el: null,
+
+  // object indexed by widget id, with state struct for each object (see _addNewWidget)
+  _widgetState: null,
+
+  // any barriers
+  _barriers: null,
+
+  // If a viewport widget is present, this will point to its state object;
+  // otherwise null.
+  _viewport: null,
+
+  // a wsRect; the inner bounds of the viewport content
+  _viewportBounds: null,
+  // a wsBorder; the overflow area to the side of the bounds where our
+  // viewport-relative widgets go
+  _viewportOverflow: null,
+
+  // a wsRect; the viewportBounds expanded by the viewportOverflow
+  _pannableBounds: null,
+
+  // a wsRect; the currently visible part of pannableBounds.
+  _viewingRect: null,
+
+  // the amount of current global offset applied to all widgets (whether
+  // static or not).  Set via offsetAll().  Can be used to push things
+  // out of the way for overlaying some other UI.
+  globalOffsetX: 0,
+  globalOffsetY: 0,
+
+  // if true (default), panning is constrained to the pannable bounds.
+  _constrainToViewport: true,
+
+  _viewportUpdateInterval: -1,
+  _viewportUpdateTimeout: -1,
+
+  _viewportUpdateHandler: null,
+  _panHandler: null,
+
+  _dragState: null,
+
+  _rectSanityCheck: true,
+
+  //
+  // init:
+  //   el: the <stack> element whose children are to be managed
+  //
+  init: function (el, ew, eh) {
+    this._el = el;
+    this._widgetState = {};
+    this._barriers = [];
+
+    let rect = this._el.getBoundingClientRect();
+    let width = rect.right - rect.left;
+    let height = rect.bottom - rect.top;
+
+    if (ew != undefined && eh != undefined) {
+      width = ew;
+      height = eh;
+    }
+
+    this._viewportOverflow = new wsBorder(0, 0, 0, 0);
+
+    this._viewingRect = new wsRect(0, 0, width, height);
+
+    log ("viewingRect:", this._viewingRect);
+
+    // listen for DOMNodeInserted/DOMNodeRemoved/DOMAttrModified
+    let children = this._el.childNodes;
+    for (let i = 0; i < children.length; i++) {
+      let c = this._el.childNodes[i];
+      if (c.tagName == "spacer")
+        this._addNewBarrierFromSpacer(c);
+      else
+        this._addNewWidget(c);
+    }
+
+    if (this._viewport) {
+      this._viewportBounds = new wsRect(0, 0, this._viewport.rect.width, this._viewport.rect.height);
+    } else {
+      this._viewportBounds = new wsRect(0, 0, 0, 0);
+    }
+
+    this._pannableBounds = this._widgetBounds();
+  },
+
+  // handleEvents: if this is called, WS will install its own event handlers
+  // on the stack element for mouse clicks and motion.  Otherwise, the application
+  // must call dragStart/dragMove/dragStop as appropriate.
+  handleEvents: function () {
+    let self = this;
+
+    let e = window;
+    e.addEventListener("mousedown", function (ev) { return self._onMouseDown(ev); }, true);
+    e.addEventListener("mouseup", function (ev) { return self._onMouseUp(ev); }, true);
+    e.addEventListener("mousemove", function (ev) { return self._onMouseMove(ev); }, true);
+  },
+
+  // moveWidgetBy: move the widget with the given id by x,y.  Should
+  // not be used on vp-relative or otherwise frozen widgets (using it
+  // on the x coordinate for x-ignore widgets and similarily for y is
+  // ok, as long as the other coordinate remains 0.)
+  moveWidgetBy: function (wid, x, y) {
+    let state = this._getState(wid);
+
+    state.rect.x += x;
+    state.rect.y += y;
+
+    this._commitState(state);
+  },
+
+  // panBy: pan the entire set of widgets by the given x and y amounts.
+  // This does the same thing as if the user dragged by the given amount.
+  // If this is called with an outstanding drag, weirdness might happen,
+  // but it also might work, so not isabling that.
+  //
+  // if ignoreBarriers is true, then barriers are ignored for the pan.
+  panBy: function (dx, dy, ignoreBarriers) {
+    let needsDragWrap = !this._dragging();
+
+    //log2("tlc rect.x start", this._widgetState['tab-list-container'].rect.x, needsDragWrap);
+
+    if (needsDragWrap)
+      this.dragStart(0, 0);
+
+    this._panBy(dx, dy, ignoreBarriers);
+
+    if (needsDragWrap)
+      this.dragStop();
+
+    //log2("tlc rect.x", this._widgetState['tab-list-container'].rect.x);
+  },
+
+  // panTo: pan the entire set of widgets so that the given x,y coordinates
+  // are in the upper left of the stack.
+  panTo: function (x, y) {
+    log2("panTo", x, y);
+    this.panBy(this._viewingRect.x - x, this._viewingRect.y - y, true);
+    log2("-panTo", x, y, "vr", this._viewingRect, "vwib", this._viewport.viewportInnerBounds);
+  },
+
+  // freeze: set a widget as frozen.  A frozen widget won't be moved
+  // in the stack -- its x,y position will still be tracked in the
+  // state, but the left/top attributes won't be overwritten.  Call unfreeze
+  // to move the widget back to where the ws thinks it should be.
+  freeze: function (wid) {
+    let state = this._getState(wid);
+
+    state.frozen = true;
+  },
+
+  unfreeze: function (wid) {
+    let state = this._getState(wid);
+    if (!state.frozen)
+      return;
+
+    state.frozen = false;
+    this._commitState(state);
+  },
+
+  // moveFrozenTo: move a frozen widget with id wid to x, y in the stack.
+  // can only be used on frozen widgets
+  moveFrozenTo: function (wid, x, y) {
+    let state = this._getState(wid);
+    if (!state.frozen)
+      throw "moveFrozenTo on non-frozen widget " + wid;
+
+    state.widget.setAttribute("left", x);
+    state.widget.setAttribute("top", y);
+  },
+
+  get pannableBounds() {
+    return this._pannableBounds.clone();
+  },
+
+  get viewingRect() {
+    return this._viewingRect.clone();
+  },
+
+  // isWidgetVisible: return true if any portion of widget with id wid is
+  // visible; otherwise return false.
+  isWidgetVisible: function (wid) {
+    let state = this._getState(wid);
+    let visibleStackRect = new wsRect(0, 0, this._viewingRect.width, this._viewingRect.height);
+
+    return visibleStackRect.intersects(state.rect);
+  },
+
+  // offsetAll: add an offset to all widgets
+  offsetAll: function (x, y) {
+    this.globalOffsetX += x;
+    this.globalOffsetY += y;
+
+    for (let wid in this._widgetState) {
+      let state = this._widgetState[wid];
+
+      state.rect.x += x;
+      state.rect.y += y;
+
+      this._commitState(state);
+    }
+  },
+
+  // setViewportBounds
+  //  nb: an object containing top, left, bottom, right properties
+  //    OR
+  //  width, height: integer values; origin assumed to be 0,0
+  //    OR
+  //  top, left, bottom, right: integer values
+  //
+  // Set the bounds of the viewport area; that is, set the size of the
+  // actual content that the viewport widget will be providing a view
+  // over.  For example, in the case of a 100x100 viewport showing a
+  // view into a 100x500 webpage, the viewport bounds would be
+  // { top: 0, left: 0, bottom: 500, right: 100 }.
+  //
+  // setViewportBounds will move all the viewport-relative widgets into
+  // place based on the new viewport bounds.
+  setViewportBounds: function () {
+    let oldBounds = this._viewportBounds.clone();
+    let oldInner = this._viewport.viewportInnerBounds.clone();
+
+    if (arguments.length == 1) {
+      this._viewportBounds.copyFromTLBR(arguments[0]);
+    } else if (arguments.length == 2) {
+      this._viewportBounds.setRect(0, 0, arguments[0], arguments[1]);
+    } else if (arguments.length == 4) {
+      this._viewportBounds.setBounds(arguments[0],
+      arguments[1],
+      arguments[2],
+      arguments[3]);
+    } else {
+      throw "Invalid number of arguments to setViewportBounds";
+    }
+
+    log2("setViewportBounds old:", oldBounds.toString(), "new:", this._viewportBounds);
+
+    let vp = this._viewport;
+
+    let dleft = this._viewportBounds.left - oldBounds.left;
+    let dright = this._viewportBounds.right - oldBounds.right;
+    let dtop = this._viewportBounds.top - oldBounds.top;
+    let dbottom = this._viewportBounds.bottom - oldBounds.bottom;
+
+    log2("setViewportBounds dltrb", dleft, dtop, dright, dbottom);
+
+    // move all vp-relative widgets to be the right offset from the bounds again
+    for (let wid in this._widgetState) {
+      let state = this._widgetState[wid];
+
+      if (state.vpRelative) {
+        log2("vpRelative widget", state.id, state.rect.x, dleft, dright);
+        if (state.vpOffsetXBefore) {
+          state.rect.x += dleft;
+        } else {
+          state.rect.x += dright;
+        }
+
+        if (state.vpOffsetYBefore) {
+          state.rect.y += dtop;
+        } else {
+          state.rect.y += dbottom;
+        }
+
+        log2("vpRelative widget", state.id, state.rect.x, dleft, dright);
+        this._commitState(state);
+      }
+    }
+
+    for (let bid in this._barriers) {
+      let barrier = this._barriers[bid];
+
+      log2("setViewportBounds: looking at barrier", bid, barrier.vpRelative, barrier.type);
+
+      if (barrier.vpRelative) {
+        if (barrier.type == "vertical") {
+          let q = "v barrier moving from " + barrier.x + " to ";
+          if (barrier.vpOffsetXBefore) {
+            barrier.x += dleft;
+          } else {
+            barrier.x += dright;
+          }
+          log2(q += barrier.x);
+        } else if (barrier.type == "horizontal") {
+          let q = "h barrier moving from " + barrier.y + " to ";
+          if (barrier.vpOffsetYBefore) {
+            barrier.y += dtop;
+          } else {
+            barrier.y += dbottom;
+          }
+          log2(q += barrier.y);
+        }
+      }
+    }
+
+    // and then define a new pannable bounds
+    this._pannableBounds = this._viewportBounds.clone().expandBy(this._viewportOverflow);
+
+    log2("new pannableBounds:", this._pannableBounds.toString());
+
+    // now let's make sure that the inner bounds are still valid
+
+    log2("viewingRect old", this._viewingRect.toString());
+
+    if (!this._pannableBounds.contains(this._viewingRect)) {
+      let dx = dright - dleft;
+      let dy = dbottom - dtop;
+
+      //console.log("panBy: ", -dx, -dy);
+
+      this._rectSanityCheck = false;
+
+      this.panBy(-dx, -dy, true);
+
+      this._rectSanityCheck = true;
+    }
+
+    log2("viewingRect new", this._viewingRect.toString());
+    log2("finished, inner bounds old:", oldInner, " new:", this._viewport.viewportInnerBounds);
+
+    if (this._viewport && this._viewportUpdateHandler) {
+      let vws = this._viewport;
+      let vwib = vws.viewportInnerBounds.clone();
+
+      vwib.left += vws.offsetLeft;
+      vwib.top += vws.offsetTop;
+      vwib.right += vws.offsetRight;
+      vwib.bottom += vws.offsetBottom;
+
+      this._viewportUpdateHandler.apply(window, [vwib]);
+    }
+  },
+
+  // setViewportHandler
+  //  uh: A function object
+  //
+  // The given function object is called at the end of every drag, providing
+  // the new area that's to be displayed in the viewport widget.
+  //
+  setViewportHandler: function (uh) {
+    this._viewportUpdateHandler = uh;
+  },
+
+  // setPanHandler
+  // uh: A function object
+  //
+  // The given functin object is called whenever elements pan; it provides
+  // the new area of the pannable bounds that's visible in the stack.
+  setPanHandler: function (uh) {
+    this._panHandler = uh;
+  },
+
+  // dragStart: start a drag, with the current coordinates being clientX,clientY
+  dragStart: function (clientX, clientY) {
+    log("(dragStart)", clientX, clientY);
+
+    if (this._dragState) {
+      reportError("dragStart with drag already in progress? what?");
+      this._dragState = null;
+    }
+
+    this._dragState = { };
+
+    let t = Date.now();
+
+    this._dragState.barrierState = [];
+
+    this._dragState.startTime = t;
+    // outer x, that is outer from the viewport coordinates.  In stack-relative coords.
+    this._dragState.outerStartX = clientX;
+    this._dragState.outerStartY = clientY;
+
+    this._dragCoordsFromClient(clientX, clientY, t);
+
+    this._dragState.outerLastUpdateDX = 0;
+    this._dragState.outerLastUpdateDY = 0;
+
+    if (this._viewport) {
+      // create a copy of these so that we can compute
+      // deltas correctly to update the viewport
+      this._viewport.dragStartRect = this._viewport.rect.clone();
+      this._viewport.dragStartOffsets = this._panRegionOffsets();
+    }
+
+    this._dragState.dragging = true;
+  },
+
+  // dragStop: stop any drag in progress
+  dragStop: function () {
+    log("(dragStop)");
+
+    if (!this._dragging())
+      return;
+
+    if (this._viewportUpdateTimeout != -1)
+      clearTimeout(this._viewportUpdateTimeout);
+
+    if (this._viewport)
+      this._viewportUpdate();
+
+    this._dragState = null;
+  },
+
+  // dragMove: process a mouse move to clientX,clientY for an ongoing drag
+  dragMove: function (clientX, clientY) {
+    if (!this._dragging())
+      return;
+
+    log("(dragMove)", clientX, clientY);
+
+    this._dragCoordsFromClient(clientX, clientY);
+
+    this._dragUpdate();
+
+    if (this._viewportUpdateInterval != -1) {
+      if (this._viewportUpdateTimeout != -1)
+        clearTimeout(this._viewportUpdateTimeout);
+      let self = this;
+      this._viewportUpdateTimeout = setTimeout(function () { self._viewportUpdate(); }, this._viewportUpdateInterval);
+    }
+  },
+
+  // updateSize: tell the WidgetStack to update its size, because it
+  // was either resized or some other event took place.
+  updateSize: function() {
+    let rect = this._el.getBoundingClientRect();
+    let oldw = this._viewingRect.width;
+    let oldh = this._viewingRect.height;
+    let width = rect.right - rect.left;
+    let height = rect.bottom - rect.top;
+
+    // XXX assumes we can only be resized from the bottom left/bottom right
+    this._viewingRect.width = width;
+    this._viewingRect.height = height;
+
+    // If the viewport changed size (the only thing that's allowed to for now),
+    // update its internal sizes
+    if (this._viewport) {
+      let vws = this._viewport;
+      let vwb = this._viewportBounds;
+      let vwib = vws.viewportInnerBounds;
+
+      let newViewportRect = this._viewport.widget.getBoundingClientRect();
+
+      let w = newViewportRect.right - newViewportRect.left;
+      let h = newViewportRect.bottom - newViewportRect.top;
+
+      if (this._viewport.widget.hasAttribute("widgetwidth") &&
+          this._viewport.widget.hasAttribute("widgetheight"))
+      {
+        w = parseInt(this._viewport.widget.getAttribute("widgetwidth"));
+        h = parseInt(this._viewport.widget.getAttribute("widgetheight"));
+      }
+
+      // the amount the viewport size changed by
+      let dw = w - vws.rect.width;
+      let dh = h - vws.rect.height;
+
+      if (dw != 0 || dh != 0) {
+        // the amount we'll need to move the viewport inner bounds origin by
+        // to make sure that it stays within the viewport
+        let dx = 0;
+        let dy = 0;
+
+        log("updateSize: viewport size:", w, h, "old:", vws.rect.width, vws.rect.height);
+
+        vws.rect.width = w;
+        vws.rect.height = h;
+
+        // if we'd blow past the left/bottom of the bounds, calculate adjustment to
+        // the inner bounds origin
+        if (vwib.right + dw > vwb.right)
+          dx = vwb.right - (vwib.right + dw);
+        if (vwib.bottom + dh > vwb.bottom)
+          dy = vwb.bottom - (vwib.bottom + dh);
+
+        // if with the new inner bounds origin we'd blow past the right/top, then just
+        // make the adjustment take us to the origin
+        if (vwib.left + dx < vwb.left)
+          dx = vwb.left - vwib.left;
+        if (vwib.top + dy < vwb.top)
+          dy = vwb.bottom - vwib.bottom;
+
+        // make the adjustments
+        vwib.right += dx;
+        vwib.top += dy;
+        // XXX should these take us to a max of vwb.width/vwb.height? or is it ok if it's bigger?
+        vwib.width += dw;
+        vwib.height += dh;
+
+        for (let wid in this._widgetState) {
+          let state = this._widgetState[wid];
+          let commit = false;
+
+          if (state.vpRelative) {
+            if (!state.vpOffsetXBefore) {
+              state.vpOffsetX -= dw;
+              state.rect.x -= dw;
+              commit = true;
+            }
+            if (!state.vpOffsetYBefore) {
+              state.vpOffsetY -= dh;
+              state.rect.y -= dh;
+              commit = true;
+            }
+          }
+
+          if (commit)
+            this._commitState(state);
+        }
+
+        this._viewportOverflow.right += dw;
+        this._viewportOverflow.bottom += dh;
+
+        this._pannableBounds = this._viewportBounds.clone().expandBy(this._viewportOverflow);
+
+        log("pb", this._pannableBounds);
+
+        this._viewportUpdate(true);
+      }
+    }
+
+    // note: the actual viewport bounds didn't change, so we don't need to move any of the relative widgets
+  },
+
+  //
+  // Internal code
+  //
+
+  _getState: function (wid) {
+    if (!(wid in this._widgetState))
+      throw "Unknown widget id '" + wid + "'; widget not in stack";
+
+    return this._widgetState[wid];
+  },
+
+  _onMouseDown: function (ev) {
+    log("(mousedown)");
+    this.dragStart(ev.screenX, ev.screenY);
+
+    // we haven't yet recognized a drag; whatever happens make
+    // sure we start one after 50ms.
+    this._dragState.dragging = false;
+
+    let self = this;
+    this._dragState.dragStartTimeout = setTimeout(function () {
+                                                    self._dragState.dragStartTimeout = -1;
+                                                    self._delayedDragStart();
+                                                    self._dragUpdate();
+                                                  }, 50);
+  },
+
+  _onMouseUp: function (ev) {
+    log("(mouseup)");
+    if (!this._dragState)
+      return;
+
+    if (this._dragState.dragStartTimeout != -1)
+      clearTimeout(this._dragState.dragStartTimeout);
+
+    this.dragStop();
+  },
+
+  _onMouseMove: function (ev) {
+    if (!this._dragging())
+      return;
+
+    this._dragCoordsFromClient(ev.screenX, ev.screenY);
+
+    if (!this._dragging() && this._dragState.outerDistSquared > 100)
+      this._delayedDragStart();
+
+    this.dragMove(ev.screenX, ev.screenY);
+  },
+
+  _dragging: function () {
+    return this._dragState && this._dragState.dragging;
+  },
+
+  // dragStart: a drag was started, either by distance or by time.
+  _delayedDragStart: function () {
+    log("(dragStart)");
+    if (this._dragging())
+      return;
+
+    if (this._dragState.dragStartTimeout != -1)
+      clearTimeout(this._dragState.dragStartTimeout);
+
+    this._dragState.dragging = true;
+  },
+
+  _panRegionOffsets: function () {
+    let ioffsetx = 0;
+    let ioffsety = 0;
+
+    // _viewingRect is the currently visible part of the entire stack,
+    // but it's in the coordinates of the _pannableBounds -- that is,
+    // the _viewingRect origin maps to 0,0 in the stack.
+    //
+    // _viewportBounds on the other hand, is in the origin of the
+    // viewport -- usually at 0,0.
+    //
+    // But, this still seems to work -- I thought there was a bug
+    // here, and there still might be.  An alternative is to just not
+    // do this, always snap the canvas back to 0,0 -unless- that would
+    // cause the viewport inner bounds to go outside the viewport
+    // bounds (in _viewportUpdate).
+
+    if (this._viewingRect.left < this._viewportBounds.left)
+      ioffsetx = this._viewportBounds.left - this._viewingRect.left;
+    else if (this._viewingRect.right > this._viewportBounds.right)
+      ioffsetx = this._viewportBounds.right - this._viewingRect.right;
+
+    if (this._viewingRect.top < this._viewportBounds.top)
+      ioffsety = this._viewportBounds.top - this._viewingRect.top;
+    else if (this._viewingRect.bottom > this._viewportBounds.bottom)
+      ioffsety = this._viewportBounds.bottom - this._viewingRect.bottom;
+
+    return [ioffsetx, ioffsety];
+  },
+
+  _viewportUpdate: function (force) {
+    if (!this._viewport)
+      return;
+
+    let originalViewportInnerBounds = this._viewport.viewportInnerBounds.clone();
+    let needsUpdate = force;
+
+    this._viewportUpdateTimeout = -1;
+
+    let vws = this._viewport;
+
+//    log2("viewportUpdate start", vws.rect.toString(), this._viewingRect.toString(), vws.originX, vws.originY);
+
+    let ioffsetx, ioffsety;
+    [ioffsetx, ioffsety] = this._panRegionOffsets();
+
+    // recover the amount the inner bounds moved by the amount the viewport widget moved.
+    // the rects are in screen space though, so we have to convert them to the virtual
+    // coordinate space.
+    if (this._dragging()) {
+      let idx = (vws.dragStartRect.x - vws.dragStartOffsets[0]) - (vws.rect.x - ioffsetx);
+      let idy = (vws.dragStartRect.y - vws.dragStartOffsets[1]) - (vws.rect.y - ioffsety);
+
+      if (idx || idy) {
+        vws.viewportInnerBounds.translate(idx, idy);
+        needsUpdate = true;
+      }
+    }
+
+    // Attempt to snap the viewport widget back to its origin, if necessary.
+    // Check to see if any part of the viewport widget is outside of the
+    // rectangle formed by 0,0,w,h, where w and h come from the current viewingRect.
+    // This must be done after the above, since we'll manipulate vws.rect here.
+    let boundsRect = new wsRect(0, 0, this._viewingRect.width, this._viewingRect.height);
+    let viewportRect = vws.rect;
+
+    if (!boundsRect.contains(viewportRect)) {
+      vws.rect.x = ioffsetx;
+      vws.rect.y = ioffsety;
+
+      this._commitState(vws);
+      needsUpdate = true;
+    }
+
+    // if we're dragging, update this so that we can call this function again
+    // durig the same drag and get the right values.
+    if (this._dragging()) {
+      vws.dragStartOffsets = [ioffsetx, ioffsety];
+      vws.dragStartRect = vws.rect.clone();
+    }
+
+    log2("_viewportUpdate - new vwib:", this._viewport.viewportInnerBounds);
+
+    if (needsUpdate && this._viewport && this._viewportUpdateHandler) {
+      let vwib = vws.viewportInnerBounds.clone();
+
+      vwib.left += vws.offsetLeft;
+      vwib.top += vws.offsetTop;
+      vwib.right += vws.offsetRight;
+      vwib.bottom += vws.offsetBottom;
+
+      this._viewportUpdateHandler.apply(window, [vwib, originalViewportInnerBounds]);
+    }
+  },
+
+  _dragCoordsFromClient: function (cx, cy, t) {
+    this._dragState.curTime = t ? t : Date.now();
+    this._dragState.outerCurX = cx;
+    this._dragState.outerCurY = cy;
+
+    let dx = this._dragState.outerCurX - this._dragState.outerStartX;
+    let dy = this._dragState.outerCurY - this._dragState.outerStartY;
+    this._dragState.outerDX = dx;
+    this._dragState.outerDY = dy;
+    this._dragState.outerDistSquared = dx*dx + dy*dy;
+  },
+
+  _panHandleBarriers: function (dx, dy) {
+    // XXX unless the barriers are sorted by position, this will break
+    // with multiple barriers that are near enough to eachother that a
+    // drag could cross more than one.
+
+    let vr = this._viewingRect;
+
+    // XXX this just stops at the first horizontal and vertical barrier it finds
+
+    // barrier_[xy] is the barrier that was used to get to the final
+    // barrier_d[xy] value.  if null, no barrier, and dx/dy shouldn't
+    // be replaced with barrier_d[xy].
+    let barrier_y = null, barrier_x = null;
+    let barrier_dy = 0, barrier_dx = 0;
+
+    for (let i = 0; i < this._barriers.length; i++) {
+      let b = this._barriers[i];
+
+      //log2("barrier", i, b.type, b.x, b.y);
+
+      if (dx != 0 && b.type == "vertical") {
+        if (barrier_x != null) {
+          delete this._dragState.barrierState[i];
+          continue;
+        }
+
+        let alreadyKnownDistance = this._dragState.barrierState[i] || 0;
+
+        //log2("alreadyKnownDistance", alreadyKnownDistance);
+
+        let dbx = 0;
+
+        //100 <= 100 && 100-(-5) > 100
+
+        if ((vr.left <= b.x && vr.left+dx > b.x) ||
+            (vr.left >= b.x && vr.left+dx < b.x))
+        {
+          dbx = b.x - vr.left;
+        } else if ((vr.right <= b.x && vr.right+dx > b.x) ||
+                   (vr.right >= b.x && vr.right+dx < b.x))
+        {
+          dbx = b.x - vr.right;
+        } else {
+          delete this._dragState.barrierState[i];
+          continue;
+        }
+
+        let leftoverDistance = dbx - dx;
+
+        //log2("initial dbx", dbx, leftoverDistance);
+
+        let dist = Math.abs(leftoverDistance + alreadyKnownDistance) - b.size;
+
+        if (dist >= 0) {
+          if (dx < 0)
+            dbx -= dist;
+          else
+            dbx += dist;
+          delete this._dragState.barrierState[i];
+        } else {
+          dbx = 0;
+          this._dragState.barrierState[i] = leftoverDistance + alreadyKnownDistance;
+        }
+
+        //log2("final dbx", dbx, "state", this._dragState.barrierState[i]);
+
+        if (Math.abs(barrier_dx) <= Math.abs(dbx)) {
+          barrier_x = b;
+          barrier_dx = dbx;
+
+          //log2("new barrier_dx", barrier_dx);
+        }
+      }
+
+      if (dy != 0 && b.type == "horizontal") {
+        if (barrier_y != null) {
+          delete this._dragState.barrierState[i];
+          continue;
+        }
+
+        let alreadyKnownDistance = this._dragState.barrierState[i] || 0;
+
+        //log2("alreadyKnownDistance", alreadyKnownDistance);
+
+        let dby = 0;
+
+        //100 <= 100 && 100-(-5) > 100
+
+        if ((vr.top <= b.y && vr.top+dy > b.y) ||
+            (vr.top >= b.y && vr.top+dy < b.y))
+        {
+          dby = b.y - vr.top;
+        } else if ((vr.bottom <= b.y && vr.bottom+dy > b.y) ||
+                   (vr.bottom >= b.y && vr.bottom+dy < b.y))
+        {
+          dby = b.y - vr.bottom;
+        } else {
+          delete this._dragState.barrierState[i];
+          continue;
+        }
+
+        let leftoverDistance = dby - dy;
+
+        //log2("initial dby", dby, leftoverDistance);
+
+        let dist = Math.abs(leftoverDistance + alreadyKnownDistance) - b.size;
+
+        if (dist >= 0) {
+          if (dy < 0)
+            dby -= dist;
+          else
+            dby += dist;
+          delete this._dragState.barrierState[i];
+        } else {
+          dby = 0;
+          this._dragState.barrierState[i] = leftoverDistance + alreadyKnownDistance;
+        }
+
+        //log2("final dby", dby, "state", this._dragState.barrierState[i]);
+
+        if (Math.abs(barrier_dy) <= Math.abs(dby)) {
+          barrier_y = b;
+          barrier_dy = dby;
+
+          //log2("new barrier_dy", barrier_dy);
+        }
+      }
+    }
+
+    if (barrier_x) {
+      //log2("did barrier_x", barrier_x, "barrier_dx", barrier_dx);
+      dx = barrier_dx;
+    }
+
+    if (barrier_y) {
+      dy = barrier_dy;
+    }
+
+    return [dx, dy];
+  },
+
+  _panBy: function (dx, dy, ignoreBarriers) {
+    // initially we work in viewingRect coords, so the direction
+    // of the move is opposite from the direction of the pan
+    dx = -dx;
+    dy = -dy;
+
+    let vr = this._viewingRect;
+
+    // check if any barriers would be crossed by this pan, and take them
+    // into account.  do this first.
+
+    log2("******* _panBy", dx, dy, "v lr", vr.left, vr.right, "tb", vr.top, vr.bottom);
+
+    if (!ignoreBarriers)
+      [dx, dy] = this._panHandleBarriers(dx, dy);
+
+    // constrain the full drag of the viewingRect to the pannableBounds.
+    // note that the viewingRect needs to move in the opposite
+    // direction of the pan, so we fiddle with the signs here (as you
+    // pan to the upper left, more of the bottom right becomes visible,
+    // so the viewing rect moves to the bottom right of the virtual surface).
+    log2("rectTranslateConstrain in", dx, dy);
+    [dx, dy] = this._rectTranslateConstrain(dx, dy, vr, this._pannableBounds);
+    log2("rectTranslateConstrain out", dx, dy);
+
+    // If the net result is that we don't have any room to move, then
+    // just return.
+    if (dx == 0 && dy == 0)
+      return;
+
+    // the viewingRect moves opposite of the actual pan direction, see above
+    vr.x += dx;
+    vr.y += dy;
+
+    // Go through each widget and move it by dx,dy.  Frozen widgets
+    // will be ignored in commitState.
+    // The widget rects are in real stack space though, so we need to subtract
+    // our (now negated) dx, dy from their coordinates.
+    for (let wid in this._widgetState) {
+      let state = this._widgetState[wid];
+
+      if (!state.ignoreX)
+        state.rect.x -= dx;
+      if (!state.ignoreY)
+        state.rect.y -= dy;
+
+      this._commitState(state);
+    }
+
+    if (this._panHandler) {
+      this._panHandler.apply(window, [vr.clone()]);
+    }
+  },
+
+  _dragUpdate: function () {
+    let dx = this._dragState.outerDX - this._dragState.outerLastUpdateDX;
+    let dy = this._dragState.outerDY - this._dragState.outerLastUpdateDY;
+
+    this._dragState.outerLastUpdateDX = this._dragState.outerDX;
+    this._dragState.outerLastUpdateDY = this._dragState.outerDY;
+
+    this._panBy(dx, dy);
+  },
+
+  //
+  // widget addition/removal
+  //
+  _addNewWidget: function (w) {
+    let wid = w.getAttribute("id");
+    if (!wid) {
+      reportError("WidgetStack: child widget without id!");
+      return;
+    }
+
+    if (w.getAttribute("hidden") == "true")
+      return;
+
+    let x = w.getAttribute("left") || 0;
+    let y = w.getAttribute("top") || 0;
+
+    // these widgets must be fixed size
+    // x and y must be ints!
+    let state = {
+      widget: w,
+      id: wid,
+
+      viewport: false,
+      ignoreX: false,
+      ignoreY: false,
+      sticky: false,
+      frozen: false,
+      vpRelative: false,
+
+      offsetLeft: 0,
+      offsetTop: 0,
+      offsetRight: 0,
+      offsetBottom: 0,
+    };
+
+    let rect = w.getBoundingClientRect();
+    state.rect = new wsRect(parseInt(x), parseInt(y),
+                            rect.right - rect.left,
+                            rect.bottom - rect.top);
+    if (w.hasAttribute("widgetwidth") && w.hasAttribute("widgetheight")) {
+      state.rect.width = parseInt(w.getAttribute("widgetwidth"));
+      state.rect.height = parseInt(w.getAttribute("widgetheight"));
+    }
+
+    if (w.hasAttribute("constraint")) {
+      let cs = w.getAttribute("constraint").split(",");
+      for each (let s in cs) {
+        if (s == "ignore-x")
+          state.ignoreX = true;
+        else if (s == "ignore-y")
+          state.ignoreY = true;
+        else if (s == "sticky")
+          state.sticky = true;
+        else if (s == "frozen") {
+          state.frozen = true;
+        } else if (s == "vp-relative")
+          state.vpRelative = true;
+      }
+    }
+
+    if (/*w.localName == "canvas" &&*/ w.hasAttribute("viewport")) {
+      if (this._viewport)
+        reportError("WidgetStack: more than one viewport canvas in stack!");
+
+      this._viewport = state;
+      state.viewport = true;
+
+      if (w.hasAttribute("vptargetx") && w.hasAttribute("vptargety") &&
+          w.hasAttribute("vptargetw") && w.hasAttribute("vptargeth"))
+      {
+        let wx = parseInt(w.getAttribute("vptargetx"));
+        let wy = parseInt(w.getAttribute("vptargety"));
+        let ww = parseInt(w.getAttribute("vptargetw"));
+        let wh = parseInt(w.getAttribute("vptargeth"));
+
+        state.offsetLeft = state.rect.left - wx;
+        state.offsetTop = state.rect.top - wy;
+        state.offsetRight = state.rect.right - (wx + ww);
+        state.offsetBottom = state.rect.bottom - (wy + wh);
+
+        state.rect = new wsRect(wx, wy, ww, wh);
+      }
+
+      // initialize inner bounds to top-left
+      state.viewportInnerBounds = new wsRect(0, 0, state.rect.width, state.rect.height);
+    }
+
+    this._widgetState[wid] = state;
+
+    log ("(New widget: " + wid + (state.viewport ? " [viewport]" : "") + " at: " + state.rect + ")");
+
+    this._updateWidgets();
+  },
+
+  _removeWidget: function (w) {
+    let wid = w.getAttribute("id");
+    delete this._widgetState[wid];
+    this._updateWidgets();
+  },
+
+  // updateWidgets:
+  //   Go through all the widgets and figure out their viewport-relative offsets.
+  // If the widget goes to the left or above the viewport widget, then
+  // vpOffsetXBefore or vpOffsetYBefore is set.
+  // See setViewportBounds for use of vpOffset* state variables, and for how
+  // the actual x and y coords of each widget are calculated based on their offsets
+  // and the viewport bounds.
+  _updateWidgets: function () {
+    let vp = this._viewport;
+
+    let ofRect = this._viewingRect.clone();
+
+    for (let wid in this._widgetState) {
+      let state = this._widgetState[wid];
+
+      if (vp && state.vpRelative) {
+        // compute the vpOffset from 0,0 assuming that the viewport rect is 0,0
+        if (state.rect.left >= vp.rect.right) {
+          state.vpOffsetXBefore = false;
+          state.vpOffsetX = state.rect.left - vp.rect.width;
+        } else {
+          state.vpOffsetXBefore = true;
+          state.vpOffsetX = state.rect.left - vp.rect.left;
+        }
+
+        if (state.rect.top >= vp.rect.bottom) {
+          state.vpOffsetYBefore = false;
+          state.vpOffsetY = state.rect.top - vp.rect.height;
+        } else {
+          state.vpOffsetYBefore = true;
+          state.vpOffsetY = state.rect.top - vp.rect.top;
+        }
+
+        log("widget", state.id, "offset", state.vpOffsetX, state.vpOffsetXBefore ? "b" : "a", state.vpOffsetY, state.vpOffsetYBefore ? "b" : "a", "rect", state.rect);
+      }
+    }
+
+    this._updateViewportOverflow();
+  },
+
+  _updateViewportOverflow: function() {
+    let vp = this._viewport;
+    if (!vp)
+      return;
+
+    let ofRect = new wsRect(0, 0, this._viewingRect.width, this._viewingRect.height);
+
+    for (let wid in this._widgetState) {
+      let state = this._widgetState[wid];
+
+      if (vp && state.vpRelative) {
+        ofRect.left = Math.min(ofRect.left, state.rect.left);
+        ofRect.top = Math.min(ofRect.top, state.rect.top);
+        ofRect.right = Math.max(ofRect.right, state.rect.right);
+        ofRect.bottom = Math.max(ofRect.bottom, state.rect.bottom);
+      }
+    }
+
+    this._viewportOverflow = new wsBorder(
+      /*top*/ ofRect.top,
+      /*left*/ ofRect.left,
+      /*bottom*/ ofRect.bottom - vp.rect.height,
+      /*right*/ ofRect.right - vp.rect.width
+    );
+
+    log("_updateViewportOverflow", this._viewportOverflow.toString());
+  },
+
+  _widgetBounds: function () {
+    let r = new wsRect(0,0,0,0);
+    for (let wid in this._widgetState) {
+      let state = this._widgetState[wid];
+
+      r = r.union(state.rect);
+    }
+
+    return r;
+  },
+
+  _commitState: function (state) {
+    // if the widget is frozen, don't actually update its left/top;
+    // presumably the caller is managing those directly for now.
+    if (state.frozen)
+      return;
+
+    state.widget.setAttribute("left", state.rect.x + state.offsetLeft);
+    state.widget.setAttribute("top", state.rect.y + state.offsetTop);
+
+    if (0) {
+
+    if (state.id == "tabs-container")
+      dump("commitState: " + state.id + " -> " + state.rect.x + " " + state.rect.y + "\n");
+
+    dump("rect: " + state.widget.getBoundingClientRect().left + " " + state.widget.getBoundingClientRect().right + " " + state.widget.getBoundingClientRect().top + " " + state.widget.getBoundingClientRect().bottom + "\n");
+    dump("display: " + state.widget.style.display + " vis: " + state.widget.style.visibility + "\n");
+    dump("style: " + state.widget.getAttribute("hidden") + "\n");
+    }
+  },
+
+  // constrain translate of rect by dx dy to bounds; return dx dy that can
+  // be used to bring rect up to the edge of bounds if we'd go over.
+  _rectTranslateConstrain: function (dx, dy, rect, bounds) {
+    if (this._rectSanityCheck && !bounds.contains(rect)) {
+      throw "Invalid rectTranslateConstrain -- rect already outside bounds! rect: " + rect.toString() + " bounds: " + bounds.toString();
+    }
+
+    let nleft = rect.left + dx;
+    let nright = rect.right + dx;
+    let ntop = rect.top + dy;
+    let nbot = rect.bottom + dy;
+
+    // did we move too far to the left or right?
+    if (nleft < bounds.left) {
+      dx = bounds.left - rect.left;
+    } else if (nright > bounds.right) {
+      dx = bounds.right - rect.right;
+    }
+
+    // did we move too far to the top or bottom?
+    if (ntop < bounds.top) {
+      dy = bounds.top - rect.top;
+    } else if (nbot > bounds.bottom) {
+      dy = bounds.bottom - rect.bottom;
+    }
+
+    return [dx, dy];
+  },
+
+  // add a new barrier from a <spacer>
+  _addNewBarrierFromSpacer: function (el) {
+    let t = el.getAttribute("barriertype");
+
+    // XXX implement these at some point
+    // t != "lr" && t != "rl" &&
+    // t != "tb" && t != "bt" &&
+
+    if (t != "horizontal" &&
+        t != "vertical")
+    {
+      throw "Invalid barrier type: " + t;
+    }
+
+    let x, y;
+
+    let barrier = {};
+    let vp = this._viewport;
+
+    barrier.type = t;
+
+    if (el.getAttribute("left"))
+      barrier.x = parseInt(el.getAttribute("left"));
+    else if (el.getAttribute("top"))
+      barrier.y = parseInt(el.getAttribute("top"));
+    else
+      throw "Barrier without top or left attribute";
+
+    if (el.getAttribute("size"))
+      barrier.size = parseInt(el.getAttribute("size"));
+    else
+      barrier.size = 10;
+
+    if (el.hasAttribute("constraint")) {
+      let cs = el.getAttribute("constraint").split(",");
+      for each (let s in cs) {
+        if (s == "ignore-x")
+          barrier.ignoreX = true;
+        else if (s == "ignore-y")
+          barrier.ignoreY = true;
+        else if (s == "sticky")
+          barrier.sticky = true;
+        else if (s == "frozen") {
+          barrier.frozen = true;
+        } else if (s == "vp-relative")
+          barrier.vpRelative = true;
+      }
+    }
+
+    if (barrier.vpRelative) {
+      if (barrier.type == "vertical") {
+        if (barrier.x >= vp.rect.right) {
+          barrier.vpOffsetXBefore = false;
+          barrier.vpOffsetX = barrier.x - vp.rect.right;
+        } else {
+          barrier.vpOffsetXBefore = true;
+          barrier.vpOffsetX = barrier.x - vp.rect.left;
+        }
+      } else if (barrier.type == "horizontal") {
+        if (barrier.y >= vp.rect.bottom) {
+          barrier.vpOffsetYBefore = false;
+          barrier.vpOffsetY = barrier.y - vp.rect.bottom;
+        } else {
+          barrier.vpOffsetYBefore = true;
+          barrier.vpOffsetY = barrier.y - vp.rect.top;
+        }
+
+        //log2("h barrier relative", barrier.vpOffsetYBefore, barrier.vpOffsetY);
+      }
+    }
+
+    this._barriers.push(barrier);
+  }
+};
--- a/chrome/content/browser-ui.js
+++ b/chrome/content/browser-ui.js
@@ -50,17 +50,17 @@ const UIMODE_BOOKMARKLIST      = 4;
 const UIMODE_TABS              = 5;
 const UIMODE_CONTROLS          = 6;
 const UIMODE_PANEL             = 7;
 
 const kMaxEngines = 4;
 const kDefaultFavIconURL = "chrome://browser/skin/images/default-favicon.png";
 
 [
-  ["gContentBox",            "contentBox"],
+  ["gContentBox",            "content"],
 ].forEach(function (elementGlobal) {
   var [name, id] = elementGlobal;
   window.__defineGetter__(name, function () {
     var element = document.getElementById(id);
     if (!element)
       return null;
     delete window[name];
     return window[name] = element;
@@ -76,30 +76,16 @@ var BrowserUI = {
   _panel : null,
   _caption : null,
   _edit : null,
   _throbber : null,
   _autocompleteNavbuttons : null,
   _favicon : null,
   _faviconLink : null,
 
-  _setContentPosition : function (aProp, aValue) {
-    let value = Math.round(aValue);
-    if (aProp == "left") {
-      gContentBox.style.marginLeft = value + "px";
-      gContentBox.style.marginRight = -value + "px";
-    } else if (aProp == "top") {
-      gContentBox.style.marginTop = value + "px";
-      gContentBox.style.marginBottom = -value + "px";
-    }
-  },
-  get _contentTop() {
-    return parseInt(gContentBox.style.marginTop);
-  },
-
   _titleChanged : function(aDocument) {
     var browser = Browser.currentBrowser;
     if (browser && aDocument != browser.contentDocument)
       return;
 
     var caption = aDocument.title;
     if (!caption) {
       caption = this.getDisplayURI(browser);
@@ -125,42 +111,27 @@ var BrowserUI = {
     }
   },
 
   _tabSelect : function(aEvent) {
     var browser = Browser.currentBrowser;
     this.setURI();
     this._titleChanged(browser.contentDocument);
     this._favicon.src = browser.mIconURL || kDefaultFavIconURL;
-    this.updateIcon(browser);
-
-    let toolbar = document.getElementById("toolbar-main");
-    if (Browser.content.currentTab.chromeTop) {
-      // content box was panned, so let's reset it
-      this._setContentPosition("top", Browser.content.currentTab.chromeTop);
-      this._setContentPosition("left", 0);
-      toolbar.top = this._contentTop - toolbar.boxObject.height;
-    }
-    else {
-      // Must be initial conditions
-      toolbar.top = 0;
-      this._setContentPosition("top", toolbar.boxObject.height);
-      this._setContentPosition("left", 0);
-    }
+    this.updateIcon();
 
     this.show(UIMODE_NONE);
   },
 
   _setIcon : function(aURI) {
     var ios = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService);
     var faviconURI = ios.newURI(aURI, null, null);
 
     var fis = Cc["@mozilla.org/browser/favicon-service;1"].getService(Ci.nsIFaviconService);
-    if (faviconURI.schemeIs("javascript") ||
-        fis.isFailedFavicon(faviconURI))
+    if (faviconURI.schemeIs("javascript") || fis.isFailedFavicon(faviconURI))
       faviconURI = ios.newURI(kDefaultFavIconURL, null, null);
 
     var browser = getBrowser();
     browser.mIconURL = faviconURI.spec;
 
     fis.setAndLoadFaviconForPage(browser.currentURI, faviconURI, true);
     this._favicon.src = faviconURI.spec;
   },
@@ -204,176 +175,23 @@ var BrowserUI = {
       var node = rootNode.getChild(i);
       items.push(node);
     }
     rootNode.containerOpen = false;
 
     return items;
   },
 
-  _dragData :  {
-    dragging : false,
-    startX : 0,
-    startY : 0,
-    dragX : 0,
-    dragY : 0,
-    lastX : 0,
-    lastY : 0,
-    sTop : 0,
-    sLeft : 0,
-    uiMode : UIMODE_NONE
-  },
-
-  _scrollToolbar : function bui_scrollToolbar(aEvent) {
-    var [scrollWidth, ] = Browser.content._contentAreaDimensions;
-    var [viewportW, ] = Browser.content._effectiveViewportDimensions;
-
-    var pannedUI = false;
-
-    if (this._dragData.dragging && Browser.content.scrollY == 0) {
-      let toolbar = document.getElementById("toolbar-main");
-      let dy = this._dragData.lastY - aEvent.screenY;
-      this._dragData.dragY += dy;
-
-      // NOTE: We should only be scrolling the toolbar if the sidebars are not
-      // visible (gContentBox.style.marginLeft == "0px")
-      let sidebarVisible = gContentBox.style.marginLeft != "0px";
-      let newTop = null;
-      if (dy > 0 && (toolbar.top > -toolbar.boxObject.height && !sidebarVisible)) {
-        // Scroll the toolbar up unless it is already scrolled up
-        newTop = this._dragData.sTop - dy;
-
-        // Clip the adjustment to just enough to hide the toolbar
-        if (newTop < -toolbar.boxObject.height)
-          newTop = -toolbar.boxObject.height;
-
-        // Reset the browser start point
-        Browser.content.dragData.sX = aEvent.screenX;
-        Browser.content.dragData.sY = aEvent.screenY;
-      }
-      else if (dy < 0 && (toolbar.top < 0 && !sidebarVisible)) {
-        // Scroll the toolbar down unless it is already down
-        newTop = this._dragData.sTop - dy;
-
-        // Clip the adjustment to just enough to fully show the toolbar
-        if (newTop > 0)
-          newTop = 0;
-      }
-
-      // Update the toolbar and browser tops. Stop the mousemove from
-      // getting to the deckbrowser.
-      if (newTop != null) {
-        toolbar.top = newTop;
-        this._setContentPosition("top", newTop + toolbar.boxObject.height);
-
-        // Cache the current top so we can use it when switching tabs
-        Browser.content.currentTab.chromeTop = this._contentTop;
-
-        pannedUI = true;
-      }
-    }
-
-    if (this._dragData.dragging && (Browser.content.scrollX == 0 || (Browser.content.scrollX + viewportW) == scrollWidth)) {
-      let tabbar = document.getElementById("tab-list-container");
-      let sidebar = document.getElementById("browser-controls");
-      let panelUI = document.getElementById("panel-container");
-      let toolbar = document.getElementById("toolbar-main");
-      let dx = this._dragData.lastX - aEvent.screenX;
-      this._dragData.dragX += dx;
-
-      if (Math.abs(this._dragData.screenX - aEvent.screenX) > 30) {
-        let newLeft = this._dragData.sLeft - dx;
-        let oldLeft = tabbar.left;
-
-        let tabbarW = tabbar.boxObject.width;
-        let sidebarW = sidebar.boxObject.width;
-        let contentW = gContentBox.boxObject.width;
-
-        // Limit the panning
-        if (newLeft > 0)
-          newLeft = 0;
-        else if (newLeft < -(tabbarW + sidebarW))
-          newLeft = -(tabbarW + sidebarW);
-
-        // Set the UI mode based on where we ended up
-        var noneMode = (gContentBox.style.marginTop == "0px" ? UIMODE_NONE : UIMODE_URLVIEW);
-        if (newLeft > -(tabbarW - tabbarW / 3) && newLeft <= 0) {
-          if (this._dragData.uiMode == UIMODE_CONTROLS) {
-            this.mode = noneMode;
-            return;
-          }
-          this.mode = UIMODE_TABS;
-        }
-        else if (newLeft >= -(tabbarW + sidebarW) && newLeft < -(tabbarW + sidebarW / 3)) {
-          if (this._dragData.uiMode == UIMODE_TABS) {
-            this.mode = noneMode;
-            return;
-          }
-          this.mode = UIMODE_CONTROLS;
-        }
-        else
-          this.mode = noneMode;
-
-        tabbar.left = newLeft;
-
-        // Never let the toolbar pan off the screen
-        let newToolbarLeft = newLeft;
-        if (newToolbarLeft < 0)
-          newToolbarLeft = 0;
-        toolbar.left = newToolbarLeft;
-
-        // Make the toolbar appear/disappear depending on the state of the sidebars
-        if (newLeft + tabbarW != 0)
-          toolbar.top = 0;
-        else
-          toolbar.top = this._contentTop - toolbar.boxObject.height;
-
-        this._setContentPosition("left", newLeft + tabbarW);
-        sidebar.left = newLeft + tabbarW + contentW;
-        panelUI.left = newLeft + tabbarW + contentW + sidebarW;
-
-        pannedUI = true;
-      }
-    }
-
-    if (pannedUI) {
-      aEvent.stopPropagation();
-
-      // Force a sync redraw
-      window.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
-            .getInterface(Components.interfaces.nsIDOMWindowUtils)
-            .processUpdates();
+  _showToolbar : function(aShow) {
+    if (aShow) {
+      ws.freeze("toolbar-main");
+      ws.moveFrozenTo("toolbar-main", 0, 0);
     }
     else {
-      // Reset our start point while the browser is doing its panning
-      this._dragData.lastX = aEvent.screenX;
-      this._dragData.lastY = aEvent.screenY;
-    }
-  },
-
-  _showToolbar : function(aShow) {
-    var toolbar = document.getElementById("toolbar-main");
-
-    if (aShow) {
-      // Always show the toolbar, either by floating or panning
-      if (toolbar.top == -toolbar.boxObject.height) {
-        // Float the toolbar over content
-        toolbar.top = 0;
-      }
-      else if (toolbar.top < 0) {
-        // Partially showing, so show it completely
-        toolbar.top = 0;
-        this._setContentPosition("top", toolbar.boxObject.height);
-      }
-    }
-    else {
-      // If we are floating the toolbar, then hide it again
-      if (gContentBox.style.marginTop == "0px") {
-        toolbar.top = -toolbar.boxObject.height;
-      }
+      ws.unfreeze("toolbar-main");
     }
   },
 
   _editToolbar : function(aEdit) {
     var toolbar = document.getElementById("toolbar-main");
     if (aEdit) {
       toolbar.setAttribute("mode", "edit");
       this._caption.hidden = true;
@@ -385,54 +203,61 @@ var BrowserUI = {
       toolbar.setAttribute("mode", "view");
       this._edit.hidden = true;
       this._edit.reallyClosePopup();
       this._caption.hidden = false;
     }
   },
 
   _showPanel : function(aMode) {
-      let tabbar = document.getElementById("tab-list-container");
-      let sidebar = document.getElementById("browser-controls");
-      let panelUI = document.getElementById("panel-container");
-      let toolbar = document.getElementById("toolbar-main");
+    let tabbar = document.getElementById("tabs-container");
+    let sidebar = document.getElementById("browser-controls");
+    let panelUI = document.getElementById("panel-container");
+    let toolbar = document.getElementById("toolbar-main");
+    let canvas = document.getElementById("canvas");
 
-      let tabbarW = tabbar.boxObject.width;
-      let sidebarW = sidebar.boxObject.width;
-      let contentW = gContentBox.boxObject.width;
+    let tabbarW = tabbar.boxObject.width;
+    let sidebarW = sidebar.boxObject.width;
+    let contentW = canvas.width;
 
-      let newLeft = -tabbarW;
-      switch (aMode) {
-        case UIMODE_NONE:
-          Shortcuts.deinit();
-          break;
-        case UIMODE_PANEL:
-          newLeft = -contentW;
-          this._initPanel();
-          break;
-        case UIMODE_CONTROLS:
-          newLeft = -(tabbarW + sidebarW);
-          break;
-        case UIMODE_TABS:
-          newLeft = 0;
-          break;
-      }
+    let newLeft = -tabbarW;
+    switch (aMode) {
+      case UIMODE_NONE:
+        Shortcuts.deinit();
+        break;
+      case UIMODE_PANEL:
+        newLeft = -contentW;
+        this._initPanel();
+        break;
+      case UIMODE_CONTROLS:
+        newLeft = -(tabbarW + sidebarW);
+        break;
+      case UIMODE_TABS:
+        newLeft = 0;
+        break;
+    }
+
+// XXX some form of this code should be in Browser.panHandler so the UIMODE is
+// set correctly when panning.
+// OR maybe we should try to removing as much of UIMODE as possible
+/*
       tabbar.left = newLeft;
 
       let newToolbarLeft = newLeft;
       if (newToolbarLeft < 0 && aMode != UIMODE_PANEL)
         newToolbarLeft = 0;
       else if (newToolbarLeft < 0 && aMode == UIMODE_PANEL)
         newToolbarLeft += tabbarW + sidebarW;
       toolbar.left = newToolbarLeft;
 
       this._setContentPosition("left", newLeft + tabbarW);
       sidebar.left = newLeft + tabbarW + contentW;
       panelUI.left = newLeft + tabbarW + contentW + sidebarW;
       panelUI.width = contentW;
+*/
   },
 
   _initPanel : function() {
     let addons = document.getElementById("addons-container");
     if (!addons.hasAttribute("src"))
       addons.setAttribute("src", "chrome://mozapps/content/extensions/extensions.xul");
     let dloads = document.getElementById("downloads-container");
     if (!dloads.hasAttribute("src"))
@@ -446,97 +271,101 @@ var BrowserUI = {
 
   _sizeControls : function(aEvent) {
     if (window != aEvent.target) {
       return
     }
     var rect = document.getElementById("browser-container").getBoundingClientRect();
     var containerW = rect.right - rect.left;
     var containerH = rect.bottom - rect.top;
+
     var toolbar = document.getElementById("toolbar-main");
     var toolbarH = toolbar.boxObject.height;
 
+    var popup = document.getElementById("popup_autocomplete");
+    popup.height = containerH - toolbarH;
+
+    // XXX need to handle make some of these work again
+/*
     var sidebar = document.getElementById("browser-controls");
     var panelUI = document.getElementById("panel-container");
-    var tabbar = document.getElementById("tab-list-container");
+    var tabbar = document.getElementById("tabs-container");
     tabbar.left = -tabbar.boxObject.width;
     panelUI.left = containerW + sidebar.boxObject.width;
     sidebar.left = containerW;
     sidebar.height = tabbar.height = (panelUI.height = containerH) - toolbarH;
     panelUI.width = containerW - sidebar.boxObject.width - tabbar.boxObject.width;
-
-    var popup = document.getElementById("popup_autocomplete");
     toolbar.width = containerW;
-    popup.height = containerH - toolbarH;
+*/
   },
 
   init : function() {
     this._caption = document.getElementById("urlbar-caption");
     this._caption.addEventListener("click", this, false);
     this._edit = document.getElementById("urlbar-edit");
     this._edit.addEventListener("blur", this, false);
     this._edit.addEventListener("keypress", this, true);
     this._edit.addEventListener("input", this, false);
     this._throbber = document.getElementById("urlbar-throbber");
     this._favicon = document.getElementById("urlbar-favicon");
     this._favicon.addEventListener("error", this, false);
     this._autocompleteNavbuttons = document.getElementById("autocomplete_navbuttons");
 
-    Browser.content.addEventListener("DOMTitleChanged", this, true);
-    Browser.content.addEventListener("DOMLinkAdded", this, true);
+    // XXX these really want to listen whatever is the current browser, not any browser
+    let browsers = document.getElementById("browsers");
+    browsers.addEventListener("DOMTitleChanged", this, true);
+    browsers.addEventListener("DOMLinkAdded", this, true);
 
-    document.getElementById("tab-list").addEventListener("TabSelect", this, true);
-
-    Browser.content.addEventListener("mousedown", this, true);
-    Browser.content.addEventListener("mouseup", this, true);
-    Browser.content.addEventListener("mousemove", this, true);
+    document.getElementById("tabs").addEventListener("TabSelect", this, true);
 
     window.addEventListener("resize", this, false);
     Shortcuts.restore();
   },
 
-  update : function(aState, aBrowser) {
-    if (aState == TOOLBARSTATE_INDETERMINATE) {
-      this._faviconLink = null;
-      aState = TOOLBARSTATE_LOADED;
-      this.setURI();
-    }
+  update : function(aState) {
+    var toolbar = document.getElementById("toolbar-main");
 
-    var toolbar = document.getElementById("toolbar-main");
-    if (aState == TOOLBARSTATE_LOADING) {
-      this.show(UIMODE_URLVIEW);
-      Browser.content.setLoading(aBrowser, true);
+    switch (aState) {
+      case TOOLBARSTATE_INDETERMINATE:
+        this._faviconAdded = false;
+        aState = TOOLBARSTATE_LOADED;
+        this.setURI();
+
+      case TOOLBARSTATE_LOADED:
+        toolbar.setAttribute("mode", "view");
 
-      toolbar.top = 0;
-      toolbar.setAttribute("mode", "loading");
-      this._favicon.src = "";
-      this._faviconLink = null;
-      this.updateIcon(aBrowser);
-    }
-    else if (aState == TOOLBARSTATE_LOADED) {
-      this._setContentPosition("top", toolbar.boxObject.height);
-      Browser.content.setLoading(aBrowser, false);
+        if (!this._faviconLink) {
+          this._faviconLink = Browser.currentBrowser.currentURI.prePath + "/favicon.ico";
+        }
+        this._setIcon(this._faviconLink);
+        this.updateIcon();
+        break;
 
-      toolbar.setAttribute("mode", "view");
+      case TOOLBARSTATE_LOADING:
+        toolbar.setAttribute("mode", "loading");
+        this.show(UIMODE_URLVIEW);
 
-      if (!this._faviconLink) {
-        this._faviconLink = aBrowser.currentURI.prePath + "/favicon.ico";
-      }
-      this._setIcon(this._faviconLink);
-      this.updateIcon(aBrowser);
+        ws.panTo(0,0, true);
+
+        this._favicon.src = "";
+        this._faviconLink = null;
+        this.updateIcon();
+        break;
     }
   },
 
-  updateIcon : function(browser) {
-    if (Browser.content.isLoading(browser)) {
-      document.getElementById("urlbar-image-deck").selectedIndex = 0;
+  updateIcon : function() {
+    if (Browser.currentTab.isLoading()) {
+      this._throbber.hidden = false;
       this._throbber.setAttribute("loading", "true");
+      this._favicon.hidden = true;
     }
     else {
-      document.getElementById("urlbar-image-deck").selectedIndex = 1;
+      this._favicon.hidden = false;
+      this._throbber.hidden = true;
       this._throbber.removeAttribute("loading");
     }
   },
 
   getDisplayURI : function(browser) {
     var uri = browser.currentURI;
 
     if (!this._URIFixup)
@@ -548,17 +377,17 @@ var BrowserUI = {
 
     return uri.spec;
   },
 
   /* Set the location to the current content */
   setURI : function() {
     var browser = Browser.currentBrowser;
 
-    // FIXME: deckbrowser should not fire TebSelect on the initial tab (bug 454028)
+    // FIXME: deckbrowser should not fire TabSelect on the initial tab (bug 454028)
     if (!browser.currentURI)
       return;
 
     var back = document.getElementById("cmd_back");
     var forward = document.getElementById("cmd_forward");
 
     back.setAttribute("disabled", !browser.canGoBack);
     forward.setAttribute("disabled", !browser.canGoForward);
@@ -595,52 +424,43 @@ var BrowserUI = {
 
   search : function() {
     var queryURI = "http://www.google.com/search?q=" + this._edit.value + "&hl=en&lr=&btnG=Search";
     getBrowser().loadURI(queryURI, null, null, false);
 
     this.show(UIMODE_URLVIEW);
   },
 
-  updateAutoComplete : function(showDefault)
-  {
+  updateAutoComplete : function(showDefault) {
     this.updateSearchEngines();
     if (showDefault || this._edit.getAttribute("nomatch"))
       this._edit.showHistoryPopup();
   },
 
-  doButtonSearch : function(button)
-  {
+  doButtonSearch : function(button) {
     if (!("engine" in button) || !button.engine)
       return;
 
     var urlbar = this._edit;
     urlbar.open = false;
     var value = urlbar.value;
 
     var submission = button.engine.getSubmission(value, null);
     getBrowser().loadURI(submission.uri.spec, null, submission.postData, false);
   },
 
   engines : null,
   updateSearchEngines : function () {
     if (this.engines)
       return;
 
-    // XXXndeakin remove the try-catch once the search service is properly built
-    try {
-      var searchService = Cc["@mozilla.org/browser/search-service;1"].
-                          getService(Ci.nsIBrowserSearchService);
-    } catch (ex) {
-      this.engines = [ ];
-      return;
-    }
-
+    var searchService = Cc["@mozilla.org/browser/search-service;1"].getService(Ci.nsIBrowserSearchService);
     var engines = searchService.getVisibleEngines({ });
     this.engines = engines;
+
     const kXULNS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
     var container = this._autocompleteNavbuttons;
     for (var e = 0; e < kMaxEngines && e < engines.length; e++) {
       var button = document.createElementNS(kXULNS, "toolbarbutton");
       var engine = engines[e];
       button.id = engine.name;
       button.setAttribute("label", engine.name);
       button.className = "searchengine";
@@ -661,18 +481,17 @@ var BrowserUI = {
 
     this.mode = aMode;
 
     var toolbar = document.getElementById("toolbar-main");
     var bookmark = document.getElementById("bookmark-container");
     var urllist = document.getElementById("urllist-container");
     var container = document.getElementById("browser-container");
 
-    if (aMode == UIMODE_URLVIEW)
-    {
+    if (aMode == UIMODE_URLVIEW) {
       this._showToolbar(true);
       this._editToolbar(false);
 
       bookmark.hidden = true;
       urllist.hidden = true;
 
       this._showPanel(UIMODE_NONE);
     }
@@ -778,27 +597,28 @@ var BrowserUI = {
   showBookmarks : function () {
     this.show(UIMODE_BOOKMARKLIST);
 
     var bms = Cc["@mozilla.org/browser/nav-bookmarks-service;1"].getService(Ci.nsINavBookmarksService);
     this._showPlaces(this._getBookmarks([bms.bookmarksMenuFolder]));
   },
 
   newTab : function() {
-    Browser.content.newTab(true);
+    ws.panTo(0,0, true);
+    Browser.newTab(true);
     this.show(UIMODE_URLEDIT);
   },
 
   closeTab : function(aTab) {
-    Browser.content.removeTab(aTab);
+    Browser.closeTab(aTab);
     this.show(UIMODE_NONE);
   },
 
   selectTab : function(aTab) {
-    Browser.content.selectTab(aTab);
+    Browser.selectTab(aTab);
     this.show(UIMODE_NONE);
   },
 
   handleEvent: function (aEvent) {
     switch (aEvent.type) {
       // Browser events
       case "DOMTitleChanged":
         this._titleChanged(aEvent.target);
@@ -821,36 +641,16 @@ var BrowserUI = {
           this._edit.reallyClosePopup();
           this.show(UIMODE_URLVIEW);
         }
         break;
       // Favicon events
       case "error":
         this._favicon.src = "chrome://browser/skin/images/default-favicon.png";
         break;
-      // UI panning events
-      case "mousedown":
-        this._dragData.dragging = true;
-        this._dragData.dragX = 0;
-        this._dragData.dragY = 0;
-        this._dragData.screenX = this._dragData.lastX = aEvent.screenX;
-        this._dragData.screenY = this._dragData.lastY = aEvent.screenY;
-        this._dragData.sTop = document.getElementById("toolbar-main").top;
-        this._dragData.sLeft = document.getElementById("tab-list-container").left;
-        this._dragData.uiMode = this.mode;
-        break;
-      case "mouseup":
-        this._dragData.dragging = false;
-        this._dragData.uiMode = UIMODE_NONE;
-        // Cause the UI to snap, if needed
-        this._showPanel(this.mode);
-        break;
-      case "mousemove":
-        this._scrollToolbar(aEvent);
-        break;
       // Window size events
       case "resize":
         this._sizeControls(aEvent);
         break;
     }
   },
 
   supportsCommand : function(cmd) {
@@ -938,17 +738,17 @@ var BrowserUI = {
         this.showBookmarks();
         break;
       case "cmd_menu":
         break;
       case "cmd_newTab":
         this.newTab();
         break;
       case "cmd_closeTab":
-        Browser.content.removeTab(Browser.content.browser);
+        this.closeTab();
         break;
       case "cmd_sanitize":
         Sanitizer.sanitize();
         break;
       case "cmd_panel":
       {
         var mode = (this.mode != UIMODE_PANEL ? UIMODE_PANEL : UIMODE_CONTROLS);
         this.show(mode);
--- a/chrome/content/browser.css
+++ b/chrome/content/browser.css
@@ -1,22 +1,18 @@
-deckbrowser {
-    -moz-binding: url("chrome://browser/content/deckbrowser.xml#deckbrowser");
-}
-
 #urlbar-edit {
     -moz-binding: url("chrome://browser/content/urlbar.xml#autocomplete-aligned");
 }
 
-#tab-list {
-    -moz-binding: url("chrome://browser/content/deckbrowser.xml#tablist");
+#tabs {
+    -moz-binding: url("chrome://browser/content/tabs.xml#tablist");
 }
 
 richlistitem[type="documenttab"] {
-    -moz-binding: url("chrome://browser/content/deckbrowser.xml#documenttab");
+    -moz-binding: url("chrome://browser/content/tabs.xml#documenttab");
 }
 
 #prefs-container > scrollbox
 {
     overflow: auto;
 }
 
 richpref[type="bool"] {
--- a/chrome/content/browser.js
+++ b/chrome/content/browser.js
@@ -20,16 +20,17 @@
  * Portions created by the Initial Developer are Copyright (C) 2008
  * the Initial Developer. All Rights Reserved.
  *
  * Contributor(s):
  *   Brad Lassey <blassey@mozilla.com>
  *   Mark Finkle <mfinkle@mozilla.com>
  *   Aleks Totic <a@totic.org>
  *   Johnathan Nightingale <johnath@mozilla.com>
+ *   Stuart Parmenter <stuart@mozilla.com>
  *
  * Alternatively, the contents of this file may be used under the terms of
  * either the GNU General Public License Version 2 or later (the "GPL"), or
  * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
  * in which case the provisions of the GPL or the LGPL are applicable instead
  * of those above. If you wish to allow use of your version of this file only
  * under the terms of either the GPL or the LGPL, and not to allow others to
  * use your version of this file under the terms of the MPL, indicate your
@@ -55,23 +56,81 @@ Cu.import("resource://gre/modules/Spatia
 __defineGetter__("gPrefService", function () {
   delete gPrefService;
   var gPrefService;
   return gPrefService = Components.classes["@mozilla.org/preferences-service;1"]
                                   .getService(Components.interfaces.nsIPrefBranch2);
 });
 
 function getBrowser() {
-  return Browser.content.browser;
+  return Browser.currentBrowser;
 }
 
+var ws = null;
+var ih = null;
+
 var Browser = {
   _content : null,
+  _tabs : [],
+  _currentTab : null,
 
-  startup : function() {
+  startup: function() {
+    var self = this;
+
+    // initalize the CanvasBrowser
+    this._content = new CanvasBrowser(document.getElementById("canvas"));
+
+    // initialize the WidgetStack
+    ws = new WidgetStack(document.getElementById("browser-container"));
+    ws.setViewportBounds({ top: 0, left: 0, right: 800, bottom: 480 });
+
+    // XXX this should live elsewhere
+    window.gSidebarVisible = false;
+    function panHandler(vr) {
+      var visibleNow = ws.isWidgetVisible("browser-controls") || ws.isWidgetVisible("tabs-container");
+
+      // XXX add code here to snap side panels fully out if they start to appear,
+      // or snap them back if they only showed up for a little bit
+
+      if (visibleNow && !gSidebarVisible) {
+        ws.freeze("toolbar-main");
+        ws.moveFrozenTo("toolbar-main", 0, 0);
+      }
+      else if (!visibleNow && gSidebarVisible) {
+        ws.unfreeze("toolbar-main");
+      }
+      gSidebarVisible = visibleNow;
+
+      // deal with checkerboard
+      /*
+      let stack = document.getElementById("browser-container");
+      stack.style.backgroundPosition =  -vr.left + "px " + -vr.top + "px";
+      */
+
+      // this is really only necessary for maemo, where we don't
+      // always repaint fast enough
+      window.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils).processUpdates();
+    }
+
+    ws.setPanHandler(panHandler);
+
+    function resizeHandler() { ws.updateSize(); }
+    window.addEventListener("resize", resizeHandler, false);
+
+    setTimeout(resizeHandler, 0);
+
+    function viewportHandler(b, ob) { self._content.viewportHandler(b, ob); }
+    ws.setViewportHandler(viewportHandler);
+
+    // initialize input handling
+    ih = new InputHandler();
+
+    // Create the first tab
+    this.newTab(true);
+
     window.controllers.appendController(this);
     window.controllers.appendController(BrowserUI);
 
     var ios = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService);
     var styleSheets = Cc["@mozilla.org/content/style-sheet-service;1"].getService(Ci.nsIStyleSheetService);
 
     // Should we hide the cursors
     var hideCursor = gPrefService.getBoolPref("browser.ui.cursor") == false;
@@ -81,51 +140,56 @@ var Browser = {
       var styleURI = ios.newURI("chrome://browser/content/content.css", null, null);
       styleSheets.loadAndRegisterSheet(styleURI, styleSheets.AGENT_SHEET);
     }
 
     // load styles for scrollbars
     var styleURI = ios.newURI("chrome://browser/content/scrollbars.css", null, null);
     styleSheets.loadAndRegisterSheet(styleURI, styleSheets.AGENT_SHEET);
 
-    this._content = document.getElementById("content");
-    this._content.progressListenerCreator = function (content, browser) {
-      return new ProgressController(content, browser);
-    };
-
     var os = Cc["@mozilla.org/observer-service;1"].getService(Ci.nsIObserverService);
     os.addObserver(gXPInstallObserver, "xpinstall-install-blocked", false);
     os.addObserver(gXPInstallObserver, "xpinstall-download-started", false);
 
+    // XXX hook up memory-pressure notification to clear out tab browsers
+    //os.addObserver(function(subject, topic, data) self.destroyEarliestBrowser(), "memory-pressure", false);
+
     BrowserUI.init();
 
     window.QueryInterface(Ci.nsIDOMChromeWindow).browserDOMWindow = new nsBrowserAccess();
 
-    this._content.addEventListener("command", this._handleContentCommand, false);
-    this._content.addEventListener("DOMUpdatePageReport", gPopupBlockerObserver.onUpdatePageReport, false);
-    this._content.tabList = document.getElementById("tab-list");
-    this._content.newTab(true);
+    let browsers = document.getElementById("browsers");
+    browsers.addEventListener("command", this._handleContentCommand, false);
+    browsers.addEventListener("DOMUpdatePageReport", gPopupBlockerObserver.onUpdatePageReport, false);
 
-    var deckbrowser = this.content;
+    /* Initialize Spatial Navigation */
+    /*
+    var deckbrowser = content;
     function panCallback(aElement) {
       // SpatialNav calls commandDispatcher.advanceFocus/rewindFocus, which
       // can mess the scroll state up. Reset it.
       deckbrowser.browser.contentWindow.scrollTo(0, 0);
 
       if (!aElement)
         return;
 
       deckbrowser.ensureElementIsVisible(aElement);
     }
-    SpatialNavigation.init(this.content, panCallback);
+    SpatialNavigation.init(content, panCallback);
+    */
 
+    /* Initialize Geolocation */
     this.setupGeolocationPrompt();
 
+
+    /* Login Manager */
     Cc["@mozilla.org/login-manager;1"].getService(Ci.nsILoginManager);
 
+
+    /* Command line arguments/initial homepage */
     // If this is an intial window launch (was a nsICommandLine passed via window params)
     // we execute some logic to load the initial launch page
     if (window.arguments && window.arguments[0]) {
       var whereURI = null;
 
       try {
         // Try to access the commandline
         var cmdLine = window.arguments[0].QueryInterface(Ci.nsICommandLine);
@@ -166,23 +230,36 @@ var Browser = {
     if (disablePlugins)
     {
       document.getElementById("plugins.enabled").pref.value = false;
       this.setPluginState(false);
     }
     gPrefService.setBoolPref("temporary.disablePlugins", false);
   },
 
+  updateViewportSize: function() {
+    // XXX make sure this is right, and then add a better function for it.
+    var [w,h] = this._content._contentAreaDimensions;
+    w = Math.ceil(this._content._pageToScreen(w));
+    h = Math.ceil(this._content._pageToScreen(h));
+
+    if (!this._currentViewportBounds || w != this._currentViewportBounds.width || h != this._currentViewportBounds.height) {
+      this._currentViewportBounds = {width: w, height: h};
+      let bounds = { top: 0, left: 0, right: Math.max(800, w), bottom: Math.max(480, h) }
+      //dump("setViewportBounds: " + bounds.toSource() + "\n");
+      ws.setViewportBounds(bounds);
+    }
+  },
+
   setPluginState: function(enabled)
   {
-    var phs = Components.classes["@mozilla.org/plugin/host;1"]
-                        .getService(Components.interfaces.nsIPluginHost);
+    var phs = Cc["@mozilla.org/plugin/host;1"].getService(Ci.nsIPluginHost);
     var plugins = phs.getPluginTags({ });
-     for (var i = 0; i < plugins.length; ++i)
-       plugins[i].disabled = !enabled;
+    for (var i = 0; i < plugins.length; ++i)
+      plugins[i].disabled = !enabled;
   },
 
   setupGeolocationPrompt: function() {
     try {
       var geolocationService = Cc["@mozilla.org/geolocation/service;1"].getService(Ci.nsIGeolocationService);
     }
     catch (ex) {
       return;
@@ -229,59 +306,139 @@ var Browser = {
     return this._content;
   },
 
   /**
    * Return the currently active <browser> object, since a <deckbrowser> may
    * have more than one
    */
   get currentBrowser() {
-    return this._content.browser;
+    return this._currentTab.browser;
+  },
+
+  get currentTab() {
+    return this._currentTab;
+  },
+
+  getTabAtIndex: function(index) {
+    if (index > this._tabs.length || index < 0)
+      return null;
+    return this._tabs[index];
+  },
+
+  getTabFromContent: function(content) {
+    for (var t = 0; t < this._tabs.length; t++) {
+      if (this._tabs[t].content == content)
+        return this._tabs[t];
+    }
+    return null;
+  },
+
+  newTab: function(bringFront) {
+    let newTab = new Tab();
+    newTab.create();
+    this._tabs.push(newTab);
+
+    let event = document.createEvent("Events");
+    event.initEvent("TabOpen", true, false);
+    newTab.content.dispatchEvent(event);
+
+    if (bringFront)
+      this.selectTab(newTab);
+
+    return newTab;
   },
 
-  supportsCommand : function(cmd) {
+  closeTab: function(tab) {
+    if (tab instanceof XULElement)
+      tab = this.getTabFromContent(tab);
+
+    if (!tab)
+      return;
+
+    let tabIndex = this._tabs.indexOf(tab);
+
+    let nextTab = this._currentTab;
+    if (this._currentTab == tab) {
+      nextTab = this.getTabAtIndex(tabIndex + 1) || this.getTabAtIndex(tabIndex - 1);
+      if (!nextTab)
+        return;
+    }
+
+    let event = document.createEvent("Events");
+    event.initEvent("TabClose", true, false);
+    tab.content.dispatchEvent(event);
+
+    tab.destroy();
+    this._tabs.splice(tabIndex, 1);
+
+    // redraw the tabs
+    for (let t = tabIndex; t < this._tabs.length; t++)
+      this._tabs[t].updateThumbnail();
+
+    this.selectTab(nextTab);
+  },
+
+  selectTab: function(tab) {
+    if (tab instanceof XULElement)
+      tab = this.getTabFromContent(tab);
+
+    if (!tab || this._currentTab == tab)
+      return;
+
+    this._currentTab = tab;
+    this._content.setCurrentBrowser(this.currentBrowser);
+    document.getElementById("tabs").selectedItem = tab.content;
+
+    ws.panTo(0,0, true);
+
+    let event = document.createEvent("Events");
+    event.initEvent("TabSelect", true, false);
+    tab.content.dispatchEvent(event);
+  },
+
+  supportsCommand: function(cmd) {
     var isSupported = false;
     switch (cmd) {
       case "cmd_fullscreen":
-      case "cmd_downloads":
         isSupported = true;
         break;
       default:
         isSupported = false;
         break;
     }
     return isSupported;
   },
 
-  isCommandEnabled : function(cmd) {
+  isCommandEnabled: function(cmd) {
     return true;
   },
 
-  doCommand : function(cmd) {
-    var browser = this.content.browser;
+  doCommand: function(cmd) {
+    var browser = this.currentBrowser;
 
     switch (cmd) {
       case "cmd_fullscreen":
         window.fullScreen = !window.fullScreen;
         break;
     }
   },
 
-  getNotificationBox : function() {
+  getNotificationBox: function() {
     return document.getElementById("notifications");
   },
 
   findState: FINDSTATE_FIND,
   openFind: function(aState) {
     this.findState = aState;
 
     var findbar = document.getElementById("findbar");
     var browser = findbar.browser;
     if (!browser) {
-      browser = this.content.browser;
+      browser = this.currentBrowser;
       findbar.browser = browser;
     }
 
     var panel = document.getElementById("findpanel");
     if (panel.state == "open")
       this.doFind(null);
     else
       panel.openPopup(document.getElementById("findpanel-placeholder"), "before_start");
@@ -373,184 +530,36 @@ var Browser = {
         } catch (e) { /* Fall back on about blank */ }
 
         Browser.currentBrowser.loadURI(url, null, null, false);
       }
     }
   }
 };
 
-function ProgressController(aTabBrowser, aBrowser) {
-  this._tabbrowser = aTabBrowser;
-  this.init(aBrowser);
-}
-
-ProgressController.prototype = {
-  _browser : null,
-
-  init : function(aBrowser) {
-    this._browser = aBrowser;
-  },
-
-  onStateChange : function(aWebProgress, aRequest, aStateFlags, aStatus) {
-    // we currently only care about state changes for the main document,
-    // not sub-frames
-    if (aWebProgress.DOMWindow != this._browser.contentWindow) {
-      return;
-    }
-
-    if (aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK) {
-      if (aStateFlags & Ci.nsIWebProgressListener.STATE_START)
-        BrowserUI.update(TOOLBARSTATE_LOADING, this._browser);
-      else if (aStateFlags & Ci.nsIWebProgressListener.STATE_STOP)
-        BrowserUI.update(TOOLBARSTATE_LOADED, this._browser);
-    }
-
-    if (aStateFlags & Ci.nsIWebProgressListener.STATE_IS_DOCUMENT) {
-      if (aStateFlags & Ci.nsIWebProgressListener.STATE_STOP) {
-        aWebProgress.DOMWindow.focus();
-
-        // update the viewport
-        this._tabbrowser.updateCanvasState();
-
-        // update the tab canvas image
-        this._tabbrowser.updateBrowser(this._browser, true);
-
-        // linkify phone numbers
-        Browser.translatePhoneNumbers();
-
-        //aWebProgress.DOMWindow.scrollbars.visible = false;
-      }
-    }
-  },
-
-  // This method is called to indicate progress changes for the currently
-  // loading page.
-  onProgressChange : function(aWebProgress, aRequest, aCurSelf, aMaxSelf, aCurTotal, aMaxTotal) {
-  },
-
-  // This method is called to indicate a change to the current location.
-  onLocationChange : function(aWebProgress, aRequest, aLocationURI) {
-
-    var location = aLocationURI ? aLocationURI.spec : "";
-    this._hostChanged = true;
-
-    // This code here does not compare uris exactly when determining
-    // whether or not the message(s) should be hidden since the message
-    // may be prematurely hidden when an install is invoked by a click
-    // on a link that looks like this:
-    //
-    // <a href="#" onclick="return install();">Install Foo</a>
-    //
-    // - which fires a onLocationChange message to uri + '#'...
-    cBrowser = Browser.currentBrowser;
-    if (cBrowser.lastURI) {
-      var oldSpec = cBrowser.lastURI.spec;
-      var oldIndexOfHash = oldSpec.indexOf("#");
-      if (oldIndexOfHash != -1)
-        oldSpec = oldSpec.substr(0, oldIndexOfHash);
-      var newSpec = location;
-      var newIndexOfHash = newSpec.indexOf("#");
-      if (newIndexOfHash != -1)
-        newSpec = newSpec.substr(0, newSpec.indexOf("#"));
-      if (newSpec != oldSpec) {
-        // Remove all the notifications, except for those which want to
-        // persist across the first location change.
-        var nBox = Browser.getNotificationBox();
-        nBox.removeTransientNotifications();
-      }
-    }
-    cBrowser.lastURI = aLocationURI;
-
-
-    if (aWebProgress.DOMWindow == this._browser.contentWindow) {
-      BrowserUI.setURI();
-      this._tabbrowser.updateBrowser(this._browser, false);
-    }
-  },
-
-  // This method is called to indicate a status changes for the currently
-  // loading page.  The message is already formatted for display.
-  onStatusChange : function(aWebProgress, aRequest, aStatus, aMessage) {
-  },
-
- // Properties used to cache security state used to update the UI
-  _state: null,
-  _host: undefined,
-  _hostChanged: false, // onLocationChange will flip this bit
-
-  // This method is called when the security state of the browser changes.
-  onSecurityChange : function(aWebProgress, aRequest, aState) {
-
-    // Don't need to do anything if the data we use to update the UI hasn't
-    // changed
-    if (this._state == aState &&
-        !this._hostChanged) {
-      return;
-    }
-    this._state = aState;
-
-    try {
-      this._host = getBrowser().contentWindow.location.host;
-    } catch(ex) {
-      this._host = null;
-    }
-
-    this._hostChanged = false;
-
-    // Don't pass in the actual location object, since it can cause us to
-    // hold on to the window object too long.  Just pass in the fields we
-    // care about. (bug 424829)
-    var location = getBrowser().contentWindow.location;
-    var locationObj = {};
-    try {
-      locationObj.host = location.host;
-      locationObj.hostname = location.hostname;
-      locationObj.port = location.port;
-    } catch (ex) {
-      // Can sometimes throw if the URL being visited has no host/hostname,
-      // e.g. about:blank. The _state for these pages means we won't need these
-      // properties anyways, though.
-    }
-    getIdentityHandler().checkIdentity(this._state, locationObj);
-
-  },
-
-  QueryInterface : function(aIID) {
-    if (aIID.equals(Components.interfaces.nsIWebProgressListener) ||
-        aIID.equals(Components.interfaces.nsISupportsWeakReference) ||
-        aIID.equals(Components.interfaces.nsISupports))
-      return this;
-
-    throw Components.results.NS_ERROR_NO_INTERFACE;
-  }
-};
-
 function nsBrowserAccess()
 {
 }
 
 nsBrowserAccess.prototype =
 {
-  QueryInterface : function(aIID)
-  {
-    if (aIID.equals(Ci.nsIBrowserDOMWindow) ||
-        aIID.equals(Ci.nsISupports))
+  QueryInterface: function(aIID) {
+    if (aIID.equals(Ci.nsIBrowserDOMWindow) || aIID.equals(Ci.nsISupports))
       return this;
     throw Components.results.NS_NOINTERFACE;
   },
 
-  openURI : function(aURI, aOpener, aWhere, aContext)
-  {
+  openURI: function(aURI, aOpener, aWhere, aContext) {
     var isExternal = (aContext == Ci.nsIBrowserDOMWindow.OPEN_EXTERNAL);
 
     if (isExternal && aURI && aURI.schemeIs("chrome")) {
       dump("use -chrome command-line option to load external chrome urls\n");
       return null;
     }
+
     var loadflags = isExternal ?
                        Ci.nsIWebNavigation.LOAD_FLAGS_FROM_EXTERNAL :
                        Ci.nsIWebNavigation.LOAD_FLAGS_NONE;
     var location;
     if (aWhere == Ci.nsIBrowserDOMWindow.OPEN_DEFAULTWINDOW) {
       switch (aContext) {
         case Ci.nsIBrowserDOMWindow.OPEN_EXTERNAL :
           aWhere = gPrefService.getIntPref("browser.link.open_external");
@@ -562,50 +571,41 @@ nsBrowserAccess.prototype =
 
     var newWindow;
     if (aWhere == Ci.nsIBrowserDOMWindow.OPEN_NEWWINDOW) {
       var url = aURI ? aURI.spec : "about:blank";
       newWindow = openDialog("chrome://browser/content/browser.xul", "_blank",
                              "all,dialog=no", url, null, null, null);
     }
     else {
-      if (aWhere == Ci.nsIBrowserDOMWindow.OPEN_NEWTAB) {
-        var tab = Browser._content.newTab(true);
-        if (tab) {
-          var content = Browser._content;
-          var browser = content.getBrowserForDisplay(content.getDisplayForTab(tab));
-          newWindow = browser.contentWindow;
-        }
-      }
-      else {
+      if (aWhere == Ci.nsIBrowserDOMWindow.OPEN_NEWTAB)
+        newWindow = Browser.newTab(true).browser.contentWindow;
+      else
         newWindow = aOpener ? aOpener.top : browser.contentWindow;
-      }
     }
 
     try {
       var referrer;
       if (aURI) {
         if (aOpener) {
           location = aOpener.location;
-          referrer = Components.classes["@mozilla.org/network/io-service;1"]
-                               .getService(Components.interfaces.nsIIOService)
-                               .newURI(location, null, null);
+          referrer = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService)
+                                                            .newURI(location, null, null);
         }
         newWindow.QueryInterface(Ci.nsIInterfaceRequestor)
                  .getInterface(Ci.nsIWebNavigation)
                  .loadURI(aURI.spec, loadflags, referrer, null, null);
       }
       newWindow.focus();
     } catch(e) { }
 
     return newWindow;
   },
 
-  isTabContentWindow : function(aWindow)
-  {
+  isTabContentWindow: function(aWindow) {
     return Browser._content.browsers.some(function (browser) browser.contentWindow == aWindow);
   }
 }
 
 /**
  * Utility class to handle manipulations of the identity indicators in the UI
  */
 function IdentityHandler() {
@@ -633,41 +633,41 @@ IdentityHandler.prototype = {
 
   // Cache the most recent SSLStatus and Location seen in checkIdentity
   _lastStatus : null,
   _lastLocation : null,
 
   /**
    * Build out a cache of the elements that we need frequently.
    */
-  _cacheElements : function() {
+  _cacheElements: function() {
     this._identityPopup = document.getElementById("identity-popup");
     this._identityBox = document.getElementById("identity-box");
     this._identityPopupContentBox = document.getElementById("identity-popup-content-box");
     this._identityPopupContentHost = document.getElementById("identity-popup-content-host");
     this._identityPopupContentOwner = document.getElementById("identity-popup-content-owner");
     this._identityPopupContentSupp = document.getElementById("identity-popup-content-supplemental");
     this._identityPopupContentVerif = document.getElementById("identity-popup-content-verifier");
     this._identityPopupEncLabel = document.getElementById("identity-popup-encryption-label");
   },
 
   /**
    * Handler for mouseclicks on the "More Information" button in the
    * "identity-popup" panel.
    */
-  handleMoreInfoClick : function(event) {
+  handleMoreInfoClick: function(event) {
     displaySecurityInfo();
     event.stopPropagation();
   },
 
   /**
    * Helper to parse out the important parts of _lastStatus (of the SSL cert in
    * particular) for use in constructing identity UI strings
   */
-  getIdentityData : function() {
+  getIdentityData: function() {
     var result = {};
     var status = this._lastStatus.QueryInterface(Components.interfaces.nsISSLStatus);
     var cert = status.serverCert;
 
     // Human readable name of Subject
     result.subjectOrg = cert.organization;
 
     // SubjectName fields, broken up for individual access
@@ -695,17 +695,17 @@ IdentityHandler.prototype = {
    * Determine the identity of the page being displayed by examining its SSL cert
    * (if available) and, if necessary, update the UI to reflect this.  Intended to
    * be called by onSecurityChange
    *
    * @param PRUint32 state
    * @param JS Object location that mirrors an nsLocation (i.e. has .host and
    *                           .hostname and .port)
    */
-  checkIdentity : function(state, location) {
+  checkIdentity: function(state, location) {
     var currentStatus = getBrowser().securityUI
                                 .QueryInterface(Components.interfaces.nsISSLStatusProvider)
                                 .SSLStatus;
     this._lastStatus = currentStatus;
     this._lastLocation = location;
 
     if (state & Components.interfaces.nsIWebProgressListener.STATE_IDENTITY_EV_TOPLEVEL)
       this.setMode(this.IDENTITY_MODE_IDENTIFIED);
@@ -713,17 +713,17 @@ IdentityHandler.prototype = {
       this.setMode(this.IDENTITY_MODE_DOMAIN_VERIFIED);
     else
       this.setMode(this.IDENTITY_MODE_UNKNOWN);
   },
 
   /**
    * Return the eTLD+1 version of the current hostname
    */
-  getEffectiveHost : function() {
+  getEffectiveHost: function() {
     // Cache the eTLDService if this is our first time through
     if (!this._eTLDService)
       this._eTLDService = Cc["@mozilla.org/network/effective-tld-service;1"]
                          .getService(Ci.nsIEffectiveTLDService);
     try {
       return this._eTLDService.getBaseDomainFromHost(this._lastLocation.hostname);
     } catch (e) {
       // If something goes wrong (e.g. hostname is an IP address) just fail back
@@ -731,17 +731,17 @@ IdentityHandler.prototype = {
       return this._lastLocation.hostname;
     }
   },
 
   /**
    * Update the UI to reflect the specified mode, which should be one of the
    * IDENTITY_MODE_* constants.
    */
-  setMode : function(newMode) {
+  setMode: function(newMode) {
     if (!this._identityBox) {
       // No identity box means the identity box is not visible, in which
       // case there's nothing to do.
       return;
     }
 
     this._identityBox.className = newMode;
     this.setIdentityMessages(newMode);
@@ -752,17 +752,17 @@ IdentityHandler.prototype = {
   },
 
   /**
    * Set up the messages for the primary identity UI based on the specified mode,
    * and the details of the SSL cert, where applicable
    *
    * @param newMode The newly set identity mode.  Should be one of the IDENTITY_MODE_* constants.
    */
-  setIdentityMessages : function(newMode) {
+  setIdentityMessages: function(newMode) {
     if (newMode == this.IDENTITY_MODE_DOMAIN_VERIFIED) {
       var iData = this.getIdentityData();
 
       // We need a port number for all lookups.  If one hasn't been specified, use
       // the https default
       var lookupHost = this._lastLocation.host;
       if (lookupHost.indexOf(':') < 0)
         lookupHost += ":443";
@@ -802,17 +802,17 @@ IdentityHandler.prototype = {
 
   /**
    * Set up the title and content messages for the identity message popup,
    * based on the specified mode, and the details of the SSL cert, where
    * applicable
    *
    * @param newMode The newly set identity mode.  Should be one of the IDENTITY_MODE_* constants.
    */
-  setPopupMessages : function(newMode) {
+  setPopupMessages: function(newMode) {
 
     this._identityPopup.className = newMode;
     this._identityPopupContentBox.className = newMode;
 
     // Set the static strings up front
     this._identityPopupEncLabel.textContent = this._staticStrings[newMode].encryption_label;
 
     // Initialize the optional strings to empty values
@@ -852,24 +852,24 @@ IdentityHandler.prototype = {
 
     // Push the appropriate strings out to the UI
     this._identityPopupContentHost.textContent = host;
     this._identityPopupContentOwner.textContent = owner;
     this._identityPopupContentSupp.textContent = supplemental;
     this._identityPopupContentVerif.textContent = verifier;
   },
 
-  hideIdentityPopup : function() {
+  hideIdentityPopup: function() {
     this._identityPopup.hidePopup();
   },
 
   /**
    * Click handler for the identity-box element in primary chrome.
    */
-  handleIdentityButtonEvent : function(event) {
+  handleIdentityButtonEvent: function(event) {
 
     event.stopPropagation();
 
     if ((event.type == "click" && event.button != 0) ||
         (event.type == "keypress" && event.charCode != KeyEvent.DOM_VK_SPACE &&
          event.keyCode != KeyEvent.DOM_VK_RETURN))
       return; // Left click, space or enter only
 
@@ -1042,8 +1042,297 @@ const gXPInstallObserver = {
         break;
     }
   }
 };
 
 function getNotificationBox(aWindow) {
   return Browser.getNotificationBox();
 }
+
+
+function ProgressController(tab) {
+  this._tab = tab;
+}
+
+ProgressController.prototype = {
+  get browser() {
+    return this._tab.browser;
+  },
+
+  onStateChange: function(aWebProgress, aRequest, aStateFlags, aStatus) {
+    // ignore notification that aren't about the main document (iframes, etc)
+    if (aWebProgress.DOMWindow != this._tab.browser.contentWindow)
+      return;
+
+    if (aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK) {
+      if (aStateFlags & Ci.nsIWebProgressListener.STATE_START)
+        this._networkStart();
+      else if (aStateFlags & Ci.nsIWebProgressListener.STATE_STOP)
+        this._networkStop();
+    } else if (aStateFlags & Ci.nsIWebProgressListener.STATE_IS_DOCUMENT) {
+      if (aStateFlags & Ci.nsIWebProgressListener.STATE_STOP) {
+        this._documentStop();
+      }
+    }
+  },
+
+  // This method is called to indicate progress changes for the currently
+  // loading page.
+  onProgressChange: function(aWebProgress, aRequest, aCurSelf, aMaxSelf, aCurTotal, aMaxTotal) {
+  },
+
+  // This method is called to indicate a change to the current location.
+  onLocationChange: function(aWebProgress, aRequest, aLocationURI) {
+    // XXX this code is not multiple-tab friendly.
+
+    var location = aLocationURI ? aLocationURI.spec : "";
+    this._hostChanged = true;
+
+    // This code here does not compare uris exactly when determining
+    // whether or not the message(s) should be hidden since the message
+    // may be prematurely hidden when an install is invoked by a click
+    // on a link that looks like this:
+    //
+    // <a href="#" onclick="return install();">Install Foo</a>
+    //
+    // - which fires a onLocationChange message to uri + '#'...
+    currentBrowser = Browser.currentBrowser;
+    if (currentBrowser.lastURI) {
+      var oldSpec = currentBrowser.lastURI.spec;
+      var oldIndexOfHash = oldSpec.indexOf("#");
+      if (oldIndexOfHash != -1)
+        oldSpec = oldSpec.substr(0, oldIndexOfHash);
+      var newSpec = location;
+      var newIndexOfHash = newSpec.indexOf("#");
+      if (newIndexOfHash != -1)
+        newSpec = newSpec.substr(0, newSpec.indexOf("#"));
+      if (newSpec != oldSpec) {
+        // Remove all the notifications, except for those which want to
+        // persist across the first location change.
+
+	// XXX
+        // var nBox = Browser.getNotificationBox();
+        // nBox.removeTransientNotifications();
+      }
+    }
+    currentBrowser.lastURI = aLocationURI;
+    if (aWebProgress.DOMWindow == Browser.currentBrowser.contentWindow) {
+      BrowserUI.setURI();
+    }
+  },
+
+  // This method is called to indicate a status changes for the currently
+  // loading page.  The message is already formatted for display.
+  onStatusChange: function(aWebProgress, aRequest, aStatus, aMessage) {
+  },
+
+  _networkStart: function() {
+    this._tab.setLoading(true);
+    //dump("started loading network\n");
+
+    if (Browser.currentBrowser == this.browser) {
+      Browser.content.startLoading();
+      BrowserUI.update(TOOLBARSTATE_LOADING);
+    }
+  },
+
+  _networkStop: function() {
+    this._tab.setLoading(false);
+
+    if (Browser.currentBrowser == this.browser) {
+      BrowserUI.update(TOOLBARSTATE_LOADED);
+    }
+  },
+
+  _documentStop: function() {
+    //dump("stop, hammer time\n");
+
+    // translate any phone numbers
+    Browser.translatePhoneNumbers();
+
+    if (Browser.currentBrowser == this.browser) {
+      // focus the dom window
+      this.browser.contentWindow.focus();
+
+      Browser.content.endLoading();
+    }
+    this._tab.updateThumbnail();
+  },
+
+ // Properties used to cache security state used to update the UI
+  _state: null,
+  _host: undefined,
+  _hostChanged: false, // onLocationChange will flip this bit
+
+  // This method is called when the security state of the browser changes.
+  onSecurityChange: function(aWebProgress, aRequest, aState) {
+    // Don't need to do anything if the data we use to update the UI hasn't
+    // changed
+    if (this._state == aState &&
+        !this._hostChanged) {
+      return;
+    }
+    this._state = aState;
+
+    try {
+      this._host = getBrowser().contentWindow.location.host;
+    }
+    catch(ex) {
+      this._host = null;
+    }
+
+    this._hostChanged = false;
+
+    // Don't pass in the actual location object, since it can cause us to
+    // hold on to the window object too long.  Just pass in the fields we
+    // care about. (bug 424829)
+    var location = getBrowser().contentWindow.location;
+    var locationObj = {};
+    try {
+      locationObj.host = location.host;
+      locationObj.hostname = location.hostname;
+      locationObj.port = location.port;
+    }
+    catch (ex) {
+      // Can sometimes throw if the URL being visited has no host/hostname,
+      // e.g. about:blank. The _state for these pages means we won't need these
+      // properties anyways, though.
+    }
+    getIdentityHandler().checkIdentity(this._state, locationObj);
+
+  },
+
+  QueryInterface: function(aIID) {
+    if (aIID.equals(Components.interfaces.nsIWebProgressListener) ||
+        aIID.equals(Components.interfaces.nsISupportsWeakReference) ||
+        aIID.equals(Components.interfaces.nsISupports))
+      return this;
+
+    throw Components.results.NS_ERROR_NO_INTERFACE;
+  }
+};
+
+
+function Tab() {
+}
+
+Tab.prototype = {
+  _id: null,
+  _browser: null,
+  _state: null,
+  _listener: null,
+  _loading: false,
+  _content: null,
+
+  get browser() {
+    return this._browser;
+  },
+
+  get content() {
+    return this._content;
+  },
+
+  isLoading: function() {
+    return this._loading;
+  },
+
+  setLoading: function(b) {
+    this._loading = b;
+  },
+
+  create: function() {
+    this._content = document.createElement("richlistitem");
+    this._content.setAttribute("type", "documenttab");
+    document.getElementById("tabs").addTab(this._content);
+
+    this._createBrowser();
+  },
+
+  destroy: function() {
+    this._destroyBrowser();
+    document.getElementById("tabs").removeTab(this._content);
+  },
+
+  _createBrowser: function() {
+    if (this._browser)
+      throw "Browser already exists";
+
+    let browser = this._browser = document.createElement("browser");
+    browser.className = "deckbrowser-browser";
+    browser.setAttribute("style", "overflow: hidden; visibility: hidden; width: 1024px; height: 800px;");
+    browser.setAttribute("contextmenu", document.getElementById("canvas").getAttribute("contextmenu"));
+    browser.setAttribute("autocompletepopup", document.getElementById("canvas").getAttribute("autocompletepopup"));
+    browser.setAttribute("type", "content");
+
+    document.getElementById("browsers").appendChild(browser);
+
+    this._listener = new ProgressController(this);
+    browser.addProgressListener(this._listener);
+  },
+
+  _destroyBrowser: function() {
+    document.getElementById("browsers").removeChild(this._browser);
+  },
+
+  saveState: function() {
+    let state = { };
+
+    this._url = browser.contentWindow.location.toString();
+    var browser = this.getBrowserForDisplay(display);
+    var doc = browser.contentDocument;
+    if (doc instanceof HTMLDocument) {
+      var tags = ["input", "textarea", "select"];
+
+      for (var t = 0; t < tags.length; t++) {
+        var elements = doc.getElementsByTagName(tags[t]);
+        for (var e = 0; e < elements.length; e++) {
+          var element = elements[e];
+          var id;
+          if (element.id)
+            id = "#" + element.id;
+          else if (element.name)
+            id = "$" + element.name;
+
+          if (id)
+            state[id] = element.value;
+        }
+      }
+    }
+
+    state._scrollX = browser.contentWindow.scrollX;
+    state._scrollY = browser.contentWindow.scrollY;
+
+    this._state = state;
+  },
+
+  restoreState: function() {
+    let state = this._state;
+    if (!state)
+      return;
+
+    let doc = this._browser.contentDocument;
+    for (item in state) {
+      var elem = null;
+      if (item.charAt(0) == "#") {
+        elem = doc.getElementById(item.substring(1));
+      }
+      else if (item.charAt(0) == "$") {
+        var list = doc.getElementsByName(item.substring(1));
+        if (list.length)
+          elem = list[0];
+      }
+
+      if (elem)
+        elem.value = state[item];
+    }
+
+    this._browser.contentWindow.scrollTo(state._scrollX, state._scrollY);
+  },
+
+  updateThumbnail: function() {
+    if (!this._browser)
+      return;
+
+    let srcCanvas = (Browser.currentBrowser == this._browser) ? document.getElementById("canvas") : null;
+    this._content.updateThumbnail(this._browser, srcCanvas);
+  }
+}
--- a/chrome/content/browser.xul
+++ b/chrome/content/browser.xul
@@ -52,23 +52,27 @@
 
 <window id="main-window"
         width="800" height="480"
         onload="Browser.startup();"
         windowtype="navigator:browser"
         title="&brandShortName;"
         titlemodifier="&brandShortName;"
         titleseparator="&mainWindow.titleseparator;"
-        xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+        xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+        xmlns:html="http://www.w3.org/1999/xhtml">
 
   <script type="application/x-javascript" src="chrome://global/content/inlineSpellCheckUI.js"/>
   <script type="application/x-javascript" src="chrome://browser/content/commandUtil.js"/>
   <script type="application/x-javascript" src="chrome://browser/content/browser.js"/>
   <script type="application/x-javascript" src="chrome://browser/content/browser-ui.js"/>
   <script type="application/x-javascript" src="chrome://browser/content/sanitize.js"/>
+  <script type="application/x-javascript" src="chrome://browser/content/CanvasBrowser.js"/>
+  <script type="application/x-javascript" src="chrome://browser/content/WidgetStack.js"/>
+  <script type="application/x-javascript" src="chrome://browser/content/InputHandler.js"/>
 
   <stringbundleset id="stringbundleset">
     <stringbundle id="bundle_browser" src="chrome://browser/locale/browser.properties"/>
     <stringbundle id="bundle_brand" src="chrome://branding/locale/brand.properties"/>
     <stringbundle id="bundle_keys" src="chrome://global/locale/keys.properties"/>
     <stringbundle id="bundle_platformKeys" src="chrome://global-platform/locale/platformKeys.properties"/>
   </stringbundleset>
 
@@ -189,35 +193,82 @@
             </vbox>
             <description id="identity-popup-encryption-label" flex="1"/>
           </hbox>
         </vbox>
       </hbox>
     </panel>
   </popupset>
 
-  <stack id="browser-container" flex="1" style="overflow: hidden;">
-    <vbox id="contentBox" style="-moz-stack-sizing: ignore; margin-top: 60px; margin: 0;">
-      <notificationbox id="notifications" flex="1">
-        <deckbrowser id="content"
-                     autocompletepopup="popup_autocomplete_content"
-                     flex="1"
-                     onnewtab="CommandUpdater.doCommand('cmd_newTab');"/>
-      </notificationbox>
+  <!-- stupid stack needs to be in a box. not sure why -->
+  <box>
+  <stack id="browser-container" flex="1" style="width: 800px; height: 480px; max-width: 800px; max-height: 480px; background: lightgrey; /*background-image: url('chrome://browser/content/checkerboard.png')*/">
+
+    <!-- begin: Browser View -->
+    <hbox id="canvasbox"
+	  style="-moz-stack-sizing: ignore; height: 1440px; width: 800px;"
+	  left="0" top="-480"
+	  vptargetx="0"
+	  vptargety="0"
+	  vptargetw="800"
+	  vptargeth="480"
+	  viewport="true">
+
+      <html:canvas id="canvas"
+		   moz-opaque="true"
+		   viewport="true"
+		   style="height: 1440px; width: 800px;"
+		   height="1440" width="800"
+		   autocompletepopup="popup_autocomplete_content"
+		   onnewtab="CommandUpdater.doCommand('cmd_newTab');"/>
+
+    </hbox>
+
+    <!-- end: Browser View -->
+
+    <!-- start: some barriers -->
+    <spacer style="-moz-stack-sizing: ignore; width: 1px; height: 1px;" barriertype="vertical" size="30" left="0" constraint="vp-relative"/>
+    <spacer style="-moz-stack-sizing: ignore; width: 1px; height: 1px;" barriertype="vertical" size="30" left="800" constraint="vp-relative"/>
+    <!-- end: barriers -->
+
+    <!-- begin: left bar -->
+    <vbox id="tabs-container" style="-moz-stack-sizing: ignore; width: 132px; height: 420px;"
+          left="-132" top="60"
+          constraint="ignore-y,vp-relative">
+      <richlistbox id="tabs" onselect="BrowserUI.selectTab(this.selectedItem);" onclosetab="BrowserUI.closeTab(this);"/>
+      <hbox>
+        <toolbarbutton id="newtab-button" command="cmd_newTab"/>
+        <toolbarbutton id="retrievetab-button" command="" hidden="true"/>
+      </hbox>
     </vbox>
+    <!-- end: left bar -->
 
-    <toolbar id="toolbar-main" style="-moz-stack-sizing: ignore; height: 60px" top="0" left="0">
+    <!-- begin: right bar -->
+    <vbox id="browser-controls" style="-moz-stack-sizing: ignore; width: 80px; height: 420px;"
+          left="800" top="60"
+          constraint="ignore-y,vp-relative">
+      <toolbarbutton id="tool-star" class="browser-control-button" command="cmd_star"/>
+      <toolbarbutton id="tool-back" class="browser-control-button" command="cmd_back"/>
+      <toolbarbutton id="tool-forward" class="browser-control-button" command="cmd_forward"/>
+      <toolbarspring/>
+      <toolbarbutton id="tool-actions" class="browser-control-button" command="cmd_actions" hidden="true"/>
+      <toolbarbutton id="tool-panel" class="browser-control-button" command="cmd_panel" type="checkbox"/>
+    </vbox>
+    <!-- end: right bar -->
+
+    <!-- begin: Main Toolbar -->
+    <toolbar id="toolbar-main" style="-moz-stack-sizing: ignore; width: 800px; height: 60px" top="-60" left="0" constraint="ignore-x,vp-relative">
       <hbox id="urlbar-container" flex="1">
         <box id="identity-box"
              onclick="getIdentityHandler().handleIdentityButtonEvent(event);"
              onkeypress="getIdentityHandler().handleIdentityButtonEvent(event);">
-          <deck id="urlbar-image-deck">
+          <box id="urlbar-image-box">
             <image id="urlbar-throbber"/>
-            <image id="urlbar-favicon"/>
-          </deck>
+            <image id="urlbar-favicon" hidden="true"/>
+          </box>
         </box>
         <hbox id="urlbar-editarea" flex="1">
           <description id="urlbar-caption" crop="end" flex="1"/>
           <textbox id="urlbar-edit"
                    type="autocomplete"
                    autocompletesearch="history"
                    enablehistory="false"
                    maxrows="6"
@@ -233,52 +284,43 @@
         <hbox id="urlbar-icons">
           <toolbarbutton id="tool-reload" class="urlbar-cap-button" command="cmd_reload"/>
           <toolbarbutton id="tool-stop" class="urlbar-cap-button" command="cmd_stop"/>
           <toolbarbutton id="tool-go" class="urlbar-cap-button" command="cmd_go"/>
         </hbox>
       </hbox>
       <toolbarbutton id="tool-bookmarks" class="urlbar-button" command="cmd_bookmarks"/>
     </toolbar>
+    <!-- end: Main Toolbar -->
 
-    <vbox id="browser-controls" style="-moz-stack-sizing: ignore;" top="60" left="0">
-      <toolbarbutton id="tool-star" class="browser-control-button" command="cmd_star"/>
-      <toolbarbutton id="tool-back" class="browser-control-button" command="cmd_back"/>
-      <toolbarbutton id="tool-forward" class="browser-control-button" command="cmd_forward"/>
-      <toolbarspring/>
-      <toolbarbutton id="tool-actions" class="browser-control-button" command="cmd_actions" hidden="true"/>
-      <toolbarbutton id="tool-panel" class="browser-control-button" command="cmd_panel" type="checkbox"/>
-    </vbox>
-
-    <hbox id="panel-container" style="-moz-stack-sizing: ignore;" top="0" left="0">
+    <hbox id="panel-container" hidden="true" style="-moz-stack-sizing: ignore;" top="60" left="880" constraint="ignore-y,vp-relative">
       <vbox id="panel-controls" oncommand="BrowserUI.switchPane(event.target.getAttribute('linkedpanel'));">
         <toolbarspring/>
         <toolbarbutton id="tool-addons" type="radio" group="1" class="panel-button" linkedpanel="addons-container"/>
         <toolbarbutton id="tool-downloads" type="radio" group="1" class="panel-button" linkedpanel="downloads-container"/>
-        <toolbarbutton id="tool-preferences" type="radio" group="1" class="panel-button" linkedpanel="prefs-container" checked="true"/>
+        <toolbarbutton id="tool-preferences" type="radio" group="1" class="panel-button" linkedpanel="prefs-container"/>
         <toolbarbutton id="tool-shortcuts" type="radio" group="1" class="panel-button" linkedpanel="shortcuts-container" hidden="true"/>
       </vbox>
-      <deck id="panel-items" flex="1" selectedIndex="2">
+      <deck id="panel-items" flex="1">
         <iframe id="addons-container" flex="1"/>
         <iframe id="downloads-container" flex="1"/>
 
         <vbox id="prefs-container" flex="1">
           <hbox pack="center" id="buttons"/>
           <scrollbox orient="vertical" id="pref-list" seltype="single" flex="1">
             <vbox class="prefsection">
               <label value="&content.title;" crop="end" flex="1"/>
             </vbox>
             <richpref pref="permissions.default.image" title="&permissions.default.image.title;" type="boolint" on="1" off="2">
               &permissions.default.image.description;
             </richpref>
             <richpref pref="javascript.enabled" type="bool" title="&javascript.enabled.title;">
               &javascript.enabled.description;
             </richpref>
-            <richpref pref="plugins.enabled" type="bool" title="&plugins.enabled.title;" id="plugins.enabled"
-                      onsynctopreference="Browser.setPluginState(this.value);">
+            <richpref pref="plugins.enabled" type="bool" title="&plugins.enabled.title;" onsyncfrompreference="Browser.setPluginState(this.value);">
               &plugins.enabled.description;
             </richpref>
 
             <vbox class="prefsection">
               <label value="&privacy.title;" crop="end" flex="1"/>
             </vbox>
             <richpref pref="network.cookie.cookieBehavior" title="&network.cookie.cookieBehavior.title;" type="boolint" on="1" off="3">
               &network.cookie.cookieBehavior.description;
@@ -300,24 +342,16 @@
               <treecol id="value" flex="3" hideheader="true" editable="true" primary="true"/>
             </treecols>
             <treechildren id="shortcuts-children"/>
           </tree>
         </vbox>
       </deck>
     </hbox>
 
-    <vbox id="tab-list-container" style="-moz-stack-sizing: ignore;" top="60" left="0">
-      <richlistbox id="tab-list" onselect="BrowserUI.selectTab(this.selectedItem);" onclosetab="BrowserUI.closeTab(this);"/>
-      <hbox>
-        <toolbarbutton id="newtab-button" command="cmd_newTab"/>
-        <toolbarbutton id="retrievetab-button" command="" hidden="true"/>
-      </hbox>
-    </vbox>
-
     <vbox id="urllist-container" hidden="true" style="-moz-stack-sizing: ignore;" top="0" left="0">
       <hbox id="urllist-items-container" flex="1">
         <richlistbox id="urllist-items" flex="1"
                      onkeypress="if (event.keyCode == event.DOM_VK_RETURN) BrowserUI.goToURI(this.selectedItem.value)"/>
       </hbox>
       <hbox pack="end">
         <button id="urllist-close" oncommand="BrowserUI.show(UIMODE_NONE)"/>
       </hbox>
@@ -350,17 +384,22 @@
         </hbox>
         <hbox id="bookmark-buttons">
           <button label="&bookmarkRemove.label;" oncommand="BookmarkHelper.remove()"/>
           <spacer flex="1"/>
           <button id="bookmark-close" oncommand="BookmarkHelper.close()"/>
         </hbox>
       </vbox>
     </vbox>
+
   </stack>
 
+  </box>
+
   <vbox id="findpanel-placeholder" sizetopopup="always">
     <panel id="findpanel" onpopupshown="Browser.doFind()">
       <findbar id="findbar"/>
    </panel>
   </vbox>
 
+  <!-- where all the (hidden) <browser> elements go; I wish this could be display: none -->
+  <box id="browsers"/>
 </window>
deleted file mode 100644
--- a/chrome/content/deckbrowser.xml
+++ /dev/null
@@ -1,1523 +0,0 @@
-<?xml version="1.0"?>
-<!DOCTYPE bindings PUBLIC "-//MOZILLA//DTD XBL V1.0//EN" "http://www.mozilla.org/xbl">
-
-<bindings
-    xmlns="http://www.mozilla.org/xbl"
-    xmlns:xbl="http://www.mozilla.org/xbl"
-    xmlns:html="http://www.w3.org/1999/xhtml"
-    xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
-
-  <binding id="deckbrowser">
-    <content>
-      <xul:deck flex="1" selectedIndex="0">
-        <xul:stack flex="1">
-          <html:div anonid="viewport" style="-moz-stack-sizing: ignore; overflow: hidden; background-image:url('chrome://browser/content/checkerboard.png')">
-            <html:canvas anonid="ccanvas"
-                         moz-opaque="true"
-                         style="height: 200%; width: 200%;"/>
-          </html:div>
-        </xul:stack>
-        <xul:deck anonid="display-list" flex="1"/>
-      </xul:deck>
-    </content>
-
-    <resources>
-      <stylesheet src="chrome://global/content/deckbrowser.css"/>
-    </resources>
-
-    <implementation implements="nsIObserver">
-      <constructor><![CDATA[
-        this._zoomLevel = 1;
-
-        var prefsvc = Components.classes["@mozilla.org/preferences-service;1"].
-                        getService(Components.interfaces.nsIPrefBranch2);
-
-        this._allowKinetic = prefsvc.getBoolPref("browser.ui.panning.kinetic");
-
-        // panning
-        this._viewport.addEventListener("mousedown", this.stackEventHandler, true);
-        // need mouseup handled on the window to catch mouseups on e.g. the toolbar
-        window.addEventListener("mouseup", this.stackEventHandler, true);
-        this._viewport.addEventListener("mousemove", this.stackEventHandler, true);
-
-        // zoom
-        // FIXME: dblclicks don't work on the device
-        // this._viewport.addEventListener("dblclick", this.stackEventHandler, true);
-        this._viewport.addEventListener("DOMMouseScroll", this.stackEventHandler, true);
-
-        var self = this;
-        var obs = Components.classes["@mozilla.org/observer-service;1"].
-                    getService(Components.interfaces.nsIObserverService);
-        obs.addObserver(function(subject, topic, data) self.destroyEarliestBrowser(),
-                        "memory-pressure", false);
-
-        this._dragStartTimeout = -1;
-
-        this.PAN_EVENTS_TO_TRACK = 2;
-        this._panEventTracker = new Array(this.PAN_EVENTS_TO_TRACK);
-        this._panEventTrackerIndex = 0;
-      ]]></constructor>
-
-      <property name="viewportDimensions" readonly="true">
-        <getter><![CDATA[
-          var rect = this._viewport.getBoundingClientRect();
-          return [rect.width, rect.height];
-        ]]></getter>
-      </property>
-
-      <property name="_effectiveViewportDimensions" readonly="true">
-        <getter><![CDATA[
-          var [w, h] = this.viewportDimensions;
-          return [this._screenToPage(w), this._screenToPage(h)];
-        ]]></getter>
-      </property>
-
-      <property name="_effectiveCanvasDimensions" readonly="true">
-        <getter><![CDATA[
-          let canvasRect = this._canvas.getBoundingClientRect();
-          return [this._screenToPage(canvasRect.width),
-                  this._screenToPage(canvasRect.height)];
-        ]]></getter>
-      </property>
-
-      <property name="dragData" readonly="true">
-        <getter>
-          <![CDATA[
-            if (!this.currentTab.dragData) {
-              this.currentTab.dragData = {
-                dragging: false,
-                dragX: 0,
-                dragY: 0,
-                sX: 0,
-                sY: 0,
-                pageX: 0,
-                pageY: 0,
-                oldPageX: 0,
-                oldPageY: 0
-              }
-            }
-            return this.currentTab.dragData;
-          ]]>
-        </getter>
-      </property>
-
-      <field name="_viewport">
-        document.getAnonymousElementByAttribute(this, "anonid", "viewport");
-      </field>
-
-      <field name="_canvas">
-        document.getAnonymousElementByAttribute(this, "anonid", "ccanvas");
-      </field>
-
-      <property name="browser" readonly="true">
-        <getter>
-          <![CDATA[
-            return this.getBrowserForDisplay(this.displayList.selectedPanel);
-          ]]>
-        </getter>
-      </property>
-
-      <property name="browsers" readonly="true">
-        <getter>
-          <![CDATA[
-            var browsers = [];
-            var tabList = this.tabList.children;
-            for (var t = 0; t < tabList.length; t++)
-              browsers.push(this.getBrowserForDisplay(this.displayList.childNodes[t]));
-            return browsers;
-          ]]>
-        </getter>
-      </property>
-
-      <property name="displayList" readonly="true">
-        <getter>
-          return document.getAnonymousElementByAttribute(this, "anonid", "display-list");
-        </getter>
-      </property>
-
-      <field name="tabList">
-        null
-      </field>
-
-      <field name="progressListenerCreator"/>
-
-      <method name="updateCanvasState">
-        <body><![CDATA[
-          // Clear the whole canvas
-          // we clear the whole canvas because the browser's width or height
-          // could be less than the area we end up actually drawing.
-          // XXX we could be smarter about this and only clear when necessary
-          var ctx = this._canvas.getContext("2d");
-          ctx.clearRect(0, 0, this._canvas.width, this._canvas.height);
-
-          // Reset pan data
-          this.dragData.pageX = 0;
-          this.dragData.pageY = 0;
-
-          this._zoomed = false;
-
-          this.zoomToPage();
-        ]]></body>
-      </method>
-
-      <field name="currentTab"/>
-
-      <method name="getDisplayForTab">
-        <parameter name="tab"/>
-        <body><![CDATA[
-          var tabList = this.tabList.children;
-          for (var t = 0; t < tabList.length; t++) {
-            if (tab == tabList[t])
-              return this.displayList.childNodes[t];
-          }
-
-          return null;
-        ]]>
-        </body>
-      </method>
-
-      <method name="getTabForDisplay">
-        <parameter name="display"/>
-        <body><![CDATA[
-          var displayList = this.displayList.childNodes;
-          for (var t = 0; t < displayList.length; t++) {
-            if (display == displayList[t])
-              return this.tabList.getItemAtIndex(t);
-          }
-
-          return null;
-        ]]>
-        </body>
-      </method>
-
-      <method name="getBrowserForDisplay">
-        <parameter name="display"/>
-        <body><![CDATA[
-          if (!display)
-            return null;
-
-          var browser = display.firstChild;
-          if (browser && browser.localName == "browser")
-            return browser;
-
-          browser = display.lastChild;
-          return (browser && browser.localName == "browser") ? browser : null;
-        ]]>
-        </body>
-      </method>
-
-      <method name="isLoading">
-        <parameter name="browser"/>
-        <body><![CDATA[
-          return browser.parentNode.loading || false;
-        ]]></body>
-      </method>
-
-      <method name="setLoading">
-        <parameter name="browser"/>
-        <parameter name="state"/>
-        <body><![CDATA[
-          browser.parentNode.loading = state;
-          if (state) {
-            var tab = this.getTabForDisplay(browser.parentNode);
-            if (tab)
-              tab.dragData = null;
-          }
-        ]]></body>
-      </method>
-
-      <method name="updateBrowser">
-        <parameter name="browser"/>
-        <parameter name="done"/>
-        <body><![CDATA[
-          var display = browser.parentNode;
-          var domWin = browser.contentWindow;
-          display.url = domWin.location.toString();
-
-          if (!done || domWin.location == "about:blank")
-            return;
-
-          this.restoreBrowserState(display);
-
-          var tab = this.getTabForDisplay(display);
-          if (tab) {
-            tab.updateTab(browser, this._canvas);
-            var canvas = display.firstChild;
-            if (canvas.localName == "canvas")
-              display.removeChild(canvas);
-          }
-        ]]></body>
-      </method>
-
-
-      <field name="browserRedrawHandler">
-        <![CDATA[
-        ({
-          deckbrowser: this,
-
-          handleEvent: function (aEvent) {
-            let self = this.deckbrowser;
-
-            if (self.dragData.dragging || self.dragData.kineticId)
-              return;
-
-            let cwin = self.browser.contentWindow;
-
-            for (let i = 0; i < aEvent.clientRects.length; i++) {
-              let e = aEvent.clientRects.item(i);
-              //dump(Math.floor(e.left + cwin.scrollX),
-              //     Math.floor(e.top + cwin.scrollY),
-              //     Math.ceil(e.width), Math.ceil(e.height));
-              self._redrawRect(Math.floor(e.left + cwin.scrollX),
-                               Math.floor(e.top + cwin.scrollY),
-                               Math.ceil(e.width), Math.ceil(e.height));
-
-            }
-          }
-        });
-        ]]>
-      </field>
-
-      <method name="selectTab">
-        <parameter name="tab"/>
-        <body><![CDATA[
-          var currentTab = this.currentTab;
-          this.currentTab = tab;
-
-          if (currentTab) {
-            var currentDisplay = this.getDisplayForTab(currentTab);
-            var currentBrowser = this.getBrowserForDisplay(currentDisplay);
-            if (currentBrowser) {
-              // stop monitor paint events for this browser
-              currentBrowser.removeEventListener("MozAfterPaint", this.browserRedrawHandler, false);
-
-              currentDisplay.url = currentBrowser.contentWindow.location.toString();
-              currentBrowser.setAttribute("type", "content");
-              currentTab.updateTab(currentBrowser);
-            }
-          }
-
-          var display = this.getDisplayForTab(tab);
-          var browser = this.getBrowserForDisplay(display);
-          if (!browser) {
-            browser = this.createBrowser(true, tab, display);
-            browser.loadURI(display.url, null, null, false);
-          }
-          display.lastAccess = Date.now();
-
-          browser.setAttribute("type", "content-primary");
-          this.displayList.selectedPanel = display;
-
-          // start monitoring paint events for this browser
-          browser.addEventListener("MozAfterPaint", this.browserRedrawHandler, false);
-          // force a repaint of the selected tab
-          this._browserToCanvas();
-
-          var event = document.createEvent("Events");
-          event.initEvent("TabSelect", true, false);
-          tab.dispatchEvent(event);
-
-        ]]></body>
-      </method>
-
-      <method name="newTab">
-        <parameter name="makeFront"/>
-        <body><![CDATA[
-          var browser = this.createBrowser(makeFront, null, null);
-          if (!browser)
-            return null;
-
-          var tab = this.getTabForDisplay(browser.parentNode);
-
-          var evt = document.createEvent("Events");
-          evt.initEvent("TabOpen", true, false);
-          tab.dispatchEvent(evt);
-
-          return tab;
-        ]]></body>
-      </method>
-
-      <method name="removeTab">
-        <parameter name="tab"/>
-        <body><![CDATA[
-          if (!tab)
-            return;
-
-          var tabList = this.tabList;
-          var nextTab = tabList.selectedItem;
-          var tabidx = tabList.getIndexOfItem(tab);
-          if (tab == tabList.selectedItem) {
-            nextTab = tabList.getItemAtIndex(tabidx + 1) || tabList.getItemAtIndex(tabidx - 1);
-            if (!nextTab)
-              return;
-          }
-
-          var display = this.getDisplayForTab(tab);
-          if (display)
-            display.parentNode.removeChild(display);
-          tabList.removeTab(tab);
-
-          // redraw the tabs
-          for (var t = tabidx; t < this.tabList.itemCount; t++) {
-            var tab = tabList.getItemAtIndex(t);
-            var browser = this.getBrowserForDisplay(this.getDisplayForTab(tab));
-            if (browser)
-              tab.updateTab(browser);
-          }
-
-          this.tabList.selectedItem = nextTab;
-          this.displayList.selectedPanel = this.getDisplayForTab(nextTab);
-          this.selectTab(nextTab);
-
-          var evt = document.createEvent("Events");
-          evt.initEvent("TabClose", true, false);
-          tab.dispatchEvent(evt);
-        ]]></body>
-      </method>
-
-      <method name="createBrowser">
-        <parameter name="makeFront"/>
-        <parameter name="tab"/>
-        <parameter name="display"/>
-        <body><![CDATA[
-          const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
-
-          var browser = document.createElementNS(XUL_NS, "browser");
-          browser.className = "deckbrowser-browser";
-          browser.setAttribute("style", "overflow: hidden");
-          browser.setAttribute("contextmenu", this.getAttribute("contextmenu"));
-          browser.setAttribute("autocompletepopup", this.getAttribute("autocompletepopup"));
-          browser.flex = 1;
-
-          if (makeFront)
-            browser.setAttribute("type", "content-primary");
-          else
-            browser.setAttribute("type", "content");
-
-          var nextDisplay;
-          var displayList = this.displayList;
-          if (tab) {
-            var nextTab = tab.nextSibling;
-            if (nextTab)
-              nextDisplay = this.getDisplayForTab(nextTab);
-          }
-
-          var uniqueId = Date.now() + displayList.childNodes.length;
-
-          if (!display) {
-            display = document.createElementNS(XUL_NS, "deck");
-            display.setAttribute("id", "display-" + uniqueId)
-            displayList.insertBefore(display, nextDisplay);
-          }
-          display.appendChild(browser);
-          display.selectedIndex = 0;
-
-          if (this.progressListenerCreator) {
-            var listener = this.progressListenerCreator(this, browser);
-            browser.addProgressListener(listener);
-            display.progressListener = listener;
-          }
-
-          if (!tab) {
-            tab = document.createElementNS(XUL_NS, "richlistitem");
-            tab.setAttribute("id", "tab-" + uniqueId)
-            tab.setAttribute("type", "documenttab");
-            this.tabList.addTab(tab);
-          }
-
-          if (tab == this.tabList.selectedItem) {
-            // already selected, but need to update the selected panel
-            display.lastAccess = Date.now();
-            displayList.selectedPanel = display;
-          }
-          else if (makeFront) {
-            this.tabList.selectedItem = tab;
-          }
-
-          return browser;
-        ]]></body>
-      </method>
-
-      <method name="destroyBrowser">
-        <parameter name="browser"/>
-        <body><![CDATA[
-          if (!browser || browser == this.browser)
-            return;
-
-          var display = browser.parentNode;
-          this.saveBrowserState(display);
-
-          var domWin = browser.contentWindow;
-
-          var tab = this.getTabForDisplay(display);
-          if (tab)
-            tab.markInvalid();
-
-          const XHTML_NS = "http://www.w3.org/1999/xhtml";
-          var canvas = document.createElementNS(XHTML_NS, "canvas");
-          canvas.setAttribute("width", domWin.innerWidth);
-          canvas.setAttribute("height", domWin.innerHeight);
-
-          var ctx = canvas.getContext("2d");
-          ctx.drawWindow(domWin, 0, 0,
-                         domWin.innerWidth, domWin.innerHeight, "white");
-          display.insertBefore(canvas, display.firstChild);
-
-          display.lastAccess = Date.now();
-          display.progressListener = null;
-          display.removeChild(browser);
-        ]]></body>
-      </method>
-
-      <method name="destroyEarliestBrowser">
-        <body><![CDATA[
-          var earliestBrowser = null;
-          var earliest = Date.now();
-          var displayList = this.displayList.childNodes;
-          for (var t = 0; t < displayList.length; t++) {
-            var display = displayList[t];
-            var browser = this.getBrowserForDisplay(display);
-            if (browser &&
-                display != this.displayList.selectedItem &&
-                display.lastAccess < earliest) {
-              earliestBrowser = browser;
-              earliest = display.lastAccess;
-            }
-          }
-
-          if (earliestBrowser)
-            this.destroyBrowser(earliestBrowser);
-        ]]></body>
-      </method>
-
-      <method name="saveBrowserState">
-        <parameter name="display"/>
-        <body><![CDATA[
-          var state = { };
-
-          var browser = this.getBrowserForDisplay(display);
-          var doc = browser.contentDocument;
-          if (doc instanceof HTMLDocument) {
-            var tags = ["input", "textarea", "select"];
-
-            for (var t = 0; t < tags.length; t++) {
-              var elements = doc.getElementsByTagName(tags[t]);
-              for (var e = 0; e < elements.length; e++) {
-                var element = elements[e];
-                var id;
-                if (element.id)
-                  id = "#" + element.id;
-                else if (element.name)
-                  id = "$" + element.name;
-
-                if (id)
-                  state[id] = element.value;
-              }
-            }
-          }
-
-          state._scrollX = browser.contentWindow.scrollX;
-          state._scrollY = browser.contentWindow.scrollY;
-
-          display.state = state;
-        ]]></body>
-      </method>
-
-      <method name="restoreBrowserState">
-        <parameter name="display"/>
-        <body><![CDATA[
-          var state = display.state;
-          if (!state)
-            return;
-
-          var browser = this.getBrowserForDisplay(display);
-          var doc = browser.contentDocument;
-          for (item in state) {
-            var elem = null;
-            if (item.charAt(0) == "#") {
-              elem = doc.getElementById(item.substring(1));
-            }
-            else if (item.charAt(0) == "$") {
-              var list = doc.getElementsByName(item.substring(1));
-              if (list.length)
-                elem = list[0];
-            }
-
-            if (elem)
-              elem.value = state[item];
-          }
-
-          this.browser.contentWindow.scrollTo(state._scrollX, state._scrollY);
-        ]]></body>
-      </method>
-
-      <property name="_canvasPageOffset" readonly="true">
-        <getter><![CDATA[
-          let [canvasW, canvasH] = this._effectiveCanvasDimensions;
-          let [viewportW, viewportH] = this._effectiveViewportDimensions;
-          let offscreenCanvasW = (canvasW - viewportW);
-          let offscreenCanvasH = (canvasH - viewportH);
-          let [contentWidth, contentHeight] = this._contentAreaDimensions;
-
-          let left = Math.max(-this.dragData.pageX, -(offscreenCanvasW / 2));
-          let rightMost = (contentWidth - canvasW);
-          if (left > rightMost && rightMost > 0)
-            left = rightMost;
-
-          let top = Math.max(-this.dragData.pageY, -(offscreenCanvasH / 2));
-          let bottomMost = (contentHeight - canvasH);
-          if (top > bottomMost && bottomMost > 0)
-            top = bottomMost;
-
-          return [left, top];
-        ]]></getter>
-      </property>
-
-      <property name="_drawOffset" readonly="true">
-        <getter><![CDATA[
-          let [offX, offY] = this._canvasPageOffset;
-          return [this.dragData.pageX + offX, this.dragData.pageY + offY];
-        ]]></getter>
-      </property>
-
-      <method name="_browserToCanvas">
-        <body><![CDATA[
-          // FIXME: canvas needs to know it's actual width/height
-          // we should be able to use _canvas.width/_canvas.height here
-          // and can, most of the time
-          var rect = this._canvas.getBoundingClientRect();
-          this._canvas.width = rect.width;
-          this._canvas.height = rect.height;
-
-          var [x, y] = this._drawOffset;
-
-          var ctx = this._canvas.getContext("2d");
-          ctx.save();
-          ctx.scale(this._zoomLevel, this._zoomLevel);
-          ctx.drawWindow(this.browser.contentWindow,
-                         x, y,
-                         this._screenToPage(rect.width),
-                         this._screenToPage(rect.height),
-                         "white",
-                         ctx.DRAWWINDOW_DO_NOT_FLUSH);
-          ctx.restore();
-
-          this._updateCanvasPosition();
-        ]]></body>
-      </method>
-
-      <method name="_redrawRect">
-        <parameter name="x"/>
-        <parameter name="y"/>
-        <parameter name="width"/>
-        <parameter name="height"/>
-        <body><![CDATA[
-          function intersect(r1, r2) {
-            let xmost1 = r1.x + r1.width;
-            let ymost1 = r1.y + r1.height;
-            let xmost2 = r2.x + r2.width;
-            let ymost2 = r2.y + r2.height;
-
-            let x = Math.max(r1.x, r2.x);
-            let y = Math.max(r1.y, r2.y);
-
-            let temp = Math.min(xmost1, xmost2);
-            if (temp <= x)
-              return null;
-
-            let width = temp - x;
-
-            temp = Math.min(ymost1, ymost2);
-            if (temp <= y)
-              return null;
-
-            let height = temp - y;
-
-            return {
-              x: x,
-              y: y,
-              width: width,
-              height: height
-            };
-          }
-
-          let r1 = { x : x,
-                     y : y,
-                     width : width,
-                     height: height };
-
-          // check to see if the input coordinates are inside the visible destination
-          let [canvasW, canvasH] = this._effectiveCanvasDimensions;
-          let r2 = {
-            x : this.dragData.pageX,
-            y : this.dragData.pageY,
-            width : canvasW,
-            height: canvasH
-          };
-
-          let dest = intersect(r1, r2);
-
-          if (!dest)
-            return;
-
-          var ctx = this._canvas.getContext("2d");
-
-          ctx.save();
-          ctx.scale(this._zoomLevel, this._zoomLevel);
-
-          var [offX, offY] = this._drawOffset;
-          ctx.translate(dest.x - offX,
-                        dest.y - offY);
-
-          ctx.drawWindow(this.browser.contentWindow,
-                         dest.x, dest.y,
-                         dest.width, dest.height,
-                         "white",
-                         ctx.DRAWWINDOW_DO_NOT_FLUSH);
-
-          ctx.restore();
-        ]]></body>
-      </method>
-
-      <method name="_updateCheckerboard">
-        <body><![CDATA[
-          let x = Math.round(this.dragData.dragX);
-          let y = Math.round(this.dragData.dragY);
-
-          this._viewport.style.backgroundPosition =  x+"px "+ y+"px";
-        ]]></body>
-      </method>
-
-      <method name="_updateCanvasPosition">
-        <body><![CDATA[
-          let x = Math.round(this.dragData.dragX);
-          let y = Math.round(this.dragData.dragY);
-
-          let [offX, offY] = this._canvasPageOffset;
-          [x, y] = [this._pageToScreen(offX) + x, this._pageToScreen(offY) + y];
-
-          this._canvas.style.marginLeft = x + "px";
-          this._canvas.style.marginRight = -x + "px";
-          this._canvas.style.marginTop = y + "px";
-          this._canvas.style.marginBottom = -y + "px";
-
-          this._updateCheckerboard();
-
-          // Force a sync redraw
-          window.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
-                .getInterface(Components.interfaces.nsIDOMWindowUtils)
-                .processUpdates();
-        ]]></body>
-      </method>
-
-      <method name="_clampZoomLevel">
-        <parameter name="aZoomLevel"/>
-        <body><![CDATA[
-          const min = 0.2;
-          const max = 2.0;
-
-          return Math.min(Math.max(min, aZoomLevel), max);
-        ]]></body>
-      </method>
-
-      <property name="zoomLevel" onget="return this._zoomLevel;">
-        <setter><![CDATA[
-          this._zoomLevel = this._clampZoomLevel(val);
-
-          this._browserToCanvas();
-
-          return val;
-        ]]></setter>
-      </property>
-
-      <method name="zoom">
-        <parameter name="aDirection"/>
-        <body><![CDATA[
-          if (aDirection == 0)
-            return;
-
-          var zoomDelta = 0.05; // 1/20
-          if (aDirection >= 0)
-            zoomDelta *= -1;
-
-          this._zoomLevel = this._clampZoomLevel(this._zoomLevel + zoomDelta);
-
-          this._browserToCanvas();
-        ]]></body>
-      </method>
-
-      <method name="zoomToPage">
-        <body><![CDATA[
-          // Adjust the zoomLevel to fit the page contents in our window
-          // width
-          var [contentW, ] = this._contentAreaDimensions;
-          var [viewportW, ] = this.viewportDimensions;
-
-          if (contentW > viewportW)
-            this._zoomLevel = viewportW / contentW;
-
-          this._browserToCanvas();
-        ]]></body>
-      </method>
-
-      <method name="zoomToElement">
-        <parameter name="aElement"/>
-        <body><![CDATA[
-          const margin = 15;
-
-          // scale to the element's width
-          var elRect = this._getPagePosition(aElement);
-          var zoomLevel = this.browser.boxObject.width / (elRect.width + (2 * margin));
-          this._zoomLevel = Math.min(zoomLevel, 10);
-
-          // pan to the element
-          this.panTo(Math.max(elRect.x - margin, 0),
-                     Math.max(0, elRect.y - margin));
-        ]]></body>
-      </method>
-
-      <!--
-        - Retrieve the content element for a given point in client coordinates
-        - (relative to the top left corner of the chrome window).
-       -->
-      <method name="elementFromPoint">
-        <parameter name="aX"/>
-        <parameter name="aY"/>
-        <body><![CDATA[
-          var [x, y] = this._clientToContentCoords(aX, aY);
-          var cwu = this.browser.contentWindow
-                        .QueryInterface(Components.interfaces.nsIInterfaceRequestor)
-                        .getInterface(Components.interfaces.nsIDOMWindowUtils);
-
-          var element =  cwu.elementFromPoint(x, y,
-                                    true,   /* ignore root scroll frame */
-                                    false); /* don't flush layout*/
-
-          return element;
-        ]]></body>
-      </method>
-
-      <!--
-        - Retrieve the page position for a given element
-        - (relative to the document origin).
-       -->
-      <method name="_getPagePosition">
-        <parameter name="aElement"/>
-        <body><![CDATA[
-          var r = aElement.getBoundingClientRect();
-          var cwin = this.browser.contentWindow;
-          var retVal = {
-            width: r.width,
-            height: r.height,
-            x: r.left + cwin.scrollX,
-            y: r.top + cwin.scrollY
-          };
-
-          return retVal;
-        ]]></body>
-      </method>
-
-      <method name="_redispatchMouseEvent">
-        <parameter name="aEvent"/>
-        <parameter name="aType"/>
-        <body><![CDATA[
-          if (!(aEvent instanceof MouseEvent)) {
-            Components.utils.reportError("_redispatchMouseEvent called with a non-mouse event");
-            return;
-          }
-
-          var [x, y] = this._clientToContentCoords(aEvent.clientX, aEvent.clientY);
-
-          // Redispatch the mouse event, ignoring the root scroll frame
-          var cwin = this.browser.contentWindow;
-          var cwu = cwin.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
-                        .getInterface(Components.interfaces.nsIDOMWindowUtils);
-          cwu.sendMouseEvent(aType || aEvent.type,
-                             x, y,
-                             aEvent.button || 0,
-                             aEvent.detail || 1,
-                             0, true);
-        ]]></body>
-      </method>
-
-      <!-- Given a set of client coordinates (relative to the app window),
-        -  returns the content coordinates relative to the viewport.
-       -->
-      <method name="_clientToContentCoords">
-        <parameter name="aClientX"/>
-        <parameter name="aClientY"/>
-        <body><![CDATA[
-          // Determine position relative to the document origin
-          // Need to adjust for the deckbrowser not being at 0,0
-          // (e.g. due to other browser UI)
-          let browserRect = this.getBoundingClientRect();
-          let clickOffsetX = this._screenToPage(aClientX - browserRect.left) + this.dragData.pageX;
-          let clickOffsetY = this._screenToPage(aClientY - browserRect.top) + this.dragData.pageY;
-
-          // Take scroll offset into account to return coordinates relative to the viewport
-          let cwin = this.browser.contentWindow;
-          return [clickOffsetX - cwin.scrollX,
-                  clickOffsetY - cwin.scrollY];
-        ]]></body>
-      </method>
-
-      <property name="_contentAreaDimensions" readonly="true">
-        <getter>
-          var cdoc = this.browser.contentDocument;
-
-          // Return the document width/height for XUL documents (which is
-          // essentially the same as the viewport width/height).
-          if (cdoc instanceof XULDocument)
-            return [cdoc.width, cdoc.height];
-
-          if (cdoc instanceof SVGDocument) {
-            let rect = cdoc.rootElement.getBoundingClientRect();
-            return [rect.width, rect.height];
-          }
-
-          // These might not exist yet depending on page load state
-          var body = cdoc.body || {};
-          var html = cdoc.documentElement || {};
-
-          var w = Math.max(body.scrollWidth, html.scrollWidth);
-          var h = Math.max(body.scrollHeight, html.scrollHeight);
-
-          if (isNaN(w) || isNaN(h)) {
-            let rect = this._viewport.getBoundingClientRect();
-            return [rect.width, rect.height];
-          }
-
-          return [w, h];
-        </getter>
-      </property>
-
-      <property name="scrollX" readonly="true">
-        <getter><![CDATA[
-          return this.dragData.pageX - this._screenToPage(this.dragData.dragX);
-         ]]></getter>
-      </property>
-
-      <property name="scrollY" readonly="true">
-        <getter><![CDATA[
-          return this.dragData.pageY - this._screenToPage(this.dragData.dragY);
-         ]]></getter>
-      </property>
-
-      <!--
-        - Given a set of page coordinates, constrain them such that they
-        - fit within the rect defined by [0,0] and [x,y], where x and y are
-        - the maximum values that can be used for the canvas' .top and .left
-        - such that it is still within the scrollable area of the page, taking
-        - into account the current zoomLevel.
-       -->
-      <method name="_constrainPanCoords">
-        <parameter name="aX"/>
-        <parameter name="aY"/>
-        <body><![CDATA[
-          var [contentAreaWidth, contentAreaHeight] = this._contentAreaDimensions;
-          var [viewportW, viewportH] = this._effectiveViewportDimensions;
-
-          var offscreenWidth = contentAreaWidth - viewportW;
-          if (offscreenWidth <= 0) {
-            // Content is narrower than viewport, no need to pan horizontally
-            aX = 0;
-          } else {
-            // min of 0, max of contentAreaWidth - viewportW
-            var newPageX = Math.min(this.dragData.pageX  + aX, offscreenWidth);
-            newPageX = Math.max(newPageX, 0);
-            aX = newPageX - this.dragData.pageX;
-          }
-
-          var offscreenHeight = contentAreaHeight - viewportH;
-          if (offscreenHeight <= 0) {
-            // Content is shorter than viewport, no need to pan vertically
-            aY = 0;
-          } else {
-            // min of 0, max of contentAreaHeight - viewportH
-            var newPageY = Math.min(this.dragData.pageY + aY, offscreenHeight);
-            newPageY = Math.max(newPageY, 0);
-            aY = newPageY - this.dragData.pageY;
-          }
-
-          return [aX, aY];
-        ]]></body>
-      </method>
-
-      <method name="_screenToPage">
-        <parameter name="aValue"/>
-        <body><![CDATA[
-          return aValue / this._zoomLevel;
-        ]]></body>
-      </method>
-
-      <method name="_pageToScreen">
-        <parameter name="aValue"/>
-        <body><![CDATA[
-          return aValue * this._zoomLevel;
-        ]]></body>
-      </method>
-
-      <method name="_moveCanvas">
-        <parameter name="aDx"/>
-        <parameter name="aDy"/>
-        <body><![CDATA[
-          // Constrain offsets to the actual scrollWidth/scrollHeight
-          var [x, y] = this._constrainPanCoords(this._screenToPage(aDx), this._screenToPage(aDy));
-
-          // Canvas needs to move up for content to scroll down
-          this.dragData.dragX = -this._pageToScreen(x);
-          this.dragData.dragY = -this._pageToScreen(y);
-
-          this._updateCanvasPosition();
-        ]]></body>
-      </method>
-
-      <method name="_startKinetic">
-        <body><![CDATA[
-          // Get the first and last mouse move event
-          let p2 = this._panEventTracker[this._panEventTrackerIndex];
-          let p1 = this._panEventTracker[(this._panEventTrackerIndex + 1) % this.PAN_EVENTS_TO_TRACK];
-          if (p2 && p1) {
-            let dx = p2.x - p1.x;
-            let dy = p2.y - p1.y;
-            let dt = p2.t - p1.t;
-            if (dt > 0) { // dt should never be less than 0
-              this.dragData.velocityX = dx / dt;
-              this.dragData.velocityY = dy / dt;
-
-              // Save the original x.y we're starting from to make sure
-              // we don't go backwards
-              this.dragData.originalX = this.dragData.dragX;
-              this.dragData.originalY = this.dragData.dragY;
-
-
-              // s = S0 + 0.5 * v0^2 * 1/CoK); s = position, s0 = initial pos
-              // v0 = initial velocity, CoK = Coefficient of Kinetic friction
-              // All in page coords
-              let idealDestScreenX = this.dragData.dragX  + Math.abs(this.dragData.velocityX)
-                                     * this.dragData.velocityX * 200;
-              let idealDestScreenY = this.dragData.dragY + Math.abs(this.dragData.velocityY)
-                                     * this.dragData.velocityY * 200
-
-              let [destPageX, destPageY] =  this._constrainPanCoords(-this._screenToPage(idealDestScreenX),
-                                                                     -this._screenToPage(idealDestScreenY));
-
-
-              // Convert to screen coords
-              this.dragData.destinationX = -this._pageToScreen(destPageX);
-              this.dragData.destinationY = -this._pageToScreen(destPageY);
-
-              // If we have a kinetic timer, kill it.  This shouldn't happen
-              if (this.dragData.kineticId)
-                window.clearInterval(this.dragData.kineticId);
-              // Start timer for kinetic movements
-              let interval =  dt / (this.PAN_EVENTS_TO_TRACK - 1);
-              this.dragData.kineticId = window.setInterval(this._doKinetic, interval, this, interval);
-            } else {
-              // dt <= 0, this is bad
-              this._endPan();
-            }
-          } else {
-            // p1 or p2 is null, either we didn't pan enough, or something went wrong
-            this._endPan()
-          }
-
-          // Clear out the old events since they aren't needed anymore
-          for (var i = 0; i < this.PAN_EVENTS_TO_TRACK; i++) {
-            this._panEventTracker[i] = null;
-          }
-        ]]></body>
-      </method>
-
-      <method name="_doKinetic">
-        <parameter name="self"/>
-        <parameter name="dt"/>
-        <body><![CDATA[
-          // record where we're starting so we can test how far we went later
-          let startX = self.dragData.dragX;
-          let startY = self.dragData.dragY;
-          const stopTheshold = 3;  //Distance from the destination point that we'll consider to be "there"
-          let dx = 0;
-          let dy = 0;
-          if (Math.abs(self.dragData.destinationX - self.dragData.dragX) < stopTheshold) {
-            dx = self.dragData.destinationX - self.dragData.dragX;
-            self.dragData.velocityX = dx/dt;
-          } else {
-            // decelerate, this assumes we decelerate perfectly to our destination
-            // it gets skewed if we're hitting an edge since we're using our progress
-            // instead of the time
-            dx = self.dragData.velocityX * dt * (Math.sqrt(Math.abs(self.dragData.destinationX - self.dragData.dragX))
-                 / Math.sqrt(Math.abs(self.dragData.destinationX - self.dragData.originalX)));
-            // if we're already at the destination, we don't want to move anymore
-            dx = self.dragData.originalX == self.dragData.destinationX ? 0 : dx;
-          }
-
-          if (Math.abs(self.dragData.destinationY - self.dragData.dragY) < stopTheshold) {
-            dx = self.dragData.destinationY - self.dragData.dragY;
-            self.dragData.velocityY = dx/dt;
-          } else {
-            // decelerate, this assumes we decelerate perfectly to our destination
-            // it gets skewed if we're hitting an edge since we're using our progress
-            // instead of the time
-            dy = self.dragData.velocityY * dt * (Math.sqrt(Math.abs(self.dragData.destinationY - self.dragData.dragY))
-                 / Math.sqrt(Math.abs(self.dragData.destinationY - self.dragData.originalY)));
-            // if we're already at the destination, we don't want to move anymore
-            dy = self.dragData.originalY == self.dragData.destinationY ? 0 : dy;
-
-          }
-
-          // Calculate the next x, y in screen space
-          let nextX = self.dragData.dragX + dx;
-          let nextY = self.dragData.dragY + dy;
-
-          // make sure we're still between original and destination coords
-          if((self.dragData.originalX > nextX &&
-              nextX > self.dragData.destinationX) ||
-             (self.dragData.originalX < nextX &&
-              nextX < self.dragData.destinationX))
-            self.dragData.dragX = nextX;
-          else
-            self.dragData.dragX = self.dragData.destinationX;
-          if((self.dragData.originalY > nextY &&
-              nextY > self.dragData.destinationY) ||
-             (self.dragData.originalY <  nextY &&
-              nextY < self.dragData.destinationY))
-            self.dragData.dragY = nextY;
-          else
-            self.dragData.dragY = self.dragData.destinationY;
-
-          self._updateCanvasPosition();
-
-          // calculate how much we've actually moved and end if less than 4px
-          let actualDx = startX - self.dragData.dragX;
-          let actualDy = startY - self.dragData.dragY;
-          if ((actualDx / (self.dragData.destinationX - self.dragData.originalX)  < 0 &&
-               actualDy / (self.dragData.destinationY - self.dragData.originalY) < 0) ||
-              (Math.abs(actualDx) < 4 && Math.abs(actualDy) < 4)) {
-            self._endKinetic();
-          }
-        ]]></body>
-      </method>
-
-      <method name="_endKinetic">
-        <body><![CDATA[
-          window.clearInterval(this.dragData.kineticId);
-          this.dragData.kineticId = 0;
-
-          this._endPan();
-        ]]></body>
-      </method>
-
-      <!-- ensures that a given content element is visible -->
-      <method name="ensureElementIsVisible">
-        <parameter name="aElement"/>
-        <body><![CDATA[
-          let elRect = this._getPagePosition(aElement);
-          let [viewportW, viewportH] = this._effectiveViewportDimensions;
-          let curRect = {
-            x: this.dragData.pageX,
-            y: this.dragData.pageY,
-            width: viewportW,
-            height: viewportH
-          }
-
-          // Adjust for part of our viewport being offscreen
-          // XXX this assumes that the browser is meant to be fullscreen
-          let browserRect = this.getBoundingClientRect();
-          curRect.height -= this._screenToPage(Math.abs(browserRect.top));
-          if (browserRect.top < 0)
-            curRect.y -= this._screenToPage(browserRect.top);
-          curRect.width -= this._screenToPage(Math.abs(browserRect.left));
-          if (browserRect.left < 0)
-            curRect.x -= this._screenToPage(browserRect.left);
-
-          let newx = curRect.x;
-          let newy = curRect.y;
-
-          if (elRect.x + elRect.width > curRect.x + curRect.width) {
-              newx = curRect.x + ((elRect.x + elRect.width)-(curRect.x + curRect.width));
-          } else if (elRect.x < curRect.x) {
-              newx = elRect.x;
-          }
-
-          if (elRect.y + elRect.height > curRect.y + curRect.height) {
-              newy = curRect.y + ((elRect.y + elRect.height)-(curRect.y + curRect.height));
-          } else if (elRect.y < curRect.y) {
-              newy = elRect.y;
-          }
-
-          this.panTo(newx, newy);
-        ]]></body>
-      </method>
-
-      <!-- Pans directly to a given content element -->
-      <method name="panToElement">
-        <parameter name="aElement"/>
-        <body><![CDATA[
-          var elRect = this._getPagePosition(aElement);
-
-          this.panTo(elRect.x, elRect.y);
-        ]]></body>
-      </method>
-
-      <!-- Pans directly to a given X/Y (in content coordinates) -->
-      <method name="panTo">
-        <parameter name="aX"/>
-        <parameter name="aY"/>
-        <body><![CDATA[
-          this.panBy(aX - this.dragData.pageX, aY - this.dragData.pageY);
-        ]]></body>
-      </method>
-
-      <!-- Pans X/Y pixels (in content coordinates) -->
-      <method name="panBy">
-        <parameter name="dX"/>
-        <parameter name="dY"/>
-        <body><![CDATA[
-          if (dX == 0 && dY == 0)
-            return;
-
-          var [deltaX, deltaY] = this._constrainPanCoords(dX, dY);
-
-          this.dragData.pageX += deltaX;
-          this.dragData.pageY += deltaY;
-
-          this._browserToCanvas();
-        ]]></body>
-      </method>
-
-      <method name="_dragStartTimer">
-        <body><![CDATA[
-          this.dragData.dragging = true;
-          this._dragStartTimeout = -1;
-        ]]></body>
-      </method>
-
-      <method name="_endPan">
-        <body><![CDATA[
-          // dragX/dragY are guaranteed to be within the correct bounds, so just
-          // update pageX/pageY directly.
-          this.dragData.pageX -= this._screenToPage(this.dragData.dragX);
-          this.dragData.pageY -= this._screenToPage(this.dragData.dragY);
-
-          // reset the drag data
-          this.dragData.dragX = 0;
-          this.dragData.dragY = 0;
-
-          // update the canvas and move it to the right position
-          this._browserToCanvas();
-        ]]></body>
-      </method>
-
-      <field name="stackEventHandler">
-        <![CDATA[
-        ({
-          deckbrowser: this,
-
-          handleEvent: function seh_handleEvent(aEvent) {
-            if (!(aEvent.type in this)) {
-              Components.reportError("MouseController called with unknown event type " + aEvent.type + "\n");
-              return;
-            }
-            this[aEvent.type](aEvent);
-          },
-
-          mousedown: function seh_mousedown(aEvent) {
-            if (aEvent.button != 0)
-              return;
-
-            // stop kinetic scrolling if it's in progress
-            // avoid setting _lastMouseDown in that case so that we don't
-            // redispatch it in mouseup
-            let dragData = this.deckbrowser.dragData;
-            if (dragData.kineticId) {
-               this.deckbrowser._endKinetic();
-            } else {
-              // Keep a reference to the event so that we can redispatch it
-              // on mouseup
-              this._lastMouseDown = aEvent;
-            }
-
-            // kinetic gets canceled above, so we should be guaranteed to not be
-            // in the panning state
-            if (dragData.dragging)
-              throw "Mousedown while panning - how'd this happen?";
-
-            // The start of the current portion drag
-            dragData.sX = aEvent.screenX;
-            dragData.sY = aEvent.screenY;
-
-            // The total delta between current mouse position and sX/sY
-            dragData.dragX = 0;
-            dragData.dragY = 0;
-
-            this.deckbrowser._dragStartTimeout = setTimeout(function (db) {
-              db._dragStartTimer();
-            }, 200, this.deckbrowser);
-          },
-
-          mouseup: function seh_mouseup(aEvent) {
-            if (aEvent.button != 0)
-              return;
-
-            // cancel scrollStart timer if it's pending
-            clearTimeout(this.deckbrowser._dragStartTimeout);
-            this.deckbrowser._dragStartTimeout = -1;
-
-            // If we're panning, stop dragging and start kinetic scrolling
-            if (this.deckbrowser.dragData.dragging) {
-              this.deckbrowser.dragData.dragging = false;
-
-              if (this.deckbrowser._allowKinetic)
-                this.deckbrowser._startKinetic();
-              else
-                this.deckbrowser._endPan();
-
-              return;
-            }
-
-            // Otherwise, dispatch the click event (if this mouseup was on the canvas)
-            if (this._lastMouseDown &&
-                aEvent.originalTarget == this.deckbrowser._canvas) {
-              // send mousedown & mouseup
-              this.deckbrowser._redispatchMouseEvent(this._lastMouseDown);
-              this._lastMouseDown = null;
-              this.deckbrowser._redispatchMouseEvent(aEvent);
-
-              // FIXME: dblclick events don't fire on the n810, check to see if
-              // we should treat this as a double-click
-              if (this._lastMouseUp &&
-                  (aEvent.timeStamp - this._lastMouseUp.timeStamp) < 400 &&
-                  Math.abs(aEvent.clientX - this._lastMouseUp.clientX) < 30 &&
-                  Math.abs(aEvent.clientY - this._lastMouseUp.clientY) < 30) {
-                this.dblclick(aEvent);
-
-                return;
-              }
-
-              this._lastMouseUp = aEvent;
-            }
-          },
-
-          mousemove: function seh_mousemove(aEvent) {
-            if (!this.deckbrowser.dragData.dragging) {
-              // If we've moved more than N pixels lets go ahead and assume we're dragging
-              // and not wait for the timeout to complete.
-              if (this.deckbrowser._dragStartTimeout != -1 &&
-                  (Math.abs(this.deckbrowser.dragData.sX - aEvent.screenX) > 10 ||
-                   Math.abs(this.deckbrowser.dragData.sY - aEvent.screenY) > 10)) {
-                  clearTimeout(this.deckbrowser._dragStartTimeout);
-                  this.deckbrowser._dragStartTimer();
-              } else {
-                return false;
-              }
-            }
-
-            var dx = this.deckbrowser.dragData.sX - aEvent.screenX;
-            var dy = this.deckbrowser.dragData.sY - aEvent.screenY;
-
-            // Filter out noise in big panning operations which are
-            // almost certainly intended to be on-axis horizontal or
-            // vertical pans.
-            if (Math.abs(dx) > 40 || Math.abs(dy) > 40) {
-              if (Math.abs(dx/dy) < 0.3) // dx is a lot less than dy, probably a vertical drag
-                dx = 0;
-              else if (Math.abs(dy/dx) < 0.3) // probably a horizontal drag
-                dy = 0;
-            }
-            let now = Date.now();
-
-            this.deckbrowser._panEventTrackerIndex = (this.deckbrowser._panEventTrackerIndex + 1) % this.deckbrowser.PAN_EVENTS_TO_TRACK;
-            var pt = {
-              x: aEvent.screenX,
-              y: aEvent.screenY,
-              t: now
-            };
-            this.deckbrowser._panEventTracker[this.deckbrowser._panEventTrackerIndex] = pt;
-
-            this.deckbrowser._moveCanvas(dx, dy);
-
-            aEvent.preventDefault();
-            return true;
-          },
-
-          DOMMouseScroll: function seh_DOMMouseScroll(aEvent) {
-            this.deckbrowser.zoom(aEvent.detail);
-          },
-
-          dblclick: function seh_dblclick(aEvent) {
-            var target = aEvent.originalTarget;
-            var dragData = this.deckbrowser.dragData;
-
-            if (this.deckbrowser._zoomed) {
-              // reset zoom, pan state
-              this.deckbrowser._zoomLevel = this._oldZoomLevel;
-              [dragData.pageX, dragData.pageY] = [dragData.oldPageX, dragData.oldPageY];
-
-              this.deckbrowser._browserToCanvas();
-              this.deckbrowser._zoomed = false;
-            } else {
-              var element = this.deckbrowser.elementFromPoint(aEvent.clientX, aEvent.clientY);
-              if (!element) {
-                Components.utils.reportError("elementFromPoint returned null\n");
-                return;
-              }
-
-              // Find the nearest non-inline ancestor
-              while (element.parentNode) {
-                var display = window.getComputedStyle(element, "").getPropertyValue("display");
-                var zoomable = /table/.test(display) || /block/.test(display);
-                if (zoomable)
-                  break;
-
-                element = element.parentNode;
-              }
-
-              // Remember pageX/pageY
-              [dragData.oldPageX, dragData.oldPageY] = [dragData.pageX, dragData.pageY];
-              this._oldZoomLevel = this.deckbrowser._zoomLevel;
-
-              this.deckbrowser.zoomToElement(element);
-              this.deckbrowser._zoomed = true;
-            }
-          }
-        });
-        ]]>
-      </field>
-    </implementation>
-  </binding>
-
-  <binding id="documenttab"
-           extends="chrome://global/content/bindings/richlistbox.xml#richlistitem">
-    <content>
-      <xul:stack anonid="page" class="documenttab-container" flex="1">
-        <html:canvas anonid="canvas" class="documenttab-canvas" width="80" height="60"/>
-        <xul:vbox align="start">
-          <xul:image anonid="close" class="documenttab-close"/>
-        </xul:vbox>
-      </xul:stack>
-    </content>
-    <implementation>
-      <constructor><![CDATA[
-        let close = document.getAnonymousElementByAttribute(this, "anonid", "close");
-        let closefn = new Function("event", this.control.getAttribute("onclosetab"));
-        var self = this;
-        close.addEventListener("mousedown", function(event) { closefn.call(self, event); event.stopPropagation(); }, true);
-      ]]></constructor>
-
-      <method name="updateTab">
-        <parameter name="browser"/>
-        <parameter name="srcCanvas"/>
-        <body>
-          <![CDATA[
-            const tabWidth = 80
-            const tabHeight = 60
-            let canvas = document.getAnonymousElementByAttribute(this, "anonid", "canvas");
-
-            let domWin = browser.contentWindow;
-            let ctx = canvas.getContext("2d");
-            if (srcCanvas) {
-              let width = tabWidth * srcCanvas.width / domWin.innerWidth;
-              let height = tabHeight * srcCanvas.height / domWin.innerHeight;
-              ctx.drawImage(srcCanvas, 0, 0, width, height)
-            } else {
-              let width = domWin.innerWidth;
-              let height = domWin.innerHeight;
-              ctx.clearRect(0, 0, tabWidth, tabHeight);
-              ctx.save();
-              ctx.scale(tabWidth / width, tabHeight / height);
-              ctx.drawWindow(domWin, 0, 0, width, height, "white");
-              ctx.restore();
-            }
-          ]]>
-        </body>
-      </method>
-
-      <method name="markInvalid">
-        <parameter name="browser"/>
-        <body>
-          <![CDATA[
-            let canvas = document.getAnonymousElementByAttribute(this, "anonid", "canvas");
-            let ctx = canvas.getContext("2d");
-
-            ctx.save();
-            ctx.strokeStyle = "red";
-            ctx.moveTo(63, 43);
-            ctx.lineTo(78, 58);
-            ctx.moveTo(78, 43);
-            ctx.lineTo(63, 58);
-            ctx.stroke();
-            ctx.restore();
-          ]]>
-        </body>
-      </method>
-    </implementation>
-  </binding>
-
-  <!-- very hacky, used to display richlistitems in multiple columns -->
-  <binding id="tablist"
-           extends="chrome://global/content/bindings/richlistbox.xml#richlistbox">
-    <content>
-      <children includes="listheader"/>
-      <xul:scrollbox allowevents="true" orient="horizontal" anonid="main-box"
-                     flex="1" style="overflow: auto;">
-        <children/>
-      </xul:scrollbox>
-    </content>
-
-    <implementation>
-      <field name="tabsPerColumn">4</field>
-      <property name="children" readonly="true">
-        <getter>
-          <![CDATA[
-            var childNodes = [];
-            for (var box = this.firstChild; box; box = box.nextSibling) {
-              for (var child = box.firstChild; child; child = child.nextSibling) {
-                if (child instanceof Components.interfaces.nsIDOMXULSelectControlItemElement)
-                  childNodes.push(child);
-              }
-            }
-            return childNodes;
-          ]]>
-        </getter>
-      </property>
-      <method name="addTab">
-        <parameter name="tab"/>
-        <body>
-          <![CDATA[
-            if (this.children.length % this.tabsPerColumn == 0)
-              this.appendChild(document.createElement("vbox"));
-            this.lastChild.appendChild(tab);
-
-            return tab;
-          ]]>
-        </body>
-      </method>
-      <method name="removeTab">
-        <parameter name="tab"/>
-        <body>
-          <![CDATA[
-            var idx = this.getIndexOfItem(tab);
-            if (idx == -1)
-              return;
-
-            // remove all later tabs and readd them so that there aren't empty columns
-            var count = this.itemCount - 1;
-            var tomove = [ ];
-            for (var c = count; c >= idx; c--) {
-              var tab = this.getItemAtIndex(c);
-              tomove.push(tab.parentNode.removeChild(tab));
-              if (!this.lastChild.hasChildNodes())
-                this.removeChild(this.lastChild);
-            }
-
-            // subtract 2 because the tab to remove should not be added back again
-            for (var m = tomove.length - 2; m >= 0; m--)
-              this.addTab(tomove[m]);
-          ]]>
-        </body>
-      </method>
-    </implementation>
-  </binding>
-
-</bindings>
new file mode 100644
--- /dev/null
+++ b/chrome/content/tabs.xml
@@ -0,0 +1,143 @@
+<?xml version="1.0"?>
+<!DOCTYPE bindings PUBLIC "-//MOZILLA//DTD XBL V1.0//EN" "http://www.mozilla.org/xbl">
+
+<bindings
+    xmlns="http://www.mozilla.org/xbl"
+    xmlns:xbl="http://www.mozilla.org/xbl"
+    xmlns:html="http://www.w3.org/1999/xhtml"
+    xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+  <binding id="documenttab"
+           extends="chrome://global/content/bindings/richlistbox.xml#richlistitem">
+    <content>
+      <xul:stack anonid="page" class="documenttab-container" flex="1">
+        <html:canvas anonid="canvas" class="documenttab-canvas" width="80" height="60"/>
+        <xul:vbox align="start">
+          <xul:image anonid="close" class="documenttab-close"/>
+        </xul:vbox>
+      </xul:stack>
+    </content>
+    <implementation>
+      <constructor><![CDATA[
+        let close = document.getAnonymousElementByAttribute(this, "anonid", "close");
+        let closefn = new Function("event", this.control.getAttribute("onclosetab"));
+        var self = this;
+        close.addEventListener("mousedown", function(event) { closefn.call(self, event); event.stopPropagation(); }, true);
+      ]]></constructor>
+
+      <method name="updateThumbnail">
+        <parameter name="browser"/>
+        <parameter name="srcCanvas"/>
+        <body>
+          <![CDATA[
+            const tabWidth = 80;
+            const tabHeight = 60;
+            let canvas = document.getAnonymousElementByAttribute(this, "anonid", "canvas");
+
+            let domWin = browser.contentWindow;
+            let ctx = canvas.getContext("2d");
+
+            if (srcCanvas) {
+              ctx.drawImage(srcCanvas, 0, 0, tabWidth, tabHeight)
+            }
+            else {
+              let width = domWin.innerWidth;
+              let height = domWin.innerHeight;
+              ctx.clearRect(0, 0, tabWidth, tabHeight);
+              ctx.save();
+              ctx.scale(tabWidth / width, tabHeight / height);
+              ctx.drawWindow(domWin, 0, 0, width, height, "white");
+              ctx.restore();
+            }
+          ]]>
+        </body>
+      </method>
+
+      <method name="markInvalid">
+        <parameter name="browser"/>
+        <body>
+          <![CDATA[
+            let canvas = document.getAnonymousElementByAttribute(this, "anonid", "canvas");
+            let ctx = canvas.getContext("2d");
+
+            ctx.save();
+            ctx.strokeStyle = "red";
+            ctx.moveTo(63, 43);
+            ctx.lineTo(78, 58);
+            ctx.moveTo(78, 43);
+            ctx.lineTo(63, 58);
+            ctx.stroke();
+            ctx.restore();
+          ]]>
+        </body>
+      </method>
+    </implementation>
+  </binding>
+
+  <!-- very hacky, used to display richlistitems in multiple columns -->
+  <binding id="tablist"
+           extends="chrome://global/content/bindings/richlistbox.xml#richlistbox">
+    <content>
+      <children includes="listheader"/>
+      <xul:scrollbox allowevents="true" orient="horizontal" anonid="main-box"
+                     flex="1" style="overflow: auto;">
+        <children/>
+      </xul:scrollbox>
+    </content>
+
+    <implementation>
+      <field name="tabsPerColumn">4</field>
+      <property name="children" readonly="true">
+        <getter>
+          <![CDATA[
+            var childNodes = [];
+            for (var box = this.firstChild; box; box = box.nextSibling) {
+              for (var child = box.firstChild; child; child = child.nextSibling) {
+                if (child instanceof Components.interfaces.nsIDOMXULSelectControlItemElement)
+                  childNodes.push(child);
+              }
+            }
+            return childNodes;
+          ]]>
+        </getter>
+      </property>
+      <method name="addTab">
+        <parameter name="tab"/>
+        <body>
+          <![CDATA[
+            if (this.children.length % this.tabsPerColumn == 0)
+              this.appendChild(document.createElement("vbox"));
+            this.lastChild.appendChild(tab);
+
+            return tab;
+          ]]>
+        </body>
+      </method>
+      <method name="removeTab">
+        <parameter name="tab"/>
+        <body>
+          <![CDATA[
+            var idx = this.getIndexOfItem(tab);
+            if (idx == -1)
+              return;
+
+            // remove all later tabs and readd them so that there aren't empty columns
+            var count = this.itemCount - 1;
+            var tomove = [ ];
+            for (var c = count; c >= idx; c--) {
+              var tab = this.getItemAtIndex(c);
+              tomove.push(tab.parentNode.removeChild(tab));
+              if (!this.lastChild.hasChildNodes())
+                this.removeChild(this.lastChild);
+            }
+
+            // subtract 2 because the tab to remove should not be added back again
+            for (var m = tomove.length - 2; m >= 0; m--)
+              this.addTab(tomove[m]);
+          ]]>
+        </body>
+      </method>
+    </implementation>
+  </binding>
+
+</bindings>
--- a/chrome/jar.mn
+++ b/chrome/jar.mn
@@ -11,28 +11,31 @@ firstrun.jar:
 
 browser.jar:
 % content browser %content/
 * content/browser.xul                  (content/browser.xul)
   content/browser.js                   (content/browser.js)
   content/browser-ui.js                (content/browser-ui.js)
   content/commandUtil.js               (content/commandUtil.js)
   content/urlbar.xml                   (content/urlbar.xml)
-  content/deckbrowser.xml              (content/deckbrowser.xml)
+  content/tabs.xml                     (content/tabs.xml)
   content/notification.xml             (content/notification.xml)
   content/browser.css                  (content/browser.css)
   content/scrollbars.css               (content/scrollbars.css)
   content/content.css                  (content/content.css)
   content/checkerboard.png             (content/checkerboard.png)
 * content/shortcuts.js                 (content/shortcuts.js)
 % content branding %content/branding/
 % style chrome://mozapps/content/extensions/extensions.xul chrome://browser/skin/extensions.css
   content/preferences/richpref.xml     (content/preferences/richpref.xml)
 * content/sanitize.xul                 (content/sanitize.xul)
 * content/sanitize.js                  (content/sanitize.js)
+  content/WidgetStack.js               (content/WidgetStack.js)
+  content/CanvasBrowser.js             (content/CanvasBrowser.js)
+  content/InputHandler.js              (content/InputHandler.js)
 
 classic.jar:
 % skin browser classic/1.0 %
   browser.css                          (skin/browser.css)
   extensions.css                       (skin/extensions.css)
   notification.css                     (skin/notification.css)
   images/close-small.png               (skin/images/close-small.png)
   images/tab_close.png                 (skin/images/tab_close.png)
--- a/chrome/skin/browser.css
+++ b/chrome/skin/browser.css
@@ -172,28 +172,33 @@ toolbarbutton.urlbar-cap-button {
   background: url("images/addressbar_endcap_l_off.png") top right no-repeat;
 }
 
 #identity-box:hover:active {
   background: url("images/addressbar_endcap_l_on.png") top right no-repeat;
 }
 
 /* favicon images are 16x16 */
-#urlbar-image-deck {
+#urlbar-image-box {
   max-width: 24px;
   max-height: 24px;
   min-width: 24px;
   min-height: 24px;
   margin: 12px 2px 2px 12px;
 }
 
 #urlbar-throbber[loading] {
   list-style-image: url("chrome://browser/skin/images/throbber.png");
 }
 
+#urlbar-favicon {
+  width: 24px;
+  height: 24px;
+}
+
 #urlbar-editarea {
   min-height: 49px;
   -moz-box-align: center;
   background: url("images/addressbar_middle.png") repeat-x;
 }
 
 #urlbar-caption {
   margin: 2px 8px 2px 8px;
@@ -461,22 +466,22 @@ toolbarbutton.panel-button {
 }
 
 /* Hide URL box */
 .ac-url-box {
   display: none;
 }
 
 /* Left sidebar (tabs)  ---------------------------------------------------- */
-#tab-list-container {
+#tabs-container {
   background: url("images/left_sidebar_middle.png") rgb(87,87,87) top right repeat-y;
   min-width: 132px;
 }
 
-#tab-list {
+#tabs {
   -moz-appearance: none;
   margin: 0;
   padding: 4px;
   -moz-padding-end: 12px;
   background-color: transparent;
 }
 
 richlistitem[type="documenttab"] {
--- a/locales/en-US/chrome/shortcuts.properties
+++ b/locales/en-US/chrome/shortcuts.properties
@@ -1,15 +1,15 @@
 cmd_back.name=Go back
 cmd_forward.name=Go forward
 cmd_reload.name=Reload page
 cmd_stop.name=Stop loading
 cmd_search.name=Search
 cmd_go.name=Load URL
-cmd_openLocation=Open location
+cmd_openLocation.name=Open location
 cmd_star.name=Star page
 cmd_bookmarks.name=View bookmarks
 cmd_find.name=Find in page
 cmd_findAgain.name=Find again
 cmd_findPrevious.name=Find previous
 cmd_menu.name=Show menu
 cmd_fullscreen.name=Use full screen
 cmd_scrollPageUp.name=Page up
new file mode 100644
--- /dev/null
+++ b/tests/wsTests.js
@@ -0,0 +1,605 @@
+
+const RED = "data:image/gif;base64,R0lGODdhAgACALMAAAAAAP///wAAAAAAAP8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACwAAAAAAgACAAAEA5BIEgA7";
+const BLUE = "data:image/gif;base64,R0lGODdhAgACALMAAAAAAP///wAAAAAAAP8AAAAAAAAAAAAAAAAA/wAAAAAAAAAAAAAAAAAAAAAAAAAAACwAAAAAAgACAAAEAxBJFAA7";
+const ORANGE = "data:image/gif;base64,R0lGODdhAgACALMAAAAAAP///wAAAP//AP8AAP+AAAD/AAAAAAAA//8A/wAAAAAAAAAAAAAAAAAAAAAAACwAAAAAAgACAAAEA7DIEgA7";
+
+var gCheckCount = 0;
+
+// test helpers
+function XUL(s, id, attrs) {
+  let e = document.createElementNS("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul", s);
+  if (id)
+    e.setAttribute("id", id);
+
+  if (attrs) {
+    for (var a in attrs)
+      e.setAttribute(a, attrs[a]);
+  }
+  return e;
+}
+
+function e(id) {
+  return document.getElementById(id);
+}
+
+function attr(id, a, v) {
+  e(id).setAttribute(a, v);
+}
+
+function CleanDocument(x, y) {
+  gCheckCount = 0;
+
+  var root = document.getElementById("testroot");
+  while (root.firstChild)
+    root.removeChild(root.firstChild);
+
+  if (x && y) {
+    var tr = document.getElementById("testroot");
+    tr.style.width = x + "px";
+    tr.style.height = y + "px";
+  }
+}
+
+function IMAGE(src, id, x, y, w, h) {
+  return XUL("image", id, { style: "-moz-stack-sizing: ignore; opacity: 0.5", src: src, width: w, height: h, left: x, top: y });
+}
+
+function SetupFourBoxes() {
+  var s = XUL("stack", "s");
+  var a = IMAGE(RED,  "a",   0,   0, 100, 100);
+  var b = IMAGE(BLUE, "b", 100,   0, 100, 100);
+  var c = IMAGE(BLUE, "c",   0, 100, 100, 100);
+  var d = IMAGE(RED,  "d", 100, 100, 100, 100);
+
+  s.appendChild(a);
+  s.appendChild(b);
+  s.appendChild(c);
+  s.appendChild(d);
+
+  document.getElementById("testroot").appendChild(s);
+
+  return s;
+}
+
+function SetupOneBoxAndViewport(x,y) {
+  var s = XUL("stack", "s");
+
+  var b = IMAGE(RED,  "b",  x-20,  y-20, 20, 20);
+
+  s.appendChild(b);
+
+  var vp = IMAGE(ORANGE, "vp", x, y, 50, 50);
+  vp.setAttribute("viewport", "true");
+  s.appendChild(vp);
+
+  document.getElementById("testroot").appendChild(s);
+
+  return s;
+}
+
+function SetupEightBoxes(x, y, sz, _wantStack) {
+  var s = XUL("stack", "s");
+
+  x = x || 0;
+  y = y || 0;
+  sz = sz || 20;
+
+  // boxes are b1-b9 from the top left, going clockwise
+  var b1 = IMAGE(RED,  "b1", x-sz, y-sz, sz, sz);
+  var b2 = IMAGE(BLUE, "b2", x,    y-sz, sz, sz);
+  var b3 = IMAGE(RED,  "b3", x+sz, y-sz, sz, sz);
+  var b4 = IMAGE(BLUE, "b4", x+sz, y,    sz, sz);
+  var b5 = IMAGE(RED,  "b5", x+sz, y+sz, sz, sz);
+  var b6 = IMAGE(BLUE, "b6", x,    y+sz, sz, sz);
+  var b7 = IMAGE(RED,  "b7", x-sz, y+sz, sz, sz);
+  var b8 = IMAGE(BLUE, "b8", x-sz, y,    sz, sz);
+
+  s.appendChild(b1);
+  s.appendChild(b2);
+  s.appendChild(b3);
+  s.appendChild(b4);
+  s.appendChild(b5);
+  s.appendChild(b6);
+  s.appendChild(b7);
+  s.appendChild(b8);
+
+  if (_wantStack)
+    return s;
+
+  document.getElementById("testroot").appendChild(s);
+
+  return s;
+}
+
+function SetupNineBoxes(x, y, sz) {
+  var s = SetupEightBoxes(x, y, sz, true);
+
+  var b0 = IMAGE(ORANGE, "b0", x, y, sz, sz);
+
+  s.appendChild(b0);
+
+  document.getElementById("testroot").appendChild(s);
+
+  return s;
+}
+
+function SetupEightBoxesAndViewport(x, y, sz) {
+  var s = SetupEightBoxes(x, y, sz, true);
+
+  var vp = IMAGE(ORANGE, "vp", x, y, 20, 20);
+  vp.setAttribute("viewport", "true");
+  s.appendChild(vp);
+
+  document.getElementById("testroot").appendChild(s);
+
+  return s;
+}
+
+function Barrier(x, y, type, vpr) {
+  if (x != undefined && y != undefined)
+    throw "Bumper with both x and y given, that won't work";
+
+  var spacer = XUL("spacer", null, { style: "-moz-stack-sizing: ignore;", barriertype: type, size: '10' });
+
+  if (x != undefined)
+    spacer.setAttribute("left", x);
+  if (y != undefined)
+    spacer.setAttribute("top", y);
+  if (vpr)
+    spacer.setAttribute("constraint", "vp-relative");
+
+  document.getElementById("s").appendChild(spacer);
+  return spacer;
+}
+
+function checkInnerBoundsInner(ws, x, y, w, h) {
+  let vwib = ws._viewport.viewportInnerBounds;
+  if (!((vwib.x != x ||
+	 vwib.y != y ||
+	 (w != undefined && vwib.width != w) ||
+	 (h != undefined && vwib.height != h))))
+    return null;
+
+  return [ vwib.x, vwib.y, vwib.width, vwib.height ];
+}
+
+function checkInnerBounds(ws, x, y, w, h) {
+  gCheckCount++;
+
+  var res = checkInnerBoundsInner(ws, x, y, w, h);
+  if (!res)
+    return null;
+
+  var err;
+  if (w == undefined || h == undefined) {
+    err = "(" + gCheckCount + ") expected [" + x + "," + y + "] got [" + res[0] + "," + res[1] + "]";
+  } else {
+    err = "(" + gCheckCount + ") expected [" + x + "," + y + "," + w + "," + h + "] got [" + res[0] + "," + res[1] + "," + res[2] + "," + res[3] + "]";
+  }
+
+  throw "checkInnerBounds failed: " + err;
+}
+
+function checkRectInner(ws, id, x, y, w, h) {
+  var e = document.getElementById(id);
+  var bb = e.getBoundingClientRect();
+  var wsb = ws._el.getBoundingClientRect();
+
+  if (!((bb.left - wsb.left) != x ||
+	(bb.top - wsb.left) != y ||
+	(w != undefined && (bb.right - bb.left) != w) ||
+	(h != undefined && (bb.bottom - bb.top) != h)))
+    return null;
+
+  return [(bb.left - wsb.left), (bb.top - wsb.left), (bb.right - bb.left), (bb.bottom - bb.top)];
+}
+
+function checkRect(ws, id, x, y, w, h) {
+  gCheckCount++;
+
+  var res = checkRectInner(ws, id, x, y, w, h);
+  if (!res)
+    return; // ok
+
+  var err;
+  if (w == undefined || h == undefined) {
+    err = "(" + gCheckCount + ") expected [" + x + "," + y + "] got [" + res[0] + "," + res[1] + "]";
+  } else {
+    err = "(" + gCheckCount + ") expected [" + x + "," + y + "," + w + "," + h + "] got [" + res[0] + "," + res[1] + "," + res[2] + "," + res[3] + "]";
+  }
+
+  throw "checkRect failed: " + err;
+}
+
+//
+// check that simple stuff works
+//
+function simple1() {
+  CleanDocument();
+  var s = SetupFourBoxes();
+
+  var ws = new WidgetStack(s);
+
+  checkRect(ws, "a", 0, 0);
+  checkRect(ws, "b", 100, 0);
+  checkRect(ws, "c", 0, 100);
+  checkRect(ws, "d", 100, 100);
+
+  ws.panBy(-10, -10);
+
+  checkRect(ws, "a", -10, -10);
+  checkRect(ws, "b",  90, -10);
+  checkRect(ws, "c", -10,  90);
+  checkRect(ws, "d",  90,  90);
+
+  // should be the same as panBy(10,10)
+  ws.dragStart(50, 50);
+  ws.dragMove(0, 0);
+  ws.dragMove(60, 60);
+  ws.dragStop();
+
+  checkRect(ws, "a", 0, 0);
+  checkRect(ws, "b", 100, 0);
+  checkRect(ws, "c", 0, 100);
+  checkRect(ws, "d", 100, 100);
+
+  return true;
+}
+
+// check that ignore-x, ignore-y, and frozen work
+function simple2() {
+  CleanDocument();
+  var s = SetupFourBoxes();
+
+  attr("b", "constraint", "ignore-x");
+  attr("c", "constraint", "ignore-y");
+  attr("d", "constraint", "frozen");
+
+  var ws = new WidgetStack(s);
+
+  ws.panBy(-20, -20);
+
+  checkRect(ws, "a", -20, -20);
+  checkRect(ws, "b", 100, -20);
+  checkRect(ws, "c", -20, 100);
+  checkRect(ws, "d", 100, 100);
+
+  ws.panBy(20, 20);
+
+  checkRect(ws, "a", 0, 0);
+  checkRect(ws, "b", 100, 0);
+  checkRect(ws, "c", 0, 100);
+  checkRect(ws, "d", 100, 100);
+
+  return true;
+}
+
+function simple3() {
+  CleanDocument(50,50);
+
+  var s = SetupNineBoxes(0, 0, 50);
+  for (var i = 1; i <= 8; i++) {
+    attr("b"+i, "constraint", "vp-relative");
+  }
+
+  Barrier(0, undefined, "vertical");
+  Barrier(25, undefined, "vertical");
+  Barrier(50, undefined, "vertical");
+  Barrier(undefined, 50, "horizontal");
+
+  var ws = new WidgetStack(s, 50, 50);
+
+  ws.panBy(-15, 0);
+
+  checkRect(ws, "b0", -5, 0);
+
+  // test that dragging does the same thing
+  ws.dragStart(0, 0);
+  ws.dragMove(5, 0);
+  ws.dragMove(10, 0);
+  ws.dragMove(15, 0);
+  ws.dragStop();
+
+  checkRect(ws, "b0", 0, 0);
+
+  // because there's a 10-px bumper, this pan should have no effect
+  ws.panBy(-5, 0);
+  checkRect(ws, "b0", 0, 0);
+
+  // now we pan beyond the right barrier by 5px
+  ws.panBy(-15, 0);
+  checkRect(ws, "b0", -5, 0);
+
+  // and then we go back.  We should just need 5 to get back to 0.
+  ws.panBy(5, 0);
+  checkRect(ws, "b0", 0, 0);
+
+  // check that we hit the middle barrier correctly
+  ws.panBy(-30, 0);
+  checkRect(ws, "b0", -20, 0);
+
+  // this should hit the middle barrier
+  ws.panBy(-10, 0);
+  checkRect(ws, "b0", -20, 0);
+
+  // and then go past it
+  ws.panBy(-20, 0);
+  checkRect(ws, "b0", -30, 0);
+
+  // reset
+  ws.panBy(40, 0);
+
+  // now let's do a simpler test of the horizontal barriers; there's only one at 50
+
+  ws.panBy(0, -5);
+  checkRect(ws, "b0", 0, 0);
+
+  ws.panBy(0, 5);
+  checkRect(ws, "b0", 0, 0);
+
+  ws.panBy(0, -20);
+  checkRect(ws, "b0", 0, -10);
+
+  ws.panBy(0, 10);
+  checkRect(ws, "b0", 0, 0);
+
+  return true;
+}
+
+// now check some viewport stuff
+function vp1() {
+  CleanDocument(50, 50);
+
+  var s = SetupOneBoxAndViewport(0, 0);
+  attr("b", "constraint", "vp-relative");
+
+  var ws = new WidgetStack(s, 50, 50);
+
+  // explicitly use this form of svb
+  ws.setViewportBounds({top: 0, left: 0, right: 200, bottom: 200});
+
+  checkRect(ws, "b", -20, -20);
+
+  ws.panBy(20, 20);
+  checkRect(ws, "b", 0, 0);
+  checkRect(ws, "vp", 20, 20);
+
+  ws.panBy(50, 50);
+  checkRect(ws, "b", 0, 0);
+  checkRect(ws, "vp", 20, 20);
+
+  ws.panBy(-20, -20);
+  ws.panBy(50, 50);
+  checkRect(ws, "b", 0, 0);
+  checkRect(ws, "vp", 20, 20);
+
+  ws.panBy(-200, -200);
+
+  checkRect(ws, "vp", 0, 0);
+  checkInnerBounds(ws, 150, 150, 50, 50);
+
+  ws.panBy(500, 500);
+
+  checkRect(ws, "vp", 20, 20);
+  checkInnerBounds(ws, 0, 0, 50, 50);
+
+  return true;
+}
+
+function vp2() {
+  CleanDocument(20, 20);
+
+  var s = SetupEightBoxesAndViewport(0, 0);
+  for (var i = 1; i <= 8; i++) {
+    attr("b"+i, "constraint", "vp-relative");
+  }
+
+  var ws = new WidgetStack(s, 20, 20);
+
+  // b5 is the bottom-right; the initial setup has a 20x20 viewport in the middle
+  checkRect(ws, "b5", 20, 20);
+
+  // explicitly use this form of svb
+  ws.setViewportBounds(0, 0, 200, 200);
+
+  // after resizing the viewport bounds, the rect should get pushed out
+  checkRect(ws, "b5", 200, 200);
+
+  ws.panBy(-500, -500);
+
+  checkRect(ws, "b5", 0, 0);
+  checkInnerBounds(ws, 180, 180, 20, 20);
+
+  return true;
+}
+
+function vp3() {
+  CleanDocument(20, 20);
+
+  var s = SetupEightBoxesAndViewport(0, 0);
+  for (var i = 1; i <= 8; i++) {
+    attr("b"+i, "constraint", "vp-relative");
+  }
+
+  attr("b2", "constraint", "vp-relative,ignore-x");
+  attr("b4", "constraint", "vp-relative,ignore-y");
+
+  var ws = new WidgetStack(s, 20, 20);
+
+  // b2 is the top-middle
+  checkRect(ws, "b2", 0, -20);
+  // b4 is the right-middle
+  checkRect(ws, "b4", 20, 0);
+
+  ws.setViewportBounds(200, 200);
+
+  checkRect(ws, "b2", 0, -20);
+  checkRect(ws, "b4", 200, 0);
+
+  // x pans shouldn't affect ignore-x widgets
+  ws.panBy(-20, 0);
+  checkRect(ws, "b2", 0, -20);
+  ws.panBy( 20, 0);
+
+  // y pans shouldn't affect ignore-y widgets
+  ws.panBy(0, -20);
+  checkRect(ws, "b4", 200, 0);
+  ws.panBy(0,  20);
+
+  return true;
+}
+
+// test whether the right things happen when the viewport size changes
+function vp4() {
+  CleanDocument(20, 20);
+
+  var s = SetupEightBoxesAndViewport(0, 0);
+  for (var i = 1; i <= 8; i++) {
+    attr("b"+i, "constraint", "vp-relative");
+  }
+
+  attr("b2", "constraint", "vp-relative,ignore-x");
+  attr("b4", "constraint", "vp-relative,ignore-y");
+
+  var ws = new WidgetStack(s, 20, 20);
+
+  ws.setViewportBounds(200, 200);
+
+  // after resizing the viewport bounds, the rect should get pushed out
+  checkRect(ws, "b4", 200, 0);
+  checkRect(ws, "b5", 200, 200);
+
+  checkInnerBounds(ws, 0, 0, 20, 20);
+
+  ws.panBy(-50, -50);
+
+  checkInnerBounds(ws, 50, 50, 20, 20);
+
+  // the viewport is now going to grow
+  ws.setViewportBounds(400, 400);
+
+  // ... and the inner bounds should remain the same
+  checkInnerBounds(ws, 50, 50, 20, 20);
+
+  // ... but b4 and b5 should be pushed out
+  checkRect(ws, "b4", 400-50, 0);
+  checkRect(ws, "b5", 400-50, 400-50);
+
+  // now move to the far corner
+  ws.panBy(-500, -500);
+
+  // now shrink again
+  ws.setViewportBounds(100, 100);
+
+  return true;
+}
+
+function vp5() {
+  CleanDocument(20, 20);
+
+  var s = SetupEightBoxesAndViewport(0, 0);
+  for (var i = 1; i <= 8; i++) {
+    attr("b"+i, "constraint", "vp-relative");
+  }
+
+  Barrier(0, undefined, "vertical", true);
+  Barrier(20, undefined, "vertical", true);
+  Barrier(undefined, 0, "horizontal", true);
+  Barrier(undefined, 100, "horizontal", true);
+
+  var ws = new WidgetStack(s, 20, 20);
+
+  ws.setViewportBounds(200, 200);
+
+  ws.panBy(-20, 0);
+  checkRect(ws, "b1", -30, -20);
+
+  ws.panBy(10, 0);
+  checkRect(ws, "b1", -20, -20);
+
+  ws.panBy(-5, 0);
+  checkRect(ws, "b1", -20, -20);
+
+
+  // check the horizontal -- we should hit the first bumper,
+  // but the other one should always be forever out of range of the vp
+  ws.panBy(0, -110);
+  checkRect(ws, "b1", -20, -120);
+
+  ws.panBy(0, -300);
+  checkRect(ws, "b1", -20, -220);
+
+  return true;
+}
+
+
+
+function vp6() {
+  CleanDocument(20, 20);
+
+  var s = SetupEightBoxesAndViewport(0, 0);
+  for (var i = 1; i <= 8; i++) {
+    attr("b"+i, "constraint", "vp-relative");
+  }
+
+  var ws = new WidgetStack(s, 20, 20);
+
+  ws.setViewportBounds(200, 200);
+  checkInnerBounds(ws, 0, 0, 20, 20);
+
+  ws.panTo(-75, -75);
+  checkInnerBounds(ws, 75, 75, 20, 20);
+
+  // scale up
+  ws.setViewportBounds(500, 500);
+  checkInnerBounds(ws, 75, 75, 20, 20);
+
+  ws.panTo(-300, -300);
+  checkInnerBounds(ws, 300, 300, 20, 20);
+
+  // scale down
+  ws.setViewportBounds(200, 200);
+  checkInnerBounds(ws, 75, 75, 20, 20);// XXX?
+
+  ws.panTo(-75, -75);
+  checkInnerBounds(ws, 75, 75, 20, 20);
+
+  return true;
+}
+
+
+
+function run(s) {
+  var r = false;
+  try {
+    r = window[s].apply(window);
+    if (r) {
+      logbase("PASSED: " + s);
+    } else {
+      logbase("FAILED: " + s + " (no exception?)");
+    }
+  } catch (e) {
+    logbase("FAILED: " + s + ": " + e);
+  }
+
+}
+
+function runTests() {
+  run("simple1");
+  run("simple2");
+  run("simple3");
+
+  run("vp1");
+  run("vp2");
+  run("vp3");
+  run("vp4");
+  run("vp5");
+  run("vp6");
+}
+
+function handleLoad() {
+  gWsDoLog = true;
+  gWsLogDiv = document.getElementById("logdiv");
+
+  runTests();
+}
new file mode 100644
--- /dev/null
+++ b/tests/wsTests.xul
@@ -0,0 +1,25 @@
+<?xml version="1.0"?>
+
+<window id="w"
+        width="800" height="800"
+        onload="handleLoad();"
+        xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+        xmlns:html="http://www.w3.org/1999/xhtml">
+
+  <script type="text/javascript" src="../chrome/content/WidgetStack.js"/>
+  <script type="text/javascript" src="wsTests.js"/>
+
+  <vbox>
+    <spacer style="height: 50px;"/>
+    <hbox>
+      <spacer style="width: 50px;"/>
+      <box id="testroot" style="width: 50px; height: 50px; background: gray; -moz-stack-sizing: ignore;"/>
+      <spacer flex="1"/>
+    </hbox>
+    <spacer style="height: 400px;" flex="1"/>
+    <hbox>
+      <spacer style="width: 400px;"/>
+      <html:div style="font: 9px sans-serif;" id="logdiv"></html:div>
+    </hbox>
+  </vbox>
+</window>