Bug 1477614 - Part 1: Implement a Flex Item Highlighter that highlights a flex item's flex-basis, flex-grow and flex-shrink. r=pbro draft
authorGabriel Luong <gabriel.luong@gmail.com>
Sun, 29 Jul 2018 23:30:14 -0400
changeset 823949 8be445026a2437c2939bff72a5e05712107b9895
parent 823948 83e552cbe07e1032c4ca346ecc10dc006dc6616d
child 823950 650b6f3dc97f894df2aab8ff6b3b297bfb2ba422
push id117831
push userbmo:gl@mozilla.com
push dateMon, 30 Jul 2018 03:30:55 +0000
reviewerspbro
bugs1477614
milestone63.0a1
Bug 1477614 - Part 1: Implement a Flex Item Highlighter that highlights a flex item's flex-basis, flex-grow and flex-shrink. r=pbro MozReview-Commit-ID: KghusKBXysg
devtools/server/actors/highlighters.css
devtools/server/actors/highlighters.js
devtools/server/actors/highlighters/flex-item.js
devtools/server/actors/highlighters/flexbox.js
devtools/server/actors/highlighters/moz.build
devtools/server/actors/utils/flex-utils.js
devtools/server/actors/utils/moz.build
--- a/devtools/server/actors/highlighters.css
+++ b/devtools/server/actors/highlighters.css
@@ -211,16 +211,49 @@
 
 :-moz-native-anonymous [class$=infobar-dimensions] {
   color: hsl(210, 30%, 85%);
   border-inline-start: 1px solid #5a6169;
   margin-inline-start: 6px;
   padding-inline-start: 6px;
 }
 
+/* Flex Item Highlighter */
+
+:-moz-native-anonymous .flex-item-basis {
+  fill: transparent;
+  stroke: #FF1AD9;
+  stroke-dasharray: 2 1;
+  stroke-opacity: 0.7;
+  stroke-width: 2;
+  shape-rendering: crispEdges;
+}
+
+:-moz-native-anonymous .flex-item-sizing {
+  background-repeat: round;
+  position: absolute;
+  transform-origin: 0 0;
+}
+
+:-moz-native-anonymous .flex-item-sizing.top {
+  background-image: url('data:image/svg+xml;utf8,<svg width="16" height="16" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path fill="rgb(148, 0, 255)" fill-opacity=".8" fill-rule="evenodd" clip-rule="evenodd" d="M12.707 7.293a1 1 0 0 0-1.414 0l-7 7a1 1 0 0 0 1.414 1.414L12 9.414l6.293 6.293a1 1 0 0 0 1.414-1.414l-7-7z"></path></svg>');
+}
+
+:-moz-native-anonymous .flex-item-sizing.left {
+  background-image: url('data:image/svg+xml;utf8,<svg width="16" height="16" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path fill="rgb(148, 0, 255)" fill-opacity=".8" fill-rule="evenodd" clip-rule="evenodd" d="M15.707 4.293a1 1 0 0 1 0 1.414L9.414 12l6.293 6.293a1 1 0 1 1-1.414 1.414l-7-7a1 1 0 0 1 0-1.414l7-7a1 1 0 0 1 1.414 0z"></path></svg>');
+}
+
+:-moz-native-anonymous .flex-item-sizing.bottom {
+  background-image: url('data:image/svg+xml;utf8,<svg width="16" height="16" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path fill="rgb(148, 0, 255)" fill-opacity=".8" fill-rule="evenodd" clip-rule="evenodd" d="M4.293 8.293a1 1 0 0 1 1.414 0L12 14.586l6.293-6.293a1 1 0 1 1 1.414 1.414l-7 7a1 1 0 0 1-1.414 0l-7-7a1 1 0 0 1 0-1.414z"></path></svg>');
+}
+
+:-moz-native-anonymous .flex-item-sizing.right {
+  background-image: url('data:image/svg+xml;utf8,<svg width="16" height="16" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path fill="rgb(148, 0, 255)" fill-opacity=".8" fill-rule="evenodd" clip-rule="evenodd" d="M8.293 4.293a1 1 0 0 1 1.414 0l7 7a1 1 0 0 1 0 1.414l-7 7a1 1 0 0 1-1.414-1.414L14.586 12 8.293 5.707a1 1 0 0 1 0-1.414z"></path></svg>');
+}
+
 /* CSS Grid Highlighter */
 
 :-moz-native-anonymous .css-grid-canvas {
   position: absolute;
   pointer-events: none;
   top: 0;
   left: 0;
   image-rendering: -moz-crisp-edges;
--- a/devtools/server/actors/highlighters.js
+++ b/devtools/server/actors/highlighters.js
@@ -704,15 +704,16 @@ HighlighterEnvironment.prototype = {
   }
 };
 
 register("BoxModelHighlighter", "box-model");
 register("CssGridHighlighter", "css-grid");
 register("CssTransformHighlighter", "css-transform");
 register("EyeDropper", "eye-dropper");
 register("FlexboxHighlighter", "flexbox");
