Flex: Draw self-alignment at the container level in flex overlay

This draws a thicker line where the align-items value of the flex
container is, as well as an arrow (or 2 arrows) pointing to where
alignment happens.

Baseline alignment is missing as it will require specific support from
the backend (the offset of the baseline).

Backend CL: https://chromium-review.googlesource.com/c/chromium/src/+/2552282/

Screenshots: https://imgur.com/a/uvkKJIl

Bug: 1139949
Change-Id: Ie9387714abc8b9953deb38e5b89f5346206dcc13
Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/2552427
Commit-Queue: Patrick Brosset <patrick.brosset@microsoft.com>
Reviewed-by: Alex Rudenko <alexrudenko@chromium.org>
diff --git a/front_end/elements/StylesSidebarPane.js b/front_end/elements/StylesSidebarPane.js
index c78f03a..c4773b8 100644
--- a/front_end/elements/StylesSidebarPane.js
+++ b/front_end/elements/StylesSidebarPane.js
@@ -49,20 +49,18 @@
 // Highlightable properties are those that can be hovered in the sidebar to trigger a specific
 // highlighting mode on the current element.
 const HIGHLIGHTABLE_PROPERTIES = [
-  {property: 'padding', mode: 'padding'},
-  {property: 'border', mode: 'border'},
-  {property: 'margin', mode: 'margin'},
-  {property: 'grid-gap', mode: 'gap'},
-  {property: 'gap', mode: 'gap'},
-  {property: 'grid-column-gap', mode: 'column-gap'},
-  {property: 'grid-row-gap', mode: 'row-gap'},
-  {property: 'column-gap', mode: 'column-gap'},
-  {property: 'row-gap', mode: 'row-gap'},
-  {property: 'grid-template-areas', mode: 'grid-areas'},
-  {property: 'grid-template-columns', mode: 'grid-template-columns'},
-  {property: 'grid-template-rows', mode: 'grid-template-rows'},
-  {property: 'justify-content', mode: 'justify-content'},
-  {property: 'align-content', mode: 'align-content'},
+  {mode: 'padding', properties: ['padding']},
+  {mode: 'border', properties: ['border']},
+  {mode: 'margin', properties: ['margin']},
+  {mode: 'gap', properties: ['gap', 'grid-gap']},
+  {mode: 'column-gap', properties: ['column-gap', 'grid-column-gap']},
+  {mode: 'row-gap', properties: ['row-gap', 'grid-row-gap']},
+  {mode: 'grid-template-columns', properties: ['grid-template-columns']},
+  {mode: 'grid-template-rows', properties: ['grid-template-rows']},
+  {mode: 'grid-template-areas', properties: ['grid-areas']},
+  {mode: 'justify-content', properties: ['justify-content']},
+  {mode: 'align-content', properties: ['align-content']},
+  {mode: 'align-items', properties: ['align-items']},
 ];
 
 /** @type {!StylesSidebarPane} */
@@ -554,8 +552,8 @@
 
     const rule = treeElement.property.ownerStyle.parentRule;
     const selectorList = (rule instanceof SDK.CSSRule.CSSStyleRule) ? rule.selectorText() : undefined;
-    for (const {property, mode} of HIGHLIGHTABLE_PROPERTIES) {
-      if (!treeElement.name.startsWith(property)) {
+    for (const {properties, mode} of HIGHLIGHTABLE_PROPERTIES) {
+      if (!properties.includes(treeElement.name)) {
         continue;
       }
       const node = this.node();
diff --git a/front_end/sdk/OverlayModel.js b/front_end/sdk/OverlayModel.js
index 3ce1d14..9daed1c 100644
--- a/front_end/sdk/OverlayModel.js
+++ b/front_end/sdk/OverlayModel.js
@@ -547,7 +547,7 @@
             pattern: Protocol.Overlay.LineStylePattern.Dashed,
           },
           mainDistributedSpace: {hatchColor: Common.Color.PageHighlight.FlexContainerBorder.toProtocolRGBA()},
-          crossDistributedSpace: {hatchColor: Common.Color.PageHighlight.FlexContainerBorder.toProtocolRGBA()}
+          crossDistributedSpace: {hatchColor: Common.Color.PageHighlight.FlexContainerBorder.toProtocolRGBA()},
         };
       }
     }
