Bug 1333561 - Part 3: Implements the box model panel in the layout view. r=jdescottes
authorGabriel Luong <gabriel.luong@gmail.com>
Sat, 28 Jan 2017 17:11:04 -0500
changeset 331584 b318e4b93a7b29e42cbfcdb7d2b67addaa0c25a5
parent 331583 bc1ade7ff7f0d65778c1d275b0d7f44efc3e9d06
child 331585 934733d09f10d70fd60bd6e0720ff4421e21d73f
push id31275
push userphilringnalda@gmail.com
push dateSun, 29 Jan 2017 17:02:37 +0000
treeherdermozilla-central@77af15f468e8 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjdescottes
bugs1333561
milestone54.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 1333561 - Part 3: Implements the box model panel in the layout view. r=jdescottes
devtools/client/inspector/inspector.xhtml
devtools/client/inspector/layout/actions/box-model.js
devtools/client/inspector/layout/actions/index.js
devtools/client/inspector/layout/actions/moz.build
devtools/client/inspector/layout/components/App.js
devtools/client/inspector/layout/components/BoxModel.js
devtools/client/inspector/layout/components/BoxModelEditable.js
devtools/client/inspector/layout/components/BoxModelInfo.js
devtools/client/inspector/layout/components/BoxModelMain.js
devtools/client/inspector/layout/components/moz.build
devtools/client/inspector/layout/layout.js
devtools/client/inspector/layout/reducers/box-model.js
devtools/client/inspector/layout/reducers/index.js
devtools/client/inspector/layout/reducers/moz.build
devtools/client/inspector/layout/types.js
devtools/client/themes/boxmodel.css
--- a/devtools/client/inspector/inspector.xhtml
+++ b/devtools/client/inspector/inspector.xhtml
@@ -8,16 +8,17 @@
 <head>
   <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
 
   <link rel="stylesheet" href="chrome://devtools/skin/widgets.css"/>
   <link rel="stylesheet" href="chrome://devtools/skin/inspector.css"/>
   <link rel="stylesheet" href="chrome://devtools/skin/rules.css"/>
   <link rel="stylesheet" href="chrome://devtools/skin/computed.css"/>
   <link rel="stylesheet" href="chrome://devtools/skin/fonts.css"/>