+register("FlexItemHighlighter", "flex-item");
 register("FontsHighlighter", "fonts");
 register("GeometryEditorHighlighter", "geometry-editor");
 register("MeasuringToolHighlighter", "measuring-tool");
 register("PausedDebuggerOverlay", "paused-debugger");
 register("RulersHighlighter", "rulers");
 register("SelectorHighlighter", "selector");
 register("ShapesHighlighter", "shapes");
new file mode 100644
--- /dev/null
+++ b/devtools/server/actors/highlighters/flex-item.js
@@ -0,0 +1,365 @@
+ /* 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 { AutoRefreshHighlighter } = require("./auto-refresh");
+const {
+  CanvasFrameAnonymousContentHelper,
+  createNode,
+  createSVGNode,
+  isNodeValid,
+} = require("./utils/markup");
+const { setIgnoreLayoutChanges } = require("devtools/shared/layout/utils");
+const nodeConstants = require("devtools/shared/dom-node-constants");
+const { getFlexItemData } = require("devtools/server/actors/utils/flex-utils");
+
+/**
+ * The FlexItemHighlighter draws the flex basis and sizing regions of a flex item.
+ *
+ * Structure:
+ * <div class="highlighter-container" role="presentation">
+ *   <div id="flex-item-root" class="flex-item-root">
+ *     <svg id="flex-item-elements" class"flex-item-elements" hidden="true">
+ *       <g class="flex-item-group">
+ *         <path id="flex-item-basis" class="flex-item-basis" d="..." />
+ *       </g>
+ *     </svg>
+ *     <div id="flex-item-sizing" class="flex-item-sizing" hidden="true"></div>
+ *   </div>
+ * </div>
+ */
+class FlexItemHighlighter extends AutoRefreshHighlighter {
+  constructor(highlighterEnv) {
+    super(highlighterEnv);
+
+    this.ID_CLASS_PREFIX = "flex-item-";
+
+    this.markup = new CanvasFrameAnonymousContentHelper(this.highlighterEnv,
+      this._buildMarkup.bind(this));
+
+    this.onPageHide = this.onPageHide.bind(this);
+    this.onWillNavigate = this.onWillNavigate.bind(this);
+
+    this.highlighterEnv.on("will-navigate", this.onWillNavigate);
+
+    const { pageListenerTarget } = highlighterEnv;
+    pageListenerTarget.addEventListener("pagehide", this.onPageHide);
+  }
+
+  _buildMarkup() {
+    const container = createNode(this.win, {
+      attributes: {
+        class: "highlighter-container",
+        role: "presentation"
+      }
+    });
+
+    // Build the root wrapper, used to adapt to the page zoom.
+    const root = createNode(this.win, {
+      parent: container,
+      attributes: {
+        id: "root",
+        class: "root",
+        role: "presentation"
+      },
+      prefix: this.ID_CLASS_PREFIX
+    });
+
+    // Build the SVG element with its path elements to render the flex basis and
+    // sizing of the flex item.
+
+    const svg = createSVGNode(this.win, {
+      nodeType: "svg",
+      parent: root,
+      attributes: {
+        id: "elements",
+        hidden: "true",
+        width: "100%",
+        height: "100%",
+        role: "presentation"
+      },
+      prefix: this.ID_CLASS_PREFIX
+    });
+
+    const group = createSVGNode(this.win, {
+      nodeType: "g",
+      parent: svg,
+      attributes: {
+        class: "group",
+        role: "presentation"
+      },
+      prefix: this.ID_CLASS_PREFIX
+    });
+
+    createSVGNode(this.win, {
+      nodeType: "path",
+      parent: group,
+      attributes: {
+        id: "basis",
+        class: "basis",
+        role: "presentation"
+      },
+      prefix: this.ID_CLASS_PREFIX
+    });
+
+    // Build the flex item sizing element that will be used to visualize flex shrink and
+    // grow.
+    createNode(this.win, {
+      parent: root,
+      attributes: {
+        id: "sizing",
+        class: "sizing",
+        hidden: "true"
+      },
+      prefix: this.ID_CLASS_PREFIX
+    });
+
+    return container;
+  }
+
+  destroy() {
+    this.highlighterEnv.off("will-navigate", this.onWillNavigate);
+
+    const { pageListenerTarget } = this.highlighterEnv;
+    if (pageListenerTarget) {
+      pageListenerTarget.removeEventListener("pagehide", this.onPageHide);
+    }
+
+    this.markup.destroy();
+
+    AutoRefreshHighlighter.prototype.destroy.call(this);
+  }
+
+  getElement(id) {
+    return this.markup.getElement(this.ID_CLASS_PREFIX + id);
+  }
+
+  _getPathCoordinates(p1, p2, p3, p4) {
+    return "M" + p1.x + "," + p1.y + " " +
+           "L" + p2.x + "," + p2.y + " " +
+           "L" + p3.x + "," + p3.y + " " +
+           "L" + p4.x + "," + p4.y + " " +
+           "L" + p4.x + "," + p1.y;
+  }
+
+  _hide() {
+    setIgnoreLayoutChanges(true);
+    this._hideFlexItem();
+    this._hideFlexSizing();
+    setIgnoreLayoutChanges(false, this.highlighterEnv.window.document.documentElement);
+  }
+
+  _hideFlexItem() {
+    this.getElement("elements").setAttribute("hidden", "true");
+  }
+
+  _hideFlexSizing() {
+    this.getElement("sizing").setAttribute("hidden", "true");
+  }
+
+  /**
+   * Overrides the AutoRefreshHighlighter's _isNodeValid method to also return true for
+   * text nodes since these can also be highlighted.
+   *
+   * @param {DOMNode} node
+   * @return {Boolean}
+   */
+  _isNodeValid(node) {
+    return node && (isNodeValid(node) || isNodeValid(node, nodeConstants.TEXT_NODE));
+  }
+
+  /**
+   * Returns whether or not the current node can be highlighted (eg, Does it have quads?).
+   *
+   * @return {Boolean} true if the current node can be highlighted and false otherwise.
+   */
+  _nodeNeedsHighlighting() {
+    return this.currentQuads.margin.length ||
+           this.currentQuads.border.length ||
+           this.currentQuads.padding.length ||
+           this.currentQuads.content.length;
+  }
+
+  /**
+   * If a page hide event is triggered for the current window's highlighter, hide the
+   * highlighter.
+   */
+  onPageHide({ target }) {
+    if (target.defaultView === this.win) {
+      this.hide();
+    }
+  }
+
+  /**
+   * Called when the page will-navigate. Used to hide the flex item highlighter.
+   */
+  onWillNavigate({ isTopLevel }) {
+    if (isTopLevel) {
+      this.hide();
+    }
+  }
+
+  _show() {
+    return this._update();
+  }
+
+  _showFlexItem() {
+    this.getElement("elements").removeAttribute("hidden");
+  }
+
+  _showFlexSizing() {
+    this.getElement("sizing").removeAttribute("hidden");
+  }
+
+  /**
+   * Updates the highlighter on the current highlighted flex item node.
+   */
+  _update() {
+    let shown = false;
+    setIgnoreLayoutChanges(true);
+
+    if (this._updateFlexItem()) {
+      this._showFlexItem();
+      shown = true;
+    } else {
+      // Nothing to highlight (not a flex item or has no quads).
+      this._hide();
+    }
+
+    setIgnoreLayoutChanges(false, this.highlighterEnv.window.document.documentElement);
+
+    return shown;
+  }
+
+  /**
+   * Updates the flex item with the current node.
+   *
+   * @return {Boolean} true if the current node has a flex item to be highlighted.
+   */
+  _updateFlexItem() {
+    if (!this._nodeNeedsHighlighting()) {
+      return false;
+    }
+
+    const flexItemData = getFlexItemData(this.options.flexData, this.currentNode);
+    if (!flexItemData) {
+      return false;
+    }
+
+    this._updateFlexBasis(flexItemData);
+    this._updateFlexSizing(flexItemData);
+
+    // Un-zoom the root wrapper if the page was zoomed.
+    const rootId = this.ID_CLASS_PREFIX + "elements";
+    this.markup.scaleRootElement(this.currentNode, rootId);
+
+    return true;
+  }
+
+  /**
+   * Updates the flex basis path to display the flex basis of the current flex item.
+   *
+   * @param  {Object} flexItemData
+   *         Object representation of the Flex item data object.
+   */
+  _updateFlexBasis(flexItemData) {
+    const basis = this.getElement("basis");
+    basis.setAttribute("d", "");
+
+    const { mainBaseSize } = flexItemData;
+    const { p1, p2, p3, p4 } = this.currentQuads.content[0];
+    const isColumn = this.options.flexDirection.startsWith("column");
+
+    const flexBasisPath = isColumn ?
+      this._getPathCoordinates(
+        p1,
+        p2,
+        { x: p3.x, y: p2.y + mainBaseSize },
+        { x: p4.x, y: p1.y + mainBaseSize }
+      )
+      :
+      this._getPathCoordinates(
+        p1,
+        { x: p1.x + mainBaseSize, y: p2.y },
+        { x: p1.x + mainBaseSize, y: p3.y },
+        p4
+      );
+
+    this.getElement("basis").setAttribute("d", flexBasisPath);
+  }
+
+  /**
+   * Updates the flex sizing node to display the flex grow or shrink sizing of the
+   * current flex item.
+   *
+   * @param  {Object} flexItemData
+   *         Object representation of the Flex item data object.
+   */
+  _updateFlexSizing(flexItemData) {
+    const sizing = this.getElement("sizing");
+    sizing.setAttribute("style", "");
+    sizing.classList.remove("top");
+    sizing.classList.remove("left");
+    sizing.classList.remove("bottom");
+    sizing.classList.remove("right");
+
+    const { mainBaseSize, mainDeltaSize } = flexItemData;
+
+    if (mainDeltaSize === 0) {
+      // No flex sizing information to display.
+      return;
+    }
+
+    const { bounds } = this.currentQuads.content[0];
+    const isColumn = this.options.flexDirection.startsWith("column");
+
+    let x, y, width, height;
+    if (mainDeltaSize < 0) {
+      // Get the correct node location and sizing to show flex shrink.
+      if (isColumn) {
+        sizing.classList.add("top");
+
+        x = bounds.left;
+        y = bounds.bottom;
+        width = bounds.width;
+        height = Math.abs(mainDeltaSize);
+      } else {
+        sizing.classList.add("left");
+
+        x = bounds.right;
+        y = bounds.top;
+        width = Math.abs(mainDeltaSize);
+        height = bounds.height;
+      }
+    } else if (mainDeltaSize > 0) {
+      // Get the correct node location and sizing to show flex grow.
+      if (isColumn) {
+        sizing.classList.add("bottom");
+
+        x = bounds.left;
+        y = bounds.top + mainBaseSize;
+        width = bounds.width;
+        height = mainDeltaSize;
+      } else {
+        sizing.classList.add("right");
+
+        x = bounds.left + mainBaseSize;
+        y = bounds.top;
+        width = mainDeltaSize;
+        height = bounds.height;
+      }
+    }
+
+    sizing.setAttribute("style", `
+      width: ${width}px;
+      height: ${height}px;
+      transform: translate(${x}px, ${y}px);
+    `);
+
+    this._showFlexSizing();
+  }
+}
+
+exports.FlexItemHighlighter = FlexItemHighlighter;
--- a/devtools/server/actors/highlighters/flexbox.js
+++ b/devtools/server/actors/highlighters/flexbox.js
@@ -17,21 +17,24 @@ const {
   updateCanvasPosition,
 } = require("./utils/canvas");
 const {
   CanvasFrameAnonymousContentHelper,
   createNode,
   getComputedStyle,
 } = require("./utils/markup");
 const {
-  getAdjustedQuads,
   getDisplayPixelRatio,
   getWindowDimensions,
   setIgnoreLayoutChanges,
 } = require("devtools/shared/layout/utils");
