Bug 1373339 - Add a button in the rules view to toggle the CSS shapes highlighter. r=gl
authorMike Park <mikeparkms@gmail.com>
Tue, 20 Jun 2017 11:23:32 -0400
changeset 421847 d425977fef75c471fa8993a1d6e24b3143ca4800
parent 421719 0bd090362de77a8bf247e3c4f97a9f76f9afc04d
child 421848 a9c11adcdfc7bada2745817f159cb23a7d65be5c
push id1517
push userjlorenzo@mozilla.com
push dateThu, 14 Sep 2017 16:50:54 +0000
treeherdermozilla-release@3b41fd564418 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersgl
bugs1373339
milestone56.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 1373339 - Add a button in the rules view to toggle the CSS shapes highlighter. r=gl Requires pref "devtools.inspector.shapesHighlighter.enabled" to be true. MozReview-Commit-ID: Ispw7ulV5o6
devtools/client/inspector/rules/test/browser.ini
devtools/client/inspector/rules/test/browser_rules_grid-highlighter-restored-after-reload.js
devtools/client/inspector/rules/test/browser_rules_shapes-toggle_01.js
devtools/client/inspector/rules/test/browser_rules_shapes-toggle_02.js
devtools/client/inspector/rules/test/browser_rules_shapes-toggle_03.js
devtools/client/inspector/rules/test/browser_rules_shapes-toggle_04.js
devtools/client/inspector/rules/test/browser_rules_shapes-toggle_05.js
devtools/client/inspector/rules/test/browser_rules_shapes-toggle_06.js
devtools/client/inspector/rules/test/head.js
devtools/client/inspector/rules/views/text-property-editor.js
devtools/client/inspector/shared/highlighters-overlay.js
devtools/client/preferences/devtools.js
devtools/client/shared/output-parser.js
devtools/client/themes/rules.css
devtools/server/actors/highlighters/shapes.js
devtools/shared/css/properties-db.js
--- a/devtools/client/inspector/rules/test/browser.ini
+++ b/devtools/client/inspector/rules/test/browser.ini
@@ -228,16 +228,22 @@ subsuite = clipboard
 skip-if = (os == 'linux' && bits == 32 && debug) # bug 1328915, disable linux32 debug devtools for timeouts
 [browser_rules_selector-highlighter-on-navigate.js]
 [browser_rules_selector-highlighter_01.js]
 [browser_rules_selector-highlighter_02.js]
 [browser_rules_selector-highlighter_03.js]
 [browser_rules_selector-highlighter_04.js]
 [browser_rules_selector-highlighter_05.js]
 [browser_rules_selector_highlight.js]
+[browser_rules_shapes-toggle_01.js]
+[browser_rules_shapes-toggle_02.js]
+[browser_rules_shapes-toggle_03.js]
+[browser_rules_shapes-toggle_04.js]
+[browser_rules_shapes-toggle_05.js]
+[browser_rules_shapes-toggle_06.js]
 [browser_rules_shorthand-overridden-lists.js]
 [browser_rules_strict-search-filter-computed-list_01.js]
 [browser_rules_strict-search-filter_01.js]
 [browser_rules_strict-search-filter_02.js]
 [browser_rules_strict-search-filter_03.js]
 [browser_rules_style-editor-link.js]
 skip-if = !debug # Bug 1309759
 [browser_rules_url-click-opens-new-tab.js]