@@ -618,6 +618,20 @@
       };
     }
 
+    if (mode === 'align-items' && this._flexFeaturesExperimentEnabled) {
+      highlightConfig.flexContainerHighlightConfig = {
+        containerBorder: {
+          color: Common.Color.PageHighlight.FlexContainerBorder.toProtocolRGBA(),
+          pattern: Protocol.Overlay.LineStylePattern.Dashed
+        },
+        lineSeparator: {
+          color: Common.Color.PageHighlight.FlexContainerBorder.toProtocolRGBA(),
+          pattern: Protocol.Overlay.LineStylePattern.Dashed
+        },
+        crossAlignment: {color: Common.Color.PageHighlight.FlexContainerBorder.toProtocolRGBA()}
+      };
+    }
+
     // the backend does not support the 'original' format because
     // it currently cannot retrieve the original format using computed styles
     const supportedColorFormats = new Set(['rgb', 'hsl', 'hex']);
diff --git a/inspector_overlay/BUILD.gn b/inspector_overlay/BUILD.gn
index 0c92fa9..92e6db3 100644
--- a/inspector_overlay/BUILD.gn
+++ b/inspector_overlay/BUILD.gn
@@ -37,7 +37,7 @@
 
   args = [
     "main.js",
-    "90000",  # max_size in bytes
+    "93000",  # max_size in bytes
     "--input_path",
     rebase_path(target_gen_dir, root_build_dir),
     "--output_path",
diff --git a/inspector_overlay/debug/tool_highlight_flex_alignment.html b/inspector_overlay/debug/tool_highlight_flex_alignment.html
new file mode 100644
index 0000000..326050c
--- /dev/null
+++ b/inspector_overlay/debug/tool_highlight_flex_alignment.html
@@ -0,0 +1,88 @@
+<!--
+Copyright 2020 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+<!DOCTYPE html>
+<html lang="en">
+<head>
+  <meta charset="utf-8">
+  <title>Debug</title>
+  <meta name="viewport" content="width=device-width,initial-scale=1">
+  <script type="module" src="/out/Default/resources/inspector_overlay/main.js"></script>
+</head>
+<body>
+  <script type="module">
+    const viewportWidth = document.documentElement.clientWidth;
+    const viewportHeight = document.documentElement.clientHeight;
+
+    dispatch(["setPlatform", "mac"]);
+    dispatch(["setOverlay", "highlight"]);
+    dispatch(["reset", {
+      viewportSize: {
+        width: viewportWidth,
+        height: viewportHeight,
+      },
+      deviceScaleFactor: 1,
+      pageScaleFactor: 1,
+      pageZoomFactor: 1,
+      emulationScaleFactor: 1,
+      scrollX: 0,
+      scrollY: 0,
+    }]);
+
+    let response = { "highlight": { "paths": [ { "path": [ "M", 633.3333740234375, 291.3333435058594, "L", 933.3333740234375, 291.3333435058594, "L", 933.3333740234375, 591.3333740234375, "L", 633.3333740234375, 591.3333740234375, "Z" ], "fillColor": "rgba(255, 0, 0, 0)", "outlineColor": "rgba(128, 0, 0, 0)", "name": "content" }, { "path": [ "M", 633.3333740234375, 291.3333435058594, "L", 933.3333740234375, 291.3333435058594, "L", 933.3333740234375, 591.3333740234375, "L", 633.3333740234375, 591.3333740234375, "Z" ], "fillColor": "rgba(0, 255, 0, 0)", "name": "padding" }, { "path": [ "M", 633.3333740234375, 291.3333435058594, "L", 933.3333740234375, 291.3333435058594, "L", 933.3333740234375, 591.3333740234375, "L", 633.3333740234375, 591.3333740234375, "Z" ], "fillColor": "rgba(0, 0, 255, 0)", "name": "border" }, { "path": [ "M", 633.3333740234375, 291.3333435058594, "L", 1186.666748046875, 291.3333435058594, "L", 1186.666748046875, 591.3333740234375, "L", 633.3333740234375, 591.3333740234375, "Z" ], "fillColor": "rgba(255, 255, 255, 0)", "name": "margin" } ], "showRulers": true, "showExtensionLines": true, "showAccessibilityInfo": true, "colorFormat": "hex", "elementInfo": { "tagName": "div", "idValue": "test-multi-line-space", "nodeWidth": "300", "nodeHeight": "300", "isKeyboardFocusable": false, "accessibleName": "", "accessibleRole": "generic", "layoutObjectName": "LayoutNGFlexibleBox", "showAccessibilityInfo": true }, "flexInfo": [
+      { "containerBorder": [ "M", 633.3333740234375, 291.3333435058594, "L", 933.3333740234375, 291.3333435058594, "L", 933.3333740234375, 591.3333740234375, "L", 633.3333740234375, 591.3333740234375, "Z" ],
+      "lines": [
+        [
+          [ "M", 650, 350, "L", 730, 350, "L", 730, 390, "L", 650, 390, "Z" ],
+          [ "M", 743.3333740234375, 352.3333435058594, "L", 823.3333740234375, 352.3333435058594, "L", 823.3333740234375, 371, "L", 743.3333740234375, 371, "Z" ],
+          [ "M", 823.3333740234375, 330, "L", 903.3333740234375, 330, "L", 903.3333740234375, 380, "L", 823.3333740234375, 380, "Z" ]
+        ],
+        [
+          [ "M", 663.3333740234375, 420, "L", 743.3333740234375, 420, "L", 743.3333740234375, 450.66668701171875, "L", 663.3333740234375, 450.66668701171875, "Z" ],
+          [ "M", 743.3333740234375, 432, "L", 823.3333740234375, 432, "L", 823.3333740234375, 440.66668701171875, "L", 743.3333740234375, 440.66668701171875, "Z" ],
+          [ "M", 830, 432, "L", 903.3333740234375, 432, "L", 903.3333740234375, 465, "L", 830, 465, "Z" ]
+        ],
+        [
+          [ "M", 743.3333740234375, 511.66668701171875, "L", 823.3333740234375, 511.66668701171875, "L", 823.3333740234375, 530.3333740234375, "L", 743.3333740234375, 530.3333740234375, "Z" ]
+        ]
+      ],
+     "alignItemsStyle": "center",
+      "isHorizontalFlow": true,
+      "flexContainerHighlightConfig": {
+        "containerBorder": {
+          "color": "rgba(255, 0, 0, 1)",
+          "pattern": "dashed"
+        },
+        "lineSeparator": {
+          "color": "rgba(0, 0, 255, 1)",
+          "pattern": "dashed"
+        },
+        "itemSeparator": {
+          "color": "rgba(0, 255, 0, 1)",
+          "pattern": "dotted"
+        },
+        "mainDistributedSpace": {
+          "hatchColor": "red",
+        },
+        "crossDistributedSpace": {
+          "hatchColor": "blue",
+        },
+        "crossAlignment": {
+          "color": "red",
+          "pattern": "solid"
+        }
+      }
+    } ] } }
+
+    // Massage the data a bit so we can see a nice flex overlay.
+    response = response.highlight;
+    delete response.elementInfo;
+    response.showRulers = false;
+    response.showExtensionLines = false;
+
+    dispatch(["drawHighlight", response]);
+  </script>
+</body>
+</html>
diff --git a/inspector_overlay/debug/tool_highlight_flex_transformed.html b/inspector_overlay/debug/tool_highlight_flex_transformed.html
new file mode 100644
index 0000000..8412910
--- /dev/null
+++ b/inspector_overlay/debug/tool_highlight_flex_transformed.html
@@ -0,0 +1,45 @@
+<!--
+Copyright 2020 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+<!DOCTYPE html>
+<html lang="en">
+<head>
+  <meta charset="utf-8">
+  <title>Debug</title>
+  <meta name="viewport" content="width=device-width,initial-scale=1">
+  <script type="module" src="/out/Default/resources/inspector_overlay/main.js"></script>
+</head>
+<body>
+  <script type="module">
+    const viewportWidth = document.documentElement.clientWidth;
+    const viewportHeight = document.documentElement.clientHeight;
+
+    dispatch(["setPlatform", "mac"]);
+    dispatch(["setOverlay", "highlight"]);
+    dispatch(["reset", {
+      viewportSize: {
+        width: viewportWidth,
+        height: viewportHeight,
+      },
+      deviceScaleFactor: 1,
+      pageScaleFactor: 1,
+      pageZoomFactor: 1,
+      emulationScaleFactor: 1,
+      scrollX: 0,
+      scrollY: 0,
+    }]);
+
+    let response = { "highlight": { "paths": [ { "path": [ "M", 195.80020141601562, 30.00569725036621, "L", 467.17559814453125, 199.5798797607422, "L", 308.1998291015625, 453.99432373046875, "L", 36.82441711425781, 284.4201354980469, "Z" ], "fillColor": "rgba(255, 0, 0, 0)", "outlineColor": "rgba(128, 0, 0, 0)", "name": "content" }, { "path": [ "M", 195.80020141601562, 30.00569725036621, "L", 467.17559814453125, 199.5798797607422, "L", 308.1998291015625, 453.99432373046875, "L", 36.82441711425781, 284.4201354980469, "Z" ], "fillColor": "rgba(0, 255, 0, 0)", "name": "padding" }, { "path": [ "M", 195.80020141601562, 30.00569725036621, "L", 467.17559814453125, 199.5798797607422, "L", 308.1998291015625, 453.99432373046875, "L", 36.82441711425781, 284.4201354980469, "Z" ], "fillColor": "rgba(0, 0, 255, 0)", "name": "border" }, { "path": [ "M", 195.80020141601562, 30.00569725036621, "L", 808.65625, 412.960693359375, "L", 649.6805419921875, 667.3751220703125, "L", 36.82441711425781, 284.4201354980469, "Z" ], "fillColor": "rgba(255, 255, 255, 0)", "name": "margin" } ], "showRulers": true, "showExtensionLines": true, "showAccessibilityInfo": true, "colorFormat": "hex", "elementInfo": { "tagName": "div", "idValue": "test-multi-line-space", "nodeWidth": "430.351", "nodeHeight": "423.989", "isKeyboardFocusable": false, "accessibleName": "", "accessibleRole": "generic", "layoutObjectName": "LayoutNGFlexibleBox", "showAccessibilityInfo": true }, "flexInfo": [ { "containerBorder": [ "M", 195.80020141601562, 30.00569725036621, "L", 467.17559814453125, 199.5798797607422, "L", 308.1998291015625, 453.99432373046875, "L", 36.82441711425781, 284.4201354980469, "Z" ], "lines": [ [ [ "M", 159.76568603515625, 87.6729736328125, "L", 227.6095428466797, 130.0665283203125, "L", 202.17340087890625, 170.7728271484375, "L", 134.32955932617188, 128.3792724609375, "Z" ], [ "M", 227.6095428466797, 130.0665283203125, "L", 295.4533996582031, 172.46006774902344, "L", 270.01727294921875, 213.16636657714844, "L", 202.17340087890625, 170.7728271484375, "Z" ], [ "M", 295.4533996582031, 172.46006774902344, "L", 363.2972412109375, 214.85360717773438, "L", 337.8611145019531, 255.55990600585938, "L", 270.01727294921875, 213.16636657714844, "Z" ], [ "M", 363.2972412109375, 214.85360717773438, "L", 431.1410827636719, 257.24713134765625, "L", 405.7049560546875, 297.9534606933594, "L", 337.8611145019531, 255.55990600585938, "Z" ] ], [ [ "M", 132.21697998046875, 207.2433319091797, "L", 200.06082153320312, 249.63687133789062, "L", 174.62469482421875, 290.34320068359375, "L", 106.78085327148438, 247.9496307373047, "Z" ], [ "M", 200.06082153320312, 249.63687133789062, "L", 267.9046630859375, 292.0304260253906, "L", 242.4685516357422, 332.7367248535156, "L", 174.62469482421875, 290.34320068359375, "Z" ], [ "M", 267.9046630859375, 292.0304260253906, "L", 335.74853515625, 334.4239501953125, "L", 310.3124084472656, 375.1302490234375, "L", 242.4685516357422, 332.7367248535156, "Z" ] ] ], "isHorizontalFlow": true, "alignItemsStyle": "stretch", "flexContainerHighlightConfig": { "containerBorder": { "color": "rgba(255, 0, 0, 0.6)", "pattern": "dashed" }, "lineSeparator": { "color": "rgba(255, 0, 0, 0.6)", "pattern": "dashed" }, "itemSeparator": { "color": "rgba(255, 0, 0, 0.6)", "pattern": "dotted" }, "mainDistributedSpace": { "fillColor": "rgba(255, 0, 0, 0.6)", "hatchColor": "rgba(255, 0, 0, 0.6)" }, "crossDistributedSpace": { "fillColor": "rgba(255, 0, 0, 0.6)", "hatchColor": "rgba(255, 0, 0, 0.6)" }, "rowGapSpace": { "fillColor": "rgba(255, 0, 0, 0.6)", "hatchColor": "rgba(255, 0, 0, 0.6)" }, "columnGapSpace": { "fillColor": "rgba(255, 0, 0, 0.6)", "hatchColor": "rgba(255, 0, 0, 0.6)" }, "crossAlignment": { "color": "red", "pattern": "solid" } } } ] } };
+
+    // Massage the data a bit so we can see a nice flex overlay.
+    response = response.highlight;
+    delete response.elementInfo;
+    response.showRulers = false;
+    response.showExtensionLines = false;
+
+    dispatch(["drawHighlight", response]);
+  </script>
+</body>
+</html>
diff --git a/inspector_overlay/highlight_common.ts b/inspector_overlay/highlight_common.ts
index 34d0108..be021cd 100644
--- a/inspector_overlay/highlight_common.ts
+++ b/inspector_overlay/highlight_common.ts
@@ -53,11 +53,12 @@
   Dashed = 'dashed'
 }
 