+  <link rel="stylesheet" href="chrome://devtools/skin/boxmodel.css"/>
   <link rel="stylesheet" href="chrome://devtools/skin/deprecated-boxmodel.css"/>
   <link rel="stylesheet" href="chrome://devtools/skin/layout.css"/>
   <link rel="stylesheet" href="chrome://devtools/skin/animationinspector.css"/>
   <link rel="stylesheet" href="resource://devtools/client/shared/components/sidebar-toggle.css"/>
   <link rel="stylesheet" href="resource://devtools/client/shared/components/tabs/tabs.css"/>
   <link rel="stylesheet" href="resource://devtools/client/shared/components/tabs/tabbar.css"/>
   <link rel="stylesheet" href="resource://devtools/client/inspector/components/inspector-tab-panel.css"/>
   <link rel="stylesheet" href="resource://devtools/client/shared/components/splitter/split-box.css"/>
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/layout/actions/box-model.js
@@ -0,0 +1,23 @@
+/* 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/. */
+
+"use strict";
+
+const {
+  UPDATE_LAYOUT,
+} = require("./index");
+
+module.exports = {
+
+  /**
+   * Update the layout state with the new layout properties.
+   */
+  updateLayout(layout) {
+    return {
+      type: UPDATE_LAYOUT,
+      layout,
+    };
+  },
+
+};
--- a/devtools/client/inspector/layout/actions/index.js
+++ b/devtools/client/inspector/layout/actions/index.js
@@ -9,15 +9,18 @@ const { createEnum } = require("devtools
 createEnum([
 
   // Update the grid highlighted state.
   "UPDATE_GRID_HIGHLIGHTED",
 
   // Update the entire grids state with the new list of grids.
   "UPDATE_GRIDS",
 
+  // Update the layout state with the latest layout properties.
+  "UPDATE_LAYOUT",
+
   // Update the grid highlighter's show grid line numbers state.
   "UPDATE_SHOW_GRID_LINE_NUMBERS",
 
   // Update the grid highlighter's show infinite lines state.
   "UPDATE_SHOW_INFINITE_LINES",
 
 ], module.exports);
--- a/devtools/client/inspector/layout/actions/moz.build
+++ b/devtools/client/inspector/layout/actions/moz.build
@@ -1,11 +1,12 @@
 # -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
 # vim: set filetype=python:
 # 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/.
 
 DevToolsModules(
+    'box-model.js',
     'grids.js',
     'highlighter-settings.js',
     'index.js',
 )
--- a/devtools/client/inspector/layout/components/App.js
+++ b/devtools/client/inspector/layout/components/App.js
@@ -3,49 +3,65 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 const { addons, createClass, createFactory, DOM: dom, PropTypes } =
   require("devtools/client/shared/vendor/react");
 const { connect } = require("devtools/client/shared/vendor/react-redux");
 
+const { LocalizationHelper } = require("devtools/shared/l10n");
+
 const Accordion = createFactory(require("./Accordion"));
+const BoxModel = createFactory(require("./BoxModel"));
 const Grid = createFactory(require("./Grid"));
 
 const Types = require("../types");
 const { getStr } = require("../utils/l10n");
 
+const BOXMODEL_STRINGS_URI = "devtools/client/locales/boxmodel.properties";
+const BOXMODEL_L10N = new LocalizationHelper(BOXMODEL_STRINGS_URI);
+
 const App = createClass({
 
   displayName: "App",
 
   propTypes: {
+    boxModel: PropTypes.shape(Types.boxModel).isRequired,
     grids: PropTypes.arrayOf(PropTypes.shape(Types.grid)).isRequired,
     highlighterSettings: PropTypes.shape(Types.highlighterSettings).isRequired,
+    onShowBoxModelEditor: PropTypes.func.isRequired,
+    onHideBoxModelHighlighter: PropTypes.func.isRequired,
+    onShowBoxModelHighlighter: PropTypes.func.isRequired,
     onToggleGridHighlighter: PropTypes.func.isRequired,
     onToggleShowGridLineNumbers: PropTypes.func.isRequired,
     onToggleShowInfiniteLines: PropTypes.func.isRequired,
   },
 
   mixins: [ addons.PureRenderMixin ],
 
   render() {
     return dom.div(
       {
         id: "layout-container",
       },
       Accordion({
         items: [
           {
+            header: BOXMODEL_L10N.getStr("boxmodel.title"),
+            component: BoxModel,
+            componentProps: this.props,
+            opened: true,
+          },
+          {
             header: getStr("layout.header"),
             component: Grid,
             componentProps: this.props,
-            opened: true
-          }
+            opened: true,
+          },
         ]
       })
     );
   },
 
 });
 
 module.exports = connect(state => state)(App);
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/layout/components/BoxModel.js
@@ -0,0 +1,52 @@
+/* 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/. */
+
+"use strict";
+
+const { addons, createClass, createFactory, DOM: dom, PropTypes } =
+  require("devtools/client/shared/vendor/react");
+
+const BoxModelInfo = createFactory(require("./BoxModelInfo"));
+const BoxModelMain = createFactory(require("./BoxModelMain"));
+
+const Types = require("../types");
+
+module.exports = createClass({
+
+  displayName: "BoxModel",
+
+  propTypes: {
+    boxModel: PropTypes.shape(Types.boxModel).isRequired,
+    onHideBoxModelHighlighter: PropTypes.func.isRequired,
+    onShowBoxModelEditor: PropTypes.func.isRequired,
+    onShowBoxModelHighlighter: PropTypes.func.isRequired,
+  },
+
+  mixins: [ addons.PureRenderMixin ],
+
+  render() {
+    let {
+      boxModel,
+      onHideBoxModelHighlighter,
+      onShowBoxModelEditor,
+      onShowBoxModelHighlighter,
+    } = this.props;
+
+    return dom.div(
+      {
+        className: "boxmodel-container",
+      },
+      BoxModelMain({
+        boxModel,
+        onHideBoxModelHighlighter,
+        onShowBoxModelEditor,
+        onShowBoxModelHighlighter,
+      }),
+      BoxModelInfo({
+        boxModel,
+      })
+    );
+  },
+
+});
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/layout/components/BoxModelEditable.js
@@ -0,0 +1,65 @@
+/* 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/. */
+
+"use strict";
+
+const { addons, createClass, DOM: dom, PropTypes } =
+  require("devtools/client/shared/vendor/react");
+const { editableItem } = require("devtools/client/shared/inplace-editor");
+
+const LONG_TEXT_ROTATE_LIMIT = 3;
+
+module.exports = createClass({
+
+  displayName: "BoxModelEditable",
+
+  propTypes: {
+    box: PropTypes.string.isRequired,
+    direction: PropTypes.string.isRequired,
+    property: PropTypes.string.isRequired,
+    textContent: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
+    onShowBoxModelEditor: PropTypes.func.isRequired,
+  },
+
+  mixins: [ addons.PureRenderMixin ],
+
+  componentDidMount() {
+    let { property, onShowBoxModelEditor } = this.props;
+
+    editableItem({
+      element: this.refs.span,
+    }, (element, event) => {
+      onShowBoxModelEditor(element, event, property);
+    });
+  },
+
+  render() {
+    let {
+      box,
+      direction,
+      property,
+      textContent,
+    } = this.props;
+
+    let rotate = (direction == "left" || direction == "right") &&
+                 textContent.toString().length > LONG_TEXT_ROTATE_LIMIT;
+
+    return dom.p(
+      {
+        className: `boxmodel-${box} boxmodel-${direction}
+          ${rotate ? "boxmodel-rotate" : ""}`,
+      },
+      dom.span(
+        {
+          className: "boxmodel-editable",
+          "data-box": box,
+          title: property,
+          ref: "span",
+        },
+        textContent
+      )
+    );
+  },
+
+});
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/layout/components/BoxModelInfo.js
@@ -0,0 +1,62 @@
+/* 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/. */
+
+"use strict";
+
+const { LocalizationHelper } = require("devtools/shared/l10n");
+const { addons, createClass, DOM: dom, PropTypes } =
+  require("devtools/client/shared/vendor/react");
+
+const Types = require("../types");
+
+const BOXMODEL_STRINGS_URI = "devtools/client/locales/boxmodel.properties";
+const BOXMODEL_L10N = new LocalizationHelper(BOXMODEL_STRINGS_URI);
+
+const SHARED_STRINGS_URI = "devtools/client/locales/shared.properties";
+const SHARED_L10N = new LocalizationHelper(SHARED_STRINGS_URI);
+
+module.exports = createClass({
+
+  displayName: "BoxModelInfo",
+
+  propTypes: {
+    boxModel: PropTypes.shape(Types.boxModel).isRequired,
+  },
+
+  mixins: [ addons.PureRenderMixin ],
+
+  render() {
+    let { boxModel } = this.props;
+    let { layout } = boxModel;
+    let { width, height, position } = layout;
+
+    return dom.div(
+      {
+        className: "boxmodel-info",
+      },
+      dom.span(
+        {
+          className: "boxmodel-element-size",
+        },
+        SHARED_L10N.getFormatStr("dimensions", width, height)
+      ),
+      dom.section(
+        {
+          className: "boxmodel-position-group",
+        },
+        dom.button({
+          className: "layout-geometry-editor devtools-button",
+          title: BOXMODEL_L10N.getStr("boxmodel.geometryButton.tooltip"),
+        }),
+        dom.span(
+          {
+            className: "boxmodel-element-position",
+          },
+          position
+        )
+      )
+    );
+  },
+
+});
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/layout/components/BoxModelMain.js
@@ -0,0 +1,261 @@
+/* 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/. */
+
+"use strict";
+
+const { addons, createClass, createFactory, DOM: dom, PropTypes } =
+  require("devtools/client/shared/vendor/react");
+
+const { LocalizationHelper } = require("devtools/shared/l10n");
+
+const BoxModelEditable = createFactory(require("./BoxModelEditable"));
+
+const Types = require("../types");
+
+const BOXMODEL_STRINGS_URI = "devtools/client/locales/boxmodel.properties";
+const BOXMODEL_L10N = new LocalizationHelper(BOXMODEL_STRINGS_URI);
+
+const SHARED_STRINGS_URI = "devtools/client/locales/shared.properties";
+const SHARED_L10N = new LocalizationHelper(SHARED_STRINGS_URI);
+
+module.exports = createClass({
+
+  displayName: "BoxModelMain",
+
+  propTypes: {
+    boxModel: PropTypes.shape(Types.boxModel).isRequired,
+    onHideBoxModelHighlighter: PropTypes.func.isRequired,
+    onShowBoxModelEditor: PropTypes.func.isRequired,
+    onShowBoxModelHighlighter: PropTypes.func.isRequired,
+  },
+
+  mixins: [ addons.PureRenderMixin ],
+
+  getBorderOrPaddingValue(property) {
+    let { layout } = this.props.boxModel;
+    return layout[property] ? parseFloat(layout[property]) : "-";
+  },
+
+  getHeightOrWidthValue(property) {
+    let { layout } = this.props.boxModel;
+
+    if (property == undefined) {
+      return "-";
+    }
+
+    property -= parseFloat(layout["border-left-width"]) +
+                parseFloat(layout["border-right-width"]) +
+                parseFloat(layout["padding-left"]) +
+                parseFloat(layout["padding-right"]);
+    property = parseFloat(property.toPrecision(6));
+
+    return property;
+  },
+
+  getMarginValue(property, direction) {
+    let { layout } = this.props.boxModel;
+    let autoMargins = layout.autoMargins || {};
+    let value = "-";
+
+    if (direction in autoMargins) {
+      value = "auto";
+    } else if (layout[property]) {
+      value = parseFloat(layout[property]);
+    }
+
+    return value;
+  },
+
+  onHighlightMouseOver(event) {
+    let region = event.target.getAttribute("data-box");
+    if (!region) {
+      this.props.onHideBoxModelHighlighter();
+    }
+
+    this.props.onShowBoxModelHighlighter({
+      region,
+      showOnly: region,
+      onlyRegionArea: true,
+    });
+  },
+
+  render() {
+    let { boxModel, onShowBoxModelEditor } = this.props;
+    let { layout } = boxModel;
+    let { width, height } = layout;
+
+    let borderTop = this.getBorderOrPaddingValue("border-top-width");
+    let borderRight = this.getBorderOrPaddingValue("border-right-width");
+    let borderBottom = this.getBorderOrPaddingValue("border-bottom-width");
+    let borderLeft = this.getBorderOrPaddingValue("border-left-width");
+
+    let paddingTop = this.getBorderOrPaddingValue("padding-top");
+    let paddingRight = this.getBorderOrPaddingValue("padding-right");
+    let paddingBottom = this.getBorderOrPaddingValue("padding-bottom");
+    let paddingLeft = this.getBorderOrPaddingValue("padding-left");
+
+    let marginTop = this.getMarginValue("margin-top", "top");
+    let marginRight = this.getMarginValue("margin-right", "right");
+    let marginBottom = this.getMarginValue("margin-bottom", "bottom");
+    let marginLeft = this.getMarginValue("margin-left", "left");
+
+    width = this.getHeightOrWidthValue(width);
+    height = this.getHeightOrWidthValue(height);
+
+    return dom.div(
+      {
+        className: "boxmodel-main",
+        onMouseOver: this.onHighlightMouseOver,
+        onMouseOut: this.props.onHideBoxModelHighlighter,
+      },
+      dom.span(
+        {
+          className: "boxmodel-legend",
+          "data-box": "margin",
+          title: BOXMODEL_L10N.getStr("boxmodel.margin"),
+        },
+        BOXMODEL_L10N.getStr("boxmodel.margin")
+      ),
+      dom.div(
+        {
+          className: "boxmodel-margins",
+          "data-box": "margin",
+          title: BOXMODEL_L10N.getStr("boxmodel.margin"),
+        },
+        dom.span(
+          {
+            className: "boxmodel-legend",
+            "data-box": "border",
+            title: BOXMODEL_L10N.getStr("boxmodel.border"),
+          },
+          BOXMODEL_L10N.getStr("boxmodel.border")
+        ),
+        dom.div(
+          {
+            className: "boxmodel-borders",
+            "data-box": "border",
+            title: BOXMODEL_L10N.getStr("boxmodel.border"),
+          },
+          dom.span(
+            {
+              className: "boxmodel-legend",
+              "data-box": "padding",
+              title: BOXMODEL_L10N.getStr("boxmodel.padding"),
+            },
+            BOXMODEL_L10N.getStr("boxmodel.padding")
+          ),
+          dom.div(
+            {
+              className: "boxmodel-paddings",
+              "data-box": "padding",
+              title: BOXMODEL_L10N.getStr("boxmodel.padding"),
+            },
+            dom.div({
+              className: "boxmodel-content",
+              "data-box": "content",
+              title: BOXMODEL_L10N.getStr("boxmodel.content"),
+            })
+          )
+        ),
+      ),
+      BoxModelEditable({
+        box: "margin",
+        direction: "top",
+        property: "margin-top",
+        textContent: marginTop,
+        onShowBoxModelEditor,
+      }),
+      BoxModelEditable({
+        box: "margin",
+        direction: "right",
+        property: "margin-right",
+        textContent: marginRight,
+        onShowBoxModelEditor,
+      }),
+      BoxModelEditable({
+        box: "margin",
+        direction: "bottom",
+        property: "margin-bottom",
+        textContent: marginBottom,
+        onShowBoxModelEditor,
+      }),
+      BoxModelEditable({
+        box: "margin",
+        direction: "left",
+        property: "margin-left",
+        textContent: marginLeft,
+        onShowBoxModelEditor,
+      }),
+      BoxModelEditable({
+        box: "border",
+        direction: "top",
+        property: "border-top-width",
+        textContent: borderTop,
+        onShowBoxModelEditor,
+      }),
+      BoxModelEditable({
+        box: "border",
+        direction: "right",
+        property: "border-right-width",
+        textContent: borderRight,
+        onShowBoxModelEditor,
+      }),
+      BoxModelEditable({
+        box: "border",
+        direction: "bottom",
+        property: "border-bottom-width",
+        textContent: borderBottom,
+        onShowBoxModelEditor,
+      }),
+      BoxModelEditable({
+        box: "border",
+        direction: "left",
+        property: "border-left-width",
+        textContent: borderLeft,
+        onShowBoxModelEditor,
+      }),
+      BoxModelEditable({
+        box: "padding",
+        direction: "top",
+        property: "padding-top",
+        textContent: paddingTop,
+        onShowBoxModelEditor,
+      }),
+      BoxModelEditable({
+        box: "padding",
+        direction: "right",
+        property: "padding-right",
+        textContent: paddingRight,
+        onShowBoxModelEditor,
+      }),
+      BoxModelEditable({
+        box: "padding",
+        direction: "bottom",
+        property: "padding-bottom",
+        textContent: paddingBottom,
+        onShowBoxModelEditor,
+      }),
+      BoxModelEditable({
+        box: "padding",
+        direction: "left",
+        property: "padding-left",
+        textContent: paddingLeft,
+        onShowBoxModelEditor,
+      }),
+      dom.p(
+        {
+          className: "boxmodel-size",
+        },
+        dom.span(
+          {
+            "data-box": "content",
+            title: BOXMODEL_L10N.getStr("boxmodel.content"),
+          },
+          SHARED_L10N.getFormatStr("dimensions", width, height)
+        )
+      )
+    );
+  },
+
+});
--- a/devtools/client/inspector/layout/components/moz.build
+++ b/devtools/client/inspector/layout/components/moz.build
@@ -3,12 +3,16 @@
 # 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/.
 
 DevToolsModules(
     'Accordion.css',
     'Accordion.js',
     'App.js',
+    'BoxModel.js',
+    'BoxModelEditable.js',
+    'BoxModelInfo.js',
+    'BoxModelMain.js',
     'Grid.js',
     'GridDisplaySettings.js',
     'GridList.js',
 )