+const {
+  compareFlexData,
+  getFlexData,
+} = require("devtools/server/actors/utils/flex-utils");
 
 const FLEXBOX_LINES_PROPERTIES = {
   "edge": {
     lineDash: [12, 10]
   },
   "item": {
     lineDash: [0, 0]
   },
@@ -714,124 +717,9 @@ class FlexboxHighlighter extends AutoRef
     root.setAttribute("style",
       `position: absolute; width: ${width}px; height: ${height}px; overflow: hidden`);
 
     setIgnoreLayoutChanges(false, this.highlighterEnv.document.documentElement);
     return true;
   }
 }
 
-/**
- * Returns an object representation of the Flex data object and its array of FlexLine
- * and FlexItem objects along with the box quads of the flex items.
- *
- * @param  {Flex} flex
- *         The Flex data object.
- * @param  {Window} win
- *         The Window object.
- * @return {Object} representation of the Flex data object.
- */
-function getFlexData(flex, win) {
-  return {
-    lines: flex.getLines().map(line => {
-      return {
-        crossSize: line.crossSize,
-        crossStart: line.crossStart,
-        firstBaselineOffset: line.firstBaselineOffset,
-        growthState: line.growthState,
-        lastBaselineOffset: line.lastBaselineOffset,
-        items: line.getItems().map(item => {
-          return {
-            crossMaxSize: item.crossMaxSize,
-            crossMinSize: item.crossMinSize,
-            mainBaseSize: item.mainBaseSize,
-            mainDeltaSize: item.mainDeltaSize,
-            mainMaxSize: item.mainMaxSize,
-            mainMinSize: item.mainMinSize,
-            node: item.node,
-            quads: getAdjustedQuads(win, item.node),
-          };
-        }),
-      };
-    })
-  };
-}
-
-/**
- * Returns whether or not the flex data has changed.
- *
- * @param  {Flex} oldFlexData
- *         The old Flex data object.
- * @param  {Flex} newFlexData
- *         The new Flex data object.
- * @return {Boolean} true if the flex data has changed and false otherwise.
- */
-function compareFlexData(oldFlexData, newFlexData) {
-  if (!oldFlexData || !newFlexData) {
-    return true;
-  }
-
-  const oldLines = oldFlexData.lines;
-  const newLines = newFlexData.lines;
-
-  if (oldLines.length !== newLines.length) {
-    return true;
-  }
-
-  for (let i = 0; i < oldLines.length; i++) {
-    const oldLine = oldLines[i];
-    const newLine = newLines[i];
-
-    if (oldLine.crossSize !== newLine.crossSize ||
-        oldLine.crossStart !== newLine.crossStart ||
-        oldLine.firstBaselineOffset !== newLine.firstBaselineOffset ||
-        oldLine.growthState !== newLine.growthState ||
-        oldLine.lastBaselineOffset !== newLine.lastBaselineOffset) {
-      return true;
-    }
-
-    const oldItems = oldLine.items;
-    const newItems = newLine.items;
-
-    if (oldItems.length !== newItems.length) {
-      return true;
-    }
-
-    for (let j = 0; j < oldItems.length; j++) {
-      const oldItem = oldItems[j];
-      const newItem = newItems[j];
-
-      if (oldItem.crossMaxSize !== newItem.crossMaxSize ||
-          oldItem.crossMinSize !== newItem.crossMinSize ||
-          oldItem.mainBaseSize !== newItem.mainBaseSize ||
-          oldItem.mainDeltaSize !== newItem.mainDeltaSize ||
-          oldItem.mainMaxSize !== newItem.mainMaxSize ||
-          oldItem.mainMinSize !== newItem.mainMinSize) {
-        return true;
-      }
-
-      const oldItemQuads = oldItem.quads;
-      const newItemQuads = newItem.quads;
-
-      if (oldItemQuads.length !== newItemQuads.length) {
-        return true;
-      }
-
-      const { bounds: oldItemBounds } = oldItemQuads[0];
-      const { bounds: newItemBounds } = newItemQuads[0];
-
-      if (oldItemBounds.bottom !== newItemBounds.bottom ||
-          oldItemBounds.height !== newItemBounds.height ||
-          oldItemBounds.left !== newItemBounds.left ||
-          oldItemBounds.right !== newItemBounds.right ||
-          oldItemBounds.top !== newItemBounds.top ||
-          oldItemBounds.width !== newItemBounds.width ||
-          oldItemBounds.x !== newItemBounds.x ||
-          oldItemBounds.y !== newItemBounds.y) {
-        return true;
-      }
-    }
-  }
-
-  return false;
-}
-
 exports.FlexboxHighlighter = FlexboxHighlighter;
