Bug 663778 - Box Model Highlighter r=jwalker
authorMichael Ratcliffe <mratcliffe@mozilla.com>
Thu, 13 Mar 2014 21:36:48 +0000
changeset 173527 8dea188bf8a0af2d7f46766e4ab3986241857058
parent 173526 9b860b0a728209dd1927e502a4114abc35c86325
child 173528 fad8f0885e5b12fe4a66d453633365e84577377b
push id26408
push usercbook@mozilla.com
push dateFri, 14 Mar 2014 11:35:49 +0000
treeherdermozilla-central@d527230a2032 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjwalker
bugs663778
milestone30.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 663778 - Box Model Highlighter r=jwalker
browser/base/content/browser.css
browser/base/content/highlighter.css
browser/devtools/framework/selection.js
browser/devtools/framework/toolbox.js
browser/devtools/markupview/markup-view.js
browser/themes/linux/browser.css
browser/themes/osx/browser.css
browser/themes/shared/devtools/highlighter.inc.css
browser/themes/windows/browser.css
toolkit/devtools/LayoutHelpers.jsm
toolkit/devtools/server/actors/highlighter.js
toolkit/devtools/server/actors/inspector.js
--- a/browser/base/content/browser.css
+++ b/browser/base/content/browser.css
@@ -1,14 +1,15 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 @namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul");
 @namespace html url("http://www.w3.org/1999/xhtml");
+@namespace svg url("http://www.w3.org/2000/svg");
 
 #main-window:not([chromehidden~="toolbar"]) {
 %ifdef XP_MACOSX
   min-width: 335px;
 %else
   min-width: 300px;
 %endif
 }
--- a/browser/base/content/highlighter.css
+++ b/browser/base/content/highlighter.css
@@ -1,35 +1,25 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 .highlighter-container {
   pointer-events: none;
 }
 
-.highlighter-outline-container {
-  overflow: hidden;
-  position: relative;
-}
-
-.highlighter-outline {
-  position: absolute;
-}
-
-.highlighter-outline[hidden] {
-  opacity: 0;
-  pointer-events: none;
-  display: -moz-box;
-}
-
-.highlighter-outline:not([disable-transitions]) {
-  transition-property: opacity, top, left, width, height;
-  transition-duration: 0.1s;
-  transition-timing-function: linear;
+/*
+ * Box model highlighter
+ */
+svg|svg.box-model-root[hidden],
+svg|line.box-model-guide-top[hidden],
+svg|line.box-model-guide-right[hidden],
+svg|line.box-model-guide-left[hidden],
+svg|line.box-model-guide-bottom[hidden] {
+  display: none;
 }
 
 /*
  * Node Infobar
  */
 .highlighter-nodeinfobar-container {
   position: relative;
 }
@@ -40,23 +30,16 @@
 }
 
 .highlighter-nodeinfobar-positioner[hidden] {
   opacity: 0;
   pointer-events: none;
   display: -moz-box;
 }
 
-.highlighter-nodeinfobar-positioner:not([disable-transitions]),
-.highlighter-nodeinfobar-positioner[disable-transitions][force-transitions] {
-  transition-property: transform, opacity, top, left;
-  transition-duration: 0.1s;
-  transition-timing-function: linear;
-}
-
 .highlighter-nodeinfobar-text {
   overflow: hidden;
   white-space: nowrap;
   text-overflow: ellipsis;
   direction: ltr;
 }
 
 html|*.highlighter-nodeinfobar-id,