-export function drawPathWithLineStyle(context: CanvasRenderingContext2D, path: Path2D, lineStyle?: LineStyle) {
+export function drawPathWithLineStyle(
+    context: CanvasRenderingContext2D, path: Path2D, lineStyle?: LineStyle, lineWidth: number = 1) {
   if (lineStyle && lineStyle.color) {
     context.save();
     context.translate(0.5, 0.5);
-    context.lineWidth = 1;
+    context.lineWidth = lineWidth;
     if (lineStyle.pattern === LinePattern.Dashed) {
       context.setLineDash([3, 3]);
     }
diff --git a/inspector_overlay/highlight_flex_common.ts b/inspector_overlay/highlight_flex_common.ts
index d5db641..cee3e46 100644
--- a/inspector_overlay/highlight_flex_common.ts
+++ b/inspector_overlay/highlight_flex_common.ts
@@ -9,6 +9,7 @@
   containerBorder: PathCommands;
   lines: Array<Array<PathCommands>>;
   isHorizontalFlow: boolean;
+  alignItemsStyle: string;
   flexContainerHighlightConfig: {
     containerBorder?: LineStyle;
     lineSeparator?: LineStyle;
@@ -17,6 +18,7 @@
     crossDistributedSpace?: BoxStyle;
     rowGapSpace?: BoxStyle;
     columnGapSpace?: BoxStyle;
+    crossAlignment?: LineStyle;
   };
 }
 
@@ -25,6 +27,13 @@
   items: Quad[];
 }
 
+const ALIGNMENT_LINE_THICKNESS = 2;
+const ALIGNMENT_ARROW_BODY_HEIGHT = 5;
+const ALIGNMENT_ARROW_BODY_WIDTH = 5;
+const ALIGNMENT_ARROW_TIP_HEIGHT = 6;
+const ALIGNMENT_ARROW_TIP_WIDTH = 11;
+const ALIGNMENT_ARROW_DISTANCE_FROM_LINE = 2;
+
 export function drawLayoutFlexContainerHighlight(
     highlight: FlexContainerHighlight, context: CanvasRenderingContext2D, deviceScaleFactor: number,
     canvasWidth: number, canvasHeight: number, emulationScaleFactor: number) {
@@ -47,6 +56,9 @@
 
   // Draw the hatching pattern outside of items.
   drawFlexSpace(highlight, context, emulationScaleFactor, highlight.containerBorder, lineQuads);
+
+  // Draw the self-alignment lines and arrows.
+  drawFlexAlignment(highlight, context, emulationScaleFactor, lineQuads);
 }
 
 function drawFlexLinesAndItems(
@@ -129,15 +141,195 @@
   }
 }
 
