Bug 663778 - Box Model Highlighter r=jwalker
authorMichael Ratcliffe <mratcliffe@mozilla.com>
Thu, 13 Mar 2014 21:36:48 +0000
changeset 173479 8dea188bf8a0af2d7f46766e4ab3986241857058
parent 173478 9b860b0a728209dd1927e502a4114abc35c86325
child 173480 fad8f0885e5b12fe4a66d453633365e84577377b
push id5677
push usermratcliffe@mozilla.com
push dateThu, 13 Mar 2014 22:29:17 +0000
treeherderfx-team@436a9dfc6cb4 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjwalker
bugs663778
milestone30.0a1
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