Bug 1435373 Part 2: Map changes from shape highlighter to correct CSS rules. r=pbro draft
authorBrad Werth <bwerth@mozilla.com>
Wed, 04 Apr 2018 13:43:34 -0700
changeset 777483 66e10dc42a668d770a1534a5786a2dcaf169bc0e
parent 777482 31bdaabbb0934a3f95008f528d8842c011e1253d
push id105223
push userbwerth@mozilla.com
push dateWed, 04 Apr 2018 20:50:11 +0000
reviewerspbro
bugs1435373
milestone61.0a1
Bug 1435373 Part 2: Map changes from shape highlighter to correct CSS rules. r=pbro MozReview-Commit-ID: IV2G4nU4h2h
devtools/client/inspector/inspector.js
devtools/client/inspector/rules/test/browser.ini
devtools/client/inspector/rules/test/browser_rules_shapes-toggle_01.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/browser_rules_shapes-toggle_07.js
devtools/client/inspector/rules/views/text-property-editor.js
devtools/client/inspector/shared/highlighters-overlay.js
devtools/client/inspector/test/browser_inspector_highlighter-cssshape_04.js
devtools/client/inspector/test/browser_inspector_highlighter-cssshape_05.js
devtools/client/inspector/test/browser_inspector_highlighter-cssshape_06.js
devtools/client/inspector/test/browser_inspector_highlighter-cssshape_07.js
devtools/client/inspector/test/browser_inspector_highlighter-cssshape_iframe_01.js
devtools/client/inspector/test/head.js
devtools/client/shared/widgets/ShapesInContextEditor.js
devtools/client/shared/widgets/moz.build
devtools/client/themes/rules.css
devtools/server/actors/highlighters/shapes.js
--- a/devtools/client/inspector/inspector.js
+++ b/devtools/client/inspector/inspector.js
@@ -1308,17 +1308,17 @@ Inspector.prototype = {
 
     this.teardownToolbar();
     this.breadcrumbs.destroy();
     this.selection.off("new-node-front", this.onNewSelection);
     this.selection.off("detached-front", this.onDetached);
 
     let markupDestroyer = this._destroyMarkup();
 
-    this.highlighters.destroy();
+    let highlighterDestroyer = this.highlighters.destroy();
     this.prefsObserver.destroy();
     this.reflowTracker.destroy();
     this.styleChangeTracker.destroy();
     this.search.destroy();
 
     this._toolbox = null;
     this.breadcrumbs = null;
     this.highlighters = null;
@@ -1330,16 +1330,17 @@ Inspector.prototype = {
     this.resultsLength = null;
     this.search = null;
     this.searchBox = null;
     this.sidebar = null;
     this.store = null;
     this.target = null;
 
     this._panelDestroyer = promise.all([
+      highlighterDestroyer,
       cssPropertiesDestroyer,
       markupDestroyer,
       sidebarDestroyer,
       ruleViewSideBarDestroyer
     ]);
 
     return this._panelDestroyer;
   },
--- a/devtools/client/inspector/rules/test/browser.ini
+++ b/devtools/client/inspector/rules/test/browser.ini
@@ -249,16 +249,17 @@ skip-if = (os == 'linux' && bits == 32 &
 [browser_rules_shadowdom_slot_rules.js]
 skip-if = !stylo # shadow DOM is only enabled with stylo.
 [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]
+skip-if = true # Bug 1443151
 [browser_rules_shapes-toggle_07.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 = true # Bug 1309759
--- a/devtools/client/inspector/rules/test/browser_rules_shapes-toggle_01.js
+++ b/devtools/client/inspector/rules/test/browser_rules_shapes-toggle_01.js
@@ -20,28 +20,32 @@ const TEST_URI = `
 
 const HIGHLIGHTER_TYPE = "ShapesHighlighter";
 
 add_task(async function() {
   await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
   let {inspector, view} = await openRuleView();
   let highlighters = view.highlighters;
 
+  info("Select a node with a shape value");
+  let onHighlighterArmed = highlighters.once("shapes-highlighter-armed");
   await selectNode("#shape", inspector);
   let container = getRuleViewProperty(view, "#shape", "clip-path").valueSpan;
   let shapesToggle = container.querySelector(".ruleview-shapeswatch");
 
   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("Wait for shapes highlighter swatch trigger to be ready");
+  await onHighlighterArmed;
   info("Toggling ON the CSS shapes highlighter from the rule-view.");
   let onHighlighterShown = highlighters.once("shapes-highlighter-shown");
   shapesToggle.click();
   await onHighlighterShown;
 
   info("Checking the CSS shapes highlighter is created and toggle button is active in " +
     "the rule-view.");
   ok(shapesToggle.classList.contains("active"),
--- a/devtools/client/inspector/rules/test/browser_rules_shapes-toggle_03.js
+++ b/devtools/client/inspector/rules/test/browser_rules_shapes-toggle_03.js
@@ -21,55 +21,63 @@ const TEST_URI = `
 const HIGHLIGHTER_TYPE = "ShapesHighlighter";
 
 add_task(async function() {
   await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
   let {inspector, view} = await openRuleView();
   let highlighters = view.highlighters;
 
   info("Selecting the first shape container.");
+  let onHighlighterArmed = highlighters.once("shapes-highlighter-armed");
   await selectNode("#shape1", inspector);
   let container = getRuleViewProperty(view, ".shape", "clip-path").valueSpan;
   let shapeToggle = container.querySelector(".ruleview-shapeswatch");
 
   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("Wait for shapes highlighter swatch trigger to be ready");
+  await onHighlighterArmed;
   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();
   await 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.");
+  onHighlighterArmed = highlighters.once("shapes-highlighter-armed");
   await selectNode("#shape2", inspector);
   let firstShapesHighlighterShown = highlighters.shapesHighlighterShown;
   container = getRuleViewProperty(view, ".shape", "clip-path").valueSpan;
   shapeToggle = container.querySelector(".ruleview-shapeswatch");
 
   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.");
+  ok(!highlighters.shapesHighlighterShown, "CSS shapes highlighter is still no longer" +
+    "shown due to selecting another node.");
+
+  info("Wait for shapes highlighter swatch trigger to be ready");
+  await onHighlighterArmed;
 
   info("Toggling ON the CSS shapes highlighter for the second shapes container " +
     "from the rule-view.");
   onHighlighterShown = highlighters.once("shapes-highlighter-shown");
   shapeToggle.click();
   await onHighlighterShown;
 
   info("Checking the CSS shapes highlighter is created for the second shapes container " +
--- a/devtools/client/inspector/rules/test/browser_rules_shapes-toggle_04.js
+++ b/devtools/client/inspector/rules/test/browser_rules_shapes-toggle_04.js
@@ -18,17 +18,21 @@ const TEST_URI = `
   <div id="shape"></div>
 `;
 
 add_task(async function() {
   await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
   let {inspector, view} = await openRuleView();
   let highlighters = view.highlighters;
 
+  info("Select a node with a shape value");
+  let onHighlighterArmed = highlighters.once("shapes-highlighter-armed");
   await selectNode("#shape", inspector);
+  info("Wait for shapes highlighter swatch trigger to be ready");
+  await onHighlighterArmed;
   let container = getRuleViewProperty(view, "#shape", "clip-path").valueSpan;
   let shapeToggle = container.querySelector(".ruleview-shapeswatch");
 
   info("Toggling ON the CSS shape highlighter from the rule-view.");
   let onHighlighterShown = highlighters.once("shapes-highlighter-shown");
   shapeToggle.click();
   await onHighlighterShown;
 
--- a/devtools/client/inspector/rules/test/browser_rules_shapes-toggle_05.js
+++ b/devtools/client/inspector/rules/test/browser_rules_shapes-toggle_05.js
@@ -18,17 +18,21 @@ const TEST_URI = `
   <div id="shape"></div>
 `;
 
 add_task(async function() {
   await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
   let {inspector, view, testActor} = await openRuleView();
   let highlighters = view.highlighters;
 
+  info("Select a node with a shape value");
+  let onHighlighterArmed = highlighters.once("shapes-highlighter-armed");
   await selectNode("#shape", inspector);
+  info("Wait for shapes highlighter swatch trigger to be ready");
+  await onHighlighterArmed;
   let container = getRuleViewProperty(view, "#shape", "clip-path").valueSpan;
   let shapeToggle = container.querySelector(".ruleview-shapeswatch");
 
   info("Toggling ON the CSS shapes highlighter from the rule-view.");
   let onHighlighterShown = highlighters.once("shapes-highlighter-shown");
   shapeToggle.click();
   await onHighlighterShown;
   ok(highlighters.shapesHighlighterShown, "CSS shapes highlighter is shown.");
--- a/devtools/client/inspector/rules/test/browser_rules_shapes-toggle_06.js
+++ b/devtools/client/inspector/rules/test/browser_rules_shapes-toggle_06.js
@@ -20,17 +20,21 @@ const TEST_URI = `
   <div class="shape" id="shape2"></div>
 `;
 
 add_task(async function() {
   await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
   let {inspector, view} = await openRuleView();
   let highlighters = view.highlighters;
 
+  info("Selecting the first shapes container.");
+  let onHighlighterArmed = highlighters.once("shapes-highlighter-armed");
   await selectNode("#shape1", inspector);
+  info("Wait for shapes highlighter swatch trigger to be ready");
+  await onHighlighterArmed;
   let clipPathContainer = getRuleViewProperty(view, ".shape", "clip-path").valueSpan;
   let clipPathShapeToggle = clipPathContainer.querySelector(".ruleview-shapeswatch");
   let shapeOutsideContainer = getRuleViewProperty(view, ".shape",
     "shape-outside").valueSpan;
   let shapeOutsideToggle = shapeOutsideContainer.querySelector(".ruleview-shapeswatch");
 
   info("Toggling ON the CSS shapes highlighter for clip-path from the rule-view.");
   let onHighlighterShown = highlighters.once("shapes-highlighter-shown");
--- a/devtools/client/inspector/rules/test/browser_rules_shapes-toggle_07.js
+++ b/devtools/client/inspector/rules/test/browser_rules_shapes-toggle_07.js
@@ -19,25 +19,24 @@ const TEST_URI = `
 
 const HIGHLIGHTER_TYPE = "ShapesHighlighter";
 
 add_task(async function() {
   await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
   let {inspector, view} = await openRuleView();
   let highlighters = view.highlighters;
 
+  info("Select a node with a shape value");
+  let onHighlighterArmed = highlighters.once("shapes-highlighter-armed");
   await selectNode("#shape", inspector);
+  info("Wait for shapes highlighter swatch trigger to be ready");
+  await onHighlighterArmed;
   let container = getRuleViewProperty(view, "#shape", "clip-path").valueSpan;
   let shapesToggle = container.querySelector(".ruleview-shapeswatch");
 
-  info("Checking the initial state of the CSS shape toggle in the rule-view.");
-  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 with transform mode on.");
   let onHighlighterShown = highlighters.once("shapes-highlighter-shown");
   EventUtils.sendMouseEvent({type: "click", metaKey: true, ctrlKey: true},
     shapesToggle, view.styleWindow);
   await onHighlighterShown;
 
   info("Checking the CSS shapes highlighter is created and transform mode is on");
   ok(highlighters.highlighters[HIGHLIGHTER_TYPE],
--- a/devtools/client/inspector/rules/views/text-property-editor.js
+++ b/devtools/client/inspector/rules/views/text-property-editor.js
@@ -22,17 +22,16 @@ const Services = require("Services");
 
 const HTML_NS = "http://www.w3.org/1999/xhtml";
 
 const SHARED_SWATCH_CLASS = "ruleview-swatch";
 const COLOR_SWATCH_CLASS = "ruleview-colorswatch";
 const BEZIER_SWATCH_CLASS = "ruleview-bezierswatch";
 const FILTER_SWATCH_CLASS = "ruleview-filterswatch";
 const ANGLE_SWATCH_CLASS = "ruleview-angleswatch";
-const INSET_POINT_TYPES = ["top", "right", "bottom", "left"];
 const FONT_FAMILY_CLASS = "ruleview-font-family";
 const SHAPE_SWATCH_CLASS = "ruleview-shapeswatch";
 
 /*
  * An actionable element is an element which on click triggers a specific action
  * (e.g. shows a color tooltip, opens a link, …).
  */
 const ACTIONABLE_ELEMENTS_SELECTORS = [
@@ -89,19 +88,20 @@ function TextPropertyEditor(ruleEditor, 
   this._onExpandClicked = this._onExpandClicked.bind(this);
   this._onStartEditing = this._onStartEditing.bind(this);
   this._onNameDone = this._onNameDone.bind(this);
   this._onValueDone = this._onValueDone.bind(this);
   this._onSwatchCommit = this._onSwatchCommit.bind(this);
   this._onSwatchPreview = this._onSwatchPreview.bind(this);
   this._onSwatchRevert = this._onSwatchRevert.bind(this);
   this._onValidate = this.ruleView.debounce(this._previewValue, 10, this);
+  this.onInContextEditorCommit = this.onInContextEditorCommit.bind(this);
+  this.onInContextEditorPreview = this.onInContextEditorPreview.bind(this);
   this.update = this.update.bind(this);
   this.updatePropertyState = this.updatePropertyState.bind(this);
-  this._onHoverShapePoint = this._onHoverShapePoint.bind(this);
 
   this._create();
   this.update();
 }
 
 TextPropertyEditor.prototype = {
   /**
    * Boolean indicating if the name or value is being currently edited.
@@ -314,18 +314,16 @@ TextPropertyEditor.prototype = {
         property: this.prop,
         defaultIncrement: this.prop.name === "opacity" ? 0.1 : 1,
         popup: this.popup,
         multiline: true,
         maxWidth: () => this.container.getBoundingClientRect().width,
         cssProperties: this.cssProperties,
         cssVariables: this.rule.elementStyle.variables,
       });
-
-      this.ruleView.highlighters.on("hover-shape-point", this._onHoverShapePoint);
     }
   },
 
   /**
    * Get the path from which to resolve requests for this
    * rule's stylesheet.
    *
    * @return {String} the stylesheet's href.
@@ -336,17 +334,17 @@ TextPropertyEditor.prototype = {
       return domRule.href || domRule.nodeHref;
     }
     return undefined;
   },
 
   /**
    * Populate the span based on changes to the TextProperty.
    */
-  update: function() {
+  update: async function() {
     if (this.ruleView.isDestroyed) {
       return;
     }
 
     this.updatePropertyState();
 
     let name = this.prop.name;
     this.nameSpan.textContent = name;
@@ -511,21 +509,32 @@ TextPropertyEditor.prototype = {
 
     let shapeToggle = this.valueSpan.querySelector(".ruleview-shapeswatch");
     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");
-        highlighters.highlightRuleViewShapePoint(highlighters.state.shapes.hoverPoint);
+      try {
+        let shapesEditor =
+          await this.ruleView.highlighters.getInContextEditor("shapesEditor");
+        if (!shapesEditor) {
+          return;
+        }
+
+        shapesEditor.link(this.prop, shapeToggle, {
+          onPreview: this.onInContextEditorPreview,
+          onCommit: this.onInContextEditorCommit
+        });
+        // Mark this toggle if this property is being currently edited; unmark otherwise.
+        shapeToggle.classList.toggle("active", shapesEditor.activeProperty === this.prop);
+      } catch (err) {
+        // Remove toggle and, with it, any expectations of triggering an editor.
+        shapeToggle.remove();
       }
     }
 
     // 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;
@@ -1030,70 +1039,29 @@ TextPropertyEditor.prototype = {
    * @return {Boolean} true if the property is a `display: [inline-]grid` declaration.
    */
   isDisplayGrid: function() {
     return this.prop.name === "display" &&
       (this.prop.value === "grid" || this.prop.value === "inline-grid");
   },
 
   /**
-   * Highlight the given shape point in the rule view. Called when "hover-shape-point"
-   * event is emitted.
+   * Live preview this property, without committing changes.
    *
-   * @param {Event} event
-   *        The "hover-shape-point" event.
-   * @param {String} point
-   *        The point to highlight.
+   * @param {String} value
+   *        The value to set the current property to.
    */
-  _onHoverShapePoint: function(point) {
-    // If there is no shape toggle, or it is not active, return.
-    let shapeToggle = this.valueSpan.querySelector(".ruleview-shapeswatch.active");
-    if (!shapeToggle) {
-      return;
-    }
-
-    let view = this.ruleView;
-    let { highlighters } = view;
-    let ruleViewEl = view.element;
-    let selector = `.ruleview-shape-point.active`;
-    for (let pointNode of ruleViewEl.querySelectorAll(selector)) {
-      this._toggleShapePointActive(pointNode, false);
-    }
-
-    if (typeof point === "string") {
-      if (point.includes(",")) {
-        point = point.split(",")[0];
-      }
-      // Because one inset value can represent multiple points, inset points use classes
-      // instead of data.
-      selector = (INSET_POINT_TYPES.includes(point)) ?
-                 `.ruleview-shape-point.${point}` :
-                 `.ruleview-shape-point[data-point='${point}']`;
-      for (let pointNode of this.valueSpan.querySelectorAll(selector)) {
-        let nodeInfo = view.getNodeInfo(pointNode);
-        if (highlighters.isRuleViewShapePoint(nodeInfo)) {
-          this._toggleShapePointActive(pointNode, true);
-        }
-      }
-    }
+  onInContextEditorPreview(value) {
+    this.ruleEditor.rule.previewPropertyValue(this.prop, value);
   },
 
   /**
-   * Toggle the class "active" on the given shape point in the rule view if the current
-   * inspector selection is highlighted by the shapes highlighter.
+   * Commit this property value. Triggers editor update.
    *
-   * @param {NodeFront} node
-   *        The NodeFront of the shape point to toggle
-   * @param {Boolean} active
-   *        Whether the shape point should be active
+   * @param {String} value
+   *        The value to set the current property to.
    */
-  _toggleShapePointActive: function(node, active) {
-    let { highlighters } = this.ruleView;
-    if (highlighters.inspector.selection.nodeFront !=
-        highlighters.shapesHighlighterShown) {
-      return;
-    }
-
-    node.classList.toggle("active", active);
-  },
+  onInContextEditorCommit(value) {
+    this.prop.setValue(value);
+  }
 };
 
 module.exports = TextPropertyEditor;
--- a/devtools/client/inspector/shared/highlighters-overlay.js
+++ b/devtools/client/inspector/shared/highlighters-overlay.js
@@ -9,29 +9,38 @@
 const Services = require("Services");
 const EventEmitter = require("devtools/shared/event-emitter");
 const {
   VIEW_NODE_VALUE_TYPE,
   VIEW_NODE_SHAPE_POINT_TYPE
 } = require("devtools/client/inspector/shared/node-types");
 
 const DEFAULT_GRID_COLOR = "#4B0082";
-const INSET_POINT_TYPES = ["top", "right", "bottom", "left"];
 
 /**
  * Highlighters overlay is a singleton managing all highlighters in the Inspector.
  */
 class HighlightersOverlay {
   /**
    * @param  {Inspector} inspector
    *         Inspector toolbox panel.
    */
   constructor(inspector) {
+     /*
+      * Collection of instantiated highlighter actors like FlexboxHighlighter,
+      * CssGridHighlighter, ShapesHighlighter and GeometryEditorHighlighter.
+      */
+     this.highlighters = {};
+     /*
+      * Collection of instantiated in-context editors, like ShapesInContextEditor, which
+      * behave like highlighters but with added editing capabilities that need to map value
+      * changes to properties in the Rule view.
+     */
+    this.editors = {};
     this.inspector = inspector;
-    this.highlighters = {};
     this.highlighterUtils = this.inspector.toolbox.highlighterUtils;
 
     // Only initialize the overlay if at least one of the highlighter types is supported.
     this.supportsHighlighters = this.highlighterUtils.supportsCustomHighlighters();
 
     // NodeFront of the flexbox container that is highlighted.
     this.flexboxHighlighterShown = null;
     // NodeFront of element that is highlighted by the geometry editor.
@@ -58,17 +67,18 @@ class HighlightersOverlay {
     this.onWillNavigate = this.onWillNavigate.bind(this);
     this.hideFlexboxHighlighter = this.hideFlexboxHighlighter.bind(this);
     this.hideGridHighlighter = this.hideGridHighlighter.bind(this);
     this.hideShapesHighlighter = this.hideShapesHighlighter.bind(this);
     this.showFlexboxHighlighter = this.showFlexboxHighlighter.bind(this);
     this.showGridHighlighter = this.showGridHighlighter.bind(this);
     this.showShapesHighlighter = this.showShapesHighlighter.bind(this);
     this._handleRejection = this._handleRejection.bind(this);
-    this._onHighlighterEvent = this._onHighlighterEvent.bind(this);
+    this.onShapesHighlighterShown = this.onShapesHighlighterShown.bind(this);
+    this.onShapesHighlighterHidden = this.onShapesHighlighterHidden.bind(this);
 
     // Add inspector events, not specific to a given view.
     this.inspector.on("markupmutation", this.onMarkupMutation);
     this.inspector.target.on("will-navigate", this.onWillNavigate);
 
     EventEmitter.decorate(this);
   }
 
@@ -116,91 +126,72 @@ class HighlightersOverlay {
 
     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.
-   */
-  async toggleShapesHighlighter(node, options = {}) {
-    options.transformMode = options.ctrlOrMetaPressed;
-
-    if (node == this.shapesHighlighterShown &&
-        options.mode === this.state.shapes.options.mode) {
-      // If meta/ctrl is not pressed, hide the highlighter.
-      if (!options.ctrlOrMetaPressed) {
-        await this.hideShapesHighlighter(node);
-        return;
-      }
-
-      // If meta/ctrl is pressed, toggle transform mode on the highlighter.
-      options.transformMode = !this.state.shapes.options.transformMode;
-    }
-
-    await this.showShapesHighlighter(node, options);
-  }
-
-  /**
    * Show the shapes highlighter for the given element with a shape.
+   * This method delegates to the in-context shapes editor which wraps the
+   * shapes highlighter with additional functionality required for passing changes
+   * back to the rule view.
    *
    * @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.
    */
   async showShapesHighlighter(node, options) {
-    let highlighter = await this._getHighlighter("ShapesHighlighter");
-    if (!highlighter) {
-      return;
-    }
-
-    let isShown = await highlighter.show(node, options);
-    if (!isShown) {
+    let shapesEditor = await this.getInContextEditor("shapesEditor");
+    if (!shapesEditor) {
       return;
     }
-
-    this.shapesHighlighterShown = node;
-    let { mode } = options;
-    this._toggleRuleViewIcon(node, false, ".ruleview-shapeswatch");
-    this._toggleRuleViewIcon(node, true, `.ruleview-shapeswatch[data-mode='${mode}']`);
-
-    try {
-      // Save shapes highlighter state.
-      let { url } = this.inspector.target;
-      let selector = await node.getUniqueSelector();
-      this.state.shapes = { selector, options, url };
-      this.shapesHighlighterShown = node;
-      this.emit("shapes-highlighter-shown", node, options);
-    } catch (e) {
-      this._handleRejection(e);
-    }
+    shapesEditor.show(node, options);
   }
 
   /**
-   * Hide the shapes highlighter for the given element with a shape.
+   * Called after the shape highlighter was shown.
    *
-   * @param  {NodeFront} node
-   *         The NodeFront of the element with a shape to unhighlight.
+   * @param  {Object} data
+   *         Data associated with the event.
+   *         Contains:
+   *         - {NodeFront} node: The NodeFront of the element that is highlighted.
+   *         - {Object} options: Options that were passed to ShapesHighlighter.show()
    */
-  async hideShapesHighlighter(node) {
-    if (!this.shapesHighlighterShown || !this.highlighters.ShapesHighlighter) {
+  onShapesHighlighterShown(data) {
+    let { node, options } = data;
+    this.shapesHighlighterShown = node;
+    this.state.shapes.options = options;
+    this.emit("shapes-highlighter-shown", node, options);
+  }
+
+  /**
+   * Hide the shapes highlighter if visible.
+   * This method delegates the to the in-context shapes editor which wraps
+   * the shapes highlighter with additional functionality.
+   */
+  async hideShapesHighlighter() {
+    let shapesEditor = await this.getInContextEditor("shapesEditor");
+    if (!shapesEditor) {
       return;
     }
+    shapesEditor.hide();
+  }
 
-    this._toggleRuleViewIcon(node, false, ".ruleview-shapeswatch");
-
-    await this.highlighters.ShapesHighlighter.hide();
+  /**
+   * Called after the shapes highlighter was hidden.
+   *
+   * @param  {Object} data
+   *         Data associated with the event.
+   *         Contains:
+   *         - {NodeFront} node: The NodeFront of the element that was highlighted.
+   */
+  onShapesHighlighterHidden(data) {
     this.emit("shapes-highlighter-hidden", this.shapesHighlighterShown,
       this.state.shapes.options);
     this.shapesHighlighterShown = null;
     this.state.shapes = {};
   }
 
   /**
    * Show the shapes highlighter for the given element, with the given point highlighted.
@@ -214,47 +205,16 @@ class HighlightersOverlay {
     if (node == this.shapesHighlighterShown) {
       let options = Object.assign({}, this.state.shapes.options);
       options.hoverPoint = point;
       await this.showShapesHighlighter(node, options);
     }
   }
 
   /**
-   * Highlight the given shape point in the rule view.
-   *
-   * @param {String} point
-   *        The point to highlight.
-   */
-  highlightRuleViewShapePoint(point) {
-    let view = this.inspector.getPanel("ruleview").view;
-    let ruleViewEl = view.element;
-    let selector = `.ruleview-shape-point.active`;
-
-    for (let pointNode of ruleViewEl.querySelectorAll(selector)) {
-      this._toggleShapePointActive(pointNode, false);
-    }
-
-    if (point !== null && point !== undefined) {
-      // Because one inset value can represent multiple points, inset points use classes
-      // instead of data.
-      selector = (INSET_POINT_TYPES.includes(point)) ?
-                 `.ruleview-shape-point.${point}` :
-                 `.ruleview-shape-point[data-point='${point}']`;
-
-      for (let pointNode of ruleViewEl.querySelectorAll(selector)) {
-        let nodeInfo = view.getNodeInfo(pointNode);
-        if (this.isRuleViewShapePoint(nodeInfo)) {
-          this._toggleShapePointActive(pointNode, true);
-        }
-      }
-    }
-  }
-
-  /**
    * Toggle the flexbox highlighter for the given flexbox container element.
    *
    * @param  {NodeFront} node
    *         The NodeFront of the flexbox container element to highlight.
    * @param  {Object} options
    *         Object used for passing options to the flexbox highlighter.
    */
   async toggleFlexboxHighlighter(node, options = {}) {
@@ -460,34 +420,16 @@ class HighlightersOverlay {
 
     await this.highlighters.GeometryEditorHighlighter.hide();
 
     this.emit("geometry-editor-highlighter-hidden");
     this.geometryEditorHighlighterShown = null;
   }
 
   /**
-   * Handle events emitted by the highlighter.
-   *
-   * @param {Object} data
-   *        The data object sent in the event.
-   */
-  _onHighlighterEvent(data) {
-    if (data.type === "shape-hover-on") {
-      this.state.shapes.hoverPoint = data.point;
-      this.emit("hover-shape-point", data.point);
-    } else if (data.type === "shape-hover-off") {
-      this.state.shapes.hoverPoint = null;
-      this.emit("hover-shape-point", null);
-    }
-
-    this.emit("highlighter-event-handled");
-  }
-
-  /**
    * Restores the saved flexbox highlighter state.
    */
   async restoreFlexboxState() {
     try {
       await this.restoreState("flexbox", this.state.flexbox, this.showFlexboxHighlighter);
     } catch (e) {
       this._handleRejection(e);
     }
@@ -500,29 +442,18 @@ class HighlightersOverlay {
     try {
       await this.restoreState("grid", this.state.grid, this.showGridHighlighter);
     } catch (e) {
       this._handleRejection(e);
     }
   }
 
   /**
-   * Restores the saved shape highlighter state.
-   */
-  async restoreShapeState() {
-    try {
-      await this.restoreState("shapes", this.state.shapes, this.showShapesHighlighter);
-    } catch (e) {
-      this._handleRejection(e);
-    }
-  }
-
-  /**
-   * Helper function called by restoreFlexboxState, restoreGridState and
-   * restoreShapeState. Restores the saved highlighter state for the given highlighter
+   * Helper function called by restoreFlexboxState, restoreGridState.
+   * Restores the saved highlighter state for the given highlighter
    * and their state.
    *
    * @param  {String} name
    *         The name of the highlighter to be restored
    * @param  {Object} state
    *         The state of the highlighter to be restored
    * @param  {Function} showFunction
    *         The function that shows the highlighter
@@ -550,16 +481,57 @@ class HighlightersOverlay {
       await showFunction(nodeFront, options);
       this.emit(`${name}-state-restored`, { restored: true });
     }
 
     this.emit(`${name}-state-restored`, { restored: false });
   }
 
   /**
+  * Get an instance of an in-context editor for the given type.
+  *
+  * In-context editors behave like highlighters but with added editing capabilities which
+  * need to write value changes back to something, like to properties in the Rule view.
+  * They typically exist in the context of the page, like the ShapesInContextEditor.
+  *
+  * @param  {String} type
+  *         Type of in-context editor. Currently supported: "shapesEditor"
+  *
+  * @return {Object|null}
+  *         Reference to instance for given type of in-context editor or null.
+  */
+  async getInContextEditor(type) {
+    if (this.editors[type]) {
+      return this.editors[type];
+    }
+
+    let editor;
+
+    switch (type) {
+      case "shapesEditor":
+        let highlighter = await this._getHighlighter("ShapesHighlighter");
+        if (!highlighter) {
+          return null;
+        }
+        const ShapesInContextEditor = require("devtools/client/shared/widgets/ShapesInContextEditor");
+
+        editor = new ShapesInContextEditor(highlighter, this.inspector, this.state);
+        editor.on("show", this.onShapesHighlighterShown);
+        editor.on("hide", this.onShapesHighlighterHidden);
+        break;
+      default:
+        throw new Error(`Unsupported in-context editor '${name}'`);
+    }
+
+    this.editors[type] = editor;
+
+    return editor;
+  }
+
+  /**
    * 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
    */
   async _getHighlighter(type) {
     let utils = this.highlighterUtils;
@@ -575,17 +547,16 @@ class HighlightersOverlay {
     } catch (e) {
       // Ignore any error
     }
 
     if (!highlighter) {
       return null;
     }
 
-    highlighter.on("highlighter-event", this._onHighlighterEvent);
     this.highlighters[type] = highlighter;
     return highlighter;
   }
 
   _handleRejection(error) {
     if (!this.destroyed) {
       console.error(error);
     }
@@ -775,24 +746,16 @@ class HighlightersOverlay {
       highlighterSettings.color = grid ? grid.color : DEFAULT_GRID_COLOR;
 
       this.toggleGridHighlighter(this.inspector.selection.nodeFront, highlighterSettings,
         "rule");
     } else if (this._isRuleViewDisplayFlex(event.target)) {
       event.stopPropagation();
 
       this.toggleFlexboxHighlighter(this.inspector.selection.nodeFront);
-    } else if (this._isRuleViewShape(event.target)) {
-      event.stopPropagation();
-
-      let settings = {
-        mode: event.target.dataset.mode,
-        ctrlOrMetaPressed: event.metaKey || event.ctrlKey
-      };
-      this.toggleShapesHighlighter(this.inspector.selection.nodeFront, settings);
     }
   }
 
   onMouseMove(event) {
     // Bail out if the target is the same as for the last mousemove.
     if (event.target === this._lastHovered) {
       return;
     }
@@ -808,17 +771,16 @@ class HighlightersOverlay {
     let nodeInfo = view.getNodeInfo(event.target);
     if (!nodeInfo) {
       return;
     }
 
     if (this.isRuleViewShapePoint(nodeInfo)) {
       let { point } = nodeInfo.value;
       this.hoverPointShapesHighlighter(this.inspector.selection.nodeFront, point);
-      this.emit("hover-shape-point", point);
       return;
     }
 
     // Choose the type of highlighter required for the hovered node.
     let type;
     if (this._isRuleViewTransform(nodeInfo) ||
         this._isComputedViewTransform(nodeInfo)) {
       type = "CssTransformHighlighter";
@@ -846,17 +808,16 @@ class HighlightersOverlay {
 
     // Otherwise, hide the highlighter.
     let view = this.isRuleView(this._lastHovered) ?
       this.inspector.getPanel("ruleview").view :
       this.inspector.getPanel("computedview").computedView;
     let nodeInfo = view.getNodeInfo(this._lastHovered);
     if (nodeInfo && this.isRuleViewShapePoint(nodeInfo)) {
       this.hoverPointShapesHighlighter(this.inspector.selection.nodeFront, null);
-      this.emit("hover-shape-point", null);
     }
     this._lastHovered = null;
     this._hideHoveredHighlighter();
   }
 
   /**
    * Handler function for "markupmutation" events. Hides the flexbox/grid/shapes
    * highlighter if the flexbox/grid/shapes container is no longer in the DOM tree.
@@ -882,41 +843,61 @@ class HighlightersOverlay {
    */
   onWillNavigate() {
     this.flexboxHighlighterShown = null;
     this.geometryEditorHighlighterShown = null;
     this.gridHighlighterShown = null;
     this.hoveredHighlighterShown = null;
     this.selectorHighlighterShown = null;
     this.shapesHighlighterShown = null;
+    this.destroyEditors();
+  }
+
+  /**
+  * Destroy and clean-up all instances of in-context editors.
+  */
+  async destroyEditors() {
+    for (let type in this.editors) {
+      this.editors[type].off("show");
+      this.editors[type].off("hide");
+      await this.editors[type].destroy();
+    }
+
+    this.editors = {};
+  }
+
+  /**
+  * Destroy and clean-up all instances of highlighters.
+  */
+  destroyHighlighters() {
+    for (let type in this.highlighters) {
+      if (this.highlighters[type]) {
+        this.highlighters[type].finalize();
+        this.highlighters[type] = null;
+      }
+    }
+
+    this.highlighters = null;
   }
 
   /**
    * Destroy this overlay instance, removing it from the view and destroying
    * all initialized highlighters.
    */
-  destroy() {
-    for (let type in this.highlighters) {
-      if (this.highlighters[type]) {
-        if (this.highlighters[type].off) {
-          this.highlighters[type].off("highlighter-event", this._onHighlighterEvent);
-        }
-        this.highlighters[type].finalize();
-        this.highlighters[type] = null;
-      }
-    }
+  async destroy() {
+    await this.destroyEditors();
+    this.destroyHighlighters();
 
     // Remove inspector events.
     this.inspector.off("markupmutation", this.onMarkupMutation);
     this.inspector.target.off("will-navigate", this.onWillNavigate);
 
     this._lastHovered = null;
 
     this.inspector = null;
-    this.highlighters = null;
     this.highlighterUtils = null;
     this.supportsHighlighters = null;
     this.state = null;
 
     this.flexboxHighlighterShown = null;
     this.geometryEditorHighlighterShown = null;
     this.gridHighlighterShown = null;
     this.hoveredHighlighterShown = null;
--- a/devtools/client/inspector/test/browser_inspector_highlighter-cssshape_04.js
+++ b/devtools/client/inspector/test/browser_inspector_highlighter-cssshape_04.js
@@ -5,233 +5,320 @@
 "use strict";
 
 // Test that shapes are updated correctly on mouse events.
 
 const TEST_URL = URL_ROOT + "doc_inspector_highlighter_cssshapes.html";
 const HIGHLIGHTER_TYPE = "ShapesHighlighter";
 
 add_task(async function() {
-  let inspector = await openInspectorForURL(TEST_URL);
-  let helper = await getHighlighterHelperFor(HIGHLIGHTER_TYPE)(inspector);
-  let {testActor} = inspector;
+  let env = await openInspectorForURL(TEST_URL);
+  let helper = await getHighlighterHelperFor(HIGHLIGHTER_TYPE)(env);
+  let {testActor, inspector} = env;
+  let view = selectRuleView(inspector);
+  let highlighters = view.highlighters;
 
-  await testPolygonMovePoint(testActor, helper);
-  await testPolygonAddPoint(testActor, helper);
-  await testPolygonRemovePoint(testActor, helper);
-  await testCircleMoveCenter(testActor, helper);
-  await testEllipseMoveRadius(testActor, helper);
-  await testInsetMoveEdges(testActor, helper);
+  let config = {inspector, view, highlighters, testActor, helper};
+
+  await testPolygonMovePoint(config);
+  await testPolygonAddPoint(config);
+  await testPolygonRemovePoint(config);
+  await testCircleMoveCenter(config);
+  await testEllipseMoveRadius(config);
+  await testInsetMoveEdges(config);
 
   helper.finalize();
 });
 
-async function testPolygonMovePoint(testActor, helper) {
-  info("Displaying polygon");
-  await helper.show("#polygon", {mode: "cssClipPath"});
-  let { mouse, highlightedNode } = helper;
+async function getComputedPropertyValue(selector, property, inspector) {
+  let highlightedNode = await getNodeFront(selector, inspector);
+  let computedStyle = await inspector.pageStyle.getComputed(highlightedNode);
+  return computedStyle[property].value;
+}
+
+async function setup(config) {
+  const { view, selector, property, inspector } = config;
+  info(`Turn on shapes highlighter for ${selector}`);
+  await selectNode(selector, inspector);
+  return await toggleShapesHighlighter(view, selector, property, true);
+}
 
-  let points = await helper.getElementAttribute("shapes-polygon", "points");
+async function teardown(config) {
+  const { view, selector, property } = config;
+  info(`Turn off shapes highlighter for ${selector}`);
+  return await toggleShapesHighlighter(view, selector, property, false);
+}
+
+async function testPolygonMovePoint(config) {
+  const {inspector, view, highlighters, testActor, helper} = config;
+  const selector = "#polygon";
+  const property = "clip-path";
+
+  await setup({selector, property, ...config});
+
+  let points = await testActor.getHighlighterNodeAttribute(
+    "shapes-polygon", "points", highlighters.highlighters[HIGHLIGHTER_TYPE]);
   let [x, y] = points.split(" ")[0].split(",");
-  let quads = await testActor.getAllAdjustedQuads("#polygon");
+  let quads = await testActor.getAllAdjustedQuads(selector);
   let { top, left, width, height } = quads.border[0].bounds;
   x = left + width * x / 100;
   y = top + height * y / 100;
   let dx = width / 10;
   let dy = height / 10;
 
+  let onRuleViewChanged = view.once("ruleview-changed");
   info("Moving first polygon point");
+  let { mouse } = helper;
   await mouse.down(x, y);
   await mouse.move(x + dx, y + dy);
   await mouse.up();
   await testActor.reflow();
+  info("Waiting for rule view changed from shape change");
+  await onRuleViewChanged;
 
-  let computedStyle = await highlightedNode.getComputedStyle();
-  let definition = computedStyle["clip-path"].value;
+  let definition = await getComputedPropertyValue(selector, property, inspector);
   ok(definition.includes(`${dx}px ${dy}px`), `Point moved to ${dx}px ${dy}px`);
+
+  await teardown({selector, property, ...config});
 }
 
-async function testPolygonAddPoint(testActor, helper) {
-  await helper.show("#polygon", {mode: "cssClipPath"});
-  let { mouse, highlightedNode } = helper;
+async function testPolygonAddPoint(config) {
+  const {inspector, view, highlighters, testActor, helper} = config;
+  const selector = "#polygon";
+  const property = "clip-path";
+
+  await setup({selector, property, ...config});
 
   // Move first point to have same x as second point, then double click between
   // the two points to add a new one.
-  let points = await helper.getElementAttribute("shapes-polygon", "points");
+  let points = await testActor.getHighlighterNodeAttribute(
+    "shapes-polygon", "points", highlighters.highlighters[HIGHLIGHTER_TYPE]);
   let pointsArray = points.split(" ");
-  let quads = await testActor.getAllAdjustedQuads("#polygon");
+  let quads = await testActor.getAllAdjustedQuads(selector);
   let { top, left, width, height } = quads.border[0].bounds;
   let [x1, y1] = pointsArray[0].split(",");
   let [x2, y2] = pointsArray[1].split(",");
   x1 = left + width * x1 / 100;
   x2 = left + width * x2 / 100;
   y1 = top + height * y1 / 100;
   y2 = top + height * y2 / 100;
 
+  let { mouse } = helper;
   await mouse.down(x1, y1);
   await mouse.move(x2, y1);
   await mouse.up();
   await testActor.reflow();
 
   let newPointX = x2;
   let newPointY = (y1 + y2) / 2;
   let options = {
     selector: ":root",
     x: newPointX,
     y: newPointY,
     center: false,
     options: {clickCount: 2}
   };
 
+  let onRuleViewChanged = view.once("ruleview-changed");
   info("Adding new polygon point");
   await testActor.synthesizeMouse(options);
   await testActor.reflow();
+  info("Waiting for rule view changed from shape change");
+  await onRuleViewChanged;
 
-  let computedStyle = await highlightedNode.getComputedStyle();
-  let definition = computedStyle["clip-path"].value;
   // Decimal precision for coordinates with percentage units is 2
   let precision = 2;
   // Round to the desired decimal precision and cast to Number to remove trailing zeroes.
   newPointX = Number((newPointX * 100 / width).toFixed(precision));
   newPointY = Number((newPointY * 100 / height).toFixed(precision));
+  let definition = await getComputedPropertyValue(selector, property, inspector);
   ok(definition.includes(`${newPointX}% ${newPointY}%`),
      "Point successfuly added");
+
+  await teardown({selector, property, ...config});
 }
 
-async function testPolygonRemovePoint(testActor, helper) {
-  await helper.show("#polygon", {mode: "cssClipPath"});
-  let { highlightedNode } = helper;
+async function testPolygonRemovePoint(config) {
+  const {inspector, highlighters, testActor, helper} = config;
+  const selector = "#polygon";
+  const property = "clip-path";
 
-  let points = await helper.getElementAttribute("shapes-polygon", "points");
+  yield setup({selector, property, ...config});
+
+  let points = await testActor.getHighlighterNodeAttribute(
+    "shapes-polygon", "points", highlighters.highlighters[HIGHLIGHTER_TYPE]);
   let [x, y] = points.split(" ")[0].split(",");
-  let quads = await testActor.getAllAdjustedQuads("#polygon");
+  let quads = await testActor.getAllAdjustedQuads(selector);
   let { top, left, width, height } = quads.border[0].bounds;
 
   let options = {
     selector: ":root",
     x: left + width * x / 100,
     y: top + height * y / 100,
     center: false,
     options: {clickCount: 2}
   };
 
-  info("Removing first polygon point");
-  await testActor.synthesizeMouse(options);
-  await testActor.reflow();
+  info("Move mouse over first point in highlighter");
+  let onEventHandled = highlighters.once("highlighter-event-handled");
+  let { mouse } = helper;
+  await mouse.move(options.x, options.y);
+  await onEventHandled;
+  let markerHidden = await testActor.getHighlighterNodeAttribute(
+    "shapes-marker-hover", "hidden", highlighters.highlighters[HIGHLIGHTER_TYPE]);
+  ok(!markerHidden, "Marker on highlighter is visible");
 
-  let computedStyle = await highlightedNode.getComputedStyle();
-  let definition = computedStyle["clip-path"].value;
+  info("Double click on first point in highlighter");
+  let onShapeChangeApplied = highlighters.once("shapes-highlighter-changes-applied");
+  await testActor.synthesizeMouse(options);
+  info("Waiting for shape changes to apply");
+  await onShapeChangeApplied;
+  let definition = await getComputedPropertyValue(selector, property, inspector);
   ok(!definition.includes(`${x}% ${y}%`), "Point successfully removed");
+
+  await teardown({selector, property, ...config});
 }
 
-async function testCircleMoveCenter(testActor, helper) {
-  await helper.show("#circle", {mode: "cssClipPath"});
-  let { mouse, highlightedNode } = helper;
+async function testCircleMoveCenter(config) {
+  const {inspector, highlighters, testActor, helper} = config;
+  const selector = "#circle";
+  const property = "clip-path";
 
-  let cx = parseFloat(await helper.getElementAttribute("shapes-ellipse", "cx"));
-  let cy = parseFloat(await helper.getElementAttribute("shapes-ellipse", "cy"));
-  let quads = await testActor.getAllAdjustedQuads("#circle");
+  await setup({selector, property, ...config});
+
+  let cx = parseFloat(await testActor.getHighlighterNodeAttribute(
+    "shapes-ellipse", "cx", highlighters.highlighters[HIGHLIGHTER_TYPE]));
+  let cy = parseFloat(await testActor.getHighlighterNodeAttribute(
+    "shapes-ellipse", "cy", highlighters.highlighters[HIGHLIGHTER_TYPE]));
+  let quads = await testActor.getAllAdjustedQuads(selector);
   let { width, height } = quads.border[0].bounds;
   let cxPixel = width * cx / 100;
   let cyPixel = height * cy / 100;
   let dx = width / 10;
   let dy = height / 10;
 
+  let onShapeChangeApplied = highlighters.once("shapes-highlighter-changes-applied");
   info("Moving circle center");
-  await mouse.down(cxPixel, cyPixel, "#circle");
-  await mouse.move(cxPixel + dx, cyPixel + dy, "#circle");
-  await mouse.up(cxPixel + dx, cyPixel + dy, "#circle");
+  let { mouse } = helper;
+  await mouse.down(cxPixel, cyPixel, selector);
+  await mouse.move(cxPixel + dx, cyPixel + dy, selector);
+  await mouse.up(cxPixel + dx, cyPixel + dy, selector);
   await testActor.reflow();
+  await onShapeChangeApplied;
 
-  let computedStyle = await highlightedNode.getComputedStyle();
-  let definition = computedStyle["clip-path"].value;
+  let definition = await getComputedPropertyValue(selector, property, inspector);
   ok(definition.includes(`at ${cx + 10}% ${cy + 10}%`),
      "Circle center successfully moved");
+
+  await teardown({selector, property, ...config});
 }
 
-async function testEllipseMoveRadius(testActor, helper) {
-  await helper.show("#ellipse", {mode: "cssClipPath"});
-  let { mouse, highlightedNode } = helper;
+async function testEllipseMoveRadius(config) {
+  const {inspector, highlighters, testActor, helper} = config;
+  const selector = "#ellipse";
+  const property = "clip-path";
+
+  await setup({selector, property, ...config});
 
-  let rx = parseFloat(await helper.getElementAttribute("shapes-ellipse", "rx"));
-  let ry = parseFloat(await helper.getElementAttribute("shapes-ellipse", "ry"));
-  let cx = parseFloat(await helper.getElementAttribute("shapes-ellipse", "cx"));
-  let cy = parseFloat(await helper.getElementAttribute("shapes-ellipse", "cy"));
+  let rx = parseFloat(await testActor.getHighlighterNodeAttribute(
+    "shapes-ellipse", "rx", highlighters.highlighters[HIGHLIGHTER_TYPE]));
+  let ry = parseFloat(await testActor.getHighlighterNodeAttribute(
+    "shapes-ellipse", "ry", highlighters.highlighters[HIGHLIGHTER_TYPE]));
+  let cx = parseFloat(await testActor.getHighlighterNodeAttribute(
+    "shapes-ellipse", "cx", highlighters.highlighters[HIGHLIGHTER_TYPE]));
+  let cy = parseFloat(await testActor.getHighlighterNodeAttribute(
+    "shapes-ellipse", "cy", highlighters.highlighters[HIGHLIGHTER_TYPE]));
   let quads = await testActor.getAllAdjustedQuads("#ellipse");
   let { width, height } = quads.content[0].bounds;
-  let computedStyle = await highlightedNode.getComputedStyle();
+  let highlightedNode = await getNodeFront(selector, inspector);
+  let computedStyle = await inspector.pageStyle.getComputed(highlightedNode);
   let paddingTop = parseFloat(computedStyle["padding-top"].value);
   let paddingLeft = parseFloat(computedStyle["padding-left"].value);
   let cxPixel = paddingLeft + width * cx / 100;
   let cyPixel = paddingTop + height * cy / 100;
   let rxPixel = cxPixel + width * rx / 100;
   let ryPixel = cyPixel + height * ry / 100;
   let dx = width / 10;
   let dy = height / 10;
 
+  let { mouse } = helper;
   info("Moving ellipse rx");
-  await mouse.down(rxPixel, cyPixel, "#ellipse");
-  await mouse.move(rxPixel + dx, cyPixel, "#ellipse");
-  await mouse.up(rxPixel + dx, cyPixel, "#ellipse");
+  await mouse.down(rxPixel, cyPixel, selector);
+  await mouse.move(rxPixel + dx, cyPixel, selector);
+  await mouse.up(rxPixel + dx, cyPixel, selector);
   await testActor.reflow();
 
   info("Moving ellipse ry");
-  await mouse.down(cxPixel, ryPixel, "#ellipse");
-  await mouse.move(cxPixel, ryPixel - dy, "#ellipse");
-  await mouse.up(cxPixel, ryPixel - dy, "#ellipse");
+  let onShapeChangeApplied = highlighters.once("shapes-highlighter-changes-applied");
+  await mouse.down(cxPixel, ryPixel, selector);
+  await mouse.move(cxPixel, ryPixel - dy, selector);
+  await mouse.up(cxPixel, ryPixel - dy, selector);
   await testActor.reflow();
+  await onShapeChangeApplied;
 
-  computedStyle = await highlightedNode.getComputedStyle();
-  let definition = computedStyle["clip-path"].value;
+  let definition = await getComputedPropertyValue(selector, property, inspector);
   ok(definition.includes(`${rx + 10}% ${ry - 10}%`),
      "Ellipse radiuses successfully moved");
+
+  await teardown({selector, property, ...config});
 }
 
-async function testInsetMoveEdges(testActor, helper) {
-  await helper.show("#inset", {mode: "cssClipPath"});
-  let { mouse, highlightedNode } = helper;
+async function testInsetMoveEdges(config) {
+  const {inspector, highlighters, testActor, helper} = config;
+  const selector = "#inset";
+  const property = "clip-path";
+
+  await setup({selector, property, ...config});
 
-  let x = parseFloat(await helper.getElementAttribute("shapes-rect", "x"));
-  let y = parseFloat(await helper.getElementAttribute("shapes-rect", "y"));
-  let width = parseFloat(await helper.getElementAttribute("shapes-rect", "width"));
-  let height = parseFloat(await helper.getElementAttribute("shapes-rect", "height"));
-  let quads = await testActor.getAllAdjustedQuads("#inset");
+  let x = parseFloat(await testActor.getHighlighterNodeAttribute(
+    "shapes-rect", "x", highlighters.highlighters[HIGHLIGHTER_TYPE]));
+  let y = parseFloat(await testActor.getHighlighterNodeAttribute(
+    "shapes-rect", "y", highlighters.highlighters[HIGHLIGHTER_TYPE]));
+  let width = parseFloat(await testActor.getHighlighterNodeAttribute(
+    "shapes-rect", "width", highlighters.highlighters[HIGHLIGHTER_TYPE]));
+  let height = parseFloat(await testActor.getHighlighterNodeAttribute(
+    "shapes-rect", "height", highlighters.highlighters[HIGHLIGHTER_TYPE]));
+  let quads = await testActor.getAllAdjustedQuads(selector);
   let { width: elemWidth, height: elemHeight } = quads.content[0].bounds;
 
   let left = elemWidth * x / 100;
   let top = elemHeight * y / 100;
   let right = left + elemWidth * width / 100;
   let bottom = top + elemHeight * height / 100;
   let xCenter = (left + right) / 2;
   let yCenter = (top + bottom) / 2;
   let dx = elemWidth / 10;
   let dy = elemHeight / 10;
+  let { mouse } = helper;
 
   info("Moving inset top");
-  await mouse.down(xCenter, top, "#inset");
-  await mouse.move(xCenter, top + dy, "#inset");
-  await mouse.up(xCenter, top + dy, "#inset");
+  await mouse.down(xCenter, top, selector);
+  await mouse.move(xCenter, top + dy, selector);
+  await mouse.up(xCenter, top + dy, selector);
   await testActor.reflow();
 
   info("Moving inset bottom");
-  await mouse.down(xCenter, bottom, "#inset");
-  await mouse.move(xCenter, bottom + dy, "#inset");
-  await mouse.up(xCenter, bottom + dy, "#inset");
+  await mouse.down(xCenter, bottom, selector);
+  await mouse.move(xCenter, bottom + dy, selector);
+  await mouse.up(xCenter, bottom + dy, selector);
   await testActor.reflow();
 
   info("Moving inset left");
-  await mouse.down(left, yCenter, "#inset");
-  await mouse.move(left + dx, yCenter, "#inset");
-  await mouse.up(left + dx, yCenter, "#inset");
+  await mouse.down(left, yCenter, selector);
+  await mouse.move(left + dx, yCenter, selector);
+  await mouse.up(left + dx, yCenter, selector);
   await testActor.reflow();
 
   info("Moving inset right");
-  await mouse.down(right, yCenter, "#inset");
-  await mouse.move(right + dx, yCenter, "#inset");
-  await mouse.up(right + dx, yCenter, "#inset");
+  let onShapeChangeApplied = highlighters.once("shapes-highlighter-changes-applied");
+  await mouse.down(right, yCenter, selector);
+  await mouse.move(right + dx, yCenter, selector);
+  await mouse.up(right + dx, yCenter, selector);
   await testActor.reflow();
+  await onShapeChangeApplied;
 
-  let computedStyle = await highlightedNode.getComputedStyle();
-  let definition = computedStyle["clip-path"].value;
+  let definition = await getComputedPropertyValue(selector, property, inspector);
   ok(definition.includes(
     `${top + dy}px ${elemWidth - right - dx}px ${100 - y - height - 10}% ${x + 10}%`),
      "Inset edges successfully moved");
+
+  await teardown({selector, property, ...config});
 }
--- a/devtools/client/inspector/test/browser_inspector_highlighter-cssshape_05.js
+++ b/devtools/client/inspector/test/browser_inspector_highlighter-cssshape_05.js
@@ -2,109 +2,114 @@
 /* Any copyright is dedicated to the Public Domain.
  http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
 // Test hovering over shape points in the rule-view and shapes highlighter.
 
 const TEST_URL = URL_ROOT + "doc_inspector_highlighter_cssshapes.html";
-
 const HIGHLIGHTER_TYPE = "ShapesHighlighter";
-const CSS_SHAPES_ENABLED_PREF = "devtools.inspector.shapesHighlighter.enabled";
 
 add_task(async function() {
-  await pushPref(CSS_SHAPES_ENABLED_PREF, true);
   let env = await openInspectorForURL(TEST_URL);
   let helper = await getHighlighterHelperFor(HIGHLIGHTER_TYPE)(env);
   let { testActor, inspector } = env;
   let view = selectRuleView(inspector);
   let highlighters = view.highlighters;
+  let config = { inspector, view, highlighters, testActor, helper };
 
-  await highlightFromRuleView(inspector, view, highlighters, testActor);
-  await highlightFromHighlighter(view, highlighters, testActor, helper);
+  await highlightFromRuleView(config);
+  await highlightFromHighlighter(config);
 });
 
-async function highlightFromRuleView(inspector, view, highlighters, testActor) {
-  await selectNode("#polygon", inspector);
-  await toggleShapesHighlighter(view, highlighters, "#polygon", "clip-path", true);
-  let container = getRuleViewProperty(view, "#polygon", "clip-path").valueSpan;
+async function setup(config) {
+  const { view, selector, property, inspector } = config;
+  info(`Turn on shapes highlighter for ${selector}`);
+  await selectNode(selector, inspector);
+  return await toggleShapesHighlighter(view, selector, property, true);
+}
+
+async function teardown(config) {
+  const { view, selector, property } = config;
+  info(`Turn off shapes highlighter for ${selector}`);
+  return await toggleShapesHighlighter(view, selector, property, false);
+}
+/*
+* Test that points hovered in the rule view will highlight corresponding points
+* in the shapes highlighter on the page.
+*/
+async function highlightFromRuleView(config) {
+  const { view, highlighters, testActor } = config;
+  const selector = "#polygon";
+  const property = "clip-path";
+
+  await setup({ selector, property, ...config });
+
+  let container = getRuleViewProperty(view, selector, property).valueSpan;
   let shapesToggle = container.querySelector(".ruleview-shapeswatch");
 
   let highlighterFront = highlighters.highlighters[HIGHLIGHTER_TYPE];
   let markerHidden = await testActor.getHighlighterNodeAttribute(
     "shapes-marker-hover", "hidden", highlighterFront);
   ok(markerHidden, "Hover marker on highlighter is not visible");
 
   info("Hover over point 0 in rule view");
   let pointSpan = container.querySelector(".ruleview-shape-point[data-point='0']");
   let onHighlighterShown = highlighters.once("shapes-highlighter-shown");
   EventUtils.synthesizeMouseAtCenter(pointSpan, {type: "mousemove"}, view.styleWindow);
   await onHighlighterShown;
 
-  ok(pointSpan.classList.contains("active"), "Hovered span is active");
-  is(highlighters.state.shapes.options.hoverPoint, "0",
-     "Hovered point is saved to state");
-
+  info("Point in shapes highlighter is marked when same point in rule view is hovered");
   markerHidden = await testActor.getHighlighterNodeAttribute(
     "shapes-marker-hover", "hidden", highlighterFront);
   ok(!markerHidden, "Marker on highlighter is visible");
 
   info("Move mouse off point");
   onHighlighterShown = highlighters.once("shapes-highlighter-shown");
   EventUtils.synthesizeMouseAtCenter(shapesToggle, {type: "mousemove"}, view.styleWindow);
   await onHighlighterShown;
 
-  ok(!pointSpan.classList.contains("active"), "Hovered span is no longer active");
-  is(highlighters.state.shapes.options.hoverPoint, null, "Hovered point is null");
-
   markerHidden = await testActor.getHighlighterNodeAttribute(
     "shapes-marker-hover", "hidden", highlighterFront);
   ok(markerHidden, "Marker on highlighter is not visible");
 
-  info("Hide shapes highlighter");
-  await toggleShapesHighlighter(view, highlighters, "#polygon", "clip-path", false);
+  yield teardown({selector, property, ...config});
 }
 
-async function highlightFromHighlighter(view, highlighters, testActor, helper) {
+/*
+* Test that points hovered in the shapes highlighter on the page will highlight
+* corresponding points in the rule view.
+*/
+async function highlightFromHighlighter(config) {
+  const { view, highlighters, testActor, helper } = config;
+  const selector = "#polygon";
+  const property = "clip-path";
+
+  await setup({ selector, property, ...config });
+
   let highlighterFront = highlighters.highlighters[HIGHLIGHTER_TYPE];
   let { mouse } = helper;
-
-  await toggleShapesHighlighter(view, highlighters, "#polygon", "clip-path", true);
-  let container = getRuleViewProperty(view, "#polygon", "clip-path").valueSpan;
+  let container = getRuleViewProperty(view, selector, property).valueSpan;
 
   info("Hover over first point in highlighter");
   let onEventHandled = highlighters.once("highlighter-event-handled");
   await mouse.move(0, 0);
   await onEventHandled;
   let markerHidden = await testActor.getHighlighterNodeAttribute(
     "shapes-marker-hover", "hidden", highlighterFront);
   ok(!markerHidden, "Marker on highlighter is visible");
 
+  info("Point in rule view is marked when same point in shapes highlighter is hovered");
   let pointSpan = container.querySelector(".ruleview-shape-point[data-point='0']");
   ok(pointSpan.classList.contains("active"), "Span for point 0 is active");
-  is(highlighters.state.shapes.hoverPoint, "0", "Hovered point is saved to state");
-
-  info("Check that point is still highlighted after moving it");
-  await mouse.down(0, 0);
-  await mouse.move(10, 10);
-  await mouse.up(10, 10);
-  markerHidden = await testActor.getHighlighterNodeAttribute(
-    "shapes-marker-hover", "hidden", highlighterFront);
-  ok(!markerHidden, "Marker on highlighter is visible after moving point");
-
-  container = getRuleViewProperty(view, "element", "clip-path").valueSpan;
-  pointSpan = container.querySelector(".ruleview-shape-point[data-point='0']");
-  ok(pointSpan.classList.contains("active"),
-     "Span for point 0 is active after moving point");
-  is(highlighters.state.shapes.hoverPoint, "0",
-     "Hovered point is saved to state after moving point");
 
   info("Move mouse off point");
   onEventHandled = highlighters.once("highlighter-event-handled");
   await mouse.move(100, 100);
   await onEventHandled;
   markerHidden = await testActor.getHighlighterNodeAttribute(
     "shapes-marker-hover", "hidden", highlighterFront);
   ok(markerHidden, "Marker on highlighter is no longer visible");
   ok(!pointSpan.classList.contains("active"), "Span for point 0 is no longer active");
-  is(highlighters.state.shapes.hoverPoint, null, "Hovered point is null");
+
+  await teardown({ selector, property, ...config });
 }
--- a/devtools/client/inspector/test/browser_inspector_highlighter-cssshape_06.js
+++ b/devtools/client/inspector/test/browser_inspector_highlighter-cssshape_06.js
@@ -3,145 +3,172 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 // Test that shapes are updated correctly on mouse events in transform mode.
 
 const TEST_URL = URL_ROOT + "doc_inspector_highlighter_cssshapes.html";
 const HIGHLIGHTER_TYPE = "ShapesHighlighter";
-const SHAPE_IDS = ["#polygon-transform", "#circle", "#ellipse", "#inset"];
+const SHAPE_SELECTORS = ["#polygon-transform", "#circle", "#ellipse", "#inset"];
 
 add_task(async function() {
-  let inspector = await openInspectorForURL(TEST_URL);
-  let helper = await getHighlighterHelperFor(HIGHLIGHTER_TYPE)(inspector);
-  let {testActor} = inspector;
+  let env = await openInspectorForURL(TEST_URL);
+  let helper = await getHighlighterHelperFor(HIGHLIGHTER_TYPE)(env);
+  let {testActor, inspector} = env;
+  let view = selectRuleView(inspector);
+  let highlighters = view.highlighters;
+  let config = { inspector, view, highlighters, testActor, helper };
 
-  await testTranslate(testActor, helper);
-  await testScale(testActor, helper);
-
-  helper.finalize();
+  await testTranslate(config);
+  await testScale(config);
 });
 
-async function testTranslate(testActor, helper) {
-  for (let shape of SHAPE_IDS) {
-    info(`Displaying ${shape}`);
-    await helper.show(shape, {mode: "cssClipPath", transformMode: true});
+async function setup(config) {
+  const { inspector, view, selector, property, options } = config;
+  await selectNode(selector, inspector);
+  return await toggleShapesHighlighter(view, selector, property, true, options);
+}
+
+async function testTranslate(config) {
+  const { testActor, helper, highlighters } = config;
+  const options = { transformMode: true };
+  const property = "clip-path";
+
+  for (let selector of SHAPE_SELECTORS) {
+    yield setup({selector, property, options, ...config});
     let { mouse } = helper;
 
-    let { center, width, height } = await getBoundingBoxInPx(testActor, helper, shape);
+    let { center, width, height } = await getBoundingBoxInPx({selector, ...config});
     let [x, y] = center;
     let dx = width / 10;
     let dy = height / 10;
 
-    info(`Translating ${shape}`);
-    await mouse.down(x, y, shape);
-    await mouse.move(x + dx, y + dy, shape);
-    await mouse.up(x + dx, y + dy, shape);
-    await testActor.reflow();
+    info(`Translating ${selector}`);
+    let onShapeChangeApplied = highlighters.once("shapes-highlighter-changes-applied");
+    await mouse.down(x, y, selector);
+    await mouse.move(x + dx, y + dy, selector);
+    await mouse.up(x + dx, y + dy, selector);
+    await onShapeChangeApplied;
 
-    let newBB = await getBoundingBoxInPx(testActor, helper);
-    isnot(newBB.center[0], x, `${shape} translated on y axis`);
-    isnot(newBB.center[1], y, `${shape} translated on x axis`);
+    let newBB = await getBoundingBoxInPx({selector, ...config});
+    isnot(newBB.center[0], x, `${selector} translated on y axis`);
+    isnot(newBB.center[1], y, `${selector} translated on x axis`);
 
-    info(`Translating ${shape} back`);
-    await mouse.down(x + dx, y + dy, shape);
-    await mouse.move(x, y, shape);
-    await mouse.up(x, y, shape);
+    info(`Translating ${selector} back`);
+    onShapeChangeApplied = highlighters.once("shapes-highlighter-changes-applied");
+    await mouse.down(x + dx, y + dy, selector);
+    await mouse.move(x, y, selector);
+    await mouse.up(x, y, selector);
     await testActor.reflow();
+    await onShapeChangeApplied;
 
-    newBB = await getBoundingBoxInPx(testActor, helper, shape);
-    is(newBB.center[0], x, `${shape} translated back on x axis`);
-    is(newBB.center[1], y, `${shape} translated back on y axis`);
+    newBB = await getBoundingBoxInPx({selector, ...config});
+    is(newBB.center[0], x, `${selector} translated back on x axis`);
+    is(newBB.center[1], y, `${selector} translated back on y axis`);
   }
 }
 
-async function testScale(testActor, helper) {
-  for (let shape of SHAPE_IDS) {
-    info(`Displaying ${shape}`);
-    await helper.show(shape, {mode: "cssClipPath", transformMode: true});
+async function testScale(config) {
+  const { testActor, helper, highlighters } = config;
+  const options = { transformMode: true };
+  const property = "clip-path";
+
+  for (let selector of SHAPE_SELECTORS) {
+    await setup({selector, property, options, ...config});
     let { mouse } = helper;
 
     let { nw, width,
-          height, center } = await getBoundingBoxInPx(testActor, helper, shape);
+          height, center } = await getBoundingBoxInPx({selector, ...config});
 
     // if the top or left edges are not visible, move the shape so it is.
     if (nw[0] < 0 || nw[1] < 0) {
       let [x, y] = center;
       let dx = Math.max(0, -nw[0]);
       let dy = Math.max(0, -nw[1]);
-      await mouse.down(x, y, shape);
-      await mouse.move(x + dx, y + dy, shape);
-      await mouse.up(x + dx, y + dy, shape);
+      await mouse.down(x, y, selector);
+      await mouse.move(x + dx, y + dy, selector);
+      await mouse.up(x + dx, y + dy, selector);
       await testActor.reflow();
       nw[0] += dx;
       nw[1] += dy;
     }
     let dx = width / 10;
     let dy = height / 10;
 
     info("Scaling from nw");
-    await mouse.down(nw[0], nw[1], shape);
-    await mouse.move(nw[0] + dx, nw[1] + dy, shape);
-    await mouse.up(nw[0] + dx, nw[1] + dy, shape);
+    let onShapeChangeApplied = highlighters.once("shapes-highlighter-changes-applied");
+    await mouse.down(nw[0], nw[1], selector);
+    await mouse.move(nw[0] + dx, nw[1] + dy, selector);
+    await mouse.up(nw[0] + dx, nw[1] + dy, selector);
     await testActor.reflow();
+    await onShapeChangeApplied;
 
-    let nwBB = await getBoundingBoxInPx(testActor, helper, shape);
-    isnot(nwBB.nw[0], nw[0], `${shape} nw moved right after nw scale`);
-    isnot(nwBB.nw[1], nw[1], `${shape} nw moved down after nw scale`);
-    isnot(nwBB.width, width, `${shape} width reduced after nw scale`);
-    isnot(nwBB.height, height, `${shape} height reduced after nw scale`);
+    let nwBB = await getBoundingBoxInPx({selector, ...config});
+    isnot(nwBB.nw[0], nw[0], `${selector} nw moved right after nw scale`);
+    isnot(nwBB.nw[1], nw[1], `${selector} nw moved down after nw scale`);
+    isnot(nwBB.width, width, `${selector} width reduced after nw scale`);
+    isnot(nwBB.height, height, `${selector} height reduced after nw scale`);
 
     info("Scaling from ne");
-    await mouse.down(nwBB.ne[0], nwBB.ne[1], shape);
-    await mouse.move(nwBB.ne[0] - dx, nwBB.ne[1] + dy, shape);
-    await mouse.up(nwBB.ne[0] - dx, nwBB.ne[1] + dy, shape);
+    onShapeChangeApplied = highlighters.once("shapes-highlighter-changes-applied");
+    await mouse.down(nwBB.ne[0], nwBB.ne[1], selector);
+    await mouse.move(nwBB.ne[0] - dx, nwBB.ne[1] + dy, selector);
+    await mouse.up(nwBB.ne[0] - dx, nwBB.ne[1] + dy, selector);
     await testActor.reflow();
+    await onShapeChangeApplied;
 
-    let neBB = await getBoundingBoxInPx(testActor, helper, shape);
-    isnot(neBB.ne[0], nwBB.ne[0], `${shape} ne moved right after ne scale`);
-    isnot(neBB.ne[1], nwBB.ne[1], `${shape} ne moved down after ne scale`);
-    isnot(neBB.width, nwBB.width, `${shape} width reduced after ne scale`);
-    isnot(neBB.height, nwBB.height, `${shape} height reduced after ne scale`);
+    let neBB = await getBoundingBoxInPx({selector, ...config});
+    isnot(neBB.ne[0], nwBB.ne[0], `${selector} ne moved right after ne scale`);
+    isnot(neBB.ne[1], nwBB.ne[1], `${selector} ne moved down after ne scale`);
+    isnot(neBB.width, nwBB.width, `${selector} width reduced after ne scale`);
+    isnot(neBB.height, nwBB.height, `${selector} height reduced after ne scale`);
 
     info("Scaling from sw");
-    await mouse.down(neBB.sw[0], neBB.sw[1], shape);
-    await mouse.move(neBB.sw[0] + dx, neBB.sw[1] - dy, shape);
-    await mouse.up(neBB.sw[0] + dx, neBB.sw[1] - dy, shape);
+    onShapeChangeApplied = highlighters.once("shapes-highlighter-changes-applied");
+    await mouse.down(neBB.sw[0], neBB.sw[1], selector);
+    await mouse.move(neBB.sw[0] + dx, neBB.sw[1] - dy, selector);
+    await mouse.up(neBB.sw[0] + dx, neBB.sw[1] - dy, selector);
     await testActor.reflow();
+    await onShapeChangeApplied;
 
-    let swBB = await getBoundingBoxInPx(testActor, helper, shape);
-    isnot(swBB.sw[0], neBB.sw[0], `${shape} sw moved right after sw scale`);
-    isnot(swBB.sw[1], neBB.sw[1], `${shape} sw moved down after sw scale`);
-    isnot(swBB.width, neBB.width, `${shape} width reduced after sw scale`);
-    isnot(swBB.height, neBB.height, `${shape} height reduced after sw scale`);
+    let swBB = await getBoundingBoxInPx({selector, ...config});
+    isnot(swBB.sw[0], neBB.sw[0], `${selector} sw moved right after sw scale`);
+    isnot(swBB.sw[1], neBB.sw[1], `${selector} sw moved down after sw scale`);
+    isnot(swBB.width, neBB.width, `${selector} width reduced after sw scale`);
+    isnot(swBB.height, neBB.height, `${selector} height reduced after sw scale`);
 
     info("Scaling from se");
-    await mouse.down(swBB.se[0], swBB.se[1], shape);
-    await mouse.move(swBB.se[0] - dx, swBB.se[1] - dy, shape);
-    await mouse.up(swBB.se[0] - dx, swBB.se[1] - dy, shape);
+    onShapeChangeApplied = highlighters.once("shapes-highlighter-changes-applied");
+    await mouse.down(swBB.se[0], swBB.se[1], selector);
+    await mouse.move(swBB.se[0] - dx, swBB.se[1] - dy, selector);
+    await mouse.up(swBB.se[0] - dx, swBB.se[1] - dy, selector);
     await testActor.reflow();
+    await onShapeChangeApplied;
 
-    let seBB = await getBoundingBoxInPx(testActor, helper, shape);
-    isnot(seBB.se[0], swBB.se[0], `${shape} se moved right after se scale`);
-    isnot(seBB.se[1], swBB.se[1], `${shape} se moved down after se scale`);
-    isnot(seBB.width, swBB.width, `${shape} width reduced after se scale`);
-    isnot(seBB.height, swBB.height, `${shape} height reduced after se scale`);
+    let seBB = await getBoundingBoxInPx({selector, ...config});
+    isnot(seBB.se[0], swBB.se[0], `${selector} se moved right after se scale`);
+    isnot(seBB.se[1], swBB.se[1], `${selector} se moved down after se scale`);
+    isnot(seBB.width, swBB.width, `${selector} width reduced after se scale`);
+    isnot(seBB.height, swBB.height, `${selector} height reduced after se scale`);
   }
 }
 
-async function getBoundingBoxInPx(testActor, helper, shape = "#polygon") {
-  let quads = await testActor.getAllAdjustedQuads(shape);
+async function getBoundingBoxInPx(config) {
+  const { testActor, selector, inspector, highlighters } = config;
+  let quads = await testActor.getAllAdjustedQuads(selector);
   let { width, height } = quads.content[0].bounds;
-  let computedStyle = await helper.highlightedNode.getComputedStyle();
+  let highlightedNode = await getNodeFront(selector, inspector);
+  let computedStyle = await inspector.pageStyle.getComputed(highlightedNode);
   let paddingTop = parseFloat(computedStyle["padding-top"].value);
   let paddingLeft = parseFloat(computedStyle["padding-left"].value);
 
   // path is always of form "Mx y Lx y Lx y Lx y Z", where x/y are numbers
-  let path = await helper.getElementAttribute("shapes-bounding-box", "d");
+  let path = await testActor.getHighlighterNodeAttribute(
+    "shapes-bounding-box", "d", highlighters.highlighters[HIGHLIGHTER_TYPE]);
   let coords = path.replace(/[MLZ]/g, "").split(" ").map((n, i) => {
     return i % 2 === 0 ? paddingLeft + width * n / 100 : paddingTop + height * n / 100;
   });
 
   let nw = [coords[0], coords[1]];
   let ne = [coords[2], coords[3]];
   let se = [coords[4], coords[5]];
   let sw = [coords[6], coords[7]];
--- a/devtools/client/inspector/test/browser_inspector_highlighter-cssshape_07.js
+++ b/devtools/client/inspector/test/browser_inspector_highlighter-cssshape_07.js
@@ -3,111 +3,132 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 // Test that shapes are updated correctly for scaling on one axis in transform mode.
 
 const TEST_URL = URL_ROOT + "doc_inspector_highlighter_cssshapes.html";
 const HIGHLIGHTER_TYPE = "ShapesHighlighter";
-const SHAPE_IDS = ["#polygon-transform", "#ellipse"];
+const SHAPE_SELECTORS = ["#polygon-transform", "#ellipse"];
 
 add_task(async function() {
-  let inspector = await openInspectorForURL(TEST_URL);
-  let helper = await getHighlighterHelperFor(HIGHLIGHTER_TYPE)(inspector);
-  let {testActor} = inspector;
+  let env = await openInspectorForURL(TEST_URL);
+  let helper = await getHighlighterHelperFor(HIGHLIGHTER_TYPE)(env);
+  let {testActor, inspector} = env;
+  let view = selectRuleView(inspector);
+  let highlighters = view.highlighters;
+  let config = { inspector, view, highlighters, testActor, helper };
 
-  await testOneDimScale(testActor, helper);
-
-  helper.finalize();
+  await testOneDimScale(config);
 });
 
-async function testOneDimScale(testActor, helper) {
-  for (let shape of SHAPE_IDS) {
-    info(`Displaying ${shape}`);
-    await helper.show(shape, {mode: "cssClipPath", transformMode: true});
+async function setup(config) {
+  const { inspector, view, selector, property, options } = config;
+  await selectNode(selector, inspector);
+  return await toggleShapesHighlighter(view, selector, property, true, options);
+}
+
+async function testOneDimScale(config) {
+  const { testActor, helper, highlighters } = config;
+  const options = { transformMode: true };
+  const property = "clip-path";
+
+  for (let selector of SHAPE_SELECTORS) {
+    await setup({selector, property, options, ...config});
     let { mouse } = helper;
 
     let { nw, width,
-          height, center } = await getBoundingBoxInPx(testActor, helper, shape);
+          height, center } = await getBoundingBoxInPx({selector, ...config});
 
     // if the top or left edges are not visible, move the shape so it is.
     if (nw[0] < 0 || nw[1] < 0) {
       let [x, y] = center;
       let dx = Math.max(0, -nw[0]);
       let dy = Math.max(0, -nw[1]);
-      await mouse.down(x, y, shape);
-      await mouse.move(x + dx, y + dy, shape);
-      await mouse.up(x + dx, y + dy, shape);
+      await mouse.down(x, y, selector);
+      await mouse.move(x + dx, y + dy, selector);
+      await mouse.up(x + dx, y + dy, selector);
       await testActor.reflow();
       nw[0] += dx;
       nw[1] += dy;
     }
     let dx = width / 10;
     let dy = height / 10;
 
     info("Scaling from w");
-    await mouse.down(nw[0], center[1], shape);
-    await mouse.move(nw[0] + dx, center[1], shape);
-    await mouse.up(nw[0] + dx, center[1], shape);
+    let onShapeChangeApplied = highlighters.once("shapes-highlighter-changes-applied");
+    await mouse.down(nw[0], center[1], selector);
+    await mouse.move(nw[0] + dx, center[1], selector);
+    await mouse.up(nw[0] + dx, center[1], selector);
     await testActor.reflow();
+    await onShapeChangeApplied;
 
-    let wBB = await getBoundingBoxInPx(testActor, helper, shape);
-    isnot(wBB.nw[0], nw[0], `${shape} nw moved right after w scale`);
-    is(wBB.nw[1], nw[1], `${shape} nw not moved down after w scale`);
-    isnot(wBB.width, width, `${shape} width reduced after w scale`);
-    is(wBB.height, height, `${shape} height not reduced after w scale`);
+    let wBB = await getBoundingBoxInPx({selector, ...config});
+    isnot(wBB.nw[0], nw[0], `${selector} nw moved right after w scale`);
+    is(wBB.nw[1], nw[1], `${selector} nw not moved down after w scale`);
+    isnot(wBB.width, width, `${selector} width reduced after w scale`);
+    is(wBB.height, height, `${selector} height not reduced after w scale`);
 
     info("Scaling from e");
-    await mouse.down(wBB.ne[0], center[1], shape);
-    await mouse.move(wBB.ne[0] - dx, center[1], shape);
-    await mouse.up(wBB.ne[0] - dx, center[1], shape);
+    onShapeChangeApplied = highlighters.once("shapes-highlighter-changes-applied");
+    await mouse.down(wBB.ne[0], center[1], selector);
+    await mouse.move(wBB.ne[0] - dx, center[1], selector);
+    await mouse.up(wBB.ne[0] - dx, center[1], selector);
     await testActor.reflow();
+    await onShapeChangeApplied;
 
-    let eBB = await getBoundingBoxInPx(testActor, helper, shape);
-    isnot(eBB.ne[0], wBB.ne[0], `${shape} ne moved left after e scale`);
-    is(eBB.ne[1], wBB.ne[1], `${shape} ne not moved down after e scale`);
-    isnot(eBB.width, wBB.width, `${shape} width reduced after e scale`);
-    is(eBB.height, wBB.height, `${shape} height not reduced after e scale`);
+    let eBB = await getBoundingBoxInPx({selector, ...config});
+    isnot(eBB.ne[0], wBB.ne[0], `${selector} ne moved left after e scale`);
+    is(eBB.ne[1], wBB.ne[1], `${selector} ne not moved down after e scale`);
+    isnot(eBB.width, wBB.width, `${selector} width reduced after e scale`);
+    is(eBB.height, wBB.height, `${selector} height not reduced after e scale`);
 
     info("Scaling from s");
-    await mouse.down(eBB.center[0], eBB.sw[1], shape);
-    await mouse.move(eBB.center[0], eBB.sw[1] - dy, shape);
-    await mouse.up(eBB.center[0], eBB.sw[1] - dy, shape);
+    onShapeChangeApplied = highlighters.once("shapes-highlighter-changes-applied");
+    await mouse.down(eBB.center[0], eBB.sw[1], selector);
+    await mouse.move(eBB.center[0], eBB.sw[1] - dy, selector);
+    await mouse.up(eBB.center[0], eBB.sw[1] - dy, selector);
     await testActor.reflow();
+    await onShapeChangeApplied;
 
-    let sBB = await getBoundingBoxInPx(testActor, helper, shape);
-    is(sBB.sw[0], eBB.sw[0], `${shape} sw not moved right after w scale`);
-    isnot(sBB.sw[1], eBB.sw[1], `${shape} sw moved down after w scale`);
-    is(sBB.width, eBB.width, `${shape} width not reduced after w scale`);
-    isnot(sBB.height, eBB.height, `${shape} height reduced after w scale`);
+    let sBB = await getBoundingBoxInPx({selector, ...config});
+    is(sBB.sw[0], eBB.sw[0], `${selector} sw not moved right after w scale`);
+    isnot(sBB.sw[1], eBB.sw[1], `${selector} sw moved down after w scale`);
+    is(sBB.width, eBB.width, `${selector} width not reduced after w scale`);
+    isnot(sBB.height, eBB.height, `${selector} height reduced after w scale`);
 
     info("Scaling from n");
-    await mouse.down(sBB.center[0], sBB.nw[1], shape);
-    await mouse.move(sBB.center[0], sBB.nw[1] + dy, shape);
-    await mouse.up(sBB.center[0], sBB.nw[1] + dy, shape);
+    onShapeChangeApplied = highlighters.once("shapes-highlighter-changes-applied");
+    await mouse.down(sBB.center[0], sBB.nw[1], selector);
+    await mouse.move(sBB.center[0], sBB.nw[1] + dy, selector);
+    await mouse.up(sBB.center[0], sBB.nw[1] + dy, selector);
     await testActor.reflow();
+    await onShapeChangeApplied;
 
-    let nBB = await getBoundingBoxInPx(testActor, helper, shape);
-    is(nBB.nw[0], sBB.nw[0], `${shape} nw not moved right after n scale`);
-    isnot(nBB.nw[1], sBB.nw[1], `${shape} nw moved down after n scale`);
-    is(nBB.width, sBB.width, `${shape} width reduced after n scale`);
-    isnot(nBB.height, sBB.height, `${shape} height not reduced after n scale`);
+    let nBB = await getBoundingBoxInPx({selector, ...config});
+    is(nBB.nw[0], sBB.nw[0], `${selector} nw not moved right after n scale`);
+    isnot(nBB.nw[1], sBB.nw[1], `${selector} nw moved down after n scale`);
+    is(nBB.width, sBB.width, `${selector} width reduced after n scale`);
+    isnot(nBB.height, sBB.height, `${selector} height not reduced after n scale`);
   }
 }
 
-async function getBoundingBoxInPx(testActor, helper, shape = "#polygon") {
-  let quads = await testActor.getAllAdjustedQuads(shape);
+async function getBoundingBoxInPx(config) {
+  const { testActor, selector, inspector, highlighters } = config;
+  let quads = await testActor.getAllAdjustedQuads(selector);
   let { width, height } = quads.content[0].bounds;
-  let computedStyle = await helper.highlightedNode.getComputedStyle();
+  let highlightedNode = await getNodeFront(selector, inspector);
+  let computedStyle = await inspector.pageStyle.getComputed(highlightedNode);
   let paddingTop = parseFloat(computedStyle["padding-top"].value);
   let paddingLeft = parseFloat(computedStyle["padding-left"].value);
 
   // path is always of form "Mx y Lx y Lx y Lx y Z", where x/y are numbers
-  let path = await helper.getElementAttribute("shapes-bounding-box", "d");
+  let path = await testActor.getHighlighterNodeAttribute(
+    "shapes-bounding-box", "d", highlighters.highlighters[HIGHLIGHTER_TYPE]);
   let coords = path.replace(/[MLZ]/g, "").split(" ").map((n, i) => {
     return i % 2 === 0 ? paddingLeft + width * n / 100 : paddingTop + height * n / 100;
   });
 
   let nw = [coords[0], coords[1]];
   let ne = [coords[2], coords[3]];
   let se = [coords[4], coords[5]];
   let sw = [coords[6], coords[7]];
--- a/devtools/client/inspector/test/browser_inspector_highlighter-cssshape_iframe_01.js
+++ b/devtools/client/inspector/test/browser_inspector_highlighter-cssshape_iframe_01.js
@@ -5,42 +5,56 @@
 "use strict";
 
 // Test that shapes in iframes are updated correctly on mouse events.
 
 const TEST_URL = URL_ROOT + "doc_inspector_highlighter_cssshapes_iframe.html";
 const HIGHLIGHTER_TYPE = "ShapesHighlighter";
 
 add_task(async function() {
-  let inspector = await openInspectorForURL(TEST_URL);
-  let helper = await getHighlighterHelperFor(HIGHLIGHTER_TYPE)(inspector);
-  let {testActor} = inspector;
+  let env = await openInspectorForURL(TEST_URL);
+  let helper = await getHighlighterHelperFor(HIGHLIGHTER_TYPE)(env);
+  let {testActor, inspector} = env;
+  let view = selectRuleView(inspector);
+  let highlighters = view.highlighters;
+  let config = {inspector, view, highlighters, testActor, helper};
 
-  await testPolygonIframeMovePoint(testActor, helper);
-
-  await helper.finalize();
+  await testPolygonIframeMovePoint(config);
 });
 
-async function testPolygonIframeMovePoint(testActor, helper) {
-  info("Displaying polygon");
-  await helper.show("#polygon", {mode: "cssClipPath"}, "#frame");
-  let { mouse, highlightedNode } = helper;
+async function testPolygonIframeMovePoint(config) {
+  const { inspector, view, testActor, helper } = config;
+  const selector = "#polygon";
+  const property = "clip-path";
+
+  info(`Turn on shapes highlighter for ${selector}`);
+  // Get a reference to the highlighter's target node inside the iframe.
+  let highlightedNode = await getNodeFrontInFrame(selector, "#frame", inspector);
+  // Select the nested node so toggling of the shapes highlighter works from the rule view
+  await selectNode(highlightedNode, inspector);
+  await toggleShapesHighlighter(view, selector, property, true);
+  let { mouse } = helper;
+
+  let onRuleViewChanged = view.once("ruleview-changed");
 
   info("Moving polygon point visible in iframe");
   await mouse.down(10, 10);
   await mouse.move(20, 20);
   await mouse.up();
   await testActor.reflow();
+  await onRuleViewChanged;
 
-  let computedStyle = await highlightedNode.getComputedStyle();
+  let computedStyle = yield inspector.pageStyle.getComputed(highlightedNode);
   let definition = computedStyle["clip-path"].value;
   ok(definition.includes("10px 10px"), "Point moved to 10px 10px");
 
+  onRuleViewChanged = view.once("ruleview-changed");
   info("Moving polygon point not visible in iframe");
   await mouse.down(110, 410);
   await mouse.move(120, 420);
   await mouse.up();
   await testActor.reflow();
+  await onRuleViewChanged;
 
-  computedStyle = await highlightedNode.getComputedStyle();
+  computedStyle = await inspector.pageStyle.getComputed(highlightedNode);
   definition = computedStyle["clip-path"].value;
   ok(definition.includes("110px 51.25%"), "Point moved to 110px 51.25%");
 }
--- a/devtools/client/inspector/test/head.js
+++ b/devtools/client/inspector/test/head.js
@@ -810,36 +810,54 @@ async function getDisplayedNodeTextConte
 }
 
 /**
  * Toggle the shapes highlighter by simulating a click on the toggle
  * in the rules view with the given selector and property
  *
  * @param {CssRuleView} view
  *        The instance of the rule-view panel
- * @param {Object} highlighters
- *        The highlighters instance of the rule-view panel
  * @param {String} selector
  *        The selector in the rule-view to look for the property in
  * @param {String} property
  *        The name of the property
  * @param {Boolean} show
  *        If true, the shapes highlighter is being shown. If false, it is being hidden
+ * @param {Options} options
+ *        Config option for the shapes highlighter. Contains:
+ *        - {Boolean} transformMode: wether to show the highlighter in transforms mode
  */
-async function toggleShapesHighlighter(view, highlighters, selector, property, show) {
-  info("Toggle shapes highlighter");
-  let container = getRuleViewProperty(view, selector, property).valueSpan;
-  let shapesToggle = container.querySelector(".ruleview-shapeswatch");
+async function toggleShapesHighlighter(view, selector, property, show, options = {}) {
+  info(`Toggle shapes highlighter ${show ? "on" : "off"} for ${property} on ${selector}`);
+  const highlighters = view.highlighters;
+  const container = getRuleViewProperty(view, selector, property).valueSpan;
+  const shapesToggle = container.querySelector(".ruleview-shapeswatch");
+  const SHAPES_IN_CONTEXT_EDITOR = "shapesEditor";
+  // On first call, a shape highlighter instance may exist, but no swatches that trigger
+  // it may be associated yet. Once a swatch that gets associated, it will "arm" the
+  // highlighter so that clicks on it will toggle the highlighter.
+  // On subsequent calls, the swatch may exist so the arming event will not be triggered.
+  if (!highlighters.editors[SHAPES_IN_CONTEXT_EDITOR] ||
+      !highlighters.editors[SHAPES_IN_CONTEXT_EDITOR].hasSwatch(shapesToggle)) {
+    info("Wait for shapes highlighter swatch to be ready");
+    await highlighters.once("shapes-highlighter-armed");
+  }
+
+  let metaKey = options.transformMode;
+  let ctrlKey = options.transformMode;
+
   if (show) {
     let onHighlighterShown = highlighters.once("shapes-highlighter-shown");
-    shapesToggle.click();
+    EventUtils.sendMouseEvent({type: "click", metaKey, ctrlKey },
+      shapesToggle, view.styleWindow);
     await onHighlighterShown;
   } else {
     let onHighlighterHidden = highlighters.once("shapes-highlighter-hidden");
-    shapesToggle.click();
+    EventUtils.sendMouseEvent({type: "click", metaKey, ctrlKey },
+      shapesToggle, view.styleWindow);
     await onHighlighterHidden;
   }
 }
 
 /**
  * Expand the provided markup container programatically and  wait for all children to
  * update.
  */
new file mode 100644
--- /dev/null
+++ b/devtools/client/shared/widgets/ShapesInContextEditor.js
@@ -0,0 +1,429 @@
+/* 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 EventEmitter = require("devtools/shared/event-emitter");
+const { debounce } = require("devtools/shared/debounce");
+
+/**
+ * The ShapesInContextEditor:
+ * - communicates with the ShapesHighlighter actor from the server;
+ * - triggers the shapes highlighter on click on a swatch element (aka toggle icon)
+ *   in the Rule view next to CSS properties like `shape-outside` and `clip-path`;
+ * - listens to events for shape change and hover point coming from the shape-highlighter;
+ * - writes shape value changes to the CSS declaration it was triggered from;
+ * - synchroninses highlighting coordinate points on mouse over between the shapes
+ *   highlighter and the shape value shown in the Rule view.
+ *
+ * It behaves like a singleton, though it is not yet implemented like that.
+ * It is instantiated once in HighlightersOverlay by calls to .getInContextEditor().
+ * It is requested by TextPropertyEditor instances for `clip-path` and `shape-outside`
+ * CSS properties to register swatches that should trigger the shapes highlighter and to
+ * which shape values should be written back.
+ */
+class ShapesInContextEditor {
+  constructor(highlighter, inspector, state) {
+    EventEmitter.decorate(this);
+
+    this.activeSwatch = null;
+    this.activeProperty = null;
+    this.inspector = inspector;
+    this.highlighter = highlighter;
+    // Refence to the NodeFront currently being highlighted.
+    this.highlighterTargetNode = null;
+    this.highligherEventHandlers = {};
+    this.highligherEventHandlers["shape-change"] = this.onShapeChange;
+    this.highligherEventHandlers["shape-hover-on"] = this.onShapeHover;
+    this.highligherEventHandlers["shape-hover-off"] = this.onShapeHover;
+    // Map with data required to preview and persist shape value changes to the Rule view.
+    // keys - TextProperty instances relevant for shape editor (clip-path, shape-outside).
+    // values - objects with references to swatch elements that trigger the shape editor
+    //          and callbacks used to preview and persist shape value changes.
+    this.links = new Map();
+    // Reference to Rule view used to listen for changes
+    this.ruleView = this.inspector.getPanel("ruleview").view;
+    // Reference of |state| from HighlightersOverlay.
+    this.state = state;
+
+    // Commit triggers expensive DOM changes in TextPropertyEditor.update()
+    // so we debounce it.
+    this.commit = debounce(this.commit, 200, this);
+    this.onChangesApplied = this.onChangesApplied.bind(this);
+    this.onHighlighterEvent = this.onHighlighterEvent.bind(this);
+    this.onNodeFrontChanged = this.onNodeFrontChanged.bind(this);
+    this.onRuleViewChanged = this.onRuleViewChanged.bind(this);
+    this.onSwatchClick = this.onSwatchClick.bind(this);
+
+    this.highlighter.on("highlighter-event", this.onHighlighterEvent);
+    this.ruleView.on("ruleview-changed", this.onRuleViewChanged);
+  }
+
+  /**
+  * The shapes in-context editor works by listening to shape value changes from the shapes
+  * highlighter and mapping them to the correct CSS property in the Rule view.
+  *
+  * In order to know where to map changes, the TextPropertyEditor instances register
+  * themselves in a map object internal to the ShapesInContextEditor.
+  * Calling the `ShapesInContextEditor.link()` method, they provide:
+  * - the TextProperty model to which shape value changes should map to (this is the key
+  * in the internal map object);
+  * - the swatch element that triggers the shapes highlighter,
+  * - callbacks that must be used in order to preview and persist the shape value changes.
+  *
+  * When the TextPropertyEditor updates, it rebuilds part of its DOM and destroys the
+  * original swatch element. Losing that reference to the swatch element breaks the
+  * ability to show the indicator for the shape editor that is on and prevents the preview
+  * functionality from working properly. Because of that, this link() method gets called
+  * on every TextPropertyEditor.update() and, if the TextProperty model used as a key is
+  * already registered, the old swatch element reference is replaced with the new one.
+  *
+  * @param {TextProperty} prop
+  *        TextProperty instance for clip-path or shape-outside from Rule view for the
+  *        selected element.
+  * @param {Node} swatch
+  *        Reference to DOM element next to shape value that toggles the shapes
+  *        highlighter. This element is destroyed after each call to |this.commit()|
+  *        because that rebuilds the DOM for the shape value in the Rule view.
+  *        Repeated calls to this method with the same prop (TextProperty) will
+  *        replace the swatch reference to the new element for consistent behaviour.
+  * @param {Object} callbacks
+  *        Collection of callbacks from the TextPropertyEditor:
+  *        - onPreview: method called to preview a shape value on the element
+  *        - onCommit: method called to commit a shape value to the Rule view.
+  */
+  link(prop, swatch, callbacks = {}) {
+    if (this.links.has(prop)) {
+      // Swatch element may have changed, replace with given reference.
+      this.replaceSwatch(prop, swatch);
+      return;
+    }
+    if (!callbacks.onPreview) {
+      callbacks.onPreview = function() {};
+    }
+    if (!callbacks.onCommit) {
+      callbacks.onCommit = function() {};
+    }
+
+    swatch.addEventListener("click", this.onSwatchClick);
+    this.links.set(prop, { swatch, callbacks });
+
+    // Event on HighlightersOverlay expected by tests to know when to click the swatch.
+    this.inspector.highlighters.emit("shapes-highlighter-armed");
+  }
+
+  /**
+  * Remove references to swatch and callbacks for the given TextProperty model so that
+  * shape value changes cannot map back to it and the shape editor cannot be triggered
+  * from its associated swatch element.
+  *
+  * @param {TextProperty} prop
+  *        TextProperty instance from Rule view.
+  */
+  async unlink(prop) {
+    let data = this.links.get(prop);
+    if (!data || !data.swatch) {
+      return;
+    }
+    if (this.activeProperty === prop) {
+      await this.hide();
+    }
+
+    data.swatch.classList.remove("active");
+    data.swatch.removeEventListener("click", this.onSwatchClick);
+    this.links.delete(prop);
+  }
+
+  /**
+  * Remove all linked references from TextPropertyEditor.
+  */
+  async unlinkAll() {
+    for (let [prop] of this.links) {
+      await this.unlink(prop);
+    }
+  }
+
+  /**
+  * If the given TextProperty exists as a key in |this.links|, replace the reference it
+  * has to the swatch element with a new node reference.
+  *
+  * @param {TextProperty} prop
+  *        TextProperty instance from Rule view.
+  * @param {Node} swatch
+  *        Reference to swatch DOM element.
+  */
+  replaceSwatch(prop, swatch) {
+    let data = this.links.get(prop);
+    if (data.swatch) {
+      // Cleanup old
+      data.swatch.removeEventListener("click", this.onSwatchClick);
+      data.swatch = undefined;
+      // Setup new
+      swatch.addEventListener("click", this.onSwatchClick);
+      data.swatch = swatch;
+    }
+  }
+
+  /**
+  * Check if the given swatch DOM element already exists in the collection of linked
+  * swatches.
+  *
+  * @param {Node} swatch
+  *        Reference to swatch DOM element.
+  * @return {Boolean}
+  *
+  */
+  hasSwatch(swatch) {
+    for (let [, data] of this.links) {
+      if (data.swatch == swatch) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  /**
+  * Called when the element style changes from the Rule view.
+  * If the TextProperty we're acting on isn't enabled anymore or overridden,
+  * turn off the shapes highlighter.
+  */
+  async onRuleViewChanged() {
+    if (this.activeProperty &&
+      (!this.activeProperty.enabled || this.activeProperty.overridden)) {
+      await this.hide();
+    }
+  }
+
+  /**
+  * Called when a swatch element is clicked. Toggles shapes highlighter to show or hide.
+  * Sets the current swatch and corresponding TextProperty as the active ones. They will
+  * be immediately unset if the toggle action is to hide the shapes highlighter.
+  *
+  * @param {MouseEvent} event
+  *        Mouse click event.
+  */
+  onSwatchClick(event) {
+    event.stopPropagation();
+    for (let [prop, data] of this.links) {
+      if (data.swatch == event.target) {
+        this.activeSwatch = data.swatch;
+        this.activeSwatch.classList.add("active");
+        this.activeProperty = prop;
+        break;
+      }
+    }
+
+    let nodeFront = this.inspector.selection.nodeFront;
+    let options =  {
+      mode: event.target.dataset.mode,
+      transformMode: event.metaKey || event.ctrlKey
+    };
+
+    this.toggle(nodeFront, options);
+  }
+
+  /**
+   * Toggle the shapes highlighter for the given element.
+   *
+   * @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.
+   */
+  async toggle(node, options) {
+    if (node == this.highlighterTargetNode) {
+      if (!options.transformMode) {
+        await this.hide();
+        return;
+      }
+
+      options.transformMode = !this.state.shapes.options.transformMode;
+    }
+
+    await this.show(node, options);
+  }
+
+  /**
+   * Show the shapes highlighter for the given element.
+   *
+   * @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.
+   */
+  async show(node, options) {
+    let isShown = await this.highlighter.show(node, options);
+    if (!isShown) {
+      return;
+    }
+
+    this.inspector.selection.on("detached-front", this.onNodeFrontChanged);
+    this.inspector.selection.on("new-node-front", this.onNodeFrontChanged);
+    this.highlighterTargetNode = node;
+    this.emit("show", { node, options });
+  }
+
+  /**
+   * Hide the shapes highlighter.
+   */
+  async hide() {
+    await this.highlighter.hide();
+
+    if (this.activeSwatch) {
+      this.activeSwatch.classList.remove("active");
+    }
+    this.activeSwatch = null;
+    this.activeProperty = null;
+
+    this.emit("hide", { node: this.highlighterTargetNode });
+    this.inspector.selection.off("detached-front", this.onNodeFrontChanged);
+    this.inspector.selection.off("new-node-front", this.onNodeFrontChanged);
+    this.highlighterTargetNode = null;
+  }
+
+  /**
+   * Handle events emitted by the highlighter.
+   * Find any callback assigned to the event type and call it with the given data object.
+   *
+   * @param {Object} data
+   *        The data object sent in the event.
+   */
+  onHighlighterEvent(data) {
+    const handler = this.highligherEventHandlers[data.type];
+    if (!handler || typeof handler !== "function") {
+      return;
+    }
+    handler.call(this, data);
+    this.inspector.highlighters.emit("highlighter-event-handled");
+  }
+
+  /**
+  * Clean up when node selection changes because Rule view and TextPropertyEditor
+  * instances are not automatically destroyed when selection changes.
+  */
+  async onNodeFrontChanged() {
+    try {
+      await this.hide();
+      await this.unlinkAll();
+    } catch (err) {
+      // Silent error.
+    }
+  }
+
+  /**
+  * Called when there's an updated shape value from the highlighter.
+  *
+  * @param  {Object} data
+  *         Data associated with the "shape-change" event.
+  *         Contains:
+  *         - {String} value: the new shape value.
+  *         - {String} type: the event type ("shape-change").
+  */
+  onShapeChange(data) {
+    this.preview(data.value);
+    this.commit(data.value);
+  }
+
+  /**
+  * Called when the mouse moves on or off of a coordinate point inside the shapes
+  * highlighter and marks/unmarks the corresponding coordinate node in the shape value
+  * from the Rule view.
+  *
+  * @param  {Object} data
+  *         Data associated with the "shape-hover" event.
+  *         Contains:
+  *         - {String|null} point: coordinate to highlight or null if nothing to highlight
+  *         - {String} type: the event type ("shape-hover-on" or "shape-hover-on").
+  */
+  onShapeHover(data) {
+    if (!this.activeProperty) {
+      return;
+    }
+
+    let shapeValueEl = this.links.get(this.activeProperty).swatch.nextSibling;
+    if (!shapeValueEl) {
+      return;
+    }
+    let pointSelector = ".ruleview-shape-point";
+    // First, unmark all highlighted coordinate nodes from Rule view
+    for (let node of shapeValueEl.querySelectorAll(`${pointSelector}.active`)) {
+      node.classList.remove("active");
+    }
+
+    // Exit if there's no coordinate to highlight.
+    if (typeof data.point !== "string") {
+      return;
+    }
+
+    let point = (data.point.includes(",")) ? data.point.split(",")[0] : data.point;
+
+    /**
+    * Build selector for coordinate nodes in shape value that must be highlighted.
+    * Coordinate values for inset() use class names instead of data attributes because
+    * a single node may represent multiple coordinates in shorthand notation.
+    * Example: inset(50px); The node wrapping 50px represents all four inset coordinates.
+    */
+    const INSET_POINT_TYPES = ["top", "right", "bottom", "left"];
+    let selector = INSET_POINT_TYPES.includes(point) ?
+                  `${pointSelector}.${point}` :
+                  `${pointSelector}[data-point='${point}']`;
+
+    for (let node of shapeValueEl.querySelectorAll(selector)) {
+      node.classList.add("active");
+    }
+  }
+
+  /**
+  * Preview this shape value on the element but do commit the changes to the Rule view.
+  *
+  * @param {String} value
+  *        The shape value to set the current property to
+  */
+  preview(value) {
+    if (!this.activeProperty) {
+      return;
+    }
+    let data = this.links.get(this.activeProperty);
+    // Update the element's styles to see live results.
+    data.callbacks.onPreview(value);
+    // Update the text of CSS value in the Rule view. This makes it inert.
+    // When .commit() is called, the value is reparsed and its DOM structure rebuilt.
+    data.swatch.nextSibling.textContent = value;
+  }
+
+  /**
+  * Commit this shape value change which triggers an expensive operation that rebuilds
+  * part of the DOM of the TextPropertyEditor. Called in a debounced manner.
+  *
+  * @param {String} value
+  *        The shape value to set the current property to
+  */
+  commit(value) {
+    if (!this.activeProperty) {
+      return;
+    }
+    this.ruleView.once("ruleview-changed", this.onChangesApplied);
+    let data = this.links.get(this.activeProperty);
+    data.callbacks.onCommit(value);
+  }
+
+  /**
+  * Called once after the shape value has been written to the element's style an Rule
+  * view updated. Triggers an event on the HighlightersOverlay that is listened to by
+  * tests in order to check if the shape value has been correctly applied.
+  */
+  onChangesApplied() {
+    this.inspector.highlighters.emit("shapes-highlighter-changes-applied");
+  }
+
+  async destroy() {
+    await this.hide();
+    await this.unlinkAll();
+    this.highlighter.off("highlighter-event", this.onHighlighterEvent);
+    this.ruleView.off("ruleview-changed", this.onRuleViewChanged);
+    this.highligherEventHandlers = {};
+  }
+}
+
+module.exports = ShapesInContextEditor;
--- a/devtools/client/shared/widgets/moz.build
+++ b/devtools/client/shared/widgets/moz.build
@@ -18,16 +18,17 @@ DevToolsModules(
     'CubicBezierWidget.js',
     'FastListWidget.js',
     'FilterWidget.js',
     'FlameGraph.js',
     'Graphs.js',
     'GraphsWorker.js',
     'LineGraphWidget.js',
     'MountainGraphWidget.js',
+    'ShapesInContextEditor.js',
     'SideMenuWidget.jsm',
     'SimpleListWidget.jsm',
     'Spectrum.js',
     'TableWidget.js',
     'TreeWidget.js',
     'VariablesView.jsm',
     'VariablesViewController.jsm',
     'view-helpers.js',
--- a/devtools/client/themes/rules.css
+++ b/devtools/client/themes/rules.css
@@ -477,17 +477,18 @@
   background-size: 1em;
 }
 
 .ruleview-grid {
   background: url("chrome://devtools/skin/images/grid.svg");
   border-radius: 0;
 }
 
-.ruleview-shape-point.active {
+.ruleview-shape-point.active,
+.ruleview-shapeswatch.active + .ruleview-shape > .ruleview-shape-point:hover {
   background-color: var(--rule-highlight-background-color);
 }
 
 .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);
--- a/devtools/server/actors/highlighters/shapes.js
+++ b/devtools/server/actors/highlighters/shapes.js
@@ -838,17 +838,17 @@ class ShapesHighlighter extends AutoRefr
       let precisionY = getDecimalPrecision(unitY);
       newX = (newX * ratioX).toFixed(precisionX);
       newY = (newY * ratioY).toFixed(precisionY);
 
       return `${newX}${unitX} ${newY}${unitY}`;
     }).join(", ");
     polygonDef = `polygon(${polygonDef}) ${this.geometryBox}`.trim();
 
-    this.currentNode.style.setProperty(this.property, polygonDef, "important");
+    this.emit("highlighter-event", { type: "shape-change", value: polygonDef });
   }
 
   /**
    * Transform a circle depending on the current transformation matrix.
    * @param {Number} transX the number of pixels the shape is translated on the x axis
    *                 before scaling
    */
   _transformCircle(transX = null) {
@@ -917,17 +917,17 @@ class ShapesHighlighter extends AutoRefr
     newBottom = `${(height - newBottom) * bottom.ratio}${bottom.unit}`;
 
     let round = this.insetRound;
     let insetDef = (round) ?
           `inset(${newTop} ${newRight} ${newBottom} ${newLeft} round ${round})` :
           `inset(${newTop} ${newRight} ${newBottom} ${newLeft})`;
     insetDef += (this.geometryBox) ? this.geometryBox : "";
 
-    this.currentNode.style.setProperty(this.property, insetDef, "important");
+    this.emit("highlighter-event", { type: "shape-change", value: insetDef });
   }
 
   /**
    * Handle a click when highlighting a polygon.
    * @param {Number} pageX the x coordinate of the click
    * @param {Number} pageY the y coordinate of the click
    */
   _handlePolygonClick(pageX, pageY) {
@@ -950,18 +950,18 @@ class ShapesHighlighter extends AutoRefr
     let ratioY = (valueY / yComputed) || 1;
 
     this.setCursor("grabbing");
     this[_dragging] = { point, unitX, unitY, valueX, valueY,
                         ratioX, ratioY, x: pageX, y: pageY };
   }
 
   /**
-   * Set the inline style of the polygon, replacing the given point with the given x/y
-   * coords.
+   * Update the dragged polygon point with the given x/y coords and update
+   * the element style.
    * @param {Number} pageX the new x coordinate of the point
    * @param {Number} pageY the new y coordinate of the point
    */
   _handlePolygonMove(pageX, pageY) {
     let { point, unitX, unitY, valueX, valueY, ratioX, ratioY, x, y } = this[_dragging];
     let deltaX = (pageX - x) * ratioX;
     let deltaY = (pageY - y) * ratioY;
     let precisionX = getDecimalPrecision(unitX);
@@ -971,21 +971,21 @@ class ShapesHighlighter extends AutoRefr
 
     let polygonDef = (this.fillRule) ? `${this.fillRule}, ` : "";
     polygonDef += this.coordUnits.map((coords, i) => {
       return (i === point) ?
         `${newX}${unitX} ${newY}${unitY}` : `${coords[0]} ${coords[1]}`;
     }).join(", ");
     polygonDef = `polygon(${polygonDef}) ${this.geometryBox}`.trim();
 
-    this.currentNode.style.setProperty(this.property, polygonDef, "important");
+    this.emit("highlighter-event", { type: "shape-change", value: polygonDef });
   }
 
   /**
-   * Set the inline style of the polygon, adding a new point.
+   * Add new point to the polygon defintion and update element style.
    * TODO: Bug 1436054 - Do not default to percentage unit when inserting new point.
    * https://bugzilla.mozilla.org/show_bug.cgi?id=1436054
    *
    * @param {Number} after the index of the point that the new point should be added after
    * @param {Number} x the x coordinate of the new point
    * @param {Number} y the y coordinate of the new point
    */
   _addPolygonPoint(after, x, y) {
@@ -993,35 +993,35 @@ class ShapesHighlighter extends AutoRefr
     polygonDef += this.coordUnits.map((coords, i) => {
       return (i === after) ? `${coords[0]} ${coords[1]}, ${x}% ${y}%` :
                              `${coords[0]} ${coords[1]}`;
     }).join(", ");
     polygonDef = `polygon(${polygonDef}) ${this.geometryBox}`.trim();
 
     this.hoveredPoint = after + 1;
     this._emitHoverEvent(this.hoveredPoint);
-    this.currentNode.style.setProperty(this.property, polygonDef, "important");
+    this.emit("highlighter-event", { type: "shape-change", value: polygonDef });
   }
 
   /**
-   * Set the inline style of the polygon, deleting the given point.
+   * Remove point from polygon defintion and update the element style.
    * @param {Number} point the index of the point to delete
    */
   _deletePolygonPoint(point) {
     let coordinates = this.coordUnits.slice();
     coordinates.splice(point, 1);
     let polygonDef = (this.fillRule) ? `${this.fillRule}, ` : "";
     polygonDef += coordinates.map((coords, i) => {
       return `${coords[0]} ${coords[1]}`;
     }).join(", ");
     polygonDef = `polygon(${polygonDef}) ${this.geometryBox}`.trim();
 
     this.hoveredPoint = null;
     this._emitHoverEvent(this.hoveredPoint);
-    this.currentNode.style.setProperty(this.property, polygonDef, "important");
+    this.emit("highlighter-event", { type: "shape-change", value: polygonDef });
   }
   /**
    * Handle a click when highlighting a circle.
    * @param {Number} pageX the x coordinate of the click
    * @param {Number} pageY the y coordinate of the click
    */
   _handleCircleClick(pageX, pageY) {
     let { width, height } = this.currentDimensions;
@@ -1055,18 +1055,18 @@ class ShapesHighlighter extends AutoRefr
       value = (isUnitless(value)) ? radius : parseFloat(value);
       let ratio = (value / radius) || 1;
 
       this[_dragging] = { point, value, origRadius: radius, unit, ratio };
     }
   }
 
   /**
-   * Set the inline style of the circle, setting the center/radius according to the
-   * mouse position.
+   * Set the center/radius of the circle according to the mouse position and
+   * update the element style.
    * @param {String} point either "center" or "radius"
    * @param {Number} pageX the x coordinate of the mouse position, in terms of %
    *        relative to the element
    * @param {Number} pageY the y coordinate of the mouse position, in terms of %
    *        relative to the element
    */
   _handleCircleMove(point, pageX, pageY) {
     let { radius, cx, cy } = this.coordUnits;
@@ -1075,30 +1075,30 @@ class ShapesHighlighter extends AutoRefr
       let { unitX, unitY, valueX, valueY, ratioX, ratioY, x, y} = this[_dragging];
       let deltaX = (pageX - x) * ratioX;
       let deltaY = (pageY - y) * ratioY;
       let newCx = `${valueX + deltaX}${unitX}`;
       let newCy = `${valueY + deltaY}${unitY}`;
       // if not defined by the user, geometryBox will be an empty string; trim() cleans up
       let circleDef = `circle(${radius} at ${newCx} ${newCy}) ${this.geometryBox}`.trim();
 
-      this.currentNode.style.setProperty(this.property, circleDef, "important");
+      this.emit("highlighter-event", { type: "shape-change", value: circleDef });
     } else if (point === "radius") {
       let { value, unit, origRadius, ratio } = this[_dragging];
       // convert center point to px, then get distance between center and mouse.
       let { x: pageCx, y: pageCy } = this.convertPercentToPageCoords(this.coordinates.cx,
                                                                      this.coordinates.cy);
       let newRadiusPx = getDistance(pageCx, pageCy, pageX, pageY);
 
       let delta = (newRadiusPx - origRadius) * ratio;
       let newRadius = `${value + delta}${unit}`;
 
       let circleDef = `circle(${newRadius} at ${cx} ${cy}) ${this.geometryBox}`.trim();
 
-      this.currentNode.style.setProperty(this.property, circleDef, "important");
+      this.emit("highlighter-event", { type: "shape-change", value: circleDef });
     }
   }
 
   /**
    * Handle a click when highlighting an ellipse.
    * @param {Number} pageX the x coordinate of the click
    * @param {Number} pageY the y coordinate of the click
    */
@@ -1142,18 +1142,18 @@ class ShapesHighlighter extends AutoRefr
       value = (isUnitless(value)) ? ry : parseFloat(value);
       let ratio = (value / ry) || 1;
 
       this[_dragging] = { point, value, origRadius: ry, unit, ratio };
     }
   }
 
   /**
-   * Set the inline style of the ellipse, setting the center/rx/ry according to the
-   * mouse position.
+   * Set center/rx/ry of the ellispe according to the mouse position and update the
+   * element style.
    * @param {String} point "center", "rx", or "ry"
    * @param {Number} pageX the x coordinate of the mouse position, in terms of %
    *        relative to the element
    * @param {Number} pageY the y coordinate of the mouse position, in terms of %
    *        relative to the element
    */
   _handleEllipseMove(point, pageX, pageY) {
     let { percentX, percentY } = this.convertPageCoordsToPercent(pageX, pageY);
@@ -1163,39 +1163,39 @@ class ShapesHighlighter extends AutoRefr
       let { unitX, unitY, valueX, valueY, ratioX, ratioY, x, y} = this[_dragging];
       let deltaX = (pageX - x) * ratioX;
       let deltaY = (pageY - y) * ratioY;
       let newCx = `${valueX + deltaX}${unitX}`;
       let newCy = `${valueY + deltaY}${unitY}`;
       let ellipseDef =
         `ellipse(${rx} ${ry} at ${newCx} ${newCy}) ${this.geometryBox}`.trim();
 
-      this.currentNode.style.setProperty(this.property, ellipseDef, "important");
+      this.emit("highlighter-event", { type: "shape-change", value: ellipseDef });
     } else if (point === "rx") {
       let { value, unit, origRadius, ratio } = this[_dragging];
       let newRadiusPercent = Math.abs(percentX - this.coordinates.cx);
       let { width } = this.currentDimensions;
       let delta = ((newRadiusPercent / 100 * width) - origRadius) * ratio;
       let newRadius = `${value + delta}${unit}`;
 
       let ellipseDef =
         `ellipse(${newRadius} ${ry} at ${cx} ${cy}) ${this.geometryBox}`.trim();
 
-      this.currentNode.style.setProperty(this.property, ellipseDef, "important");
+      this.emit("highlighter-event", { type: "shape-change", value: ellipseDef });
     } else if (point === "ry") {
       let { value, unit, origRadius, ratio } = this[_dragging];
       let newRadiusPercent = Math.abs(percentY - this.coordinates.cy);
       let { height } = this.currentDimensions;
       let delta = ((newRadiusPercent / 100 * height) - origRadius) * ratio;
       let newRadius = `${value + delta}${unit}`;
 
       let ellipseDef =
         `ellipse(${rx} ${newRadius} at ${cx} ${cy}) ${this.geometryBox}`.trim();
 
-      this.currentNode.style.setProperty(this.property, ellipseDef, "important");
+      this.emit("highlighter-event", { type: "shape-change", value: ellipseDef });
     }
   }
 
   /**
    * Handle a click when highlighting an inset.
    * @param {Number} pageX the x coordinate of the click
    * @param {Number} pageY the y coordinate of the click
    */
@@ -1215,18 +1215,18 @@ class ShapesHighlighter extends AutoRefr
     value = (isUnitless(value)) ? computedValue : parseFloat(value);
     let ratio = (value / computedValue) || 1;
     let origValue = (point === "left" || point === "right") ? pageX : pageY;
 
     this[_dragging] = { point, value, origValue, unit, ratio };
   }
 
   /**
-   * Set the inline style of the inset, setting top/left/right/bottom according to the
-   * mouse position.
+   * Set the top/left/right/bottom of the inset shape according to the mouse position
+   * and update the element style.
    * @param {String} point "top", "left", "right", or "bottom"
    * @param {Number} pageX the x coordinate of the mouse position, in terms of %
    *        relative to the element
    * @param {Number} pageY the y coordinate of the mouse position, in terms of %
    *        relative to the element
    * @memberof ShapesHighlighter
    */
   _handleInsetMove(point, pageX, pageY) {
@@ -1248,17 +1248,17 @@ class ShapesHighlighter extends AutoRefr
       bottom = `${value - delta}${unit}`;
     }
     let insetDef = (round) ?
       `inset(${top} ${right} ${bottom} ${left} round ${round})` :
       `inset(${top} ${right} ${bottom} ${left})`;
 
     insetDef += (this.geometryBox) ? this.geometryBox : "";
 
-    this.currentNode.style.setProperty(this.property, insetDef, "important");
+    this.emit("highlighter-event", { type: "shape-change", value: insetDef });
   }
 
   _handleMouseMoveNotDragging(pageX, pageY) {
     let { percentX, percentY } = this.convertPageCoordsToPercent(pageX, pageY);
     if (this.transformMode) {
       let point = this.getTransformPointAt(percentX, percentY);
       this.hoveredPoint = point;
       this._handleMarkerHover(point);