Backed out 2 changesets (bug 1435373) for devtools failures at devtools/client/shared/test/browser_telemetry_button_eyedropper.js and devtools/client/inspector/rules/test/browser_rules_eyedropper.js and devtools/client/inspector/test/browser_inspector_highlighter-cssshape_04.js on a CLOSED TREE
authorAndreea Pavel <apavel@mozilla.com>
Thu, 15 Mar 2018 01:50:45 +0200
changeset 408222 41df1e7a38d533b52624c5529ec387828a34cb22
parent 408221 dd1c00afe429fd961e7f7423a0eeb44619bc9f54
child 408223 cf17754e8ab3b698ef04ec8f3d7b2f9e79cb45df
push id100893
push userapavel@mozilla.com
push dateWed, 14 Mar 2018 23:51:08 +0000
treeherdermozilla-inbound@41df1e7a38d5 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
bugs1435373
milestone61.0a1
backs out195379cf14f053d716d904b75ce4c00d8046e32a
14a22276dc5342bfeea0662a8f82a4c2daddd7ee
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Backed out 2 changesets (bug 1435373) for devtools failures at devtools/client/shared/test/browser_telemetry_button_eyedropper.js and devtools/client/inspector/rules/test/browser_rules_eyedropper.js and devtools/client/inspector/test/browser_inspector_highlighter-cssshape_04.js on a CLOSED TREE Backed out changeset 195379cf14f0 (bug 1435373) Backed out changeset 14a22276dc53 (bug 1435373)
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
devtools/server/actors/utils/shapes-utils.js
--- a/devtools/client/inspector/inspector.js
+++ b/devtools/client/inspector/inspector.js
@@ -1074,17 +1074,18 @@ Inspector.prototype = {
       return;
     }
 
     let onExpand = this.markup.expandNode(this.selection.nodeFront);
 
     // Restore the highlighter states prior to emitting "new-root".
     yield Promise.all([
       this.highlighters.restoreFlexboxState(),
-      this.highlighters.restoreGridState()
+      this.highlighters.restoreGridState(),
+      this.highlighters.restoreShapeState()
     ]);
 
     this.emit("new-root");
 
     // Wait for full expand of the selected node in order to ensure
     // the markup view is fully emitted before firing 'reloaded'.
     // 'reloaded' is used to know when the panel is fully updated
     // after a page reload.
@@ -1307,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();
 
-    let highlighterDestroyer = this.highlighters.destroy();
+    this.highlighters.destroy();
     this.prefsObserver.destroy();
     this.reflowTracker.destroy();
     this.search.destroy();
 
     this._toolbox = null;
     this.breadcrumbs = null;
     this.highlighters = null;
     this.panelDoc = null;