--- a/browser/devtools/framework/selection.js
+++ b/browser/devtools/framework/selection.js
@@ -155,30 +155,32 @@ Selection.prototype = {
     if (this.isNode()) {
       return this.node.ownerDocument;
     }
     return null;
   },
 
   setNodeFront: function(value, reason="unknown") {
     this.reason = reason;
-    if (value !== this._nodeFront) {
-      let rawValue = null;
-      if (value && value.isLocal_toBeDeprecated()) {
-        rawValue = value.rawNode();
-      }
-      this.emit("before-new-node", rawValue, reason);
-      this.emit("before-new-node-front", value, reason);
-      let previousNode = this._node;
-      let previousFront = this._nodeFront;
-      this._node = rawValue;
-      this._nodeFront = value;
-      this.emit("new-node", previousNode, this.reason);
-      this.emit("new-node-front", value, this.reason);
+
+    // We used to return here if the node had not changed but we now need to
+    // set the node even if it is already set otherwise it is not possible to
+    // e.g. highlight the same node twice.
+    let rawValue = null;
+    if (value && value.isLocal_toBeDeprecated()) {
+      rawValue = value.rawNode();
     }
+    this.emit("before-new-node", rawValue, reason);
+    this.emit("before-new-node-front", value, reason);
+    let previousNode = this._node;
+    let previousFront = this._nodeFront;
+    this._node = rawValue;
+    this._nodeFront = value;
+    this.emit("new-node", previousNode, this.reason);
+    this.emit("new-node-front", value, this.reason);
   },
 
   get documentFront() {
     return this._walker.document(this._nodeFront);
   },
 
   get nodeFront() {
     return this._nodeFront;
--- a/browser/devtools/framework/toolbox.js
+++ b/browser/devtools/framework/toolbox.js
@@ -68,16 +68,18 @@ function Toolbox(target, selectedTool, h
   this._telemetry = new Telemetry();
 
   this._toolRegistered = this._toolRegistered.bind(this);
   this._toolUnregistered = this._toolUnregistered.bind(this);
   this._refreshHostTitle = this._refreshHostTitle.bind(this);
   this._splitConsoleOnKeypress = this._splitConsoleOnKeypress.bind(this)
   this.destroy = this.destroy.bind(this);
   this.highlighterUtils = new ToolboxHighlighterUtils(this);
+  this._highlighterReady = this._highlighterReady.bind(this);
+  this._highlighterHidden = this._highlighterHidden.bind(this);
 
   this._target.on("close", this.destroy);
 
   if (!hostType) {
     hostType = Services.prefs.getCharPref(this._prefs.LAST_HOST);
   }
   if (!selectedTool) {
     selectedTool = Services.prefs.getCharPref(this._prefs.LAST_TOOL);
@@ -1092,55 +1094,70 @@ Toolbox.prototype = {
    * Returns a promise that resolves when the fronts are initialized
    */
   initInspector: function() {
     if (!this._initInspector) {
       this._initInspector = Task.spawn(function*() {
         this._inspector = InspectorFront(this._target.client, this._target.form);
         this._walker = yield this._inspector.getWalker();
         this._selection = new Selection(this._walker);
+
         if (this.highlighterUtils.isRemoteHighlightable) {
-          this._highlighter = yield this._inspector.getHighlighter();
+          let autohide = !gDevTools.testing;
+
+          this.walker.on("highlighter-ready", this._highlighterReady);
+          this.walker.on("highlighter-hide", this._highlighterHidden);
+
+          this._highlighter = yield this._inspector.getHighlighter(autohide);
         }
       }.bind(this));
     }
     return this._initInspector;
   },
 
   /**
    * Destroy the inspector/walker/selection fronts
    * Returns a promise that resolves when the fronts are destroyed
    */
   destroyInspector: function() {
+    if (this._destroying) {
+      return this._destroying;
+    }
+
     if (!this._inspector) {
       return promise.resolve();
     }
 
     let outstanding = () => {
       return Task.spawn(function*() {
         yield this.highlighterUtils.stopPicker();
         yield this._inspector.destroy();
         if (this._highlighter) {
           yield this._highlighter.destroy();
         }
         if (this._selection) {
           this._selection.destroy();
         }
 
+        this.walker.off("highlighter-ready", this._highlighterReady);
+        this.walker.off("highlighter-hide", this._highlighterHidden);
+
         this._inspector = null;
         this._highlighter = null;
         this._selection = null;
         this._walker = null;
       }.bind(this));
     };
 
     // Releasing the walker (if it has been created)
     // This can fail, but in any case, we want to continue destroying the
     // inspector/highlighter/selection
-    let walker = this._walker ? this._walker.release() : promise.resolve();
+    let walker = (this._destroying = this._walker) ?
+                 this._walker.release() :
+                 promise.resolve();
     return walker.then(outstanding, outstanding);
   },
 
   /**
    * Get the toolbox's notification box
    *
    * @return The notification box element.
    */
@@ -1219,17 +1236,25 @@ Toolbox.prototype = {
       return target.destroy();
     }).then(() => {
       this.emit("destroyed");
       // Free _host after the call to destroyed in order to let a chance
       // to destroyed listeners to still query toolbox attributes
       this._host = null;
       this._toolPanels.clear();
     }).then(null, console.error);
-  }
+  },
+
+  _highlighterReady: function() {
+    this.emit("highlighter-ready");
+  },
+
+  _highlighterHidden: function() {
+    this.emit("highlighter-hide");
+  },
 };
 
 /**
  * The ToolboxHighlighterUtils is what you should use for anything related to
  * node highlighting and picking.
  * It encapsulates the logic to connecting to the HighlighterActor.
  */
 function ToolboxHighlighterUtils(toolbox) {
@@ -1279,39 +1304,40 @@ ToolboxHighlighterUtils.prototype = {
   startPicker: function() {
     if (this._isPicking) {
       return promise.resolve();
     }
 
     let deferred = promise.defer();
 
     let done = () => {
+      this._isPicking = true;
       this.toolbox.emit("picker-started");
       this.toolbox.on("select", this.stopPicker);
       deferred.resolve();
     };
 
     promise.all([
       this.toolbox.initInspector(),
       this.toolbox.selectTool("inspector")
     ]).then(() => {
-      this._isPicking = true;
       this.toolbox._pickerButton.setAttribute("checked", "true");
 
       if (this.isRemoteHighlightable) {
-        this.toolbox.highlighter.pick().then(done);
-
         this.toolbox.walker.on("picker-node-hovered", this._onPickerNodeHovered);
         this.toolbox.walker.on("picker-node-picked", this._onPickerNodePicked);
+
+        this.toolbox.highlighter.pick().then(done);
       } else {
-        this.toolbox.walker.pick().then(node => {
-          this.toolbox.selection.setNodeFront(node, "picker-node-picked");
-          this.stopPicker();
+        return this.toolbox.walker.pick().then(node => {
+          this.toolbox.selection.setNodeFront(node, "picker-node-picked").then(() => {
+            this.stopPicker();
+            done();
+          });
         });
-        done();
       }
     });
 
     return deferred.promise;
   },
 
   /**
    * Stop the element picker
--- a/browser/devtools/markupview/markup-view.js
+++ b/browser/devtools/markupview/markup-view.js
@@ -160,29 +160,29 @@ MarkupView.prototype = {
       }
       this._containers.get(nodeFront).hovered = true;
 
       this._hoveredNode = nodeFront;
     }
   },
 
   _onMouseLeave: function() {
-    this._hideBoxModel();
+    this._hideBoxModel(true);
     if (this._hoveredNode) {
       this._containers.get(this._hoveredNode).hovered = false;
     }
     this._hoveredNode = null;
   },
 
   _showBoxModel: function(nodeFront, options={}) {
     this._inspector.toolbox.highlighterUtils.highlightNodeFront(nodeFront, options);
   },
 
-  _hideBoxModel: function() {
-    this._inspector.toolbox.highlighterUtils.unhighlight();
+  _hideBoxModel: function(forceHide) {
+    this._inspector.toolbox.highlighterUtils.unhighlight(forceHide);
   },
 
   _briefBoxModelTimer: null,
   _brieflyShowBoxModel: function(nodeFront, options) {
     let win = this._frame.contentWindow;
 
     if (this._briefBoxModelTimer) {
       win.clearTimeout(this._briefBoxModelTimer);
--- a/browser/themes/linux/browser.css
+++ b/browser/themes/linux/browser.css
@@ -3,16 +3,17 @@
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 %endif
 
 @import url("chrome://global/skin/");
 
 @namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul");
 @namespace html url("http://www.w3.org/1999/xhtml");
+@namespace svg url("http://www.w3.org/2000/svg");
 
 %include ../shared/browser.inc
 %include linuxShared.inc
 %filter substitution
 
 %define forwardTransitionLength 150ms
 %define conditionalForwardWithUrlbar window:not([chromehidden~="toolbar"]) #urlbar-container
 %define conditionalForwardWithUrlbarWidth 27
--- a/browser/themes/osx/browser.css
+++ b/browser/themes/osx/browser.css
@@ -9,16 +9,17 @@
 %define forwardTransitionLength 150ms
 %define conditionalForwardWithUrlbar window:not([chromehidden~="toolbar"]) #urlbar-container
 %define conditionalForwardWithUrlbarWidth 27
 %define spaceAboveTabbar 9px
 %define toolbarButtonPressed :hover:active:not([disabled="true"]):not([cui-areatype="menu-panel"])
 
 @namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul");
 @namespace html url("http://www.w3.org/1999/xhtml");
+@namespace svg url("http://www.w3.org/2000/svg");
 
 #urlbar:-moz-lwtheme:not([focused="true"]),
 .searchbar-textbox:-moz-lwtheme:not([focused="true"]) {
   opacity: .9;
 }
 
 #navigator-toolbox::after {
   -moz-box-ordinal-group: 101; /* tabs toolbar is 100 */
--- a/browser/themes/shared/devtools/highlighter.inc.css
+++ b/browser/themes/shared/devtools/highlighter.inc.css
@@ -1,20 +1,48 @@
 %if 0
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 %endif
 
-/* Highlighter */
+/* Box model highlighter */
+svg|g.box-model-container {
+  opacity: 0.4;
+}
+
+svg|polygon.box-model-content {
+  fill: #80d4ff;
+}
+
+svg|polygon.box-model-padding {
+  fill: #66cc52;
+}
+
+svg|polygon.box-model-border {
+  fill: #ffe431;
+}
 
-.highlighter-outline {
-  box-shadow: 0 0 0 1px black;
-  outline: 1px dashed white;
-  outline-offset: -1px;
+svg|polygon.box-model-margin {
+  fill: #d89b28;
+}
+
+svg|polygon.box-model-content,
+svg|polygon.box-model-padding,
+svg|polygon.box-model-border,
+svg|polygon.box-model-margin {
+  stroke: none;
+}
+
+svg|line.box-model-guide-top,
+svg|line.box-model-guide-right,
+svg|line.box-model-guide-bottom,
+svg|line.box-model-guide-left {
+  stroke: #08C;
+  stroke-dasharray: 5 3;
 }
 
 /* Highlighter - Node Infobar */
 
 .highlighter-nodeinfobar {
   color: hsl(216,33%,97%);
   border-radius: 3px;
   background: hsl(214,13%,24%) no-repeat padding-box;
--- a/browser/themes/windows/browser.css
+++ b/browser/themes/windows/browser.css
@@ -1,16 +1,17 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 @import url("chrome://global/skin/");
 
 @namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul");
 @namespace html url("http://www.w3.org/1999/xhtml");
+@namespace svg url("http://www.w3.org/2000/svg");
 
 %include ../shared/browser.inc
 %include windowsShared.inc
 %filter substitution
 %define toolbarShadowColor hsla(209,67%,12%,0.35)
 %define navbarTextboxCustomBorder border-color: rgba(0,0,0,.32);
 %define forwardTransitionLength 150ms
 %define conditionalForwardWithUrlbar window:not([chromehidden~="toolbar"]) #urlbar-container
--- a/toolkit/devtools/LayoutHelpers.jsm
+++ b/toolkit/devtools/LayoutHelpers.jsm
@@ -19,87 +19,83 @@ this.LayoutHelpers = LayoutHelpers = fun
   this._topDocShell = aTopLevelWindow.QueryInterface(Ci.nsIInterfaceRequestor)
                                      .getInterface(Ci.nsIWebNavigation)
                                      .QueryInterface(Ci.nsIDocShell);
 };
 
 LayoutHelpers.prototype = {
 
   /**
-   * Compute the position and the dimensions for the visible portion
-   * of a node, relativalely to the root window.
+   * Get box quads adjusted for iframes and zoom level.
    *
-   * @param nsIDOMNode aNode
-   *        a DOM element to be highlighted
+   * @param {DOMNode} node
+   *        The node for which we are to get the box model region quads
+   * @param  {String} region
+   *         The box model region to return:
+   *         "content", "padding", "border" or "margin"
    */
-  getDirtyRect: function LH_getDirectyRect(aNode) {
-    let frameWin = aNode.ownerDocument.defaultView;
-    let clientRect = aNode.getBoundingClientRect();
-
-    // Go up in the tree of frames to determine the correct rectangle.
-    // clientRect is read-only, we need to be able to change properties.
-    rect = {top: clientRect.top,
-            left: clientRect.left,
-            width: clientRect.width,
-            height: clientRect.height};
-
-    // We iterate through all the parent windows.
-    while (true) {
-
-      // Does the selection overflow on the right of its window?
-      let diffx = frameWin.innerWidth - (rect.left + rect.width);
-      if (diffx < 0) {
-        rect.width += diffx;
-      }
-
-      // Does the selection overflow on the bottom of its window?
-      let diffy = frameWin.innerHeight - (rect.top + rect.height);
-      if (diffy < 0) {
-        rect.height += diffy;
-      }
+  getAdjustedQuads: function(node, region) {
+    if (!node) {
+      return;
+    }
 
-      // Does the selection overflow on the left of its window?
-      if (rect.left < 0) {
-        rect.width += rect.left;
-        rect.left = 0;
-      }
-
-      // Does the selection overflow on the top of its window?
-      if (rect.top < 0) {
-        rect.height += rect.top;
-        rect.top = 0;
-      }
-
-      // Selection has been clipped to fit in its own window.
+    let [quads] = node.getBoxQuads({
+      box: region
+    });
 
-      // Are we in the top-level window?
-      if (this.isTopLevelWindow(frameWin)) {
-        break;
-      }
-
-      let frameElement = this.getFrameElement(frameWin);
-      if (!frameElement) {
-        break;
-      }
-
-      // We are in an iframe.
-      // We take into account the parent iframe position and its
-      // offset (borders and padding).
-      let frameRect = frameElement.getBoundingClientRect();
-
-      let [offsetTop, offsetLeft] =
-        this.getIframeContentOffset(frameElement);
-
-      rect.top += frameRect.top + offsetTop;
-      rect.left += frameRect.left + offsetLeft;
-
-      frameWin = this.getParentWindow(frameWin);
+    if (!quads) {
+      return;
     }
 
-    return rect;
+    let [xOffset, yOffset] = this._getNodeOffsets(node);
+    let scale = this.calculateScale(node);
+
+    return {
+      p1: {
+        w: quads.p1.w * scale,
+        x: quads.p1.x * scale + xOffset,
+        y: quads.p1.y * scale + yOffset,
+        z: quads.p1.z * scale
+      },
+      p2: {
+        w: quads.p2.w * scale,
+        x: quads.p2.x * scale + xOffset,
+        y: quads.p2.y * scale + yOffset,
+        z: quads.p2.z * scale
+      },
+      p3: {
+        w: quads.p3.w * scale,
+        x: quads.p3.x * scale + xOffset,
+        y: quads.p3.y * scale + yOffset,
+        z: quads.p3.z * scale
+      },
+      p4: {
+        w: quads.p4.w * scale,
+        x: quads.p4.x * scale + xOffset,
+        y: quads.p4.y * scale + yOffset,
+        z: quads.p4.z * scale
+      },
+      bounds: {
+        bottom: quads.bounds.bottom * scale + yOffset,
+        height: quads.bounds.height * scale,
+        left: quads.bounds.left * scale + xOffset,
+        right: quads.bounds.right * scale + xOffset,
+        top: quads.bounds.top * scale + yOffset,
+        width: quads.bounds.width * scale,
+        x: quads.bounds.x * scale + xOffset,
+        y: quads.bounds.y * scale + yOffset
+      }
+    };
+  },
+
+  calculateScale: function(node) {
+    let win = node.ownerDocument.defaultView;
+    let winUtils = win.QueryInterface(Ci.nsIInterfaceRequestor)
+                      .getInterface(Ci.nsIDOMWindowUtils);
+    return winUtils.fullZoom;
   },
 
   /**
    * Compute the absolute position and the dimensions of a node, relativalely
    * to the root window.
    *
    * @param nsIDOMNode aNode
    *        a DOM element to get the bounds for
@@ -107,17 +103,17 @@ LayoutHelpers.prototype = {
    *        the content window holding the node
    */
   getRect: function LH_getRect(aNode, aContentWindow) {
     let frameWin = aNode.ownerDocument.defaultView;
     let clientRect = aNode.getBoundingClientRect();
 
     // Go up in the tree of frames to determine the correct rectangle.
     // clientRect is read-only, we need to be able to change properties.
-    rect = {top: clientRect.top + aContentWindow.pageYOffset,
+    let rect = {top: clientRect.top + aContentWindow.pageYOffset,
             left: clientRect.left + aContentWindow.pageXOffset,
             width: clientRect.width,
             height: clientRect.height};
 
     // We iterate through all the parent windows.
     while (true) {
 
       // Are we in the top-level window?
@@ -174,36 +170,16 @@ LayoutHelpers.prototype = {
 
     let borderTop = parseInt(style.getPropertyValue("border-top-width"));
     let borderLeft = parseInt(style.getPropertyValue("border-left-width"));
 
     return [borderTop + paddingTop, borderLeft + paddingLeft];
   },
 
   /**
-   * Apply the page zoom factor.
-   */
-  getZoomedRect: function LH_getZoomedRect(aWin, aRect) {
-    // get page zoom factor, if any
-    let zoom =
-      aWin.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
-        .getInterface(Components.interfaces.nsIDOMWindowUtils)
-        .fullZoom;
-
-    // adjust rect for zoom scaling
-    let aRectScaled = {};
-    for (let prop in aRect) {
-      aRectScaled[prop] = aRect[prop] * zoom;
-    }
-
-    return aRectScaled;
-  },
-
-
-  /**
    * Find an element from the given coordinates. This method descends through
    * frames to find the element the user clicked inside frames.
    *
    * @param DOMDocument aDocument the document to look into.
    * @param integer aX
    * @param integer aY
    * @returns Node|null the element node found at the given coordinates.
    */
@@ -238,18 +214,17 @@ LayoutHelpers.prototype = {
   /**
    * Scroll the document so that the element "elem" appears in the viewport.
    *
    * @param Element elem the element that needs to appear in the viewport.
    * @param bool centered true if you want it centered, false if you want it to
    * appear on the top of the viewport. It is true by default, and that is
    * usually what you want.
    */
-  scrollIntoViewIfNeeded:
-  function LH_scrollIntoViewIfNeeded(elem, centered) {
+  scrollIntoViewIfNeeded: function(elem, centered) {
     // We want to default to centering the element in the page,
     // so as to keep the context of the element.
     centered = centered === undefined? true: !!centered;
 
     let win = elem.ownerDocument.defaultView;
     let clientRect = elem.getBoundingClientRect();
 
     // The following are always from the {top, bottom, left, right}
@@ -392,9 +367,143 @@ LayoutHelpers.prototype = {
     }
 
     let winUtils = win.
       QueryInterface(Components.interfaces.nsIInterfaceRequestor).
       getInterface(Components.interfaces.nsIDOMWindowUtils);
 
     return winUtils.containerElement;
   },
+
+  /**
+   * Get the x and y offsets for a node taking iframes into account.
+   *
+   * @param {DOMNode} node
+   *        The node for which we are to get the offset
+   */
+  _getNodeOffsets: function(node) {
+    let xOffset = 0;
+    let yOffset = 0;
+    let frameWin = node.ownerDocument.defaultView;
+    let scale = this.calculateScale(node);
+
+    while (true) {
+      // Are we in the top-level window?
+      if (this.isTopLevelWindow(frameWin)) {
+        break;
+      }
+
+      let frameElement = this.getFrameElement(frameWin);
+      if (!frameElement) {
+        break;
+      }
+
+      // We are in an iframe.
+      // We take into account the parent iframe position and its
+      // offset (borders and padding).
+      let frameRect = frameElement.getBoundingClientRect();
+
+      let [offsetTop, offsetLeft] =
+        this.getIframeContentOffset(frameElement);
+
+      xOffset += frameRect.left + offsetLeft;
+      yOffset += frameRect.top + offsetTop;
+
+      frameWin = this.getParentWindow(frameWin);
+    }
+
+    return [xOffset * scale, yOffset * scale];
+  },
+
+
+
+  /********************************************************************
+   * GetBoxQuads POLYFILL START TODO: Remove this when bug 917755 is fixed.
+   ********************************************************************/
+  _getBoxQuadsFromRect: function(rect, node) {
+    let scale = this.calculateScale(node);
+    let [xOffset, yOffset] = this._getNodeOffsets(node);
+
+    let out = {
+      p1: {
+        x: rect.left * scale + xOffset,
+        y: rect.top * scale + yOffset
+      },
+      p2: {
+        x: (rect.left + rect.width) * scale + xOffset,
+        y: rect.top * scale + yOffset
+      },
+      p3: {
+        x: (rect.left + rect.width) * scale + xOffset,
+        y: (rect.top + rect.height) * scale + yOffset
+      },
+      p4: {
+        x: rect.left * scale + xOffset,
+        y: (rect.top + rect.height) * scale + yOffset
+      }
+    };
+
+    out.bounds = {
+      bottom: out.p4.y,
+      height: out.p4.y - out.p1.y,
+      left: out.p1.x,
+      right: out.p2.x,
+      top: out.p1.y,
+      width: out.p2.x - out.p1.x,
+      x: out.p1.x,
+      y: out.p1.y
+    };
+
+    return out;
+  },
+
+  _parseNb: function(distance) {
+    let nb = parseFloat(distance, 10);
+    return isNaN(nb) ? 0 : nb;
+  },
+
+  getAdjustedQuadsPolyfill: function(node, region) {
+    // Get the border-box rect
+    // Note that this is relative to the node's viewport, so before we can use
+    // it, will need to go back up the frames like getRect
+    let borderRect = node.getBoundingClientRect();
+
+    // If the boxType is border, no need to go any further, we're done
+    if (region === "border") {
+      return this._getBoxQuadsFromRect(borderRect, node);
+    }
+
+    // Else, need to get margin/padding/border distances
+    let style = node.ownerDocument.defaultView.getComputedStyle(node);
+    let camel = s => s.substring(0, 1).toUpperCase() + s.substring(1);
+    let distances = {border:{}, padding:{}, margin: {}};
+
+    for (let side of ["top", "right", "bottom", "left"]) {
+      distances.border[side] = this._parseNb(style["border" + camel(side) + "Width"]);
+      distances.padding[side] = this._parseNb(style["padding" + camel(side)]);
+      distances.margin[side] = this._parseNb(style["margin" + camel(side)]);
+    }
+
+    // From the border-box rect, calculate the content-box, padding-box and
+    // margin-box rects
+    function offsetRect(rect, offsetType, dir=1) {
+      return {
+        top: rect.top + (dir * distances[offsetType].top),
+        left: rect.left + (dir * distances[offsetType].left),
+        width: rect.width - (dir * (distances[offsetType].left + distances[offsetType].right)),
+        height: rect.height - (dir * (distances[offsetType].top + distances[offsetType].bottom))
+      };
+    }
+
+    if (region === "margin") {
+      return this._getBoxQuadsFromRect(offsetRect(borderRect, "margin", -1), node);
+    } else if (region === "padding") {
+      return this._getBoxQuadsFromRect(offsetRect(borderRect, "border"), node);
+    } else if (region === "content") {
+      let paddingRect = offsetRect(borderRect, "border");
+      return this._getBoxQuadsFromRect(offsetRect(paddingRect, "padding"), node);
+    }
+  },
+
+  /********************************************************************
+   * GetBoxQuads POLYFILL END
+   ********************************************************************/
 };
--- a/toolkit/devtools/server/actors/highlighter.js
+++ b/toolkit/devtools/server/actors/highlighter.js
@@ -4,52 +4,66 @@
 
 "use strict";
 
 const {Cu, Cc, Ci} = require("chrome");
 const Services = require("Services");
 const protocol = require("devtools/server/protocol");
 const {Arg, Option, method} = protocol;
 const events = require("sdk/event/core");
+
+const EventEmitter = require("devtools/toolkit/event-emitter");
+const GUIDE_STROKE_WIDTH = 1;
+
 // Make sure the domnode type is known here
 require("devtools/server/actors/inspector");
 
 Cu.import("resource://gre/modules/devtools/LayoutHelpers.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
 // FIXME: add ":visited" and ":link" after bug 713106 is fixed
 const PSEUDO_CLASSES = [":hover", ":active", ":focus"];
 const HIGHLIGHTED_PSEUDO_CLASS = ":-moz-devtools-highlighted";
 let HELPER_SHEET = ".__fx-devtools-hide-shortcut__ { visibility: hidden !important } ";
 HELPER_SHEET += ":-moz-devtools-highlighted { outline: 2px dashed #F06!important; outline-offset: -2px!important } ";
 const XHTML_NS = "http://www.w3.org/1999/xhtml";
+const SVG_NS = "http://www.w3.org/2000/svg";
 const HIGHLIGHTER_PICKED_TIMER = 1000;
+const INFO_BAR_OFFSET = 5;
 
 /**
  * The HighlighterActor is the server-side entry points for any tool that wishes
  * to highlight elements in the content document.
  *
  * The highlighter can be retrieved via the inspector's getHighlighter method.
  */
 
 /**
  * The HighlighterActor class
  */
 let HighlighterActor = protocol.ActorClass({
   typeName: "highlighter",
 
-  initialize: function(inspector) {
+  initialize: function(inspector, autohide) {
     protocol.Actor.prototype.initialize.call(this, null);
 
+    this._autohide = autohide;
     this._inspector = inspector;
     this._walker = this._inspector.walker;
     this._tabActor = this._inspector.tabActor;
 
+    this._highlighterReady = this._highlighterReady.bind(this);
+    this._highlighterHidden = this._highlighterHidden.bind(this);
+
     if (this._supportsBoxModelHighlighter()) {
-      this._boxModelHighlighter = new BoxModelHighlighter(this._tabActor);
+      this._boxModelHighlighter =
+        new BoxModelHighlighter(this._tabActor, this._inspector);
+
+        this._boxModelHighlighter.on("ready", this._highlighterReady);
+        this._boxModelHighlighter.on("hide", this._highlighterHidden);
     } else {
       this._boxModelHighlighter = new SimpleOutlineHighlighter(this._tabActor);
     }
   },
 
   get conn() this._inspector && this._inspector.conn,
 
   /**
@@ -58,45 +72,48 @@ let HighlighterActor = protocol.ActorCla
    */
   _supportsBoxModelHighlighter: function() {
     return this._tabActor.browser && !!this._tabActor.browser.parentNode;
   },
 
   destroy: function() {
     protocol.Actor.prototype.destroy.call(this);
     if (this._boxModelHighlighter) {
+      this._boxModelHighlighter.off("ready", this._highlighterReady);
+      this._boxModelHighlighter.off("hide", this._highlighterHidden);
       this._boxModelHighlighter.destroy();
       this._boxModelHighlighter = null;
     }
+    this._autohide = null;
     this._inspector = null;
     this._walker = null;
     this._tabActor = null;
   },
 
   /**
    * Display the box model highlighting on a given NodeActor.
    * There is only one instance of the box model highlighter, so calling this
    * method several times won't display several highlighters, it will just move
    * the highlighter instance to these nodes.
    *
    * @param NodeActor The node to be highlighted
    * @param Options See the request part for existing options. Note that not
-   * all options may be supported by all types of highlighters. The simple
-   * outline highlighter for instance does not scrollIntoView
+   * all options may be supported by all types of highlighters.
    */
   showBoxModel: method(function(node, options={}) {
     if (node && this._isNodeValidForHighlighting(node.rawNode)) {
       this._boxModelHighlighter.show(node.rawNode, options);
     } else {
       this._boxModelHighlighter.hide();
     }
   }, {
     request: {
       node: Arg(0, "domnode"),
-      scrollIntoView: Option(1)
+      scrollIntoView: Option(1),
+      region: Option(1)
     }
   }),
 
   _isNodeValidForHighlighting: function(node) {
     // Is it null or dead?
     let isNotDead = node && !Cu.isDeadWrapper(node);
 
     // Is it connected to the document?
@@ -130,34 +147,37 @@ let HighlighterActor = protocol.ActorCla
    * mousemove, and click listeners on the content document to fire
    * events and let connected clients know when nodes are hovered over or
    * clicked.
    *
    * Once a node is picked, events will cease, and listeners will be removed.
    */
   _isPicking: false,
   _hoveredNode: null,
+
   pick: method(function() {
     if (this._isPicking) {
       return null;
     }
     this._isPicking = true;
 
     this._preventContentEvent = event => {
       event.stopPropagation();
       event.preventDefault();
     };
 
     this._onPick = event => {
       this._preventContentEvent(event);
       this._stopPickerListeners();
       this._isPicking = false;
-      this._tabActor.window.setTimeout(() => {
-        this._boxModelHighlighter.hide();
-      }, HIGHLIGHTER_PICKED_TIMER);
+      if (this._autohide) {
+        this._tabActor.window.setTimeout(() => {
+          this._boxModelHighlighter.hide();
+        }, HIGHLIGHTER_PICKED_TIMER);
+      }
       events.emit(this._walker, "picker-node-picked", this._findAndAttachElement(event));
     };
 
     this._onHovered = event => {
       this._preventContentEvent(event);
       let res = this._findAndAttachElement(event);
       if (this._hoveredNode !== res.node) {
         this._boxModelHighlighter.show(res.node.rawNode);
@@ -212,16 +232,24 @@ let HighlighterActor = protocol.ActorCla
     let target = this._getPickerListenerTarget();
     target.removeEventListener("mousemove", this._onHovered, true);
     target.removeEventListener("click", this._onPick, true);
     target.removeEventListener("mousedown", this._preventContentEvent, true);
     target.removeEventListener("mouseup", this._preventContentEvent, true);
     target.removeEventListener("dblclick", this._preventContentEvent, true);
   },
 
+  _highlighterReady: function() {
+    events.emit(this._inspector.walker, "highlighter-ready");
+  },
+
+  _highlighterHidden: function() {
+    events.emit(this._inspector.walker, "highlighter-hide");
+  },
+
   cancelPick: method(function() {
     if (this._isPicking) {
       this._boxModelHighlighter.hide();
       this._stopPickerListeners();
       this._isPicking = false;
       this._hoveredNode = null;
     }
   })
@@ -242,69 +270,108 @@ let HighlighterFront = protocol.FrontCla
  * Usage example:
  *
  * let h = new BoxModelHighlighter(browser);
  * h.show(node);
  * h.hide();
  * h.destroy();
  *
  * Structure:
- *  <stack class="highlighter-container">
- *    <box class="highlighter-outline-container">
- *      <box class="highlighter-outline" />
- *    </box>
- *    <box class="highlighter-nodeinfobar-container">
- *      <box class="highlighter-nodeinfobar-positioner" position="top/bottom">
- *        <box class="highlighter-nodeinfobar-arrow highlighter-nodeinfobar-arrow-top"/>
- *        <hbox class="highlighter-nodeinfobar">
- *          <hbox class="highlighter-nodeinfobar-text">tagname#id.class1.class2</hbox>
- *        </hbox>
- *        <box class="highlighter-nodeinfobar-arrow highlighter-nodeinfobar-arrow-bottom"/>
- *      </box>
- *    </box>
- *  </stack>
+ * <stack class="highlighter-container">
+ *   <svg class="box-model-root" hidden="true">
+ *     <g class="box-model-container">
+ *       <polygon class="box-model-margin" points="317,122 747,36 747,181 317,267" />
+ *       <polygon class="box-model-border" points="317,128 747,42 747,161 317,247" />
+ *       <polygon class="box-model-padding" points="323,127 747,42 747,161 323,246" />
+ *       <polygon class="box-model-content" points="335,137 735,57 735,152 335,232" />
+ *     </g>
+ *     <line class="box-model-guide-top" x1="0" y1="592" x2="99999" y2="592" />
+ *     <line class="box-model-guide-right" x1="735" y1="0" x2="735" y2="99999" />
+ *     <line class="box-model-guide-bottom" x1="0" y1="612" x2="99999" y2="612" />
+ *     <line class="box-model-guide-left" x1="334" y1="0" x2="334" y2="99999" />
+ *   </svg>
+ *   <box class="highlighter-nodeinfobar-container">
+ *     <box class="highlighter-nodeinfobar-positioner" position="top" />
+ *       <box class="highlighter-nodeinfobar-arrow highlighter-nodeinfobar-arrow-top" />
+ *       <hbox class="highlighter-nodeinfobar">
+ *         <hbox class="highlighter-nodeinfobar-text" align="center" flex="1">
+ *           <span class="highlighter-nodeinfobar-tagname">Node name</span>
+ *           <span class="highlighter-nodeinfobar-id">Node id</span>
+ *           <span class="highlighter-nodeinfobar-classes">.someClass</span>
+ *           <span class="highlighter-nodeinfobar-pseudo-classes">:hover</span>
+ *         </hbox>
+ *       </hbox>
+ *       <box class="highlighter-nodeinfobar-arrow highlighter-nodeinfobar-arrow-bottom"/>
+ *     </box>
+ *   </box>
+ * </stack>
  */
-function BoxModelHighlighter(tabActor) {
+function BoxModelHighlighter(tabActor, inspector) {
   this.browser = tabActor.browser;
   this.win = tabActor.window;
   this.chromeDoc = this.browser.ownerDocument;
   this.chromeWin = this.chromeDoc.defaultView;
+  this._inspector = inspector;
 
   this.layoutHelpers = new LayoutHelpers(this.win);
   this.chromeLayoutHelper = new LayoutHelpers(this.chromeWin);
 
   this.transitionDisabler = null;
   this.pageEventsMuter = null;
   this._update = this._update.bind(this);
+  this.handleEvent = this.handleEvent.bind(this);
   this.currentNode = null;
 
+  EventEmitter.decorate(this);
   this._initMarkup();
 }
 
 BoxModelHighlighter.prototype = {
+  get zoom() {
+    return this.win.QueryInterface(Ci.nsIInterfaceRequestor)
+               .getInterface(Ci.nsIDOMWindowUtils).fullZoom;
+  },
+
   _initMarkup: function() {
     let stack = this.browser.parentNode;
 
-    this.highlighterContainer = this.chromeDoc.createElement("stack");
-    this.highlighterContainer.className = "highlighter-container";
+    this._highlighterContainer = this.chromeDoc.createElement("stack");
+    this._highlighterContainer.className = "highlighter-container";
+
+    this._svgRoot = this._createSVGNode("root", "svg", this._highlighterContainer);
+
+    this._boxModelContainer = this._createSVGNode("container", "g", this._svgRoot);
+
+    this._boxModelNodes = {
+      margin: this._createSVGNode("margin", "polygon", this._boxModelContainer),
+      border: this._createSVGNode("border", "polygon", this._boxModelContainer),
+      padding: this._createSVGNode("padding", "polygon", this._boxModelContainer),
+      content: this._createSVGNode("content", "polygon", this._boxModelContainer)
+    };
 
-    this.outline = this.chromeDoc.createElement("box");
-    this.outline.className = "highlighter-outline";
+    this._guideNodes = {
+      top: this._createSVGNode("guide-top", "line", this._svgRoot),
+      right: this._createSVGNode("guide-right", "line", this._svgRoot),
+      bottom: this._createSVGNode("guide-bottom", "line", this._svgRoot),
+      left: this._createSVGNode("guide-left", "line", this._svgRoot)
+    };
 
-    let outlineContainer = this.chromeDoc.createElement("box");
-    outlineContainer.appendChild(this.outline);
-    outlineContainer.className = "highlighter-outline-container";
-    this.highlighterContainer.appendChild(outlineContainer);
+    this._guideNodes.top.setAttribute("stroke-width", GUIDE_STROKE_WIDTH);
+    this._guideNodes.right.setAttribute("stroke-width", GUIDE_STROKE_WIDTH);
+    this._guideNodes.bottom.setAttribute("stroke-width", GUIDE_STROKE_WIDTH);
+    this._guideNodes.left.setAttribute("stroke-width", GUIDE_STROKE_WIDTH);
+
+    this._highlighterContainer.appendChild(this._svgRoot);
 
     let infobarContainer = this.chromeDoc.createElement("box");
     infobarContainer.className = "highlighter-nodeinfobar-container";
-    this.highlighterContainer.appendChild(infobarContainer);
+    this._highlighterContainer.appendChild(infobarContainer);
 
     // Insert the highlighter right after the browser
-    stack.insertBefore(this.highlighterContainer, stack.childNodes[1]);
+    stack.insertBefore(this._highlighterContainer, stack.childNodes[1]);
 
     // Building the infobar
     let infobarPositioner = this.chromeDoc.createElement("box");
     infobarPositioner.className = "highlighter-nodeinfobar-positioner";
     infobarPositioner.setAttribute("position", "top");
     infobarPositioner.setAttribute("disabled", "true");
 
     let nodeInfobar = this.chromeDoc.createElement("hbox");
@@ -357,66 +424,74 @@ BoxModelHighlighter.prototype = {
       idLabel: idLabel,
       classesBox: classesBox,
       pseudoClassesBox: pseudoClassesBox,
       positioner: infobarPositioner,
       barHeight: barHeight,
     };
   },
 
+  _createSVGNode: function(classPostfix, nodeType, parent) {
+    let node = this.chromeDoc.createElementNS(SVG_NS, nodeType);
+    node.setAttribute("class", "box-model-" + classPostfix);
+
+    parent.appendChild(node);
+
+    return node;
+  },
+
   /**
    * Destroy the nodes. Remove listeners.
    */
   destroy: function() {
     this.hide();
 
     this.chromeWin.clearTimeout(this.transitionDisabler);
     this.chromeWin.clearTimeout(this.pageEventsMuter);
 
-    this._contentRect = null;
-    this._highlightRect = null;
-    this.outline = null;
     this.nodeInfo = null;
 
-    this.highlighterContainer.remove();
-    this.highlighterContainer = null;
+    this._highlighterContainer.remove();
+    this._highlighterContainer = null;
 
-    this.win = null
+    this.rect = null;
+    this.win = null;
     this.browser = null;
     this.chromeDoc = null;
     this.chromeWin = null;
     this.currentNode = null;
   },
 
   /**
    * Show the highlighter on a given node
    *
    * @param {DOMNode} node
+   * @param {Object} options
+   *        Object used for passing options
    */
   show: function(node, options={}) {
-    if (!this.currentNode || node !== this.currentNode) {
-      this.currentNode = node;
+    this.currentNode = node;
 
-      this._showInfobar();
-      this._computeZoomFactor();
-      this._detachPageListeners();
-      this._attachPageListeners();
-      this._update();
-      this._trackMutations();
+    this._showInfobar();
+    this._detachPageListeners();
+    this._attachPageListeners();
+    this._update();
+    this._trackMutations();
 
-      if (options.scrollIntoView) {
-        this.chromeLayoutHelper.scrollIntoViewIfNeeded(node);
-      }
+    if (options.scrollIntoView) {
+      this.chromeLayoutHelper.scrollIntoViewIfNeeded(node);
     }
   },
 
   _trackMutations: function() {
     if (this.currentNode) {
       let win = this.currentNode.ownerDocument.defaultView;
-      this.currentNodeObserver = new win.MutationObserver(this._update);
+      this.currentNodeObserver = new win.MutationObserver(() => {
+        this._update();
+      });
       this.currentNodeObserver.observe(this.currentNode, {attributes: true});
     }
   },
 
   _untrackMutations: function() {
     if (this.currentNode) {
       if (this.currentNodeObserver) {
         // The following may fail with a "can't access dead object" exception
@@ -428,136 +503,208 @@ BoxModelHighlighter.prototype = {
       }
     }
   },
 
   /**
    * Update the highlighter on the current highlighted node (the one that was
    * passed as an argument to show(node)).
    * Should be called whenever node size or attributes change
-   * @param {Boolean} brieflyDisableTransitions
-   *        In case _update is called during scrolling or repaint, set this
-   *        to true to avoid transitions
+   * @param {Object} options
+   *        Object used for passing options. Valid options are:
+   *          - box: "content", "padding", "border" or "margin." This specifies
+   *            the box that the guides should outline. Default is content.
    */
-  _update: function(brieflyDisableTransitions) {
+  _update: function(options={}) {
     if (this.currentNode) {
-      let rect = this.layoutHelpers.getDirtyRect(this.currentNode);
-
-      if (this._highlightRectangle(rect, brieflyDisableTransitions)) {
-        this._moveInfobar();
-        this._updateInfobar();
+      if (this._highlightBoxModel(options)) {
+        this._showInfobar();
       } else {
         // Nothing to highlight (0px rectangle like a <script> tag for instance)
         this.hide();
       }
+      this.emit("ready");
     }
   },
 
   /**
    * Hide the highlighter, the outline and the infobar.
    */
   hide: function() {
     if (this.currentNode) {
       this._untrackMutations();
       this.currentNode = null;
-      this._hideOutline();
+      this._hideBoxModel();
       this._hideInfobar();
       this._detachPageListeners();
     }
+    this.emit("hide");
   },
 
   /**
    * Hide the infobar
    */
   _hideInfobar: function() {
-    this.nodeInfo.positioner.setAttribute("force-transitions", "true");
     this.nodeInfo.positioner.setAttribute("hidden", "true");
   },
 
   /**
    * Show the infobar
    */
   _showInfobar: function() {
     this.nodeInfo.positioner.removeAttribute("hidden");
-    this._moveInfobar();
-    this.nodeInfo.positioner.removeAttribute("force-transitions");
+    this._updateInfobar();
   },
 
   /**
-   * Hide the outline
+   * Hide the box model
    */
-  _hideOutline: function() {
-    this.outline.setAttribute("hidden", "true");
+  _hideBoxModel: function() {
+    this._svgRoot.setAttribute("hidden", "true");
   },
 
   /**
-   * Show the outline
+   * Show the box model
    */
-  _showOutline: function() {
-    this.outline.removeAttribute("hidden");
+  _showBoxModel: function() {
+    this._svgRoot.removeAttribute("hidden");
   },
 
   /**
-   * Highlight a rectangular region.
+   * Highlight the box model.
    *
-   * @param {object} aRect
-   *        The rectangle region to highlight.
-   * @param {boolean} brieflyDisableTransitions
-   *        Set to true to avoid transitions during the highlighting
+   * @param {Object} options
+   *        Object used for passing options. Valid options are:
+   *          - region: "content", "padding", "border" or "margin." This specifies
+   *            the region that the guides should outline. Default is content.
    * @return {boolean}
    *         True if the rectangle was highlighted, false otherwise.
    */
-  _highlightRectangle: function(aRect, brieflyDisableTransitions) {
-    if (!aRect) {
-      return false;
-    }
-
-    let oldRect = this._contentRect;
-
-    if (oldRect && aRect.top == oldRect.top && aRect.left == oldRect.left &&
-        aRect.width == oldRect.width && aRect.height == oldRect.height) {
-      this._showOutline();
-      return true; // same rectangle
-    }
-
-    let aRectScaled = this.layoutHelpers.getZoomedRect(this.win, aRect);
+  _highlightBoxModel: function(options) {
     let isShown = false;
 
-    if (aRectScaled.left >= 0 && aRectScaled.top >= 0 &&
-        aRectScaled.width > 0 && aRectScaled.height > 0) {
+    options.region = options.region || "content";
+
+    // TODO: Remove this polyfill
+    this.rect =
+      this.layoutHelpers.getAdjustedQuadsPolyfill(this.currentNode, "margin");
+
+    if (!this.rect) {
+      return;
+    }
 
-      // The bottom div and the right div are flexibles (flex=1).
-      // We don't need to resize them.
-      let top = "top:" + aRectScaled.top + "px;";
-      let left = "left:" + aRectScaled.left + "px;";
-      let width = "width:" + aRectScaled.width + "px;";
-      let height = "height:" + aRectScaled.height + "px;";
+    if (this.rect.bounds.width > 0 && this.rect.bounds.height > 0) {
+      for (let boxType in this._boxModelNodes) {
+        // TODO: Remove this polyfill
+        let {p1, p2, p3, p4} = boxType === "margin" ? this.rect :
+          this.layoutHelpers.getAdjustedQuadsPolyfill(this.currentNode, boxType);
 
-      if (brieflyDisableTransitions) {
-        this._brieflyDisableTransitions();
+        let boxNode = this._boxModelNodes[boxType];
+        boxNode.setAttribute("points",
+                             p1.x + "," + p1.y + " " +
+                             p2.x + "," + p2.y + " " +
+                             p3.x + "," + p3.y + " " +
+                             p4.x + "," + p4.y);
+
+        if (boxType === options.region) {
+          this._showGuides(p1, p2, p3, p4);
+        }
       }
 
-      this.outline.setAttribute("style", top + left + width + height);
-
       isShown = true;
-      this._showOutline();
+      this._showBoxModel();
     } else {
       // Only return false if the element really is invisible.
       // A height of 0 and a non-0 width corresponds to a visible element that
       // is below the fold for instance
-      if (aRectScaled.width > 0 || aRectScaled.height > 0) {
+      if (this.rect.width > 0 || this.rect.height > 0) {
         isShown = true;
-        this._hideOutline();
+        this._hideBoxModel();
+      }
+    }
+    return isShown;
+  },
+
+  /**
+   * We only want to show guides for horizontal and vertical edges as this helps
+   * to line them up. This method finds these edges and displays a guide there.
+   *
+   * @param  {DOMPoint} p1
+   *                    Point 1
+   * @param  {DOMPoint} p2
+   *                    Point 2
+   * @param  {DOMPoint} p3 [description]
+   *                    Point 3
+   * @param  {DOMPoint} p4 [description]
+   *                    Point 4
+   */
+  _showGuides: function(p1, p2, p3, p4) {
+    let allX = [p1.x, p2.x, p3.x, p4.x].sort();
+    let allY = [p1.y, p2.y, p3.y, p4.y].sort();
+    let toShowX = [];
+    let toShowY = [];
+
+    for (let arr of [allX, allY]) {
+      for (let i = 0; i < arr.length; i++) {
+        let val = arr[i];
+
+        if (i !== arr.lastIndexOf(val)) {
+          if (arr === allX) {
+            toShowX.push(val);
+          } else {
+            toShowY.push(val);
+          }
+          arr.splice(arr.lastIndexOf(val), 1);
+        }
       }
     }
 
-    this._contentRect = aRect; // save orig (non-scaled) rect
-    this._highlightRect = aRectScaled; // and save the scaled rect.
+    // Move guide into place or hide it if no valid co-ordinate was found.
+    this._updateGuide(this._guideNodes.top, toShowY[0]);
+    this._updateGuide(this._guideNodes.right, toShowX[1]);
+    this._updateGuide(this._guideNodes.bottom, toShowY[1]);
+    this._updateGuide(this._guideNodes.left, toShowX[0]);
+  },
+
+  /**
+   * Move a guide to the appropriate position and display it. If no point is
+   * passed then the guide is hidden.
+   *
+   * @param  {SVGLine} guide
+   *         The guide to update
+   * @param  {Integer} point
+   *         x or y co-ordinate. If this is undefined we hide the guide.
+   */
+  _updateGuide: function(guide, point=-1) {
+    if (point > 0) {
+      let offset = GUIDE_STROKE_WIDTH / 2;
 
-    return isShown;
+      if (guide === this._guideNodes.top || guide === this._guideNodes.left) {
+        point -= offset;
+      } else {
+        point += offset;
+      }
+
+      if (guide === this._guideNodes.top || guide === this._guideNodes.bottom) {
+        guide.setAttribute("x1", 0);
+        guide.setAttribute("y1", point);
+        guide.setAttribute("x2", "100%");
+        guide.setAttribute("y2", point);
+      } else {
+        guide.setAttribute("x1", point);
+        guide.setAttribute("y1", 0);
+        guide.setAttribute("x2", point);
+        guide.setAttribute("y2", "100%");
+      }
+      guide.removeAttribute("hidden");
+      return true;
+    } else {
+      guide.setAttribute("hidden", "true");
+      return false;
+    }
   },
 
   /**
    * Update node information (tagName#id.class)
    */
   _updateInfobar: function() {
     if (this.currentNode) {
       // Tag name
@@ -574,63 +721,52 @@ BoxModelHighlighter.prototype = {
 
       // Pseudo-classes
       let pseudos = PSEUDO_CLASSES.filter(pseudo => {
         return DOMUtils.hasPseudoClassLock(this.currentNode, pseudo);
       }, this);
 
       let pseudoBox = this.nodeInfo.pseudoClassesBox;
       pseudoBox.textContent = pseudos.join("");
+
+      this._moveInfobar();
     }
   },
 
   /**
    * Move the Infobar to the right place in the highlighter.
    */
   _moveInfobar: function() {
-    if (this._highlightRect) {
+    if (this.rect) {
+      let bounds = this.rect.bounds;
       let winHeight = this.win.innerHeight * this.zoom;
       let winWidth = this.win.innerWidth * this.zoom;
 
-      let rect = {top: this._highlightRect.top,
-                  left: this._highlightRect.left,
-                  width: this._highlightRect.width,
-                  height: this._highlightRect.height};
-
-      rect.top = Math.max(rect.top, 0);
-      rect.left = Math.max(rect.left, 0);
-      rect.width = Math.max(rect.width, 0);
-      rect.height = Math.max(rect.height, 0);
-
-      rect.top = Math.min(rect.top, winHeight);
-      rect.left = Math.min(rect.left, winWidth);
-
       this.nodeInfo.positioner.removeAttribute("disabled");
       // Can the bar be above the node?
-      if (rect.top < this.nodeInfo.barHeight) {
+      if (bounds.top < this.nodeInfo.barHeight) {
         // No. Can we move the toolbar under the node?
-        if (rect.top + rect.height +
-            this.nodeInfo.barHeight > winHeight) {
+        if (bounds.bottom + this.nodeInfo.barHeight > winHeight) {
           // No. Let's move it inside.
-          this.nodeInfo.positioner.style.top = rect.top + "px";
+          this.nodeInfo.positioner.style.top = bounds.top + "px";
           this.nodeInfo.positioner.setAttribute("position", "overlap");
         } else {
           // Yes. Let's move it under the node.
-          this.nodeInfo.positioner.style.top = rect.top + rect.height + "px";
+          this.nodeInfo.positioner.style.top = bounds.bottom - INFO_BAR_OFFSET + "px";
           this.nodeInfo.positioner.setAttribute("position", "bottom");
         }
       } else {
         // Yes. Let's move it on top of the node.
         this.nodeInfo.positioner.style.top =
-          rect.top - this.nodeInfo.barHeight + "px";
+          bounds.top + INFO_BAR_OFFSET - this.nodeInfo.barHeight + "px";
         this.nodeInfo.positioner.setAttribute("position", "top");
       }
 
       let barWidth = this.nodeInfo.positioner.getBoundingClientRect().width;
-      let left = rect.left + rect.width / 2 - barWidth / 2;
+      let left = bounds.right - bounds.width / 2 - barWidth / 2;
 
       // Make sure the whole infobar is visible
       if (left < 0) {
         left = 0;
         this.nodeInfo.positioner.setAttribute("hide-arrow", "true");
       } else {
         if (left + barWidth > winWidth) {
           left = winWidth - barWidth;
@@ -643,74 +779,51 @@ BoxModelHighlighter.prototype = {
     } else {
       this.nodeInfo.positioner.style.left = "0";
       this.nodeInfo.positioner.style.top = "0";
       this.nodeInfo.positioner.setAttribute("position", "top");
       this.nodeInfo.positioner.setAttribute("hide-arrow", "true");
     }
   },
 
-  /**
-   * Store page zoom factor.
-   */
-  _computeZoomFactor: function() {
-    this.zoom =
-      this.win.QueryInterface(Ci.nsIInterfaceRequestor)
-      .getInterface(Ci.nsIDOMWindowUtils)
-      .fullZoom;
-  },
+  _attachPageListeners: function() {
+    if (this.currentNode) {
+      let win = this.currentNode.ownerGlobal;
 
-  _attachPageListeners: function() {
-    this.browser.addEventListener("resize", this, true);
-    this.browser.addEventListener("scroll", this, true);
-    this.browser.addEventListener("MozAfterPaint", this, true);
+      win.addEventListener("scroll", this, false);
+      win.addEventListener("resize", this, false);
+      win.addEventListener("MozAfterPaint", this, false);
+    }
   },
 
   _detachPageListeners: function() {
-    this.browser.removeEventListener("resize", this, true);
-    this.browser.removeEventListener("scroll", this, true);
-    this.browser.removeEventListener("MozAfterPaint", this, true);
+    if (this.currentNode) {
+      let win = this.currentNode.ownerGlobal;
+
+      win.removeEventListener("scroll", this, false);
+      win.removeEventListener("resize", this, false);
+      win.removeEventListener("MozAfterPaint", this, false);
+    }
   },
 
   /**
    * Generic event handler.
    *
    * @param nsIDOMEvent aEvent
    *        The DOM event object.
    */
   handleEvent: function(event) {
     switch (event.type) {
       case "resize":
-        this._computeZoomFactor();
-        break;
       case "MozAfterPaint":
       case "scroll":
-        this._update(true);
+        this._update();
         break;
     }
   },
-
-  /**
-   * Disable the CSS transitions for a short time to avoid laggy animations
-   * during scrolling or resizing.
-   */
-  _brieflyDisableTransitions: function() {
-    if (this.transitionDisabler) {
-      this.chromeWin.clearTimeout(this.transitionDisabler);
-    } else {
-      this.outline.setAttribute("disable-transitions", "true");
-      this.nodeInfo.positioner.setAttribute("disable-transitions", "true");
-    }
-    this.transitionDisabler =
-      this.chromeWin.setTimeout(() => {
-        this.outline.removeAttribute("disable-transitions");
-        this.nodeInfo.positioner.removeAttribute("disable-transitions");
-        this.transitionDisabler = null;
-      }, 500);
-  }
 };
 
 /**
  * The SimpleOutlineHighlighter is a class that has the same API than the
  * BoxModelHighlighter, but adds a pseudo-class on the target element itself
  * to draw a simple outline.
  * It is used by the HighlighterActor too, but in case the more complex
  * BoxModelHighlighter can't be attached (which is the case for FirefoxOS and
--- a/toolkit/devtools/server/actors/inspector.js
+++ b/toolkit/devtools/server/actors/inspector.js
@@ -843,16 +843,22 @@ var WalkerActor = protocol.ActorClass({
     },
     "picker-node-picked" : {
       type: "pickerNodePicked",
       node: Arg(0, "disconnectedNode")
     },
     "picker-node-hovered" : {
       type: "pickerNodeHovered",
       node: Arg(0, "disconnectedNode")
+    },
+    "highlighter-ready" : {
+      type: "highlighter-ready"
+    },
+    "highlighter-hide" : {
+      type: "highlighter-hide"
     }
   },
 
   /**
    * Create the WalkerActor
    * @param DebuggerServerConnection conn
    *    The server connection.
    */
@@ -2540,27 +2546,27 @@ var InspectorActor = protocol.ActorClass
     return this._pageStylePromise;
   }, {
     request: {},
     response: {
       pageStyle: RetVal("pagestyle")
     }
   }),
 
-  getHighlighter: method(function () {
+  getHighlighter: method(function (autohide) {
     if (this._highlighterPromise) {
       return this._highlighterPromise;
     }
 
     this._highlighterPromise = this.getWalker().then(walker => {
-      return HighlighterActor(this);
+      return HighlighterActor(this, autohide);
     });
     return this._highlighterPromise;
   }, {
-    request: {},
+    request: { autohide: Arg(0, "boolean") },
     response: {
       highligter: RetVal("highlighter")
     }
   }),
 
   /**
    * Get the node's image data if any (for canvas and img nodes).
    * Returns an imageData object with the actual data being a LongStringActor