--- a/devtools/client/inspector/layout/layout.js
+++ b/devtools/client/inspector/layout/layout.js
@@ -1,54 +1,68 @@
 /* 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/. */
 
 "use strict";
 
 const Services = require("Services");
 const { Task } = require("devtools/shared/task");
+const { getCssProperties } = require("devtools/shared/fronts/css-properties");
+const { ReflowFront } = require("devtools/shared/fronts/reflow");
+
+const { InplaceEditor } = require("devtools/client/shared/inplace-editor");
 const { createFactory, createElement } = require("devtools/client/shared/vendor/react");
 const { Provider } = require("devtools/client/shared/vendor/react-redux");
 
 const {
+  updateLayout,
+} = require("./actions/box-model");
+const {
   updateGridHighlighted,
   updateGrids,
 } = require("./actions/grids");
 const {
   updateShowGridLineNumbers,
   updateShowInfiniteLines,
 } = require("./actions/highlighter-settings");
 
 const App = createFactory(require("./components/App"));
 const Store = require("./store");
 
+const EditingSession = require("./utils/editing-session");
+
 const { LocalizationHelper } = require("devtools/shared/l10n");
 const INSPECTOR_L10N =
   new LocalizationHelper("devtools/client/locales/inspector.properties");
 
+const NUMERIC = /^-?[\d\.]+$/;
 const SHOW_GRID_LINE_NUMBERS = "devtools.gridinspector.showGridLineNumbers";
 const SHOW_INFINITE_LINES_PREF = "devtools.gridinspector.showInfiniteLines";
 
 function LayoutView(inspector, window) {
   this.document = window.document;
   this.highlighters = inspector.highlighters;
   this.inspector = inspector;
   this.store = null;
   this.walker = this.inspector.walker;
 
+  this.updateBoxModel = this.updateBoxModel.bind(this);
+
   this.onGridLayoutChange = this.onGridLayoutChange.bind(this);
   this.onHighlighterChange = this.onHighlighterChange.bind(this);
+  this.onNewSelection = this.onNewSelection.bind(this);
   this.onSidebarSelect = this.onSidebarSelect.bind(this);
 
+  this.init();
+
   this.highlighters.on("grid-highlighter-hidden", this.onHighlighterChange);
   this.highlighters.on("grid-highlighter-shown", this.onHighlighterChange);
+  this.inspector.selection.on("new-node-front", this.onNewSelection);
   this.inspector.sidebar.on("select", this.onSidebarSelect);
-
-  this.init();
 }
 
 LayoutView.prototype = {
 
   /**
    * Initializes the layout view by fetching the LayoutFront from the walker, creating
    * the redux store and adding the view into the inspector sidebar.
    */
@@ -58,16 +72,105 @@ LayoutView.prototype = {
     }
 
     this.layoutInspector = yield this.inspector.walker.getLayoutInspector();
     let store = this.store = Store();
 
     this.loadHighlighterSettings();
 
     let app = App({
+      /**
+       * Hides the box-model highlighter on the currently selected element.
+       */
+      onHideBoxModelHighlighter: () => {
+        let toolbox = this.inspector.toolbox;
+        toolbox.highlighterUtils.unhighlight();
+      },
+
+      /**
+       * Shows the inplace editor when a box model editable value is clicked on the
+       * box model panel.
+       *
+       * @param  {DOMNode} element
+       *         The element that was clicked.
+       * @param  {Event} event
+       *         The event object.
+       * @param  {String} property
+       *         The name of the property.
+       */
+      onShowBoxModelEditor: (element, event, property) => {
+        let session = new EditingSession({
+          inspector: this.inspector,
+          doc: this.document,
+          elementRules: this.elementRules,
+        });
+        let initialValue = session.getProperty(property);
+
+        let editor = new InplaceEditor({
+          element: element,
+          initial: initialValue,
+          contentType: InplaceEditor.CONTENT_TYPES.CSS_VALUE,
+          property: {
+            name: property
+          },
+          start: self => {
+            self.elt.parentNode.classList.add("boxmodel-editing");
+          },
+          change: value => {
+            if (NUMERIC.test(value)) {
+              value += "px";
+            }
+
+            let properties = [
+              { name: property, value: value }
+            ];
+
+            if (property.substring(0, 7) == "border-") {
+              let bprop = property.substring(0, property.length - 5) + "style";
+              let style = session.getProperty(bprop);
+              if (!style || style == "none" || style == "hidden") {
+                properties.push({ name: bprop, value: "solid" });
+              }
+            }
+
+            session.setProperties(properties).catch(e => console.error(e));
+          },
+          done: (value, commit) => {
+            editor.elt.parentNode.classList.remove("boxmodel-editing");
+            if (!commit) {
+              session.revert().then(() => {
+                session.destroy();
+              }, e => console.error(e));
+              return;
+            }
+
+            let node = this.inspector.selection.nodeFront;
+            this.inspector.pageStyle.getLayout(node, {
+              autoMargins: true,
+            }).then(layout => {
+              this.store.dispatch(updateLayout(layout));
+            }, e => console.error(e));
+          },
+          contextMenu: this.inspector.onTextBoxContextMenu,
+          cssProperties: getCssProperties(this.inspector.toolbox)
+        }, event);
+      },
+
+      /**
+       * Shows the box-model highlighter on the currently selected element.
+       *
+       * @param  {Object} options
+       *         Options passed to the highlighter actor.
+       */
+      onShowBoxModelHighlighter: (options = {}) => {
+        let toolbox = this.inspector.toolbox;
+        let nodeFront = this.inspector.selection.nodeFront;
+
+        toolbox.highlighterUtils.highlightNodeFront(nodeFront, options);
+      },
 
       /**
        * Handler for a change in the input checkboxes in the GridList component.
        * Toggles on/off the grid highlighter for the provided grid container element.
        *
        * @param  {NodeFront} node
        *         The NodeFront of the grid container element for which the grid
        *         highlighter is toggled on/off for.
@@ -115,17 +218,16 @@ LayoutView.prototype = {
         let { grids, highlighterSettings } = this.store.getState();
 
         for (let grid of grids) {
           if (grid.highlighted) {
             this.highlighters.showGridHighlighter(grid.nodeFront, highlighterSettings);
           }
         }
       },
-
     });
 
     let provider = createElement(Provider, {
       store,
       id: "layoutview",
       title: INSPECTOR_L10N.getStr("inspector.sidebar.layoutViewTitle2"),
       key: "layoutview",
     }, app);
@@ -142,19 +244,26 @@ LayoutView.prototype = {
 
   /**
    * Destruction function called when the inspector is destroyed. Removes event listeners
    * and cleans up references.
    */
   destroy() {
     this.highlighters.off("grid-highlighter-hidden", this.onHighlighterChange);
     this.highlighters.off("grid-highlighter-shown", this.onHighlighterChange);
+    this.inspector.selection.off("new-node-front", this.onNewSelection);
     this.inspector.sidebar.off("select", this.onSidebarSelect);
     this.layoutInspector.off("grid-layout-changed", this.onGridLayoutChange);
 
+    if (this.reflowFront) {
+      this.untrackReflows();
+      this.reflowFront.destroy();
+      this.reflowFront = null;
+    }
+
     this.document = null;
     this.inspector = null;
     this.layoutInspector = null;
     this.store = null;
     this.walker = null;
   },
 
   /**
@@ -162,36 +271,112 @@ LayoutView.prototype = {
    */
   isPanelVisible() {
     return this.inspector.toolbox.currentToolId === "inspector" &&
            this.inspector.sidebar &&
            this.inspector.sidebar.getCurrentTabID() === "layoutview";
   },
 
   /**
+   * Returns true if the layout panel is visible and the current node is valid to
+   * be displayed in the view.
+   */
+  isPanelVisibleAndNodeValid() {
+    return this.isPanelVisible() &&
+           this.inspector.selection.isConnected() &&
+           this.inspector.selection.isElementNode();
+  },
+
+  /**
    * Load the grid highligher display settings into the store from the stored preferences.
    */
   loadHighlighterSettings() {
     let { dispatch } = this.store;
 
     let showGridLineNumbers = Services.prefs.getBoolPref(SHOW_GRID_LINE_NUMBERS);
     let showInfinteLines = Services.prefs.getBoolPref(SHOW_INFINITE_LINES_PREF);
 
     dispatch(updateShowGridLineNumbers(showGridLineNumbers));
     dispatch(updateShowInfiniteLines(showInfinteLines));
   },
 
   /**
-   * Refreshes the layout view by dispatching the new grid data. This is called when the
+   * Starts listening to reflows in the current tab.
+   */
+  trackReflows() {
+    if (!this.reflowFront) {
+      let { target } = this.inspector;
+      if (target.form.reflowActor) {
+        this.reflowFront = ReflowFront(target.client,
+                                       target.form);
+      } else {
+        return;
+      }
+    }
+
+    this.reflowFront.on("reflows", this.updateBoxModel);
+    this.reflowFront.start();
+  },
+
+  /**
+   * Stops listening to reflows in the current tab.
+   */
+  untrackReflows() {
+    if (!this.reflowFront) {
+      return;
+    }
+
+    this.reflowFront.off("reflows", this.updateBoxModel);
+    this.reflowFront.stop();
+  },
+
+  /**
+   * Updates the box model panel by dispatching the new layout data.
+   */
+  updateBoxModel() {
+    let lastRequest = Task.spawn((function* () {
+      if (!(this.isPanelVisible() &&
+          this.inspector.selection.isConnected() &&
+          this.inspector.selection.isElementNode())) {
+        return null;
+      }
+
+      let node = this.inspector.selection.nodeFront;
+      let layout = yield this.inspector.pageStyle.getLayout(node, {
+        autoMargins: true,
+      });
+      let styleEntries = yield this.inspector.pageStyle.getApplied(node, {});
+      this.elementRules = styleEntries.map(e => e.rule);
+
+      // Update the redux store with the latest layout properties and update the box
+      // model view.
+      this.store.dispatch(updateLayout(layout));
+
+      // If a subsequent request has been made, wait for that one instead.
+      if (this._lastRequest != lastRequest) {
+        return this._lastRequest;
+      }
+
+      this._lastRequest = null;
+
+      this.inspector.emit("boxmodel-view-updated");
+      return null;
+    }).bind(this)).catch(console.error);
+
+    this._lastRequest = lastRequest;
+  },
+
+  /**
+   * Updates the grid panel by dispatching the new grid data. This is called when the
    * layout view becomes visible or the view needs to be updated with new grid data.
    *
    * @param {Array|null} gridFronts
    *        Optional array of all GridFront in the current page.
    */
-  refresh: Task.async(function* (gridFronts) {
+  updateGridPanel: Task.async(function* (gridFronts) {
     // Stop refreshing if the inspector or store is already destroyed.
     if (!this.inspector || !this.store) {
       return;
     }
 
     // Get all the GridFront from the server if no gridFronts were provided.
     if (!gridFronts) {
       gridFronts = yield this.layoutInspector.getAllGrids(this.walker.rootNode);
@@ -216,17 +401,17 @@ LayoutView.prototype = {
   /**
    * Handler for "grid-layout-changed" events emitted from the LayoutActor.
    *
    * @param  {Array} grids
    *         Array of all GridFront in the current page.
    */
   onGridLayoutChange(grids) {
     if (this.isPanelVisible()) {
-      this.refresh(grids);
+      this.updateGridPanel(grids);
     }
   },
 
   /**
    * Handler for "grid-highlighter-shown" and "grid-highlighter-hidden" events emitted
    * from the HighlightersOverlay. Updates the NodeFront's grid highlighted state.
    *
    * @param  {Event} event
@@ -236,26 +421,44 @@ LayoutView.prototype = {
    *         is shown for.
    */
   onHighlighterChange(event, nodeFront) {
     let highlighted = event === "grid-highlighter-shown";
     this.store.dispatch(updateGridHighlighted(nodeFront, highlighted));
   },
 
   /**
+   * Selection 'new-node-front' event handler.
+   */
+  onNewSelection: function () {
+    if (!this.isPanelVisibleAndNodeValid()) {
+      return;
+    }
+
+    this.updateBoxModel();
+  },
+
+  /**
    * Handler for the inspector sidebar select event. Starts listening for
    * "grid-layout-changed" if the layout panel is visible. Otherwise, stop
    * listening for grid layout changes. Finally, refresh the layout view if
    * it is visible.
    */
   onSidebarSelect() {
     if (!this.isPanelVisible()) {
       this.layoutInspector.off("grid-layout-changed", this.onGridLayoutChange);
+      this.untrackReflows();
       return;
     }
 
+    if (this.inspector.selection.isConnected() &&
+        this.inspector.selection.isElementNode()) {
+      this.trackReflows();
+    }
+
     this.layoutInspector.on("grid-layout-changed", this.onGridLayoutChange);
-    this.refresh();
+    this.updateBoxModel();
+    this.updateGridPanel();
   },
 
 };
 
 module.exports = LayoutView;
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/layout/reducers/box-model.js
@@ -0,0 +1,31 @@
+/* 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/. */
+
+"use strict";
+
+const {
+  UPDATE_LAYOUT,
+} = require("../actions/index");
+
+const INITIAL_BOX_MODEL = {
+  layout: {},
+};
+
+let reducers = {
+
+  [UPDATE_LAYOUT](boxModel, { layout }) {
+    return Object.assign({}, boxModel, {
+      layout,
+    });
+  },
+
+};
+
+module.exports = function (boxModel = INITIAL_BOX_MODEL, action) {
+  let reducer = reducers[action.type];
+  if (!reducer) {
+    return boxModel;
+  }
+  return reducer(boxModel, action);
+};
--- a/devtools/client/inspector/layout/reducers/index.js
+++ b/devtools/client/inspector/layout/reducers/index.js
@@ -1,8 +1,9 @@
 /* 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/. */
 
 "use strict";
 
+exports.boxModel = require("./box-model");
 exports.grids = require("./grids");
 exports.highlighterSettings = require("./highlighter-settings");
--- a/devtools/client/inspector/layout/reducers/moz.build
+++ b/devtools/client/inspector/layout/reducers/moz.build
@@ -1,11 +1,12 @@
 # -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
 # vim: set filetype=python:
 # 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/.
 
 DevToolsModules(
+    'box-model.js',
     'grids.js',
     'highlighter-settings.js',
     'index.js',
 )
--- a/devtools/client/inspector/layout/types.js
+++ b/devtools/client/inspector/layout/types.js
@@ -2,16 +2,26 @@
  * 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/. */
 
 "use strict";
 
 const { PropTypes } = require("devtools/client/shared/vendor/react");
 
 /**
+ * The box model data for the current selected node.
+ */
+exports.boxModel = {
+
+  // The layout information of the current selected node
+  layout: PropTypes.object,
+
+};
+
+/**
  * A single grid container in the document.
  */
 exports.grid = {
 
   // The id of the grid
   id: PropTypes.number,
 
   // The grid fragment object of the grid container
--- a/devtools/client/themes/boxmodel.css
+++ b/devtools/client/themes/boxmodel.css
@@ -1,130 +1,128 @@
 /* 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/ */
 
-#boxmodel-wrapper {
-  border-bottom-style: solid;
-  border-bottom-width: 1px;
-  border-color: var(--theme-splitter-color);
-}
+/**
+ * This is the stylesheet of the Box Model view implemented in the layout panel.
+ */
 
-#boxmodel-container {
+.boxmodel-container {
   /* The view will grow bigger as the window gets resized, until 400px */
   max-width: 400px;
   margin: 0px auto;
   padding: 0;
 }
 
 /* Header */
 
-#boxmodel-header,
-#boxmodel-info {
+.boxmodel-header,
+.boxmodel-info {
   display: flex;
   align-items: center;
   padding: 4px 17px;
 }
 
-#layout-geometry-editor {
+.layout-geometry-editor {
   visibility: hidden;
 }
 
-#layout-geometry-editor::before {
+.layout-geometry-editor::before {
   background: url(images/geometry-editor.svg) no-repeat center center / 16px 16px;
 }
 
 /* Main: contains the box-model regions */
 
-#boxmodel-main {
+.boxmodel-main {
   position: relative;
   box-sizing: border-box;
   /* The regions are semi-transparent, so the white background is partly
      visible */
   background-color: white;
   color: var(--theme-selection-color);
   /* Make sure there is some space between the window's edges and the regions */
-  margin: 0 14px 4px 14px;
+  margin: 14px 14px 4px 14px;
   width: calc(100% - 2 * 14px);
 }
 
 .boxmodel-margin,
 .boxmodel-size {
   color: var(--theme-highlight-blue);
 }
 
 /* Regions are 3 nested elements with wide borders and outlines */
 
-#boxmodel-content {
+.boxmodel-content {
   height: 18px;
 }
 
-#boxmodel-margins,
-#boxmodel-borders,
-#boxmodel-padding {
+.boxmodel-margins,
+.boxmodel-borders,
+.boxmodel-paddings {
   border-color: hsla(210,100%,85%,0.2);
   border-width: 18px;
   border-style: solid;
   outline: dotted 1px hsl(210,100%,85%);
 }
 
-#boxmodel-margins {
+.boxmodel-margins {
   /* This opacity applies to all of the regions, since they are nested */
   opacity: .8;
 }
 
 /* Regions colors */
 
-#boxmodel-margins {
+.boxmodel-margins {
   border-color: #edff64;
 }
 
-#boxmodel-borders {
+.boxmodel-borders {
   border-color: #444444;
 }
 
-#boxmodel-padding {
+.boxmodel-paddings {
   border-color: #6a5acd;
 }
 
-#boxmodel-content {
+.boxmodel-content {
   background-color: #87ceeb;
 }
 
-.theme-firebug #boxmodel-main,
-.theme-firebug #boxmodel-borders,
-.theme-firebug #boxmodel-content {
+.theme-firebug .boxmodel-main,
+.theme-firebug .boxmodel-borders,
+.theme-firebug .boxmodel-content {
   border-style: solid;
 }
 
-.theme-firebug #boxmodel-main,
-.theme-firebug #boxmodel-header {
+.theme-firebug .boxmodel-main,
+.theme-firebug .boxmodel-header {
   font-family: var(--proportional-font-family);
 }
 
-.theme-firebug #boxmodel-main {
+.theme-firebug .boxmodel-main {
   color: var(--theme-body-color);
   font-size: var(--theme-toolbar-font-size);
 }
 
-.theme-firebug #boxmodel-header {
+.theme-firebug .boxmodel-header {
   font-size: var(--theme-toolbar-font-size);
 }
 
 /* Editable region sizes are contained in absolutely positioned <p> */
 
-#boxmodel-main > p {
+.boxmodel-main > p {
   position: absolute;
   pointer-events: none;
   margin: 0;
   text-align: center;
 }
 
-#boxmodel-main > p > span,
-#boxmodel-main > p > input {
+.boxmodel-main > p > span,
+.boxmodel-main > p > input {
   vertical-align: middle;
   pointer-events: auto;
 }
 
 /* Coordinates for the region sizes */
 
 .boxmodel-top,
 .boxmodel-bottom {
@@ -230,29 +228,24 @@
   border: 1px dashed transparent;
   -moz-user-select: text;
 }
 
 .boxmodel-editable:hover {
   border-bottom-color: hsl(0, 0%, 50%);
 }
 
-.styleinspector-propertyeditor {
-  border: 1px solid #ccc;
-  padding: 0;
-}
-
 /* Make sure the content size doesn't appear as editable like the other sizes */
 
 .boxmodel-size > span {
   cursor: default;
 }
 
 /* Box Model Info: contains the position and size of the element */
 
-#boxmodel-element-size {
+.boxmodel-element-size {
   flex: 1;
 }
 
-#boxmodel-position-group {
+.boxmodel-position-group {
   display: flex;
   align-items: center;
 }