@@ -1327,17 +1328,16 @@ 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
@@ -247,17 +247,16 @@ skip-if = (os == 'linux' && bits == 32 &
 [browser_rules_selector-highlighter_05.js]
 [browser_rules_selector_highlight.js]
 [browser_rules_shapes-toggle_01.js]
 [browser_rules_shapes-toggle_02.js]
 [browser_rules_shapes-toggle_03.js]
 [browser_rules_shapes-toggle_04.js]
 [browser_rules_shapes-toggle_05.js]
 [browser_rules_shapes-toggle_06.js]
-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,31 +20,28 @@ const TEST_URI = `
 
 const HIGHLIGHTER_TYPE = "ShapesHighlighter";
 
 add_task(function* () {
   yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
   let {inspector, view} = yield openRuleView();
   let highlighters = view.highlighters;
 
-  info("Select a node with a shape value");
-  let onHighlighterArmed = highlighters.once("shapes-highlighter-armed");
   yield 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");
-  yield onHighlighterArmed;
+
   info("Toggling ON the CSS shapes highlighter from the rule-view.");
   let onHighlighterShown = highlighters.once("shapes-highlighter-shown");
   shapesToggle.click();
   yield onHighlighterShown;
 
   info("Checking the CSS shapes highlighter is created and toggle button is active in " +
     "the rule-view.");
   ok(shapesToggle.classList.contains("active"),
--- 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,63 +21,55 @@ const TEST_URI = `
 const HIGHLIGHTER_TYPE = "ShapesHighlighter";
 
 add_task(function* () {
   yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
   let {inspector, view} = yield openRuleView();
   let highlighters = view.highlighters;
 
   info("Selecting the first shape container.");
-  let onHighlighterArmed = highlighters.once("shapes-highlighter-armed");
   yield 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");
-  yield 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();
   yield onHighlighterShown;
 
   info("Checking the CSS shapes highlighter is created and toggle button is active in " +
     "the rule-view.");
   ok(shapeToggle.classList.contains("active"),
     "shapes highlighter toggle is active.");
   ok(highlighters.highlighters[HIGHLIGHTER_TYPE],
     "CSS shapes highlighter created in the rule-view.");
   ok(highlighters.shapesHighlighterShown, "CSS shapes highlighter is shown.");
 
   info("Selecting the second shapes container.");
-  onHighlighterArmed = highlighters.once("shapes-highlighter-armed");
   yield 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 no longer" +
-    "shown due to selecting another node.");
-
-  info("Wait for shapes highlighter swatch trigger to be ready");
-  yield onHighlighterArmed;
+  ok(highlighters.shapesHighlighterShown, "CSS shapes highlighter is still shown.");
 
   info("Toggling ON the CSS shapes highlighter for the second shapes container " +
     "from the rule-view.");
   onHighlighterShown = highlighters.once("shapes-highlighter-shown");
   shapeToggle.click();
   yield onHighlighterShown;
 
   info("Checking the CSS shapes highlighter is created for the second shapes container " +
--- 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,21 +18,17 @@ const TEST_URI = `
   <div id="shape"></div>
 `;
 
 add_task(function* () {
   yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
   let {inspector, view} = yield openRuleView();
   let highlighters = view.highlighters;
 
-  info("Select a node with a shape value");
-  let onHighlighterArmed = highlighters.once("shapes-highlighter-armed");
   yield selectNode("#shape", inspector);
-  info("Wait for shapes highlighter swatch trigger to be ready");
-  yield 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();
   yield 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,21 +18,17 @@ const TEST_URI = `
   <div id="shape"></div>
 `;
 
 add_task(function* () {
   yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
   let {inspector, view, testActor} = yield openRuleView();
   let highlighters = view.highlighters;
 
-  info("Select a node with a shape value");
-  let onHighlighterArmed = highlighters.once("shapes-highlighter-armed");
   yield selectNode("#shape", inspector);
-  info("Wait for shapes highlighter swatch trigger to be ready");
-  yield 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();
   yield 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,21 +20,17 @@ const TEST_URI = `
   <div class="shape" id="shape2"></div>
 `;
 
 add_task(function* () {
   yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
   let {inspector, view} = yield openRuleView();
   let highlighters = view.highlighters;
 
-  info("Selecting the first shapes container.");
-  let onHighlighterArmed = highlighters.once("shapes-highlighter-armed");
   yield selectNode("#shape1", inspector);
-  info("Wait for shapes highlighter swatch trigger to be ready");
-  yield 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,24 +19,25 @@ const TEST_URI = `
 
 const HIGHLIGHTER_TYPE = "ShapesHighlighter";
 
 add_task(function* () {
   yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
   let {inspector, view} = yield openRuleView();
   let highlighters = view.highlighters;
 
-  info("Select a node with a shape value");
-  let onHighlighterArmed = highlighters.once("shapes-highlighter-armed");
   yield selectNode("#shape", inspector);
-  info("Wait for shapes highlighter swatch trigger to be ready");
-  yield 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);
   yield 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,16 +22,17 @@ 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 = [
@@ -88,20 +89,19 @@ 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,16 +314,18 @@ 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.
@@ -334,17 +336,17 @@ TextPropertyEditor.prototype = {
       return domRule.href || domRule.nodeHref;
     }
     return undefined;
   },
 
   /**
    * Populate the span based on changes to the TextProperty.
    */
-  update: async function() {
+  update: function() {
     if (this.ruleView.isDestroyed) {
       return;
     }
 
     this.updatePropertyState();
 
     let name = this.prop.name;
     this.nameSpan.textContent = name;
@@ -509,32 +511,21 @@ 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);
 
-      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();
+      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);
       }
     }
 
     // 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;
@@ -1039,29 +1030,70 @@ 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");
   },
 
   /**
-   * Live preview this property, without committing changes.
+   * Highlight the given shape point in the rule view. Called when "hover-shape-point"
+   * event is emitted.
    *
-   * @param {String} value
-   *        The value to set the current property to.
+   * @param {Event} event
+   *        The "hover-shape-point" event.
+   * @param {String} point
+   *        The point to highlight.
    */
-  onInContextEditorPreview(value) {
-    this.ruleEditor.rule.previewPropertyValue(this.prop, value);
+  _onHoverShapePoint: function(event, 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);
+        }
+      }
+    }
   },
 
   /**
-   * Commit this property value. Triggers editor update.
+   * Toggle the class "active" on the given shape point in the rule view if the current
+   * inspector selection is highlighted by the shapes highlighter.
    *
-   * @param {String} value
-   *        The value to set the current property to.
+   * @param {NodeFront} node
+   *        The NodeFront of the shape point to toggle
+   * @param {Boolean} active
+   *        Whether the shape point should be active
    */
-  onInContextEditorCommit(value) {
-    this.prop.setValue(value);
-  }
+  _toggleShapePointActive: function(node, active) {
+    let { highlighters } = this.ruleView;
+    if (highlighters.inspector.selection.nodeFront !=
+        highlighters.shapesHighlighterShown) {
+      return;
+    }
+
+    node.classList.toggle("active", active);
+  },
 };
 
 module.exports = TextPropertyEditor;
--- a/devtools/client/inspector/shared/highlighters-overlay.js
+++ b/devtools/client/inspector/shared/highlighters-overlay.js
@@ -7,39 +7,31 @@
 "use strict";
 
 const Services = require("Services");
 const EventEmitter = require("devtools/shared/old-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.inspector = inspector;
     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.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.
@@ -66,18 +58,17 @@ 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.onShapesHighlighterShown = this.onShapesHighlighterShown.bind(this);
-    this.onShapesHighlighterHidden = this.onShapesHighlighterHidden.bind(this);
+    this._onHighlighterEvent = this._onHighlighterEvent.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);
   }
 
@@ -125,72 +116,91 @@ 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 shapesEditor = await this.getInContextEditor("shapesEditor");
-    if (!shapesEditor) {
+    let highlighter = await this._getHighlighter("ShapesHighlighter");
+    if (!highlighter) {
+      return;
+    }
+
+    let isShown = await highlighter.show(node, options);
+    if (!isShown) {
       return;
     }
-    shapesEditor.show(node, options);
+
+    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);
+    }
   }
 
   /**
-   * Called after the shape highlighter was shown.
+   * Hide the shapes highlighter for the given element with a shape.
    *
-   * @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()
+   * @param  {NodeFront} node
+   *         The NodeFront of the element with a shape to unhighlight.
    */
-  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) {
+  async hideShapesHighlighter(node) {
+    if (!this.shapesHighlighterShown || !this.highlighters.ShapesHighlighter) {
       return;
     }
-    shapesEditor.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._toggleRuleViewIcon(node, false, ".ruleview-shapeswatch");
+
+    await this.highlighters.ShapesHighlighter.hide();
     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.
@@ -204,16 +214,47 @@ 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 = {}) {
@@ -419,16 +460,34 @@ 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);
     }
@@ -441,18 +500,29 @@ class HighlightersOverlay {
     try {
       await this.restoreState("grid", this.state.grid, this.showGridHighlighter);
     } catch (e) {
       this._handleRejection(e);
     }
   }
 
   /**
-   * Helper function called by restoreFlexboxState, restoreGridState.
-   * Restores the saved highlighter state for the given highlighter
+   * 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
    * 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
@@ -480,57 +550,16 @@ 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;
@@ -546,16 +575,17 @@ 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);
     }
@@ -745,16 +775,24 @@ 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;
     }
@@ -770,16 +808,17 @@ 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";
@@ -807,16 +846,17 @@ 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.
@@ -842,61 +882,41 @@ 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.
    */
-  async destroy() {
-    await this.destroyEditors();
-    this.destroyHighlighters();
+  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;
+      }
+    }
 
     // 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,318 +5,233 @@
 "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(function* () {
-  let env = yield openInspectorForURL(TEST_URL);
-  let helper = yield getHighlighterHelperFor(HIGHLIGHTER_TYPE)(env);
-  let {testActor, inspector} = env;
-  let view = selectRuleView(inspector);
-  let highlighters = view.highlighters;
+  let inspector = yield openInspectorForURL(TEST_URL);
+  let helper = yield getHighlighterHelperFor(HIGHLIGHTER_TYPE)(inspector);
+  let {testActor} = inspector;
 
-  let config = {inspector, view, highlighters, testActor, helper};
-
-  yield testPolygonMovePoint(config);
-  yield testPolygonAddPoint(config);
-  yield testPolygonRemovePoint(config);
-  yield testCircleMoveCenter(config);
-  yield testEllipseMoveRadius(config);
-  yield testInsetMoveEdges(config);
+  yield testPolygonMovePoint(testActor, helper);
+  yield testPolygonAddPoint(testActor, helper);
+  yield testPolygonRemovePoint(testActor, helper);
+  yield testCircleMoveCenter(testActor, helper);
+  yield testEllipseMoveRadius(testActor, helper);
+  yield testInsetMoveEdges(testActor, helper);
 
   helper.finalize();
 });
 
-function* getComputedPropertyValue(selector, property, inspector) {
-  let highlightedNode = yield getNodeFront(selector, inspector);
-  let computedStyle = yield inspector.pageStyle.getComputed(highlightedNode);
-  return computedStyle[property].value;
-}
-
-function* setup(config) {
-  const { view, selector, property, inspector } = config;
-  info(`Turn on shapes highlighter for ${selector}`);
-  yield selectNode(selector, inspector);
-  return yield toggleShapesHighlighter(view, selector, property, true);
-}
+function* testPolygonMovePoint(testActor, helper) {
+  info("Displaying polygon");
+  yield helper.show("#polygon", {mode: "cssClipPath"});
+  let { mouse, highlightedNode } = helper;
 
-function* teardown(config) {
-  const { view, selector, property } = config;
-  info(`Turn off shapes highlighter for ${selector}`);
-  return yield toggleShapesHighlighter(view, selector, property, false);
-}
-
-function* testPolygonMovePoint(config) {
-  const {inspector, view, highlighters, testActor, helper} = config;
-  const selector = "#polygon";
-  const property = "clip-path";
-
-  yield setup({selector, property, ...config});
-
-  let points = yield testActor.getHighlighterNodeAttribute(
-    "shapes-polygon", "points", highlighters.highlighters[HIGHLIGHTER_TYPE]);
+  let points = yield helper.getElementAttribute("shapes-polygon", "points");
   let [x, y] = points.split(" ")[0].split(",");
-  let quads = yield testActor.getAllAdjustedQuads(selector);
+  let quads = yield testActor.getAllAdjustedQuads("#polygon");
   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;
   yield mouse.down(x, y);
   yield mouse.move(x + dx, y + dy);
   yield mouse.up();
   yield testActor.reflow();
-  info("Waiting for rule view changed from shape change");
-  yield onRuleViewChanged;
 
-  let definition = yield getComputedPropertyValue(selector, property, inspector);
+  let computedStyle = yield highlightedNode.getComputedStyle();
+  let definition = computedStyle["clip-path"].value;
   ok(definition.includes(`${dx}px ${dy}px`), `Point moved to ${dx}px ${dy}px`);
-
-  yield teardown({selector, property, ...config});
 }
 
-function* testPolygonAddPoint(config) {
-  const {inspector, view, highlighters, testActor, helper} = config;
-  const selector = "#polygon";
-  const property = "clip-path";
-
-  yield setup({selector, property, ...config});
+function* testPolygonAddPoint(testActor, helper) {
+  yield helper.show("#polygon", {mode: "cssClipPath"});
+  let { mouse, highlightedNode } = helper;
 
   // Move first point to have same x as second point, then double click between
   // the two points to add a new one.
-  let points = yield testActor.getHighlighterNodeAttribute(
-    "shapes-polygon", "points", highlighters.highlighters[HIGHLIGHTER_TYPE]);
+  let points = yield helper.getElementAttribute("shapes-polygon", "points");
   let pointsArray = points.split(" ");
-  let quads = yield testActor.getAllAdjustedQuads(selector);
+  let quads = yield testActor.getAllAdjustedQuads("#polygon");
   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;
   yield mouse.down(x1, y1);
   yield mouse.move(x2, y1);
   yield mouse.up();
   yield 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");
   yield testActor.synthesizeMouse(options);
   yield testActor.reflow();
-  info("Waiting for rule view changed from shape change");
-  yield onRuleViewChanged;
 
+  let computedStyle = yield 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 = yield getComputedPropertyValue(selector, property, inspector);
   ok(definition.includes(`${newPointX}% ${newPointY}%`),
      "Point successfuly added");
-
-  yield teardown({selector, property, ...config});
 }
 
-function* testPolygonRemovePoint(config) {
-  const {inspector, highlighters, testActor, helper} = config;
-  const selector = "#polygon";
-  const property = "clip-path";
+function* testPolygonRemovePoint(testActor, helper) {
+  yield helper.show("#polygon", {mode: "cssClipPath"});
+  let { highlightedNode } = helper;
 
-  yield setup({selector, property, ...config});
-
-  let points = yield testActor.getHighlighterNodeAttribute(
-    "shapes-polygon", "points", highlighters.highlighters[HIGHLIGHTER_TYPE]);
+  let points = yield helper.getElementAttribute("shapes-polygon", "points");
   let [x, y] = points.split(" ")[0].split(",");
-  let quads = yield testActor.getAllAdjustedQuads(selector);
+  let quads = yield testActor.getAllAdjustedQuads("#polygon");
   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("Move mouse over first point in highlighter");
-  let onEventHandled = highlighters.once("highlighter-event-handled");
-  let { mouse } = helper;
-  yield mouse.move(options.x, options.y);
-  yield onEventHandled;
-  let markerHidden = yield testActor.getHighlighterNodeAttribute(
-    "shapes-marker-hover", "hidden", highlighters.highlighters[HIGHLIGHTER_TYPE]);
-  ok(!markerHidden, "Marker on highlighter is visible");
+  info("Removing first polygon point");
+  yield testActor.synthesizeMouse(options);
+  yield testActor.reflow();
 
-  info("Double click on first point in highlighter");
-  let onShapeChangeApplied = highlighters.once("shapes-highlighter-changes-applied");
-  yield testActor.synthesizeMouse(options);
-  info("Waiting for shape changes to apply");
-  yield onShapeChangeApplied;
-  let definition = yield getComputedPropertyValue(selector, property, inspector);
+  let computedStyle = yield highlightedNode.getComputedStyle();
+  let definition = computedStyle["clip-path"].value;
   ok(!definition.includes(`${x}% ${y}%`), "Point successfully removed");
-
-  yield teardown({selector, property, ...config});
 }
 
-function* testCircleMoveCenter(config) {
-  const {inspector, highlighters, testActor, helper} = config;
-  const selector = "#circle";
-  const property = "clip-path";
+function* testCircleMoveCenter(testActor, helper) {
+  yield helper.show("#circle", {mode: "cssClipPath"});
+  let { mouse, highlightedNode } = helper;
 
-  yield setup({selector, property, ...config});
-
-  let cx = parseFloat(yield testActor.getHighlighterNodeAttribute(
-    "shapes-ellipse", "cx", highlighters.highlighters[HIGHLIGHTER_TYPE]));
-  let cy = parseFloat(yield testActor.getHighlighterNodeAttribute(
-    "shapes-ellipse", "cy", highlighters.highlighters[HIGHLIGHTER_TYPE]));
-  let quads = yield testActor.getAllAdjustedQuads(selector);
+  let cx = parseFloat(yield helper.getElementAttribute("shapes-ellipse", "cx"));
+  let cy = parseFloat(yield helper.getElementAttribute("shapes-ellipse", "cy"));
+  let quads = yield testActor.getAllAdjustedQuads("#circle");
   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");
-  let { mouse } = helper;
-  yield mouse.down(cxPixel, cyPixel, selector);
-  yield mouse.move(cxPixel + dx, cyPixel + dy, selector);
-  yield mouse.up(cxPixel + dx, cyPixel + dy, selector);
-  yield onShapeChangeApplied;
+  yield mouse.down(cxPixel, cyPixel, "#circle");
+  yield mouse.move(cxPixel + dx, cyPixel + dy, "#circle");
+  yield mouse.up(cxPixel + dx, cyPixel + dy, "#circle");
+  yield testActor.reflow();
 
-  let definition = yield getComputedPropertyValue(selector, property, inspector);
+  let computedStyle = yield highlightedNode.getComputedStyle();
+  let definition = computedStyle["clip-path"].value;
   ok(definition.includes(`at ${cx + 10}% ${cy + 10}%`),
      "Circle center successfully moved");
-
-  yield teardown({selector, property, ...config});
 }
 
-function* testEllipseMoveRadius(config) {
-  const {inspector, highlighters, testActor, helper} = config;
-  const selector = "#ellipse";
-  const property = "clip-path";
-
-  yield setup({selector, property, ...config});
+function* testEllipseMoveRadius(testActor, helper) {
+  yield helper.show("#ellipse", {mode: "cssClipPath"});
+  let { mouse, highlightedNode } = helper;
 
-  let rx = parseFloat(yield testActor.getHighlighterNodeAttribute(
-    "shapes-ellipse", "rx", highlighters.highlighters[HIGHLIGHTER_TYPE]));
-  let ry = parseFloat(yield testActor.getHighlighterNodeAttribute(
-    "shapes-ellipse", "ry", highlighters.highlighters[HIGHLIGHTER_TYPE]));
-  let cx = parseFloat(yield testActor.getHighlighterNodeAttribute(
-    "shapes-ellipse", "cx", highlighters.highlighters[HIGHLIGHTER_TYPE]));
-  let cy = parseFloat(yield testActor.getHighlighterNodeAttribute(
-    "shapes-ellipse", "cy", highlighters.highlighters[HIGHLIGHTER_TYPE]));
+  let rx = parseFloat(yield helper.getElementAttribute("shapes-ellipse", "rx"));
+  let ry = parseFloat(yield helper.getElementAttribute("shapes-ellipse", "ry"));
+  let cx = parseFloat(yield helper.getElementAttribute("shapes-ellipse", "cx"));
+  let cy = parseFloat(yield helper.getElementAttribute("shapes-ellipse", "cy"));
   let quads = yield testActor.getAllAdjustedQuads("#ellipse");
   let { width, height } = quads.content[0].bounds;
-  let highlightedNode = yield getNodeFront(selector, inspector);
-  let computedStyle = yield inspector.pageStyle.getComputed(highlightedNode);
+  let computedStyle = yield highlightedNode.getComputedStyle();
   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");
-  yield mouse.down(rxPixel, cyPixel, selector);
-  yield mouse.move(rxPixel + dx, cyPixel, selector);
-  yield mouse.up(rxPixel + dx, cyPixel, selector);
+  yield mouse.down(rxPixel, cyPixel, "#ellipse");
+  yield mouse.move(rxPixel + dx, cyPixel, "#ellipse");
+  yield mouse.up(rxPixel + dx, cyPixel, "#ellipse");
   yield testActor.reflow();
 
   info("Moving ellipse ry");
-  let onShapeChangeApplied = highlighters.once("shapes-highlighter-changes-applied");
-  yield mouse.down(cxPixel, ryPixel, selector);
-  yield mouse.move(cxPixel, ryPixel - dy, selector);
-  yield mouse.up(cxPixel, ryPixel - dy, selector);
+  yield mouse.down(cxPixel, ryPixel, "#ellipse");
+  yield mouse.move(cxPixel, ryPixel - dy, "#ellipse");
+  yield mouse.up(cxPixel, ryPixel - dy, "#ellipse");
   yield testActor.reflow();
-  yield onShapeChangeApplied;
 
-  let definition = yield getComputedPropertyValue(selector, property, inspector);
+  computedStyle = yield highlightedNode.getComputedStyle();
+  let definition = computedStyle["clip-path"].value;
   ok(definition.includes(`${rx + 10}% ${ry - 10}%`),
      "Ellipse radiuses successfully moved");
-
-  yield teardown({selector, property, ...config});
 }
 
-function* testInsetMoveEdges(config) {
-  const {inspector, highlighters, testActor, helper} = config;
-  const selector = "#inset";
-  const property = "clip-path";
-
-  yield setup({selector, property, ...config});
+function* testInsetMoveEdges(testActor, helper) {
+  yield helper.show("#inset", {mode: "cssClipPath"});
+  let { mouse, highlightedNode } = helper;
 
-  let x = parseFloat(yield testActor.getHighlighterNodeAttribute(
-    "shapes-rect", "x", highlighters.highlighters[HIGHLIGHTER_TYPE]));
-  let y = parseFloat(yield testActor.getHighlighterNodeAttribute(
-    "shapes-rect", "y", highlighters.highlighters[HIGHLIGHTER_TYPE]));
-  let width = parseFloat(yield testActor.getHighlighterNodeAttribute(
-    "shapes-rect", "width", highlighters.highlighters[HIGHLIGHTER_TYPE]));
-  let height = parseFloat(yield testActor.getHighlighterNodeAttribute(
-    "shapes-rect", "height", highlighters.highlighters[HIGHLIGHTER_TYPE]));
-  let quads = yield testActor.getAllAdjustedQuads(selector);
+  let x = parseFloat(yield helper.getElementAttribute("shapes-rect", "x"));
+  let y = parseFloat(yield helper.getElementAttribute("shapes-rect", "y"));
+  let width = parseFloat(yield helper.getElementAttribute("shapes-rect", "width"));
+  let height = parseFloat(yield helper.getElementAttribute("shapes-rect", "height"));
+  let quads = yield testActor.getAllAdjustedQuads("#inset");
   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");
-  yield mouse.down(xCenter, top, selector);
-  yield mouse.move(xCenter, top + dy, selector);
-  yield mouse.up(xCenter, top + dy, selector);
+  yield mouse.down(xCenter, top, "#inset");
+  yield mouse.move(xCenter, top + dy, "#inset");
+  yield mouse.up(xCenter, top + dy, "#inset");
   yield testActor.reflow();
 
   info("Moving inset bottom");
-  yield mouse.down(xCenter, bottom, selector);
-  yield mouse.move(xCenter, bottom + dy, selector);
-  yield mouse.up(xCenter, bottom + dy, selector);
+  yield mouse.down(xCenter, bottom, "#inset");
+  yield mouse.move(xCenter, bottom + dy, "#inset");
+  yield mouse.up(xCenter, bottom + dy, "#inset");
   yield testActor.reflow();
 
   info("Moving inset left");
-  yield mouse.down(left, yCenter, selector);
-  yield mouse.move(left + dx, yCenter, selector);
-  yield mouse.up(left + dx, yCenter, selector);
+  yield mouse.down(left, yCenter, "#inset");
+  yield mouse.move(left + dx, yCenter, "#inset");
+  yield mouse.up(left + dx, yCenter, "#inset");
   yield testActor.reflow();
 
   info("Moving inset right");
-  let onShapeChangeApplied = highlighters.once("shapes-highlighter-changes-applied");
-  yield mouse.down(right, yCenter, selector);
-  yield mouse.move(right + dx, yCenter, selector);
-  yield mouse.up(right + dx, yCenter, selector);
-  yield onShapeChangeApplied;
+  yield mouse.down(right, yCenter, "#inset");
+  yield mouse.move(right + dx, yCenter, "#inset");
+  yield mouse.up(right + dx, yCenter, "#inset");
+  yield testActor.reflow();
 
-  let definition = yield getComputedPropertyValue(selector, property, inspector);
+  let computedStyle = yield highlightedNode.getComputedStyle();
+  let definition = computedStyle["clip-path"].value;
   ok(definition.includes(
     `${top + dy}px ${elemWidth - right - dx}px ${100 - y - height - 10}% ${x + 10}%`),
      "Inset edges successfully moved");
-
-  yield 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,114 +2,109 @@
 /* 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(function* () {
+  yield pushPref(CSS_SHAPES_ENABLED_PREF, true);
   let env = yield openInspectorForURL(TEST_URL);
   let helper = yield getHighlighterHelperFor(HIGHLIGHTER_TYPE)(env);
   let { testActor, inspector } = env;
   let view = selectRuleView(inspector);
   let highlighters = view.highlighters;
-  let config = { inspector, view, highlighters, testActor, helper };
 
-  yield highlightFromRuleView(config);
-  yield highlightFromHighlighter(config);
+  yield highlightFromRuleView(inspector, view, highlighters, testActor);
+  yield highlightFromHighlighter(view, highlighters, testActor, helper);
 });
 
-function* setup(config) {
-  const { view, selector, property, inspector } = config;
-  info(`Turn on shapes highlighter for ${selector}`);
-  yield selectNode(selector, inspector);
-  return yield toggleShapesHighlighter(view, selector, property, true);
-}
-
-function* teardown(config) {
-  const { view, selector, property } = config;
-  info(`Turn off shapes highlighter for ${selector}`);
-  return yield toggleShapesHighlighter(view, selector, property, false);
-}
-/*
-* Test that points hovered in the rule view will highlight corresponding points
-* in the shapes highlighter on the page.
-*/
-function* highlightFromRuleView(config) {
-  const { view, highlighters, testActor } = config;
-  const selector = "#polygon";
-  const property = "clip-path";
-
-  yield setup({ selector, property, ...config });
-
-  let container = getRuleViewProperty(view, selector, property).valueSpan;
+function* highlightFromRuleView(inspector, view, highlighters, testActor) {
+  yield selectNode("#polygon", inspector);
+  yield toggleShapesHighlighter(view, highlighters, "#polygon", "clip-path", true);
+  let container = getRuleViewProperty(view, "#polygon", "clip-path").valueSpan;
   let shapesToggle = container.querySelector(".ruleview-shapeswatch");
 
   let highlighterFront = highlighters.highlighters[HIGHLIGHTER_TYPE];
   let markerHidden = yield 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);
   yield onHighlighterShown;
 
-  info("Point in shapes highlighter is marked when same point in rule view is hovered");
+  ok(pointSpan.classList.contains("active"), "Hovered span is active");
+  is(highlighters.state.shapes.options.hoverPoint, "0",
+     "Hovered point is saved to state");
+
   markerHidden = yield 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);
   yield onHighlighterShown;
 
+  ok(!pointSpan.classList.contains("active"), "Hovered span is no longer active");
+  is(highlighters.state.shapes.options.hoverPoint, null, "Hovered point is null");
+
   markerHidden = yield testActor.getHighlighterNodeAttribute(
     "shapes-marker-hover", "hidden", highlighterFront);
   ok(markerHidden, "Marker on highlighter is not visible");
 
-  yield teardown({selector, property, ...config});
+  info("Hide shapes highlighter");
+  yield toggleShapesHighlighter(view, highlighters, "#polygon", "clip-path", false);
 }
 
-/*
-* Test that points hovered in the shapes highlighter on the page will highlight
-* corresponding points in the rule view.
-*/
-function* highlightFromHighlighter(config) {
-  const { view, highlighters, testActor, helper } = config;
-  const selector = "#polygon";
-  const property = "clip-path";
-
-  yield setup({ selector, property, ...config });
-
+function* highlightFromHighlighter(view, highlighters, testActor, helper) {
   let highlighterFront = highlighters.highlighters[HIGHLIGHTER_TYPE];
   let { mouse } = helper;
-  let container = getRuleViewProperty(view, selector, property).valueSpan;
+
+  yield toggleShapesHighlighter(view, highlighters, "#polygon", "clip-path", true);
+  let container = getRuleViewProperty(view, "#polygon", "clip-path").valueSpan;
 
   info("Hover over first point in highlighter");
   let onEventHandled = highlighters.once("highlighter-event-handled");
   yield mouse.move(0, 0);
   yield onEventHandled;
   let markerHidden = yield 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");
+  yield mouse.down(0, 0);
+  yield mouse.move(10, 10);
+  yield mouse.up(10, 10);
+  markerHidden = yield 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");
   yield mouse.move(100, 100);
   yield onEventHandled;
   markerHidden = yield 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");
-
-  yield teardown({ selector, property, ...config });
+  is(highlighters.state.shapes.hoverPoint, null, "Hovered point is null");
 }
--- a/devtools/client/inspector/test/browser_inspector_highlighter-cssshape_06.js
+++ b/devtools/client/inspector/test/browser_inspector_highlighter-cssshape_06.js
@@ -3,173 +3,145 @@
  * 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_SELECTORS = ["#polygon-transform", "#circle", "#ellipse", "#inset"];
+const SHAPE_IDS = ["#polygon-transform", "#circle", "#ellipse", "#inset"];
 
 add_task(function* () {
-  let env = yield openInspectorForURL(TEST_URL);
-  let helper = yield getHighlighterHelperFor(HIGHLIGHTER_TYPE)(env);
-  let {testActor, inspector} = env;
-  let view = selectRuleView(inspector);
-  let highlighters = view.highlighters;
-  let config = { inspector, view, highlighters, testActor, helper };
+  let inspector = yield openInspectorForURL(TEST_URL);
+  let helper = yield getHighlighterHelperFor(HIGHLIGHTER_TYPE)(inspector);
+  let {testActor} = inspector;
 
-  yield testTranslate(config);
-  yield testScale(config);
+  yield testTranslate(testActor, helper);
+  yield testScale(testActor, helper);
+
+  helper.finalize();
 });
 
-function* setup(config) {
-  const { inspector, view, selector, property, options } = config;
-  yield selectNode(selector, inspector);
-  return yield toggleShapesHighlighter(view, selector, property, true, options);
-}
-
-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});
+function* testTranslate(testActor, helper) {
+  for (let shape of SHAPE_IDS) {
+    info(`Displaying ${shape}`);
+    yield helper.show(shape, {mode: "cssClipPath", transformMode: true});
     let { mouse } = helper;
 
-    let { center, width, height } = yield getBoundingBoxInPx({selector, ...config});
+    let { center, width, height } = yield getBoundingBoxInPx(testActor, helper, shape);
     let [x, y] = center;
     let dx = width / 10;
     let dy = height / 10;
-    let onShapeChangeApplied;
 
-    info(`Translating ${selector}`);
-    onShapeChangeApplied = highlighters.once("shapes-highlighter-changes-applied");
-    yield mouse.down(x, y, selector);
-    yield mouse.move(x + dx, y + dy, selector);
-    yield mouse.up(x + dx, y + dy, selector);
-    yield onShapeChangeApplied;
+    info(`Translating ${shape}`);
+    yield mouse.down(x, y, shape);
+    yield mouse.move(x + dx, y + dy, shape);
+    yield mouse.up(x + dx, y + dy, shape);
+    yield testActor.reflow();
 
-    let newBB = yield getBoundingBoxInPx({selector, ...config});
-    isnot(newBB.center[0], x, `${selector} translated on y axis`);
-    isnot(newBB.center[1], y, `${selector} translated on x axis`);
+    let newBB = yield getBoundingBoxInPx(testActor, helper);
+    isnot(newBB.center[0], x, `${shape} translated on y axis`);
+    isnot(newBB.center[1], y, `${shape} translated on x axis`);
 
-    info(`Translating ${selector} back`);
-    onShapeChangeApplied = highlighters.once("shapes-highlighter-changes-applied");
-    yield mouse.down(x + dx, y + dy, selector);
-    yield mouse.move(x, y, selector);
-    yield mouse.up(x, y, selector);
+    info(`Translating ${shape} back`);
+    yield mouse.down(x + dx, y + dy, shape);
+    yield mouse.move(x, y, shape);
+    yield mouse.up(x, y, shape);
     yield testActor.reflow();
-    yield onShapeChangeApplied;
 
-    newBB = yield 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`);
+    newBB = yield 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`);
   }
 }
 
-function* testScale(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});
+function* testScale(testActor, helper) {
+  for (let shape of SHAPE_IDS) {
+    info(`Displaying ${shape}`);
+    yield helper.show(shape, {mode: "cssClipPath", transformMode: true});
     let { mouse } = helper;
 
     let { nw, width,
-          height, center } = yield getBoundingBoxInPx({selector, ...config});
+          height, center } = yield getBoundingBoxInPx(testActor, helper, shape);
 
     // 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]);
-      yield mouse.down(x, y, selector);
-      yield mouse.move(x + dx, y + dy, selector);
-      yield mouse.up(x + dx, y + dy, selector);
+      yield mouse.down(x, y, shape);
+      yield mouse.move(x + dx, y + dy, shape);
+      yield mouse.up(x + dx, y + dy, shape);
       yield testActor.reflow();
       nw[0] += dx;
       nw[1] += dy;
     }
     let dx = width / 10;
     let dy = height / 10;
-    let onShapeChangeApplied;
 
     info("Scaling from nw");
-    onShapeChangeApplied = highlighters.once("shapes-highlighter-changes-applied");
-    yield mouse.down(nw[0], nw[1], selector);
-    yield mouse.move(nw[0] + dx, nw[1] + dy, selector);
-    yield mouse.up(nw[0] + dx, nw[1] + dy, selector);
+    yield mouse.down(nw[0], nw[1], shape);
+    yield mouse.move(nw[0] + dx, nw[1] + dy, shape);
+    yield mouse.up(nw[0] + dx, nw[1] + dy, shape);
     yield testActor.reflow();
-    yield onShapeChangeApplied;
 
-    let nwBB = yield 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`);
+    let nwBB = yield 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`);
 
     info("Scaling from ne");
-    onShapeChangeApplied = highlighters.once("shapes-highlighter-changes-applied");
-    yield mouse.down(nwBB.ne[0], nwBB.ne[1], selector);
-    yield mouse.move(nwBB.ne[0] - dx, nwBB.ne[1] + dy, selector);
-    yield mouse.up(nwBB.ne[0] - dx, nwBB.ne[1] + dy, selector);
+    yield mouse.down(nwBB.ne[0], nwBB.ne[1], shape);
+    yield mouse.move(nwBB.ne[0] - dx, nwBB.ne[1] + dy, shape);
+    yield mouse.up(nwBB.ne[0] - dx, nwBB.ne[1] + dy, shape);
     yield testActor.reflow();
-    yield onShapeChangeApplied;
 
-    let neBB = yield 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`);
+    let neBB = yield 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`);
 
     info("Scaling from sw");
-    onShapeChangeApplied = highlighters.once("shapes-highlighter-changes-applied");
-    yield mouse.down(neBB.sw[0], neBB.sw[1], selector);
-    yield mouse.move(neBB.sw[0] + dx, neBB.sw[1] - dy, selector);
-    yield mouse.up(neBB.sw[0] + dx, neBB.sw[1] - dy, selector);
+    yield mouse.down(neBB.sw[0], neBB.sw[1], shape);
+    yield mouse.move(neBB.sw[0] + dx, neBB.sw[1] - dy, shape);
+    yield mouse.up(neBB.sw[0] + dx, neBB.sw[1] - dy, shape);
     yield testActor.reflow();
-    yield onShapeChangeApplied;
 
-    let swBB = yield 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`);
+    let swBB = yield 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`);
 
     info("Scaling from se");
-    onShapeChangeApplied = highlighters.once("shapes-highlighter-changes-applied");
-    yield mouse.down(swBB.se[0], swBB.se[1], selector);
-    yield mouse.move(swBB.se[0] - dx, swBB.se[1] - dy, selector);
-    yield mouse.up(swBB.se[0] - dx, swBB.se[1] - dy, selector);
+    yield mouse.down(swBB.se[0], swBB.se[1], shape);
+    yield mouse.move(swBB.se[0] - dx, swBB.se[1] - dy, shape);
+    yield mouse.up(swBB.se[0] - dx, swBB.se[1] - dy, shape);
     yield testActor.reflow();
-    yield onShapeChangeApplied;
 
-    let seBB = yield 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`);
+    let seBB = yield 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`);
   }
 }
 
-function* getBoundingBoxInPx(config) {
-  const { testActor, selector, inspector, highlighters } = config;
-  let quads = yield testActor.getAllAdjustedQuads(selector);
+function* getBoundingBoxInPx(testActor, helper, shape = "#polygon") {
+  let quads = yield testActor.getAllAdjustedQuads(shape);
   let { width, height } = quads.content[0].bounds;
-  let highlightedNode = yield getNodeFront(selector, inspector);
-  let computedStyle = yield inspector.pageStyle.getComputed(highlightedNode);
+  let computedStyle = yield helper.highlightedNode.getComputedStyle();
   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 = yield testActor.getHighlighterNodeAttribute(
-    "shapes-bounding-box", "d", highlighters.highlighters[HIGHLIGHTER_TYPE]);
+  let path = yield helper.getElementAttribute("shapes-bounding-box", "d");
   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,132 +3,111 @@
  * 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_SELECTORS = ["#polygon-transform", "#ellipse"];
+const SHAPE_IDS = ["#polygon-transform", "#ellipse"];
 
 add_task(function* () {
-  let env = yield openInspectorForURL(TEST_URL);
-  let helper = yield getHighlighterHelperFor(HIGHLIGHTER_TYPE)(env);
-  let {testActor, inspector} = env;
-  let view = selectRuleView(inspector);
-  let highlighters = view.highlighters;
-  let config = { inspector, view, highlighters, testActor, helper };
+  let inspector = yield openInspectorForURL(TEST_URL);
+  let helper = yield getHighlighterHelperFor(HIGHLIGHTER_TYPE)(inspector);
+  let {testActor} = inspector;
 
-  yield testOneDimScale(config);
+  yield testOneDimScale(testActor, helper);
+
+  helper.finalize();
 });
 
-function* setup(config) {
-  const { inspector, view, selector, property, options } = config;
-  yield selectNode(selector, inspector);
-  return yield toggleShapesHighlighter(view, selector, property, true, options);
-}
-
-function* testOneDimScale(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});
+function* testOneDimScale(testActor, helper) {
+  for (let shape of SHAPE_IDS) {
+    info(`Displaying ${shape}`);
+    yield helper.show(shape, {mode: "cssClipPath", transformMode: true});
     let { mouse } = helper;
 
     let { nw, width,
-          height, center } = yield getBoundingBoxInPx({selector, ...config});
+          height, center } = yield getBoundingBoxInPx(testActor, helper, shape);
 
     // 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]);
-      yield mouse.down(x, y, selector);
-      yield mouse.move(x + dx, y + dy, selector);
-      yield mouse.up(x + dx, y + dy, selector);
+      yield mouse.down(x, y, shape);
+      yield mouse.move(x + dx, y + dy, shape);
+      yield mouse.up(x + dx, y + dy, shape);
       yield testActor.reflow();
       nw[0] += dx;
       nw[1] += dy;
     }
     let dx = width / 10;
     let dy = height / 10;
-    let onShapeChangeApplied;
 
     info("Scaling from w");
-    onShapeChangeApplied = highlighters.once("shapes-highlighter-changes-applied");
-    yield mouse.down(nw[0], center[1], selector);
-    yield mouse.move(nw[0] + dx, center[1], selector);
-    yield mouse.up(nw[0] + dx, center[1], selector);
+    yield mouse.down(nw[0], center[1], shape);
+    yield mouse.move(nw[0] + dx, center[1], shape);
+    yield mouse.up(nw[0] + dx, center[1], shape);
     yield testActor.reflow();
-    yield onShapeChangeApplied;
 
-    let wBB = yield 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`);
+    let wBB = yield 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`);
 
     info("Scaling from e");
-    onShapeChangeApplied = highlighters.once("shapes-highlighter-changes-applied");
-    yield mouse.down(wBB.ne[0], center[1], selector);
-    yield mouse.move(wBB.ne[0] - dx, center[1], selector);
-    yield mouse.up(wBB.ne[0] - dx, center[1], selector);
+    yield mouse.down(wBB.ne[0], center[1], shape);
+    yield mouse.move(wBB.ne[0] - dx, center[1], shape);
+    yield mouse.up(wBB.ne[0] - dx, center[1], shape);
     yield testActor.reflow();
-    yield onShapeChangeApplied;
 
-    let eBB = yield 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`);
+    let eBB = yield 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`);
 
     info("Scaling from s");
-    onShapeChangeApplied = highlighters.once("shapes-highlighter-changes-applied");
-    yield mouse.down(eBB.center[0], eBB.sw[1], selector);
-    yield mouse.move(eBB.center[0], eBB.sw[1] - dy, selector);
-    yield mouse.up(eBB.center[0], eBB.sw[1] - dy, selector);
+    yield mouse.down(eBB.center[0], eBB.sw[1], shape);
+    yield mouse.move(eBB.center[0], eBB.sw[1] - dy, shape);
+    yield mouse.up(eBB.center[0], eBB.sw[1] - dy, shape);
     yield testActor.reflow();
-    yield onShapeChangeApplied;
 
-    let sBB = yield 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`);
+    let sBB = yield 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`);
 
     info("Scaling from n");
-    onShapeChangeApplied = highlighters.once("shapes-highlighter-changes-applied");
-    yield mouse.down(sBB.center[0], sBB.nw[1], selector);
-    yield mouse.move(sBB.center[0], sBB.nw[1] + dy, selector);
-    yield mouse.up(sBB.center[0], sBB.nw[1] + dy, selector);
+    yield mouse.down(sBB.center[0], sBB.nw[1], shape);
+    yield mouse.move(sBB.center[0], sBB.nw[1] + dy, shape);
+    yield mouse.up(sBB.center[0], sBB.nw[1] + dy, shape);
     yield testActor.reflow();
-    yield onShapeChangeApplied;
 
-    let nBB = yield 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`);
+    let nBB = yield 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`);
   }
 }
 
-function* getBoundingBoxInPx(config) {
-  const { testActor, selector, inspector, highlighters } = config;
-  let quads = yield testActor.getAllAdjustedQuads(selector);
+function* getBoundingBoxInPx(testActor, helper, shape = "#polygon") {
+  let quads = yield testActor.getAllAdjustedQuads(shape);
   let { width, height } = quads.content[0].bounds;
-  let highlightedNode = yield getNodeFront(selector, inspector);
-  let computedStyle = yield inspector.pageStyle.getComputed(highlightedNode);
+  let computedStyle = yield helper.highlightedNode.getComputedStyle();
   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 = yield testActor.getHighlighterNodeAttribute(
-    "shapes-bounding-box", "d", highlighters.highlighters[HIGHLIGHTER_TYPE]);
+  let path = yield helper.getElementAttribute("shapes-bounding-box", "d");
   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,55 +5,42 @@
 "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(function* () {
-  let env = yield openInspectorForURL(TEST_URL);
-  let helper = yield getHighlighterHelperFor(HIGHLIGHTER_TYPE)(env);
-  let {testActor, inspector} = env;
-  let view = selectRuleView(inspector);
-  let highlighters = view.highlighters;
-  let config = {inspector, view, highlighters, testActor, helper};
+  let inspector = yield openInspectorForURL(TEST_URL);
+  let helper = yield getHighlighterHelperFor(HIGHLIGHTER_TYPE)(inspector);
+  let {testActor} = inspector;
 
-  yield testPolygonIframeMovePoint(config);
+  yield testPolygonIframeMovePoint(testActor, helper);
+
+  yield helper.finalize();
 });
 
-function* testPolygonIframeMovePoint(config) {
-  const { inspector, view, testActor, helper } = config;
-  const selector = "#polygon";
-  const property = "clip-path";
+function* testPolygonIframeMovePoint(testActor, helper) {
+  info("Displaying polygon");
+  yield helper.show("#polygon", {mode: "cssClipPath"}, "#frame");
+  let { mouse, highlightedNode } = helper;
 
-  info(`Turn on shapes highlighter for ${selector}`);
-  // Get a reference to the highlighter's target node inside the iframe.
-  let highlightedNode = yield getNodeFrontInFrame(selector, "#frame", inspector);
-  // Select the nested node so toggling of the shapes highlighter works from the rule view
-  yield selectNode(highlightedNode, inspector);
-  yield toggleShapesHighlighter(view, selector, property, true);
-  let { mouse } = helper;
-
-  let onRuleViewChanged = view.once("ruleview-changed");
   info("Moving polygon point visible in iframe");
   yield mouse.down(10, 10);
   yield mouse.move(20, 20);
   yield mouse.up();
   yield testActor.reflow();
-  yield onRuleViewChanged;
 
-  let computedStyle = yield inspector.pageStyle.getComputed(highlightedNode);
+  let computedStyle = yield highlightedNode.getComputedStyle();
   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");
   yield mouse.down(110, 410);
   yield mouse.move(120, 420);
   yield mouse.up();
   yield testActor.reflow();
-  yield onRuleViewChanged;
 
-  computedStyle = yield inspector.pageStyle.getComputed(highlightedNode);
+  computedStyle = yield highlightedNode.getComputedStyle();
   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
@@ -812,50 +812,31 @@ function* getDisplayedNodeTextContent(se
 }
 
 /**
  * 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
  */
-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");
-    yield highlighters.once("shapes-highlighter-armed");
-  }
-
-  let metaKey = options.transformMode;
-  let ctrlKey = options.transformMode;
-
+function* toggleShapesHighlighter(view, highlighters, selector, property, show) {
+  info("Toggle shapes highlighter");
+  let container = getRuleViewProperty(view, selector, property).valueSpan;
+  let shapesToggle = container.querySelector(".ruleview-shapeswatch");
   if (show) {
     let onHighlighterShown = highlighters.once("shapes-highlighter-shown");
-    EventUtils.sendMouseEvent({type: "click", metaKey, ctrlKey },
-      shapesToggle, view.styleWindow);
+    shapesToggle.click();
     yield onHighlighterShown;
   } else {
     let onHighlighterHidden = highlighters.once("shapes-highlighter-hidden");
-    EventUtils.sendMouseEvent({type: "click", metaKey, ctrlKey },
-      shapesToggle, view.styleWindow);
+    shapesToggle.click();
     yield onHighlighterHidden;
   }
 }
deleted file mode 100644
--- a/devtools/client/shared/widgets/ShapesInContextEditor.js
+++ /dev/null
@@ -1,429 +0,0 @@
-/* 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,17 +18,16 @@ 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,18 +477,17 @@
   background-size: 1em;
 }
 
 .ruleview-grid {
   background: url("chrome://devtools/skin/images/grid.svg");
   border-radius: 0;
 }
 
-.ruleview-shape-point.active,
-.ruleview-shapeswatch.active + .ruleview-shape > .ruleview-shape-point:hover {
+.ruleview-shape-point.active {
   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
@@ -836,19 +836,20 @@ class ShapesHighlighter extends AutoRefr
       let [newX, newY] = apply(this.transformMatrix, vector);
       let precisionX = getDecimalPrecision(unitX);
       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();
+    polygonDef = (this.geometryBox) ? `polygon(${polygonDef}) ${this.geometryBox}` :
+                                      `polygon(${polygonDef})`;
 
-    this.emit("highlighter-event", { type: "shape-change", value: polygonDef });
+    this.currentNode.style.setProperty(this.property, polygonDef, "important");
   }
 
   /**
    * 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) {
@@ -859,19 +860,21 @@ class ShapesHighlighter extends AutoRefr
     let [newCx, newCy] = apply(this.transformMatrix, [valueX / ratioX, valueY / ratioY]);
     if (transX !== null) {
       // As part of scaling, the shape is translated to be tangent to the line y=0.
       // To get the new radius, we translate the new cx back to that point and get
       // the distance to the line y=0.
       radius = `${Math.abs((newCx - transX) * ratioRad)}${unitRad}`;
     }
 
-    let circleDef = `circle(${radius} at ${newCx * ratioX}${unitX} ` +
-        `${newCy * ratioY}${unitY}) ${this.geometryBox}`.trim();
-    this.emit("highlighter-event", { type: "shape-change", value: circleDef });
+    let circleDef = (this.geometryBox) ?
+      `circle(${radius} at ${newCx * ratioX}${unitX} ` +
+        `${newCy * ratioY}${unitY} ${this.geometryBox}` :
+      `circle(${radius} at ${newCx * ratioX}${unitX} ${newCy * ratioY}${unitY}`;
+    this.currentNode.style.setProperty(this.property, circleDef, "important");
   }
 
   /**
    * Transform an ellipse depending on the current transformation matrix.
    * @param {Number} transX the number of pixels the shape is translated on the x axis
    *                 before scaling
    * @param {Number} transY the number of pixels the shape is translated on the y axis
    *                 before scaling
@@ -885,19 +888,22 @@ class ShapesHighlighter extends AutoRefr
     if (transX !== null && transY !== null) {
       // As part of scaling, the shape is translated to be tangent to the lines y=0 & x=0.
       // To get the new radii, we translate the new center back to that point and get the
       // distances to the line x=0 and y=0.
       rx = `${Math.abs((newCx - transX) * ratioRX)}${unitRX}`;
       ry = `${Math.abs((newCy - transY) * ratioRY)}${unitRY}`;
     }
 
-    let ellipseDef = `ellipse(${rx} ${ry} at ${newCx * ratioX}${unitX} ` +
-          `${newCy * ratioY}${unitY}) ${this.geometryBox}`.trim();
-    this.emit("highlighter-event", { type: "shape-change", value: ellipseDef });
+    let ellipseDef = (this.geometryBox) ?
+        `ellipse(${rx} ${ry} at ${newCx * ratioX}${unitX} ` +
+          `${newCy * ratioY}${unitY}) ${this.geometryBox}` :
+        `ellipse(${rx} ${ry} at ${newCx * ratioX}${unitX} ` +
+          `${newCy * ratioY}${unitY})`;
+    this.currentNode.style.setProperty(this.property, ellipseDef, "important");
   }
 
   /**
    * Transform an inset depending on the current transformation matrix.
    */
   _transformInset() {
     let { top, left, right, bottom } = this[_dragging].pointsInfo;
     let { width, height } = this.currentDimensions;
@@ -917,17 +923,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.emit("highlighter-event", { type: "shape-change", value: insetDef });
+    this.currentNode.style.setProperty(this.property, insetDef, "important");
   }
 
   /**
    * 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 +956,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 };
   }
 
   /**
-   * Update the dragged polygon point with the given x/y coords and update
-   * the element style.
+   * Set the inline style of the polygon, replacing the given point with the given x/y
+   * coords.
    * @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);
@@ -969,59 +975,62 @@ class ShapesHighlighter extends AutoRefr
     let newX = (valueX + deltaX).toFixed(precisionX);
     let newY = (valueY + deltaY).toFixed(precisionY);
 
     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();
+    polygonDef = (this.geometryBox) ? `polygon(${polygonDef}) ${this.geometryBox}` :
+                                      `polygon(${polygonDef})`;
 
-    this.emit("highlighter-event", { type: "shape-change", value: polygonDef });
+    this.currentNode.style.setProperty(this.property, polygonDef, "important");
   }
 
   /**
-   * Add new point to the polygon defintion and update element style.
+   * Set the inline style of the polygon, adding a new point.
    * 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) {
     let polygonDef = (this.fillRule) ? `${this.fillRule}, ` : "";
     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();
+    polygonDef = (this.geometryBox) ? `polygon(${polygonDef}) ${this.geometryBox}` :
+                                      `polygon(${polygonDef})`;
 
     this.hoveredPoint = after + 1;
     this._emitHoverEvent(this.hoveredPoint);
-    this.emit("highlighter-event", { type: "shape-change", value: polygonDef });
+    this.currentNode.style.setProperty(this.property, polygonDef, "important");
   }
 
   /**
-   * Remove point from polygon defintion and update the element style.
+   * Set the inline style of the polygon, deleting the given point.
    * @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();
+    polygonDef = (this.geometryBox) ? `polygon(${polygonDef}) ${this.geometryBox}` :
+                                      `polygon(${polygonDef})`;
 
     this.hoveredPoint = null;
     this._emitHoverEvent(this.hoveredPoint);
-    this.emit("highlighter-event", { type: "shape-change", value: polygonDef });
+    this.currentNode.style.setProperty(this.property, polygonDef, "important");
   }
   /**
    * 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,50 +1064,53 @@ 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 center/radius of the circle according to the mouse position and
-   * update the element style.
+   * Set the inline style of the circle, setting the center/radius according to the
+   * mouse position.
    * @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;
 
     if (point === "center") {
       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();
+      let circleDef = (this.geometryBox) ?
+            `circle(${radius} at ${newCx} ${newCy}) ${this.geometryBox}` :
+            `circle(${radius} at ${newCx} ${newCy})`;
 
-      this.emit("highlighter-event", { type: "shape-change", value: circleDef });
+      this.currentNode.style.setProperty(this.property, circleDef, "important");
     } 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();
+      let circleDef = (this.geometryBox) ?
+                      `circle(${newRadius} at ${cx} ${cy} ${this.geometryBox}` :
+                      `circle(${newRadius} at ${cx} ${cy}`;
 
-      this.emit("highlighter-event", { type: "shape-change", value: circleDef });
+      this.currentNode.style.setProperty(this.property, circleDef, "important");
     }
   }
 
   /**
    * 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,60 +1154,63 @@ class ShapesHighlighter extends AutoRefr
       value = (isUnitless(value)) ? ry : parseFloat(value);
       let ratio = (value / ry) || 1;
 
       this[_dragging] = { point, value, origRadius: ry, unit, ratio };
     }
   }
 
   /**
-   * Set center/rx/ry of the ellispe according to the mouse position and update the
-   * element style.
+   * Set the inline style of the ellipse, setting the center/rx/ry according to the
+   * mouse position.
    * @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);
     let { rx, ry, cx, cy } = this.coordUnits;
 
     if (point === "center") {
       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();
+      let ellipseDef = (this.geometryBox) ?
+        `ellipse(${rx} ${ry} at ${newCx} ${newCy}) ${this.geometryBox}` :
+        `ellipse(${rx} ${ry} at ${newCx} ${newCy})`;
 
-      this.emit("highlighter-event", { type: "shape-change", value: ellipseDef });
+      this.currentNode.style.setProperty(this.property, ellipseDef, "important");
     } 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();
+      let ellipseDef = (this.geometryBox) ?
+        `ellipse(${newRadius} ${ry} at ${cx} ${cy}) ${this.geometryBox}` :
+        `ellipse(${newRadius} ${ry} at ${cx} ${cy})`;
 
-      this.emit("highlighter-event", { type: "shape-change", value: ellipseDef });
+      this.currentNode.style.setProperty(this.property, ellipseDef, "important");
     } 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();
+      let ellipseDef = (this.geometryBox) ?
+        `ellipse(${rx} ${newRadius} at ${cx} ${cy}) ${this.geometryBox}` :
+        `ellipse(${rx} ${newRadius} at ${cx} ${cy})`;
 
-      this.emit("highlighter-event", { type: "shape-change", value: ellipseDef });
+      this.currentNode.style.setProperty(this.property, ellipseDef, "important");
     }
   }
 
   /**
    * 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 +1230,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 top/left/right/bottom of the inset shape according to the mouse position
-   * and update the element style.
+   * Set the inline style of the inset, setting top/left/right/bottom according to the
+   * mouse position.
    * @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 +1263,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.emit("highlighter-event", { type: "shape-change", value: insetDef });
+    this.currentNode.style.setProperty(this.property, insetDef, "important");
   }
 
   _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);
--- a/devtools/server/actors/utils/shapes-utils.js
+++ b/devtools/server/actors/utils/shapes-utils.js
@@ -4,17 +4,17 @@
  * Get the distance between two points on a plane.
  * @param {Number} x1 the x coord of the first point
  * @param {Number} y1 the y coord of the first point
  * @param {Number} x2 the x coord of the second point
  * @param {Number} y2 the y coord of the second point
  * @returns {Number} the distance between the two points
  */
 const getDistance = (x1, y1, x2, y2) => {
-  return Math.round(Math.hypot(x2 - x1, y2 - y1));
+  return Math.hypot(x2 - x1, y2 - y1);
 };
 
 /**
  * Determine if the given x/y coords are along the edge of the given ellipse.
  * We allow for a small area around the edge that still counts as being on the edge.
  * @param {Number} x the x coordinate of the click
  * @param {Number} y the y coordinate of the click
  * @param {Number} cx the x coordinate of the center of the ellipse