--- a/devtools/client/inspector/rules/test/browser_rules_grid-highlighter-restored-after-reload.js
+++ b/devtools/client/inspector/rules/test/browser_rules_grid-highlighter-restored-after-reload.js
@@ -45,24 +45,24 @@ add_task(function* () {
   info("Toggling ON the CSS grid highlighter from the rule-view.");
   let onHighlighterShown = highlighters.once("grid-highlighter-shown");
   gridToggle.click();
   yield onHighlighterShown;
 
   ok(highlighters.gridHighlighterShown, "CSS grid highlighter is shown.");
 
   info("Reload the page, expect the highlighter to be displayed once again");
-  let onStateRestored = highlighters.once("state-restored");
+  let onStateRestored = highlighters.once("grid-state-restored");
   yield refreshTab(gBrowser.selectedTab);
   let { restored } = yield onStateRestored;
   ok(restored, "The highlighter state was restored");
 
   info("Check that the grid highlighter can be displayed after reloading the page");
   ok(highlighters.gridHighlighterShown, "CSS grid highlighter is shown.");
 
   info("Navigate to another URL, and check that the highlighter is hidden");
   let otherUri = "data:text/html;charset=utf-8," + encodeURIComponent(OTHER_URI);
-  onStateRestored = highlighters.once("state-restored");
+  onStateRestored = highlighters.once("grid-state-restored");
   yield navigateTo(inspector, otherUri);
   ({ restored } = yield onStateRestored);
   ok(!restored, "The highlighter state was not restored");
   ok(!highlighters.gridHighlighterShown, "CSS grid highlighter is hidden.");
 });
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_shapes-toggle_01.js
@@ -0,0 +1,63 @@
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test toggling the shapes highlighter in the rule view and the display of the
+// shapes highlighter.
+
+const TEST_URI = `
+  <style type='text/css'>
+    #shape {
+      width: 800px;
+      height: 800px;
+      clip-path: circle(25%);
+    }
+  </style>
+  <div id="shape"></div>
+`;
+
+const HIGHLIGHTER_TYPE = "ShapesHighlighter";
+
+add_task(function* () {
+  yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+  let {inspector, view} = yield openRuleView();
+  let highlighters = view.highlighters;
+
+  yield selectNode("#shape", inspector);
+  let container = getRuleViewProperty(view, "#shape", "clip-path").valueSpan;
+  let shapesToggle = container.querySelector(".ruleview-shape");
+
+  info("Checking the initial state of the CSS shape toggle in the rule-view.");
+  ok(shapesToggle, "Shapes highlighter toggle is visible.");
+  ok(!shapesToggle.classList.contains("active"),
+    "Shapes highlighter toggle button is not active.");
+  ok(!highlighters.highlighters[HIGHLIGHTER_TYPE],
+    "No CSS shapes highlighter exists in the rule-view.");
+  ok(!highlighters.shapesHighlighterShown, "No CSS shapes highlighter is shown.");
+
+  info("Toggling ON the CSS shapes highlighter from the rule-view.");
+  let onHighlighterShown = highlighters.once("shapes-highlighter-shown");
+  shapesToggle.click();
+  yield onHighlighterShown;
+
+  info("Checking the CSS shapes highlighter is created and toggle button is active in " +
+    "the rule-view.");
+  ok(shapesToggle.classList.contains("active"),
+    "Shapes highlighter toggle is active.");
+  ok(highlighters.highlighters[HIGHLIGHTER_TYPE],
+    "CSS shapes highlighter created in the rule-view.");
+  ok(highlighters.shapesHighlighterShown, "CSS shapes highlighter is shown.");
+
+  info("Toggling OFF the CSS shapes highlighter from the rule-view.");
+  let onHighlighterHidden = highlighters.once("shapes-highlighter-hidden");
+  shapesToggle.click();
+  yield onHighlighterHidden;
+
+  info("Checking the CSS shapes highlighter is not shown and toggle button is not " +
+    "active in the rule-view.");
+  ok(!shapesToggle.classList.contains("active"),
+    "shapes highlighter toggle button is not active.");
+  ok(!highlighters.shapesHighlighterShown, "No CSS shapes highlighter is shown.");
+});
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_shapes-toggle_02.js
@@ -0,0 +1,72 @@
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test toggling the shapes highlighter in the rule view from an overridden
+// declaration.
+
+const TEST_URI = `
+  <style type='text/css'>
+    #shape {
+      width: 800px;
+      height: 800px;
+      clip-path: circle(25%);
+    }
+    div {
+      clip-path: circle(30%);
+    }
+  </style>
+  <div id="shape"></div>
+`;
+
+const HIGHLIGHTER_TYPE = "ShapesHighlighter";
+
+add_task(function* () {
+  yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+  let {inspector, view} = yield openRuleView();
+  let highlighters = view.highlighters;
+
+  yield selectNode("#shape", inspector);
+  let container = getRuleViewProperty(view, "#shape", "clip-path").valueSpan;
+  let shapeToggle = container.querySelector(".ruleview-shape");
+  let overriddenContainer = getRuleViewProperty(view, "div", "clip-path").valueSpan;
+  let overriddenShapeToggle = overriddenContainer.querySelector(".ruleview-shape");
+
+  info("Checking the initial state of the CSS shapes toggle in the rule-view.");
+  ok(shapeToggle && overriddenShapeToggle, "Shapes highlighter toggles are visible.");
+  ok(!shapeToggle.classList.contains("active") &&
+    !overriddenShapeToggle.classList.contains("active"),
+    "Shapes highlighter toggle buttons are not active.");
+  ok(!highlighters.highlighters[HIGHLIGHTER_TYPE],
+    "No CSS shapes highlighter exists in the rule-view.");
+  ok(!highlighters.shapesHighlighterShown, "No CSS shapes highlighter is shown.");
+
+  info("Toggling ON the shapes highlighter from the overridden rule in the rule-view.");
+  let onHighlighterShown = highlighters.once("shapes-highlighter-shown");
+  overriddenShapeToggle.click();
+  yield onHighlighterShown;
+
+  info("Checking the shapes highlighter is created and toggle buttons are active in " +
+    "the rule-view.");
+  ok(shapeToggle.classList.contains("active") &&
+    overriddenShapeToggle.classList.contains("active"),
+    "shapes highlighter toggle is active.");
+  ok(highlighters.highlighters[HIGHLIGHTER_TYPE],
+    "CSS shapes highlighter created in the rule-view.");
+  ok(highlighters.shapesHighlighterShown, "CSS shapes highlighter is shown.");
+
+  info("Toggling off the shapes highlighter from the normal shapes declaration in the " +
+    "rule-view.");
+  let onHighlighterHidden = highlighters.once("shapes-highlighter-hidden");
+  shapeToggle.click();
+  yield onHighlighterHidden;
+
+  info("Checking the CSS shapes highlighter is not shown and toggle buttons are not " +
+    "active in the rule-view.");
+  ok(!shapeToggle.classList.contains("active") &&
+    !overriddenShapeToggle.classList.contains("active"),
+    "shapes highlighter toggle buttons are not active.");
+  ok(!highlighters.shapesHighlighterShown, "No CSS shapes highlighter is shown.");
+});
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_shapes-toggle_03.js
@@ -0,0 +1,92 @@
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test toggling the shapes highlighter in the rule view with multiple shapes in the page.
+
+const TEST_URI = `
+  <style type='text/css'>
+    .shape {
+      width: 800px;
+      height: 800px;
+      clip-path: circle(25%);
+    }
+  </style>
+  <div class="shape" id="shape1"></div>
+  <div class="shape" id="shape2"></div>
+`;
+
+const HIGHLIGHTER_TYPE = "ShapesHighlighter";
+
+add_task(function* () {
+  yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+  let {inspector, view} = yield openRuleView();
+  let highlighters = view.highlighters;
+
+  info("Selecting the first shape container.");
+  yield selectNode("#shape1", inspector);
+  let container = getRuleViewProperty(view, ".shape", "clip-path").valueSpan;
+  let shapeToggle = container.querySelector(".ruleview-shape");
+
+  info("Checking the state of the CSS shape toggle for the first shape container " +
+    "in the rule-view.");
+  ok(shapeToggle, "shape highlighter toggle is visible.");
+  ok(!shapeToggle.classList.contains("active"),
+    "shape highlighter toggle button is not active.");
+  ok(!highlighters.highlighters[HIGHLIGHTER_TYPE],
+    "No CSS shape highlighter exists in the rule-view.");
+  ok(!highlighters.shapesHighlighterShown, "No CSS shapes highlighter is shown.");
+
+  info("Toggling ON the CSS shapes highlighter for the first shapes container from the " +
+    "rule-view.");
+  let onHighlighterShown = highlighters.once("shapes-highlighter-shown");
+  shapeToggle.click();
+  yield onHighlighterShown;
+
+  info("Checking the CSS shapes highlighter is created and toggle button is active in " +
+    "the rule-view.");
+  ok(shapeToggle.classList.contains("active"),
+    "shapes highlighter toggle is active.");
+  ok(highlighters.highlighters[HIGHLIGHTER_TYPE],
+    "CSS shapes highlighter created in the rule-view.");
+  ok(highlighters.shapesHighlighterShown, "CSS shapes highlighter is shown.");
+
+  info("Selecting the second shapes container.");
+  yield selectNode("#shape2", inspector);
+  let firstShapesHighlighterShown = highlighters.shapesHighlighterShown;
+  container = getRuleViewProperty(view, ".shape", "clip-path").valueSpan;
+  shapeToggle = container.querySelector(".ruleview-shape");
+
+  info("Checking the state of the CSS shapes toggle for the second shapes container " +
+    "in the rule-view.");
+  ok(shapeToggle, "shapes highlighter toggle is visible.");
+  ok(!shapeToggle.classList.contains("active"),
+    "shapes highlighter toggle button is not active.");
+  ok(highlighters.shapesHighlighterShown, "CSS shapes highlighter is still shown.");
+
+  info("Toggling ON the CSS shapes highlighter for the second shapes container " +
+    "from the rule-view.");
+  onHighlighterShown = highlighters.once("shapes-highlighter-shown");
+  shapeToggle.click();
+  yield onHighlighterShown;
+
+  info("Checking the CSS shapes highlighter is created for the second shapes container " +
+    "and toggle button is active in the rule-view.");
+  ok(shapeToggle.classList.contains("active"),
+    "shapes highlighter toggle is active.");
+  ok(highlighters.shapesHighlighterShown != firstShapesHighlighterShown,
+    "shapes highlighter for the second shapes container is shown.");
+
+  info("Selecting the first shapes container.");
+  yield selectNode("#shape1", inspector);
+  container = getRuleViewProperty(view, ".shape", "clip-path").valueSpan;
+  shapeToggle = container.querySelector(".ruleview-shape");
+
+  info("Checking the state of the CSS shapes toggle for the first shapes container " +
+    "in the rule-view.");
+  ok(shapeToggle, "shapes highlighter toggle is visible.");
+  ok(!shapeToggle.classList.contains("active"),
+    "shapes highlighter toggle button is not active.");
+});
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_shapes-toggle_04.js
@@ -0,0 +1,46 @@
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test toggling the shapes highlighter in the rule view and modifying the 'clip-path'
+// declaration.
+
+const TEST_URI = `
+  <style type='text/css'>
+    #shape {
+      width: 800px;
+      height: 800px;
+      clip-path: circle(25%);
+    }
+  </style>
+  <div id="shape"></div>
+`;
+
+add_task(function* () {
+  yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+  let {inspector, view} = yield openRuleView();
+  let highlighters = view.highlighters;
+
+  yield selectNode("#shape", inspector);
+  let container = getRuleViewProperty(view, "#shape", "clip-path").valueSpan;
+  let shapeToggle = container.querySelector(".ruleview-shape");
+
+  info("Toggling ON the CSS shape highlighter from the rule-view.");
+  let onHighlighterShown = highlighters.once("shapes-highlighter-shown");
+  shapeToggle.click();
+  yield onHighlighterShown;
+
+  info("Edit the clip-path property to ellipse.");
+  let editor = yield focusEditableField(view, container, 30);
+  let onDone = view.once("ruleview-changed");
+  editor.input.value = "ellipse(30% 20%);";
+  EventUtils.synthesizeKey("VK_RETURN", {}, view.styleWindow);
+  yield onDone;
+
+  info("Check the shape highlighter and shape toggle button are still visible.");
+  shapeToggle = container.querySelector(".ruleview-shape");
+  ok(shapeToggle, "Shape highlighter toggle is visible.");
+  ok(highlighters.shapesHighlighterShown, "CSS shape highlighter is shown.");
+});
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_shapes-toggle_05.js
@@ -0,0 +1,43 @@
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that the shapes highlighter is hidden when the highlighted shape container is
+// removed from the page.
+
+const TEST_URI = `
+  <style type='text/css'>
+    #shape {
+      width: 800px;
+      height: 800px;
+      clip-path: circle(25%);
+    }
+  </style>
+  <div id="shape"></div>
+`;
+
+add_task(function* () {
+  yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+  let {inspector, view, testActor} = yield openRuleView();
+  let highlighters = view.highlighters;
+
+  yield selectNode("#shape", inspector);
+  let container = getRuleViewProperty(view, "#shape", "clip-path").valueSpan;
+  let shapeToggle = container.querySelector(".ruleview-shape");
+
+  info("Toggling ON the CSS shapes highlighter from the rule-view.");
+  let onHighlighterShown = highlighters.once("shapes-highlighter-shown");
+  shapeToggle.click();
+  yield onHighlighterShown;
+  ok(highlighters.shapesHighlighterShown, "CSS shapes highlighter is shown.");
+
+  let onHighlighterHidden = highlighters.once("shapes-highlighter-hidden");
+  info("Remove the #shapes container in the content page");
+  testActor.eval(`
+    content.document.querySelector("#shape").remove();
+  `);
+  yield onHighlighterHidden;
+  ok(!highlighters.shapesHighlighterShown, "CSS shapes highlighter is hidden.");
+});
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_shapes-toggle_06.js
@@ -0,0 +1,78 @@
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test toggling the shapes highlighter in the rule view with clip-path and shape-outside
+// on the same element.
+
+const TEST_URI = `
+  <style type='text/css'>
+    .shape {
+      width: 800px;
+      height: 800px;
+      clip-path: circle(25%);
+      shape-outside: circle(25%);
+    }
+  </style>
+  <div class="shape" id="shape1"></div>
+  <div class="shape" id="shape2"></div>
+`;
+
+add_task(function* () {
+  yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+  let {inspector, view} = yield openRuleView();
+  let highlighters = view.highlighters;
+
+  yield selectNode("#shape1", inspector);
+  let clipPathContainer = getRuleViewProperty(view, ".shape", "clip-path").valueSpan;
+  let clipPathShapeToggle = clipPathContainer.querySelector(".ruleview-shape");
+  let shapeOutsideContainer = getRuleViewProperty(view, ".shape",
+    "shape-outside").valueSpan;
+  let shapeOutsideToggle = shapeOutsideContainer.querySelector(".ruleview-shape");
+
+  info("Toggling ON the CSS shapes highlighter for clip-path from the rule-view.");
+  let onHighlighterShown = highlighters.once("shapes-highlighter-shown");
+  clipPathShapeToggle.click();
+  yield onHighlighterShown;
+  ok(highlighters.shapesHighlighterShown, "CSS shapes highlighter is shown.");
+  ok(clipPathShapeToggle.classList.contains("active"),
+     "clip-path toggle button is active.");
+  ok(!shapeOutsideToggle.classList.contains("active"),
+     "shape-outside toggle button is not active.");
+
+  info("Toggling ON the CSS shapes highlighter for shape-outside from the rule-view.");
+  onHighlighterShown = highlighters.once("shapes-highlighter-shown");
+  shapeOutsideToggle.click();
+  yield onHighlighterShown;
+  ok(highlighters.shapesHighlighterShown, "CSS shapes highlighter is shown.");
+  ok(!clipPathShapeToggle.classList.contains("active"),
+     "clip-path toggle button is not active.");
+  ok(shapeOutsideToggle.classList.contains("active"),
+     "shape-outside toggle button is active.");
+
+  info("Selecting the second shapes container.");
+  yield selectNode("#shape2", inspector);
+  clipPathContainer = getRuleViewProperty(view, ".shape", "clip-path").valueSpan;
+  clipPathShapeToggle = clipPathContainer.querySelector(".ruleview-shape");
+  shapeOutsideContainer = getRuleViewProperty(view, ".shape",
+    "shape-outside").valueSpan;
+  shapeOutsideToggle = shapeOutsideContainer.querySelector(".ruleview-shape");
+  ok(!clipPathShapeToggle.classList.contains("active"),
+     "clip-path toggle button is not active.");
+  ok(!shapeOutsideToggle.classList.contains("active"),
+     "shape-outside toggle button is not active.");
+
+  info("Selecting the first shapes container.");
+  yield selectNode("#shape1", inspector);
+  clipPathContainer = getRuleViewProperty(view, ".shape", "clip-path").valueSpan;
+  clipPathShapeToggle = clipPathContainer.querySelector(".ruleview-shape");
+  shapeOutsideContainer = getRuleViewProperty(view, ".shape",
+    "shape-outside").valueSpan;
+  shapeOutsideToggle = shapeOutsideContainer.querySelector(".ruleview-shape");
+  ok(!clipPathShapeToggle.classList.contains("active"),
+     "clip-path toggle button is not active.");
+  ok(shapeOutsideToggle.classList.contains("active"),
+     "shape-outside toggle button is active.");
+});
--- a/devtools/client/inspector/rules/test/head.js
+++ b/devtools/client/inspector/rules/test/head.js
@@ -18,18 +18,21 @@ var {getInplaceEditorForSpan: inplaceEdi
   require("devtools/client/shared/inplace-editor");
 
 const ROOT_TEST_DIR = getRootDirectory(gTestPath);
 const FRAME_SCRIPT_URL = ROOT_TEST_DIR + "doc_frame_script.js";
 
 const STYLE_INSPECTOR_L10N
       = new LocalizationHelper("devtools/shared/locales/styleinspector.properties");
 
+Services.prefs.setBoolPref("devtools.inspector.shapesHighlighter.enabled", true);
+
 registerCleanupFunction(() => {
   Services.prefs.clearUserPref("devtools.defaultColorUnit");
+  Services.prefs.clearUserPref("devtools.inspector.shapesHighlighter.enabled");
 });
 
 /**
  * The rule-view tests rely on a frame-script to be injected in the content test
  * page. So override the shared-head's addTab to load the frame script after the
  * tab was added.
  * FIXME: Refactor the rule-view tests to use the testActor instead of a frame
  * script, so they can run on remote targets too.
--- a/devtools/client/inspector/rules/views/text-property-editor.js
+++ b/devtools/client/inspector/rules/views/text-property-editor.js
@@ -353,16 +353,17 @@ TextPropertyEditor.prototype = {
       angleSwatchClass: SHARED_SWATCH_CLASS + " " + ANGLE_SWATCH_CLASS,
       bezierClass: "ruleview-bezier",
       bezierSwatchClass: SHARED_SWATCH_CLASS + " " + BEZIER_SWATCH_CLASS,
       colorClass: "ruleview-color",
       colorSwatchClass: SHARED_SWATCH_CLASS + " " + COLOR_SWATCH_CLASS,
       filterClass: "ruleview-filter",
       filterSwatchClass: SHARED_SWATCH_CLASS + " " + FILTER_SWATCH_CLASS,
       gridClass: "ruleview-grid",
+      shapeClass: "ruleview-shape",
       defaultColorType: !propDirty,
       urlClass: "theme-link",
       baseURI: this.sheetHref
     };
     let frag = outputParser.parseCssProperty(name, val, parserOptions);
     this.valueSpan.innerHTML = "";
     this.valueSpan.appendChild(frag);
 
@@ -437,16 +438,30 @@ TextPropertyEditor.prototype = {
     if (gridToggle) {
       gridToggle.setAttribute("title", l10n("rule.gridToggle.tooltip"));
       if (this.ruleView.highlighters.gridHighlighterShown ===
           this.ruleView.inspector.selection.nodeFront) {
         gridToggle.classList.add("active");
       }
     }
 
+    let shapeToggle = this.valueSpan.querySelector(".ruleview-shape");
+    if (shapeToggle) {
+      let mode = "css" + name.split("-").map(s => {
+        return s[0].toUpperCase() + s.slice(1);
+      }).join("");
+      shapeToggle.setAttribute("data-mode", mode);
+
+      let { highlighters, inspector } = this.ruleView;
+      if (highlighters.shapesHighlighterShown === inspector.selection.nodeFront &&
+          highlighters.state.shapes.options.mode === mode) {
+        shapeToggle.classList.add("active");
+      }
+    }
+
     // Now that we have updated the property's value, we might have a pending
     // click on the value container. If we do, we have to trigger a click event
     // on the right element.
     if (this._hasPendingClick) {
       this._hasPendingClick = false;
       let elToClick;
 
       if (this._clickedElementOptions !== null) {
--- a/devtools/client/inspector/shared/highlighters-overlay.js
+++ b/devtools/client/inspector/shared/highlighters-overlay.js
@@ -30,28 +30,32 @@ function HighlightersOverlay(inspector) 
   // NodeFront of element that is highlighted by the geometry editor.
   this.geometryEditorHighlighterShown = null;
   // NodeFront of the grid container that is highlighted.
   this.gridHighlighterShown = null;
   // Name of the highlighter shown on mouse hover.
   this.hoveredHighlighterShown = null;
   // Name of the selector highlighter shown.
   this.selectorHighlighterShown = null;
+  // NodeFront of the shape that is highlighted
+  this.shapesHighlighterShown = null;
   // Saved state to be restore on page navigation.
   this.state = {
-    // Only the grid highlighter state is saved at the moment.
-    grid: {}
+    grid: {},
+    shapes: {}
   };
 
   this.onClick = this.onClick.bind(this);
   this.onMarkupMutation = this.onMarkupMutation.bind(this);
   this.onMouseMove = this.onMouseMove.bind(this);
   this.onMouseOut = this.onMouseOut.bind(this);
   this.onWillNavigate = this.onWillNavigate.bind(this);
   this.onNavigate = this.onNavigate.bind(this);
+  this.showGridHighlighter = this.showGridHighlighter.bind(this);
+  this.showShapesHighlighter = this.showShapesHighlighter.bind(this);
   this._handleRejection = this._handleRejection.bind(this);
 
   // Add inspector events, not specific to a given view.
   this.inspector.on("markupmutation", this.onMarkupMutation);
   this.inspector.target.on("navigate", this.onNavigate);
   this.inspector.target.on("will-navigate", this.onWillNavigate);
 
   EventEmitter.decorate(this);
@@ -97,16 +101,91 @@ HighlightersOverlay.prototype = {
 
     let el = view.element;
     el.removeEventListener("click", this.onClick, true);
     el.removeEventListener("mousemove", this.onMouseMove);
     el.removeEventListener("mouseout", this.onMouseOut);
   },
 
   /**
+   * Toggle the shapes highlighter for the given element with a shape.
+   *
+   * @param  {NodeFront} node
+   *         The NodeFront of the element with a shape to highlight.
+   * @param  {Object} options
+   *         Object used for passing options to the shapes highlighter.
+   */
+  toggleShapesHighlighter: Task.async(function* (node, options = {}) {
+    if (node == this.shapesHighlighterShown &&
+        options.mode === this.state.shapes.options.mode) {
+      yield this.hideShapesHighlighter(node);
+      return;
+    }
+
+    yield this.showShapesHighlighter(node, options);
+  }),
+
+  /**
+   * Show the shapes highlighter for the given element with a shape.
+   *
+   * @param  {NodeFront} node
+   *         The NodeFront of the element with a shape to highlight.
+   * @param  {Object} options
+   *         Object used for passing options to the shapes highlighter.
+   */
+  showShapesHighlighter: Task.async(function* (node, options) {
+    let highlighter = yield this._getHighlighter("ShapesHighlighter");
+    if (!highlighter) {
+      return;
+    }
+
+    let isShown = yield highlighter.show(node, options);
+    if (!isShown) {
+      return;
+    }
+
+    this.shapesHighlighterShown = node;
+    let { mode } = options;
+    this._toggleRuleViewIcon(node, false, ".ruleview-shape");
+    this._toggleRuleViewIcon(node, true, `.ruleview-shape[data-mode='${mode}']`);
+
+    try {
+      // Save shapes highlighter state.
+      let { url } = this.inspector.target;
+      let selector = yield node.getUniqueSelector();
+      this.state.shapes = { selector, options, url };
+
+      this.shapesHighlighterShown = node;
+      this.emit("shapes-highlighter-shown", node, options);
+    } catch (e) {
+      this._handleRejection(e);
+    }
+  }),
+
+  /**
+   * Hide the shapes highlighter for the given element with a shape.
+   *
+   * @param  {NodeFront} node
+   *         The NodeFront of the element with a shape to unhighlight.
+   */
+  hideShapesHighlighter: Task.async(function* (node) {
+    if (!this.shapesHighlighterShown || !this.highlighters.ShapesHighlighter) {
+      return;
+    }
+
+    this._toggleRuleViewIcon(node, false, ".ruleview-shape");
+
+    yield this.highlighters.ShapesHighlighter.hide();
+    this.emit("shapes-highlighter-hidden", this.shapesHighlighterShown,
+      this.state.shapes.options);
+    this.shapesHighlighterShown = null;
+    this.state.shapes = {};
+  }),
+
+  /**
    * Toggle the grid highlighter for the given grid container element.
    *
    * @param  {NodeFront} node
    *         The NodeFront of the grid container element to highlight.
    * @param  {Object} options
    *         Object used for passing options to the grid highlighter.
    * @param. {String|null} trigger
    *         String name matching "grid" or "rule" to indicate where the
@@ -136,17 +215,17 @@ HighlightersOverlay.prototype = {
       return;
     }
 
     let isShown = yield highlighter.show(node, options);
     if (!isShown) {
       return;
     }
 
-    this._toggleRuleViewGridIcon(node, true);
+    this._toggleRuleViewIcon(node, true, ".ruleview-grid");
 
     if (trigger == "grid") {
       Services.telemetry.scalarAdd("devtools.grid.gridinspector.opened", 1);
     } else if (trigger == "rule") {
       Services.telemetry.scalarAdd("devtools.rules.gridinspector.opened", 1);
     }
 
     try {
@@ -170,17 +249,17 @@ HighlightersOverlay.prototype = {
    * @param  {NodeFront} node
    *         The NodeFront of the grid container element to unhighlight.
    */
   hideGridHighlighter: Task.async(function* (node) {
     if (!this.gridHighlighterShown || !this.highlighters.CssGridHighlighter) {
       return;
     }
 
-    this._toggleRuleViewGridIcon(node, false);
+    this._toggleRuleViewIcon(node, false, ".ruleview-grid");
 
     yield this.highlighters.CssGridHighlighter.hide();
 
     // Emit the NodeFront of the grid container element that the grid highlighter was
     // hidden for.
     this.emit("grid-highlighter-hidden", this.gridHighlighterShown,
       this.state.grid.options);
     this.gridHighlighterShown = null;
@@ -237,42 +316,49 @@ HighlightersOverlay.prototype = {
     yield this.highlighters.GeometryEditorHighlighter.hide();
 
     this.emit("geometry-editor-highlighter-hidden");
     this.geometryEditorHighlighterShown = null;
   }),
 
   /**
    * Restore the saved highlighter states.
-   *
+   * @param {String} name
+   *        The name of the highlighter to be restored
+   * @param {String} selector
+   *        The selector of the node that was previously highlighted
+   * @param {Object} options
+   *        The options previously supplied to the highlighter
+   * @param {String} url
+   *        The URL of the page the highlighter was active on
+   * @param {Function} showFunction
+   *        The function that shows the highlighter
    * @return {Promise} that resolves when the highlighter state was restored, and the
-   *          expected highlighters are displayed.
+   *         expected highlighters are displayed.
    */
-  restoreState: Task.async(function* () {
-    let { selector, options, url } = this.state.grid;
-
+  restoreState: Task.async(function* (name, {selector, options, url}, showFunction) {
     if (!selector || url !== this.inspector.target.url) {
       // Bail out if no selector was saved, or if we are on a different page.
-      this.emit("state-restored", { restored: false });
+      this.emit(`${name}-state-restored`, { restored: false });
       return;
     }
 
     // Wait for the new root to be ready in the inspector.
     yield this.onInspectorNewRoot;
 
     let walker = this.inspector.walker;
     let rootNode = yield walker.getRootNode();
     let nodeFront = yield walker.querySelector(rootNode, selector);
 
     if (nodeFront) {
-      yield this.showGridHighlighter(nodeFront, options);
-      this.emit("state-restored", { restored: true });
+      yield showFunction(nodeFront, options);
+      this.emit(`${name}-state-restored`, { restored: true });
     }
 
-    this.emit("state-restored", { restored: false });
+    this.emit(`${name}-state-restored`, { restored: false });
   }),
 
   /**
    * Get a highlighter front given a type. It will only be initialized once.
    *
    * @param  {String} type
    *         The highlighter type. One of this.highlighters.
    * @return {Promise} that resolves to the highlighter
@@ -302,33 +388,35 @@ HighlightersOverlay.prototype = {
 
   _handleRejection: function (error) {
     if (!this.destroyed) {
       console.error(error);
     }
   },
 
   /**
-   * Toggle all the grid icons in the rule view if the current inspector selection is the
-   * highlighted node.
+   * Toggle all the icons with the given selector in the rule view if the current
+   * inspector selection is the highlighted node.
    *
    * @param  {NodeFront} node
-   *         The NodeFront of the grid container element to highlight.
+   *         The NodeFront of the element with a shape to highlight.
    * @param  {Boolean} active
-   *         Whether or not the grid icon should be active.
+   *         Whether or not the shape icon should be active.
+   * @param  {String} selector
+   *         The selector of the rule view icon to toggle.
    */
-  _toggleRuleViewGridIcon: function (node, active) {
+  _toggleRuleViewIcon: function (node, active, selector) {
     if (this.inspector.selection.nodeFront != node) {
       return;
     }
 
     let ruleViewEl = this.inspector.getPanel("ruleview").view.element;
 
-    for (let gridIcon of ruleViewEl.querySelectorAll(".ruleview-grid")) {
-      gridIcon.classList.toggle("active", active);
+    for (let icon of ruleViewEl.querySelectorAll(selector)) {
+      icon.classList.toggle("active", active);
     }
   },
 
   /**
    * Hide the currently shown hovered highlighter.
    */
   _hideHoveredHighlighter: function () {
     if (!this.hoveredHighlighterShown ||
@@ -369,46 +457,59 @@ HighlightersOverlay.prototype = {
    * @param  {DOMNode} node
    * @return {Boolean}
    */
   _isRuleViewDisplayGrid: function (node) {
     return this.isRuleView && node.classList.contains("ruleview-grid");
   },
 
   /**
+   * Does the current clicked node have the shapes highlighter toggle in the
+   * rule-view.
+   *
+   * @param  {DOMNode} node
+   * @return {Boolean}
+   */
+  _isRuleViewShape: function (node) {
+    return this.isRuleView && node.classList.contains("ruleview-shape");
+  },
+
+  /**
    * Is the current hovered node a css transform property value in the rule-view.
    *
    * @param  {Object} nodeInfo
    * @return {Boolean}
    */
   _isRuleViewTransform: function (nodeInfo) {
     let isTransform = nodeInfo.type === VIEW_NODE_VALUE_TYPE &&
                       nodeInfo.value.property === "transform";
     let isEnabled = nodeInfo.value.enabled &&
                     !nodeInfo.value.overridden &&
                     !nodeInfo.value.pseudoElement;
     return this.isRuleView && isTransform && isEnabled;
   },
 
   onClick: function (event) {
-    // Bail out if the target is not a grid property value.
-    if (!this._isRuleViewDisplayGrid(event.target)) {
-      return;
-    }
+    if (this._isRuleViewDisplayGrid(event.target)) {
+      event.stopPropagation();
 
-    event.stopPropagation();
+      let { store } = this.inspector;
+      let { grids, highlighterSettings } = store.getState();
+      let grid = grids.find(g => g.nodeFront == this.inspector.selection.nodeFront);
+
+      highlighterSettings.color = grid ? grid.color : DEFAULT_GRID_COLOR;
 
-    let { store } = this.inspector;
-    let { grids, highlighterSettings } = store.getState();
-    let grid = grids.find(g => g.nodeFront == this.inspector.selection.nodeFront);
+      this.toggleGridHighlighter(this.inspector.selection.nodeFront, highlighterSettings,
+        "rule");
+    } else if (this._isRuleViewShape(event.target)) {
+      event.stopPropagation();
 
-    highlighterSettings.color = grid ? grid.color : DEFAULT_GRID_COLOR;
-
-    this.toggleGridHighlighter(this.inspector.selection.nodeFront, highlighterSettings,
-      "rule");
+      let settings = { mode: event.target.dataset.mode };
+      this.toggleShapesHighlighter(this.inspector.selection.nodeFront, settings);
+    }
   },
 
   onMouseMove: function (event) {
     // Bail out if the target is the same as for the last mousemove.
     if (event.target === this._lastHovered) {
       return;
     }
 
@@ -453,58 +554,75 @@ HighlightersOverlay.prototype = {
     }
 
     // Otherwise, hide the highlighter.
     this._lastHovered = null;
     this._hideHoveredHighlighter();
   },
 
   /**
-   * Handler function for "markupmutation" events. Hides the grid highlighter if the grid
-   * container is no longer in the DOM tree.
+   * Handler function for "markupmutation" events. Hides the grid/shapes highlighter
+   * if the grid/shapes container is no longer in the DOM tree.
    */
   onMarkupMutation: Task.async(function* (evt, mutations) {
     let hasInterestingMutation = mutations.some(mut => mut.type === "childList");
-    if (!hasInterestingMutation || !this.gridHighlighterShown) {
+    if (!hasInterestingMutation) {
       // Bail out if the mutations did not remove nodes, or if no grid highlighter is
       // displayed.
       return;
     }
 
-    let nodeFront = this.gridHighlighterShown;
+    if (this.gridHighlighterShown) {
+      let nodeFront = this.gridHighlighterShown;
 
-    try {
-      let isInTree = yield this.inspector.walker.isInDOMTree(nodeFront);
-      if (!isInTree) {
-        this.hideGridHighlighter(nodeFront);
+      try {
+        let isInTree = yield this.inspector.walker.isInDOMTree(nodeFront);
+        if (!isInTree) {
+          this.hideGridHighlighter(nodeFront);
+        }
+      } catch (e) {
+        console.error(e);
       }
-    } catch (e) {
-      console.error(e);
+    }
+
+    if (this.shapesHighlighterShown) {
+      let nodeFront = this.shapesHighlighterShown;
+
+      try {
+        let isInTree = yield this.inspector.walker.isInDOMTree(nodeFront);
+        if (!isInTree) {
+          this.hideShapesHighlighter(nodeFront);
+        }
+      } catch (e) {
+        console.error(e);
+      }
     }
   }),
 
   /**
    * Restore saved highlighter state after navigate.
    */
   onNavigate: Task.async(function* () {
     try {
-      yield this.restoreState();
+      yield this.restoreState("grid", this.state.grid, this.showGridHighlighter);
+      yield this.restoreState("shapes", this.state.shapes, this.showShapesHighlighter);
     } catch (e) {
       this._handleRejection(e);
     }
   }),
 
   /**
    * Clear saved highlighter shown properties on will-navigate.
    */
   onWillNavigate: function () {
     this.geometryEditorHighlighterShown = null;
     this.gridHighlighterShown = null;
     this.hoveredHighlighterShown = null;
     this.selectorHighlighterShown = null;
+    this.shapesHighlighterShown = null;
 
     // The inspector panel should emit the new-root event when it is ready after navigate.
     this.onInspectorNewRoot = this.inspector.once("new-root");
   },
 
   /**
    * Destroy this overlay instance, removing it from the view and destroying
    * all initialized highlighters.
@@ -529,14 +647,15 @@ HighlightersOverlay.prototype = {
     this.highlighterUtils = null;
     this.supportsHighlighters = null;
     this.state = null;
 
     this.geometryEditorHighlighterShown = null;
     this.gridHighlighterShown = null;
     this.hoveredHighlighterShown = null;
     this.selectorHighlighterShown = null;
+    this.shapesHighlighterShown = null;
 
     this.destroyed = true;
   }
 };
 
 module.exports = HighlightersOverlay;
--- a/devtools/client/preferences/devtools.js
+++ b/devtools/client/preferences/devtools.js
@@ -59,16 +59,18 @@ pref("devtools.inspector.imagePreviewToo
 // Enable user agent style inspection in rule-view
 pref("devtools.inspector.showUserAgentStyles", false);
 // Show all native anonymous content (like controls in <video> tags)
 pref("devtools.inspector.showAllAnonymousContent", false);
 // Enable the MDN docs tooltip
 pref("devtools.inspector.mdnDocsTooltip.enabled", false);
 // Enable the new color widget
 pref("devtools.inspector.colorWidget.enabled", false);
+// Enable the CSS shapes highlighter
+pref("devtools.inspector.shapesHighlighter.enabled", false);
 
 // Enable the Font Inspector
 pref("devtools.fontinspector.enabled", true);
 
 // Counter to promote the inspector layout view.
 // @remove after release 56 (See Bug 1355747)
 pref("devtools.promote.layoutview", 1);
 // Whether or not to show the promote bar in the layout view
--- a/devtools/client/shared/output-parser.js
+++ b/devtools/client/shared/output-parser.js
@@ -5,24 +5,26 @@
 "use strict";
 
 const {angleUtils} = require("devtools/client/shared/css-angle");
 const {colorUtils} = require("devtools/shared/css/color");
 const {getCSSLexer} = require("devtools/shared/css/lexer");
 const EventEmitter = require("devtools/shared/event-emitter");
 const {
   ANGLE_TAKING_FUNCTIONS,
+  BASIC_SHAPE_FUNCTIONS,
   BEZIER_KEYWORDS,
   COLOR_TAKING_FUNCTIONS,
   CSS_TYPES
 } = require("devtools/shared/css/properties-db");
 const Services = require("Services");
 
 const HTML_NS = "http://www.w3.org/1999/xhtml";
 const CSS_GRID_ENABLED_PREF = "layout.css.grid.enabled";
+const CSS_SHAPES_ENABLED_PREF = "devtools.inspector.shapesHighlighter.enabled";
 
 /**
  * This module is used to process text for output by developer tools. This means
  * linking JS files with the debugger, CSS files with the style editor, JS
  * functions with the debugger, placing color swatches next to colors and
  * adding doorhanger previews where possible (images, angles, lengths,
  * border radius, cubic-bezier etc.).
  *
@@ -72,16 +74,17 @@ OutputParser.prototype = {
    *         A document fragment containing color swatches etc.
    */
   parseCssProperty: function (name, value, options = {}) {
     options = this._mergeOptions(options);
 
     options.expectCubicBezier = this.supportsType(name, CSS_TYPES.TIMING_FUNCTION);
     options.expectDisplay = name === "display";
     options.expectFilter = name === "filter";
+    options.expectShape = name === "clip-path" || name === "shape-outside";
     options.supportsColor = this.supportsType(name, CSS_TYPES.COLOR) ||
                             this.supportsType(name, CSS_TYPES.GRADIENT);
 
     // The filter property is special in that we want to show the
     // swatch even if the value is invalid, because this way the user
     // can easily use the editor to fix it.
     if (options.expectFilter || this._cssPropertySupportsValue(name, value)) {
       return this._parse(value, options);
@@ -157,24 +160,22 @@ OutputParser.prototype = {
          outerMostFunctionTakesColor);
     };
 
     let angleOK = function (angle) {
       return (new angleUtils.CssAngle(angle)).valid;
     };
 
     let spaceNeeded = false;
-    while (true) {
-      let token = tokenStream.nextToken();
-      if (!token) {
-        break;
-      }
+    let token = tokenStream.nextToken();
+    while (token) {
       if (token.tokenType === "comment") {
         // This doesn't change spaceNeeded, because we didn't emit
         // anything to the output.
+        token = tokenStream.nextToken();
         continue;
       }
 
       switch (token.tokenType) {
         case "function": {
           if (COLOR_TAKING_FUNCTIONS.includes(token.text) ||
               ANGLE_TAKING_FUNCTIONS.includes(token.text)) {
             // The function can accept a color or an angle argument, and we know
@@ -192,16 +193,20 @@ OutputParser.prototype = {
             let functionText = this._collectFunctionText(token, text,
                                                          tokenStream);
 
             if (options.expectCubicBezier && token.text === "cubic-bezier") {
               this._appendCubicBezier(functionText, options);
             } else if (colorOK() &&
                        colorUtils.isValidCSSColor(functionText, this.cssColor4)) {
               this._appendColor(functionText, options);
+            } else if (options.expectShape &&
+                       Services.prefs.getBoolPref(CSS_SHAPES_ENABLED_PREF) &&
+                       BASIC_SHAPE_FUNCTIONS.includes(token.text)) {
+              this._appendShape(functionText, options);
             } else {
               this._appendTextNode(functionText);
             }
           }
           break;
         }
 
         case "ident":
@@ -268,16 +273,18 @@ OutputParser.prototype = {
       }
 
       // If this token might possibly introduce token pasting when
       // color-cycling, require a space.
       spaceNeeded = (token.tokenType === "ident" || token.tokenType === "at" ||
                      token.tokenType === "id" || token.tokenType === "hash" ||
                      token.tokenType === "number" || token.tokenType === "dimension" ||
                      token.tokenType === "percentage" || token.tokenType === "dimension");
+
+      token = tokenStream.nextToken();
     }
 
     let result = this._toDOM();
 
     if (options.expectFilter && !options.filterSwatch) {
       result = this._wrapFilter(text, options, result);
     }
 
@@ -348,16 +355,31 @@ OutputParser.prototype = {
     let value = this._createNode("span", {});
     value.textContent = grid;
 
     container.appendChild(toggle);
     container.appendChild(value);
     this.parsed.push(container);
   },
 
+  _appendShape: function (shape, options) {
+    let container = this._createNode("span", {});
+
+    let toggle = this._createNode("span", {
+      class: options.shapeClass
+    });
+
+    let value = this._createNode("span", {});
+    value.textContent = shape;
+
+    container.appendChild(toggle);
+    container.appendChild(value);
+    this.parsed.push(container);
+  },
+
   /**
    * Append a angle value to the output
    *
    * @param {String} angle
    *        angle to append
    * @param {Object} options
    *        Options object. For valid options and default values see
    *        _mergeOptions()
@@ -692,16 +714,17 @@ OutputParser.prototype = {
    *                                    // that follows the swatch.
    *           - colorSwatchClass: ""   // The class to use for color swatches.
    *           - filterSwatch: false    // A special case for parsing a
    *                                    // "filter" property, causing the
    *                                    // parser to skip the call to
    *                                    // _wrapFilter.  Used only for
    *                                    // previewing with the filter swatch.
    *           - gridClass: ""          // The class to use for the grid icon.
+   *           - shapeClass: ""         // The class to use for the shape icon.
    *           - supportsColor: false   // Does the CSS property support colors?
    *           - urlClass: ""           // The class to be used for url() links.
    *           - baseURI: undefined     // A string used to resolve
    *                                    // relative links.
    * @return {Object}
    *         Overridden options object
    */
   _mergeOptions: function (overrides) {
@@ -710,16 +733,17 @@ OutputParser.prototype = {
       angleClass: "",
       angleSwatchClass: "",
       bezierClass: "",
       bezierSwatchClass: "",
       colorClass: "",
       colorSwatchClass: "",
       filterSwatch: false,
       gridClass: "",
+      shapeClass: "",
       supportsColor: false,
       urlClass: "",
       baseURI: undefined,
     };
 
     for (let item in overrides) {
       defaults[item] = overrides[item];
     }
--- a/devtools/client/themes/rules.css
+++ b/devtools/client/themes/rules.css
@@ -447,17 +447,18 @@
   height: 100%;
 }
 
 .ruleview-overridden-item:last-child:after {
   display: none;
 }
 
 .ruleview-grid,
-.ruleview-swatch {
+.ruleview-swatch,
+.ruleview-shape {
   cursor: pointer;
   border-radius: 50%;
   width: 1em;
   height: 1em;
   vertical-align: middle;
   /* align the swatch with its value */
   margin-top: -1px;
   margin-inline-end: 5px;
@@ -465,16 +466,22 @@
   position: relative;
 }
 
 .ruleview-grid {
   background: url("chrome://devtools/skin/images/grid.svg");
   border-radius: 0;
 }
 
+.ruleview-shape {
+  background: url("chrome://devtools/skin/images/tool-shadereditor.svg");
+  border-radius: 0;
+  background-size: 1em;
+}
+
 .ruleview-colorswatch::before {
   content: '';
   background-color: #eee;
   background-image: linear-gradient(45deg, #ccc 25%, transparent 25%, transparent 75%, #ccc 75%, #ccc),
                     linear-gradient(45deg, #ccc 25%, transparent 25%, transparent 75%, #ccc 75%, #ccc);
   background-size: 12px 12px;
   background-position: 0 0, 6px 6px;
   position: absolute;
@@ -591,17 +598,18 @@
 }
 
 .ruleview-selectorhighlighter:hover {
   filter: url(images/filters.svg#checked-icon-state);
 }
 
 .ruleview-grid.active,
 .ruleview-selectorhighlighter:active,
-.ruleview-selectorhighlighter.highlighted {
+.ruleview-selectorhighlighter.highlighted,
+.ruleview-shape.active {
   filter: url(images/filters.svg#checked-icon-state) brightness(0.9);
 }
 
 #ruleview-add-rule-button::before {
   background-image: url("chrome://devtools/skin/images/add.svg");
 }
 
 #pseudo-class-panel-toggle::before {
--- a/devtools/server/actors/highlighters/shapes.js
+++ b/devtools/server/actors/highlighters/shapes.js
@@ -34,19 +34,21 @@ class ShapesHighlighter extends AutoRefr
     this.ID_CLASS_PREFIX = "shapes-";
 
     this.referenceBox = "border";
     this.useStrokeBox = false;
     this.geometryBox = "";
 
     this.markup = new CanvasFrameAnonymousContentHelper(this.highlighterEnv,
       this._buildMarkup.bind(this));
+    this.onPageHide = this.onPageHide.bind(this);
 
     let { pageListenerTarget } = this.highlighterEnv;
     DOM_EVENTS.forEach(event => pageListenerTarget.addEventListener(event, this));
+    pageListenerTarget.addEventListener("pagehide", this.onPageHide);
   }
 
   _buildMarkup() {
     let container = createNode(this.win, {
       attributes: {
         "class": "highlighter-container"
       }
     });
@@ -1257,16 +1259,24 @@ class ShapesHighlighter extends AutoRefr
   _hide() {
     setIgnoreLayoutChanges(true);
 
     this._hideShapes();
     this.getElement("markers").setAttribute("d", "");
 
     setIgnoreLayoutChanges(false, this.highlighterEnv.window.document.documentElement);
   }
+
+  onPageHide({ target }) {
+    // If a page hide event is triggered for current window's highlighter, hide the
+    // highlighter.
+    if (target.defaultView === this.win) {
+      this.hide();
+    }
+  }
 }
 
 /**
  * Get the "raw" (i.e. non-computed) shape definition on the given node.
  * @param {nsIDOMNode} node the node to analyze
  * @param {String} property the CSS property for which a value should be retrieved.
  * @returns {String} the value of the given CSS property on the given node.
  */
--- a/devtools/shared/css/properties-db.js
+++ b/devtools/shared/css/properties-db.js
@@ -70,16 +70,18 @@ exports.COLOR_TAKING_FUNCTIONS = ["linea
  * Functions that accept an angle argument. This list can be manually edited.
  */
 exports.ANGLE_TAKING_FUNCTIONS = ["linear-gradient", "-moz-linear-gradient",
                                   "repeating-linear-gradient",
                                   "-moz-repeating-linear-gradient", "rotate", "rotateX",
                                   "rotateY", "rotateZ", "rotate3d", "skew", "skewX",
                                   "skewY", "hue-rotate"];
 
+exports.BASIC_SHAPE_FUNCTIONS = ["polygon", "circle", "ellipse", "inset"];
+
 /**
  * The list of all CSS Pseudo Elements.
  *
  * This list can be updated with `mach devtools-css-db`.
  */
 exports.PSEUDO_ELEMENTS = db.PSEUDO_ELEMENTS;
 
 /**