+function drawFlexAlignment(
+    highlight: FlexContainerHighlight, context: CanvasRenderingContext2D, emulationScaleFactor: number,
+    lineQuads: LineQuads[]) {
+  for (const {quad} of lineQuads) {
+    drawFlexAlignmentForLine(highlight, context, emulationScaleFactor, quad);
+  }
+}
+
+function drawFlexAlignmentForLine(
+    highlight: FlexContainerHighlight, context: CanvasRenderingContext2D, emulationScaleFactor: number,
+    lineQuad: Quad) {
+  const {alignItemsStyle, isHorizontalFlow} = highlight;
+  const {crossAlignment} = highlight.flexContainerHighlightConfig;
+  if (!crossAlignment || !crossAlignment.color) {
+    return;
+  }
+
+  // Note that the order of the 2 points in the array matters as it is used to determine where the arrow will be drawn.
+  //
+  // first                second
+  // point                point
+  //   o--------------------o
+  //             ^
+  //             |
+  //           arrow
+  //
+  //
+  //           arrow
+  // second      |        first
+  // point       V        point
+  //   o--------------------o
+  const linesToDraw: [Position, Position][] = [];
+
+  switch (alignItemsStyle) {
+    case 'flex-start':
+      linesToDraw.push([
+        isHorizontalFlow ? lineQuad.p1 : lineQuad.p4,
+        isHorizontalFlow ? lineQuad.p2 : lineQuad.p1,
+      ]);
+      break;
+    case 'flex-end':
+      linesToDraw.push([
+        isHorizontalFlow ? lineQuad.p3 : lineQuad.p2,
+        isHorizontalFlow ? lineQuad.p4 : lineQuad.p3,
+      ]);
+      break;
+    case 'center':
+      if (isHorizontalFlow) {
+        linesToDraw.push([
+          {
+            x: (lineQuad.p1.x + lineQuad.p4.x) / 2,
+            y: (lineQuad.p1.y + lineQuad.p4.y) / 2,
+          },
+          {
+            x: (lineQuad.p2.x + lineQuad.p3.x) / 2,
+            y: (lineQuad.p2.y + lineQuad.p3.y) / 2,
+          },
+        ]);
+        linesToDraw.push([
+          {
+            x: (lineQuad.p2.x + lineQuad.p3.x) / 2,
+            y: (lineQuad.p2.y + lineQuad.p3.y) / 2,
+          },
+          {
+            x: (lineQuad.p1.x + lineQuad.p4.x) / 2,
+            y: (lineQuad.p1.y + lineQuad.p4.y) / 2,
+          },
+        ]);
+      } else {
+        linesToDraw.push([
+          {
+            x: (lineQuad.p1.x + lineQuad.p2.x) / 2,
+            y: (lineQuad.p1.y + lineQuad.p2.y) / 2,
+          },
+          {
+            x: (lineQuad.p3.x + lineQuad.p4.x) / 2,
+            y: (lineQuad.p3.y + lineQuad.p4.y) / 2,
+          },
+        ]);
+        linesToDraw.push([
+          {
+            x: (lineQuad.p3.x + lineQuad.p4.x) / 2,
+            y: (lineQuad.p3.y + lineQuad.p4.y) / 2,
+          },
+          {
+            x: (lineQuad.p1.x + lineQuad.p2.x) / 2,
+            y: (lineQuad.p1.y + lineQuad.p2.y) / 2,
+          },
+        ]);
+      }
+      break;
+    case 'stretch':
+    case 'normal':
+      linesToDraw.push([
+        isHorizontalFlow ? lineQuad.p1 : lineQuad.p4,
+        isHorizontalFlow ? lineQuad.p2 : lineQuad.p1,
+      ]);
+      linesToDraw.push([
+        isHorizontalFlow ? lineQuad.p3 : lineQuad.p2,
+        isHorizontalFlow ? lineQuad.p4 : lineQuad.p3,
+      ]);
+      break;
+  }
+
+  for (const points of linesToDraw) {
+    const path = segmentToPath(points);
+    drawPathWithLineStyle(
+        context, buildPath(path, emptyBounds(), emulationScaleFactor), crossAlignment, ALIGNMENT_LINE_THICKNESS);
+    drawAlignmentArrow(highlight, context, emulationScaleFactor, points[0], points[1]);
+  }
+}
+
+/**
+ * Draw an arrow pointed at the middle of a segment. The segment isn't necessarily vertical or horizontal.
+ *
+ * start          C             end
+ *  o-------------x--------------o
+ *               / \
+ *              /   \
+ *             /_   _\
+ *               | |
+ *               |_|
+ */
+function drawAlignmentArrow(
+    highlight: FlexContainerHighlight, context: CanvasRenderingContext2D, emulationScaleFactor: number,
+    startPoint: Position, endPoint: Position) {
+  const {crossAlignment} = highlight.flexContainerHighlightConfig;
+  if (!crossAlignment || !crossAlignment.color) {
+    return;
+  }
+
+  // The angle of the segment.
+  const angle = Math.atan2(endPoint.y - startPoint.y, endPoint.x - startPoint.x);
+
+  // Where the tip of the arrow meets the segment, plus some offset so they don't overlap.
+  const C = {
+    x: (-ALIGNMENT_ARROW_DISTANCE_FROM_LINE * Math.cos(angle - .5 * Math.PI)) + ((startPoint.x + endPoint.x) / 2),
+    y: (-ALIGNMENT_ARROW_DISTANCE_FROM_LINE * Math.sin(angle - .5 * Math.PI)) + ((startPoint.y + endPoint.y) / 2),
+  };
+
+  const path = buildPath(
+      [
+        'M',
+        C.x,
+        C.y,
+        'L',
+        C.x + (ALIGNMENT_ARROW_TIP_WIDTH / 2),
+        C.y + ALIGNMENT_ARROW_TIP_HEIGHT,
+        'L',
+        C.x + (ALIGNMENT_ARROW_BODY_WIDTH / 2),
+        C.y + ALIGNMENT_ARROW_TIP_HEIGHT,
+        'L',
+        C.x + (ALIGNMENT_ARROW_BODY_WIDTH / 2),
+        C.y + ALIGNMENT_ARROW_TIP_HEIGHT + ALIGNMENT_ARROW_BODY_HEIGHT,
+        'L',
+        C.x - (ALIGNMENT_ARROW_BODY_WIDTH / 2),
+        C.y + ALIGNMENT_ARROW_TIP_HEIGHT + ALIGNMENT_ARROW_BODY_HEIGHT,
+        'L',
+        C.x - (ALIGNMENT_ARROW_BODY_WIDTH / 2),
+        C.y + ALIGNMENT_ARROW_TIP_HEIGHT,
+        'L',
+        C.x - (ALIGNMENT_ARROW_TIP_WIDTH / 2),
+        C.y + ALIGNMENT_ARROW_TIP_HEIGHT,
+        'Z',
+      ],
+      emptyBounds(), emulationScaleFactor);
+
+  context.save();
+  context.translate(C.x, C.y);
+  context.rotate(angle);
+  context.translate(-C.x, -C.y);
+
+  context.fillStyle = crossAlignment.color;
+  context.fill(path);
+
+  context.restore();
+}
+
 function drawHatchPatternInQuad(
     outerQuad: Quad, quadsToClip: Quad[], boxStyle: BoxStyle|undefined, context: CanvasRenderingContext2D,
     emulationScaleFactor: number) {
   if (!boxStyle || !boxStyle.hatchColor) {
     return;
   }
+
+  const angle = Math.atan2(outerQuad.p2.y - outerQuad.p1.y, outerQuad.p2.x - outerQuad.p1.x) * 180 / Math.PI;
   const bounds = emptyBounds();
   const path = createPathForQuad(outerQuad, quadsToClip, bounds, emulationScaleFactor);
-  hatchFillPath(context, path, bounds, 10, boxStyle.hatchColor, 0, false);
+  hatchFillPath(context, path, bounds, 10, boxStyle.hatchColor, angle, false);
 }
 
 /**
@@ -194,6 +386,10 @@
   return skipEndLine ? startLine : [...startLine, 'M', quad.p3.x, quad.p3.y, 'L', quad.p2.x, quad.p2.y];
 }
 
+function segmentToPath(segment: [Position, Position]): PathCommands {
+  return ['M', segment[0].x, segment[0].y, 'L', segment[1].x, segment[1].y];
+}
+
 /**
  * Transform a path array (as returned by the backend) that corresponds to a rectangle into a quad.
  * @param commands