--- a/devtools/server/actors/highlighters/moz.build
+++ b/devtools/server/actors/highlighters/moz.build
@@ -10,16 +10,17 @@ DIRS += [
 
 DevToolsModules(
     'accessible.js',
     'auto-refresh.js',
     'box-model.js',
     'css-grid.js',
     'css-transform.js',
     'eye-dropper.js',
+    'flex-item.js',
     'flexbox.js',
     'fonts.js',
     'geometry-editor.js',
     'measuring-tool.js',
     'paused-debugger.js',
     'rulers.js',
     'selector.js',
     'shapes.js',
new file mode 100644
--- /dev/null
+++ b/devtools/server/actors/utils/flex-utils.js
@@ -0,0 +1,157 @@
+/* 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 { getAdjustedQuads } = require("devtools/shared/layout/utils");
+
+/**
+ * Returns whether or not the Flex data has changed.
+ *
+ * @param  {Flex} oldFlexData
+ *         The old Flex data object.
+ * @param  {Flex} newFlexData
+ *         The new Flex data object.
+ * @return {Boolean} true if the flex data has changed and false otherwise.
+ */
+function compareFlexData(oldFlexData, newFlexData) {
+  if (!oldFlexData || !newFlexData) {
+    return true;
+  }
+
+  const oldLines = oldFlexData.lines;
+  const newLines = newFlexData.lines;
+
+  if (oldLines.length !== newLines.length) {
+    return true;
+  }
+
+  for (let i = 0; i < oldLines.length; i++) {
+    const oldLine = oldLines[i];
+    const newLine = newLines[i];
+
+    if (oldLine.crossSize !== newLine.crossSize ||
+        oldLine.crossStart !== newLine.crossStart ||
+        oldLine.firstBaselineOffset !== newLine.firstBaselineOffset ||
+        oldLine.growthState !== newLine.growthState ||
+        oldLine.lastBaselineOffset !== newLine.lastBaselineOffset) {
+      return true;
+    }
+
+    const oldItems = oldLine.items;
+    const newItems = newLine.items;
+
+    if (oldItems.length !== newItems.length) {
+      return true;
+    }
+
+    for (let j = 0; j < oldItems.length; j++) {
+      const oldItem = oldItems[j];
+      const newItem = newItems[j];
+
+      if (oldItem.crossMaxSize !== newItem.crossMaxSize ||
+          oldItem.crossMinSize !== newItem.crossMinSize ||
+          oldItem.mainBaseSize !== newItem.mainBaseSize ||
+          oldItem.mainDeltaSize !== newItem.mainDeltaSize ||
+          oldItem.mainMaxSize !== newItem.mainMaxSize ||
+          oldItem.mainMinSize !== newItem.mainMinSize) {
+        return true;
+      }
+
+      const oldItemQuads = oldItem.quads;
+      const newItemQuads = newItem.quads;
+
+      if (oldItemQuads.length !== newItemQuads.length) {
+        return true;
+      }
+
+      const { bounds: oldItemBounds } = oldItemQuads[0];
+      const { bounds: newItemBounds } = newItemQuads[0];
+
+      if (oldItemBounds.bottom !== newItemBounds.bottom ||
+          oldItemBounds.height !== newItemBounds.height ||
+          oldItemBounds.left !== newItemBounds.left ||
+          oldItemBounds.right !== newItemBounds.right ||
+          oldItemBounds.top !== newItemBounds.top ||
+          oldItemBounds.width !== newItemBounds.width ||
+          oldItemBounds.x !== newItemBounds.x ||
+          oldItemBounds.y !== newItemBounds.y) {
+        return true;
+      }
+    }
+  }
+
+  return false;
+}
+
+/**
+ * Returns an object representation of the Flex data object and its array of FlexLine
+ * and FlexItem objects along with the box quads of the flex items.
+ *
+ * @param  {Flex} flex
+ *         The Flex data object.
+ * @param  {Window} win
+ *         The Window object.
+ * @return {Object|null} representation of the Flex data object.
+ */
+function getFlexData(flex, win) {
+  if (!flex) {
+    return null;
+  }
+
+  return {
+    lines: flex.getLines().map(line => {
+      return {
+        crossSize: line.crossSize,
+        crossStart: line.crossStart,
+        firstBaselineOffset: line.firstBaselineOffset,
+        growthState: line.growthState,
+        lastBaselineOffset: line.lastBaselineOffset,
+        items: line.getItems().map(item => {
+          return {
+            crossMaxSize: item.crossMaxSize,
+            crossMinSize: item.crossMinSize,
+            mainBaseSize: item.mainBaseSize,
+            mainDeltaSize: item.mainDeltaSize,
+            mainMaxSize: item.mainMaxSize,
+            mainMinSize: item.mainMinSize,
+            node: item.node,
+            quads: getAdjustedQuads(win, item.node),
+          };
+        }),
+      };
+    })
+  };
+}
+
+/**
+ * Returns an object representation of the Flex item data for a given a node from the
+ * Flex data object.
+ *
+ * @param  {Object} flexData
+ *         Object representation of the Flex data object.
+ * @param  {DOMNode} node
+ *         The flex item node that we want the flex data for.
+ * @return {Object|null} representation of the Flex item data. See getFlexData() above to
+ * see the properties of the Flex item data object.
+ */
+function getFlexItemData(flexData, node) {
+  if (!flexData) {
+    return null;
+  }
+
+  for (const flexLine of flexData.lines) {
+    for (const flexItem of flexLine.items) {
+      if (flexItem.node === node) {
+        return flexItem;
+      }
+    }
+  }
+
+  return null;
+}
+
+exports.compareFlexData = compareFlexData;
+exports.getFlexData = getFlexData;
+exports.getFlexItemData = getFlexItemData;
--- a/devtools/server/actors/utils/moz.build
+++ b/devtools/server/actors/utils/moz.build
@@ -6,16 +6,17 @@
 
 DevToolsModules(
     'actor-registry-utils.js',
     'audionodes.json',
     'automation-timeline.js',
     'breakpoint-actor-map.js',
     'css-grid-utils.js',
     'event-loop.js',
+    'flex-utils.js',
     'make-debugger.js',
     'map-uri-to-addon-id.js',
     'shapes-utils.js',
     'source-actor-store.js',
     'stack.js',
     'TabSources.js',
     'walker-search.js',
 )