Merge mozilla-central to mozilla-inbound. CLOSED TREE
authorCsoregi Natalia <ncsoregi@mozilla.com>
Mon, 15 Apr 2019 12:48:34 +0300
changeset 469593 a57f27d3ccd0
parent 469592 790532ec4738 (current diff)
parent 469468 6c9e7cba261d (diff)
child 469594 974f32848a0d
push id35875
push userccoroiu@mozilla.com
push dateTue, 16 Apr 2019 04:06:16 +0000
treeherdermozilla-central@a83cab75b00d [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
milestone68.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Merge mozilla-central to mozilla-inbound. CLOSED TREE
toolkit/locales/en-US/toolkit/global/videocontrols.ftl
toolkit/themes/shared/pictureinpicture/toggle.css
--- a/browser/base/content/test/static/browser_parsable_css.js
+++ b/browser/base/content/test/static/browser_parsable_css.js
@@ -60,19 +60,16 @@ let whitelist = [
   {sourceName: /webide\/skin\/logs\.css$/i,
    intermittent: true,
    errorMessage: /Property contained reference to invalid variable.*color/i,
    isFromDevTools: true},
   {sourceName: /webide\/skin\/logs\.css$/i,
    intermittent: true,
    errorMessage: /Property contained reference to invalid variable.*background/i,
    isFromDevTools: true},
-  {sourceName: /pictureinpicture\/toggle.css$/i,
-   errorMessage: /Unknown pseudo-class.*moz-native-anonymous/i,
-   isFromDevTools: false},
 ];
 
 if (!Services.prefs.getBoolPref("layout.css.xul-box-display-values.content.enabled")) {
   // These are UA sheets which use non-content-exposed `display` values.
   whitelist.push({
     sourceName: /(skin\/shared\/Heartbeat|((?:res|gre-resources)\/(ua|html)))\.css$/i,
     errorMessage: /Error in parsing value for .*\bdisplay\b/i,
     isFromDevTools: false,
--- a/browser/components/controlcenter/content/panel.inc.xul
+++ b/browser/components/controlcenter/content/panel.inc.xul
@@ -8,18 +8,17 @@
        photon="true"
        role="alertdialog"
        aria-labelledby="identity-popup-mainView-panel-header-span"
        onpopupshown="gIdentityHandler.onPopupShown(event);"
        onpopuphidden="gIdentityHandler.onPopupHidden(event);"
        orient="vertical">
 
   <panelmultiview id="identity-popup-multiView"
-                  mainViewId="identity-popup-mainView"
-                  disablekeynav="true">
+                  mainViewId="identity-popup-mainView">
     <panelview id="identity-popup-mainView"
                descriptionheightworkaround="true">
       <vbox id="identity-popup-mainView-panel-header">
         <label>
           <html:span id="identity-popup-mainView-panel-header-span"/>
         </label>
       </vbox>
 
--- a/browser/components/customizableui/PanelMultiView.jsm
+++ b/browser/components/customizableui/PanelMultiView.jsm
@@ -639,16 +639,20 @@ var PanelMultiView = class extends Assoc
 
     // Do not re-enter the process if navigation is already in progress. Since
     // there is only one active view at any given time, we can do this check
     // safely, even considering that during the navigation process the actual
     // view to which prevPanelView refers will change.
     if (!prevPanelView.active) {
       return;
     }
+    // If prevPanelView._doingKeyboardActivation is true, it will be reset to
+    // false synchronously. Therefore, we must capture it before we use any
+    // "await" statements.
+    let doingKeyboardActivation = prevPanelView._doingKeyboardActivation;
     // Marking the view that is about to scrolled out of the visible area as
     // inactive will prevent re-entrancy and also disable keyboard navigation.
     // From this point onwards, "await" statements can be used safely.
     prevPanelView.active = false;
 
     // Provide visual feedback while navigation is in progress, starting before
     // the transition starts and ending when the previous view is invisible.
     if (anchor) {
@@ -687,16 +691,17 @@ var PanelMultiView = class extends Assoc
 
       await this._transitionViews(prevPanelView.node, viewNode, false, anchor);
     } finally {
       if (anchor) {
         anchor.removeAttribute("open");
       }
     }
 
+    nextPanelView.focusWhenActive = doingKeyboardActivation;
     this._activateView(nextPanelView);
   }
 
   /**
    * Navigates backwards by sliding out the most recent subview.
    */
   goBack() {
     this._goBack().catch(Cu.reportError);
@@ -809,17 +814,17 @@ var PanelMultiView = class extends Assoc
   /**
    * Activates the specified view and raises the ViewShown event, unless the
    * view was closed in the meantime.
    */
   _activateView(panelView) {
     if (panelView.isOpenIn(this)) {
       panelView.active = true;
       if (panelView.focusWhenActive) {
-        panelView.focusFirstNavigableElement();
+        panelView.focusFirstNavigableElement(false, true);
         panelView.focusWhenActive = false;
       }
       panelView.dispatchCustomEvent("ViewShown");
 
       // Re-enable panel autopositioning.
       this._panel.autoPosition = true;
     }
   }
@@ -1395,130 +1400,152 @@ var PanelView = class extends Associated
     // Now we can make all the necessary DOM changes at once.
     for (let { element, bounds } of items) {
       gMultiLineElementsMap.set(element, { bounds, textContent: element.textContent });
       element.style.height = bounds.height + "px";
     }
   }
 
   /**
-   * Array of enabled elements that can be selected with the keyboard. This
-   * means all buttons, menulists, and text links including the back button.
-   *
-   * This list is cached until the view is closed, so elements that become
-   * enabled later may not be navigable.
+   * Determine whether an element can only be navigated to with tab/shift+tab,
+   * not the arrow keys.
    */
-  get _navigableElements() {
-    if (this.__navigableElements) {
-      return this.__navigableElements;
-    }
+  _isNavigableWithTabOnly(element) {
+    let tag = element.localName;
+    return tag == "menulist" || tag == "textbox" || tag == "input"
+           || tag == "textarea";
+  }
 
-    let navigableElements = Array.from(this.node.querySelectorAll(
-      ":-moz-any(button,toolbarbutton,menulist,.text-link,.navigable):not([disabled])"));
-    return this.__navigableElements = navigableElements.filter(element => {
-      // Set the "tabindex" attribute to make sure the element is focusable.
-      if (!element.hasAttribute("tabindex")) {
-        element.setAttribute("tabindex", "0");
+  /**
+   * Make a TreeWalker for keyboard navigation.
+   *
+   * @param {Boolean} arrowKey If `true`, elements only navigable with tab are
+   *        excluded.
+   */
+  _makeNavigableTreeWalker(arrowKey) {
+    let filter = node => {
+      if (node.disabled) {
+        return NodeFilter.FILTER_REJECT;
+      }
+      let bounds = this._getBoundsWithoutFlushing(node);
+      if (bounds.width == 0 || bounds.height == 0) {
+        return NodeFilter.FILTER_REJECT;
+      }
+      if (node.tagName == "button" || node.tagName == "toolbarbutton" ||
+          node.classList.contains("text-link") ||
+          node.classList.contains("navigable") ||
+          (!arrowKey && this._isNavigableWithTabOnly(node))) {
+        // Set the tabindex attribute to make sure the node is focusable.
+        if (!node.hasAttribute("tabindex")) {
+          node.setAttribute("tabindex", "-1");
+        }
+        return NodeFilter.FILTER_ACCEPT;
       }
-      if (element.hasAttribute("disabled")) {
-        return false;
-      }
-      let bounds = this._getBoundsWithoutFlushing(element);
-      return bounds.width > 0 && bounds.height > 0;
-    });
+      return NodeFilter.FILTER_SKIP;
+    };
+    return this.document.createTreeWalker(this.node, NodeFilter.SHOW_ELEMENT,
+      filter);
+  }
+
+  /**
+   * Get a TreeWalker which finds elements navigable with tab/shift+tab.
+   */
+  get _tabNavigableWalker() {
+    if (!this.__tabNavigableWalker) {
+      this.__tabNavigableWalker = this._makeNavigableTreeWalker(false);
+    }
+    return this.__tabNavigableWalker;
+  }
+
+  /**
+   * Get a TreeWalker which finds elements navigable with up/down arrow keys.
+   */
+  get _arrowNavigableWalker() {
+    if (!this.__arrowNavigableWalker) {
+      this.__arrowNavigableWalker = this._makeNavigableTreeWalker(true);
+    }
+    return this.__arrowNavigableWalker;
   }
 
   /**
    * Element that is currently selected with the keyboard, or null if no element
    * is selected. Since the reference is held weakly, it can become null or
    * undefined at any time.
-   *
-   * The element is usually, but not necessarily, among the _navigableElements.
    */
   get selectedElement() {
     return this._selectedElement && this._selectedElement.get();
   }
   set selectedElement(value) {
     if (!value) {
       delete this._selectedElement;
     } else {
       this._selectedElement = Cu.getWeakReference(value);
     }
   }
 
   /**
    * Focuses and moves keyboard selection to the first navigable element.
    * This is a no-op if there are no navigable elements.
+   *
+   * @param {Boolean} homeKey   `true` if this is for the home key.
+   * @param {Boolean} skipBack   `true` if the Back button should be skipped.
    */
-  focusFirstNavigableElement() {
-    this.selectedElement = this._navigableElements[0];
+  focusFirstNavigableElement(homeKey = false, skipBack = false) {
+    // The home key is conceptually similar to the up/down arrow keys.
+    let walker = homeKey ?
+      this._arrowNavigableWalker : this._tabNavigableWalker;
+    walker.currentNode = walker.root;
+    this.selectedElement = walker.firstChild();
+    if (skipBack && walker.currentNode
+        && walker.currentNode.classList.contains("subviewbutton-back")
+        && walker.nextNode()) {
+      this.selectedElement = walker.currentNode;
+    }
     this.focusSelectedElement();
   }
 
   /**
    * Focuses and moves keyboard selection to the last navigable element.
    * This is a no-op if there are no navigable elements.
+   *
+   * @param {Boolean} endKey   `true` if this is for the end key.
    */
-  focusLastNavigableElement() {
-    this.selectedElement = this._navigableElements[this._navigableElements.length - 1];
+  focusLastNavigableElement(endKey = false) {
+    // The end key is conceptually similar to the up/down arrow keys.
+    let walker = endKey ?
+      this._arrowNavigableWalker : this._tabNavigableWalker;
+    walker.currentNode = walker.root;
+    this.selectedElement = walker.lastChild();
     this.focusSelectedElement();
   }
 
   /**
    * Based on going up or down, select the previous or next focusable element.
    *
    * @param {Boolean} isDown   whether we're going down (true) or up (false).
+   * @param {Boolean} arrowKey   `true` if this is for the up/down arrow keys.
    *
    * @return {DOMNode} the element we selected.
    */
-  moveSelection(isDown) {
-    let buttons = this._navigableElements;
-    let lastSelected = this.selectedElement;
-    let newButton = null;
-    let maxIdx = buttons.length - 1;
-    if (lastSelected) {
-      let buttonIndex = buttons.indexOf(lastSelected);
-      if (buttonIndex != -1) {
-        // Buttons may get selected whilst the panel is shown, so add an extra
-        // check here.
-        do {
-          buttonIndex = buttonIndex + (isDown ? 1 : -1);
-        } while (buttons[buttonIndex] && buttons[buttonIndex].disabled);
-        if (isDown && buttonIndex > maxIdx)
-          buttonIndex = 0;
-        else if (!isDown && buttonIndex < 0)
-          buttonIndex = maxIdx;
-        newButton = buttons[buttonIndex];
-      } else {
-        // The previously selected item is no longer selectable. Find the next item:
-        let allButtons = lastSelected.closest("panelview").getElementsByTagName("toolbarbutton");
-        let maxAllButtonIdx = allButtons.length - 1;
-        let allButtonIndex = allButtons.indexOf(lastSelected);
-        while (allButtonIndex >= 0 && allButtonIndex <= maxAllButtonIdx) {
-          allButtonIndex++;
-          // Check if the next button is in the list of focusable buttons.
-          buttonIndex = buttons.indexOf(allButtons[allButtonIndex]);
-          if (buttonIndex != -1) {
-            // If it is, just use that button if we were going down, or the previous one
-            // otherwise. If this was the first button, newButton will end up undefined,
-            // which is fine because we'll fall back to using the last button at the
-            // bottom of this method.
-            newButton = buttons[isDown ? buttonIndex : buttonIndex - 1];
-            break;
-          }
-        }
-      }
+  moveSelection(isDown, arrowKey = false) {
+    let walker = arrowKey ?
+      this._arrowNavigableWalker : this._tabNavigableWalker;
+    let oldSel = this.selectedElement;
+    let newSel;
+    if (oldSel) {
+      walker.currentNode = oldSel;
+      newSel = isDown ? walker.nextNode() : walker.previousNode();
     }
-
     // If we couldn't find something, select the first or last item:
-    if (!newButton) {
-      newButton = buttons[isDown ? 0 : maxIdx];
+    if (!newSel) {
+      walker.currentNode = walker.root;
+      newSel = isDown ? walker.firstChild() : walker.lastChild();
     }
-    this.selectedElement = newButton;
-    return newButton;
+    this.selectedElement = newSel;
+    return newSel;
   }
 
   /**
    * Allow for navigating subview buttons using the arrow keys and the Enter key.
    * The Up and Down keys can be used to navigate the list up and down and the
    * Enter, Right or Left - depending on the text direction - key can be used to
    * simulate a click on the currently selected button.
    * The Right or Left key - depending on the text direction - can be used to
@@ -1533,77 +1560,114 @@ var PanelView = class extends Associated
    *
    * @param {KeyEvent} event
    */
   keyNavigation(event) {
     if (!this.active) {
       return;
     }
 
-    let buttons = this._navigableElements;
-    if (!buttons.length) {
-      return;
-    }
-
     let stop = () => {
       event.stopPropagation();
       event.preventDefault();
     };
 
+    // If the focused element is only navigable with tab, it wants the arrow
+    // keys, etc. We shouldn't handle any keys except tab and shift+tab.
+    // We make a function for this for performance reasons: we only want to
+    // check this for keys we potentially care about, not *all* keys.
+    let tabOnly = () => {
+      // We use the real focus rather than this.selectedElement because focus
+      // might have been moved without keyboard navigation (e.g. mouse click)
+      // and this.selectedElement is only updated for keyboard navigation.
+      let focus = this.document.activeElement;
+      if (!focus) {
+        return false;
+      }
+      // Make sure the focus is actually inside the panel.
+      // (It might not be if the panel was opened with the mouse.)
+      // We use Node.compareDocumentPosition because Node.contains doesn't
+      // behave as expected for anonymous content; e.g. the input inside a
+      // textbox.
+      if (!(this.node.compareDocumentPosition(focus)
+            & Node.DOCUMENT_POSITION_CONTAINED_BY)) {
+        return false;
+      }
+      return this._isNavigableWithTabOnly(focus);
+    };
+
     let keyCode = event.code;
     switch (keyCode) {
       case "ArrowDown":
       case "ArrowUp":
+        if (tabOnly()) {
+          break;
+        }
+        // Fall-through...
       case "Tab": {
         stop();
         let isDown = (keyCode == "ArrowDown") ||
                      (keyCode == "Tab" && !event.shiftKey);
-        let button = this.moveSelection(isDown);
+        let button = this.moveSelection(isDown, keyCode != "Tab");
         button.focus();
         break;
       }
       case "Home":
+        if (tabOnly()) {
+          break;
+        }
         stop();
-        this.focusFirstNavigableElement();
+        this.focusFirstNavigableElement(true);
         break;
       case "End":
+        if (tabOnly()) {
+          break;
+        }
         stop();
-        this.focusLastNavigableElement();
+        this.focusLastNavigableElement(true);
         break;
       case "ArrowLeft":
       case "ArrowRight": {
+        if (tabOnly()) {
+          break;
+        }
         stop();
         if ((!this.window.RTL_UI && keyCode == "ArrowLeft") ||
             (this.window.RTL_UI && keyCode == "ArrowRight")) {
           this.node.panelMultiView.goBack();
           break;
         }
         // If the current button is _not_ one that points to a subview, pressing
         // the arrow key shouldn't do anything.
         let button = this.selectedElement;
         if (!button || !button.classList.contains("subviewbutton-nav")) {
           break;
         }
         // Fall-through...
       }
       case "Space":
       case "Enter": {
+        if (tabOnly()) {
+          break;
+        }
         let button = this.selectedElement;
         if (!button)
           break;
         stop();
 
+        this._doingKeyboardActivation = true;
         // Unfortunately, 'tabindex' doesn't execute the default action, so
         // we explicitly do this here.
         // We are sending a command event and then a click event.
         // This is done in order to mimic a "real" mouse click event.
         // The command event executes the action, then the click event closes the menu.
         button.doCommand();
         let clickEvent = new event.target.ownerGlobal.MouseEvent("click", {"bubbles": true});
         button.dispatchEvent(clickEvent);
+        this._doingKeyboardActivation = false;
         break;
       }
     }
   }
 
   /**
    * Focus the last selected element in the view, if any.
    */
@@ -1613,16 +1677,15 @@ var PanelView = class extends Associated
       selected.focus();
     }
   }
 
   /**
    * Clear all traces of keyboard navigation happening right now.
    */
   clearNavigation() {
-    delete this.__navigableElements;
     let selected = this.selectedElement;
     if (selected) {
       selected.blur();
       this.selectedElement = null;
     }
   }
 };
--- a/browser/components/customizableui/content/panelUI.inc.xul
+++ b/browser/components/customizableui/content/panelUI.inc.xul
@@ -4,17 +4,17 @@
 
 <panel id="widget-overflow"
        role="group"
        type="arrow"
        noautofocus="true"
        position="bottomcenter topright"
        photon="true"
        hidden="true">
-  <panelmultiview mainViewId="widget-overflow-mainView" disablekeynav="true">
+  <panelmultiview mainViewId="widget-overflow-mainView">
     <panelview id="widget-overflow-mainView"
                context="toolbar-context-menu">
       <vbox class="panel-subview-body">
         <vbox id="widget-overflow-list" class="widget-overflow-list"
               overflowfortoolbar="nav-bar"/>
         <toolbarseparator id="widget-overflow-fixed-separator" hidden="true"/>
         <vbox id="widget-overflow-fixed-list" class="widget-overflow-list" hidden="true" />
       </vbox>
--- a/browser/components/customizableui/test/browser.ini
+++ b/browser/components/customizableui/test/browser.ini
@@ -170,16 +170,17 @@ tags = fullscreen
 [browser_check_tooltips_in_navbar.js]
 [browser_editcontrols_update.js]
 subsuite = clipboard
 [browser_customization_context_menus.js]
 [browser_newtab_button_customizemode.js]
 [browser_open_from_popup.js]
 [browser_open_in_lazy_tab.js]
 [browser_PanelMultiView_focus.js]
+[browser_PanelMultiView_keyboard.js]
 [browser_reload_tab.js]
 [browser_sidebar_toggle.js]
 skip-if = verify
 [browser_tabbar_big_widgets.js]
 [browser_remote_tabs_button.js]
 skip-if = (verify && debug && (os == 'linux' || os == 'mac'))
 [browser_widget_animation.js]
 [browser_lwt_telemetry.js]
--- a/browser/components/customizableui/test/browser_PanelMultiView_focus.js
+++ b/browser/components/customizableui/test/browser_PanelMultiView_focus.js
@@ -6,41 +6,55 @@
 /**
  * Test the focus behavior when opening PanelViews.
  */
 
 const {PanelMultiView} = ChromeUtils.import("resource:///modules/PanelMultiView.jsm");
 
 let gAnchor;
 let gPanel;
+let gPanelMultiView;
 let gMainView;
 let gMainButton;
+let gMainSubButton;
+let gSubView;
+let gSubButton;
 
 add_task(async function setup() {
   let navBar = document.getElementById("nav-bar");
   gAnchor = document.createXULElement("toolbarbutton");
   // Must be focusable in order for key presses to work.
   gAnchor.style["-moz-user-focus"] = "normal";
   navBar.appendChild(gAnchor);
-  gPanel = document.createXULElement("panel");
-  navBar.appendChild(gPanel);
-  let panelMultiView = document.createXULElement("panelmultiview");
-  panelMultiView.setAttribute("mainViewId", "testMainView");
-  gPanel.appendChild(panelMultiView);
-  gMainView = document.createXULElement("panelview");
-  gMainView.id = "testMainView";
-  panelMultiView.appendChild(gMainView);
-  gMainButton = document.createXULElement("button");
-  gMainView.appendChild(gMainButton);
-
   let onPress = event => PanelMultiView.openPopup(gPanel, gAnchor, {
     triggerEvent: event,
   });
   gAnchor.addEventListener("keypress", onPress);
   gAnchor.addEventListener("click", onPress);
+  gPanel = document.createXULElement("panel");
+  navBar.appendChild(gPanel);
+  gPanelMultiView = document.createXULElement("panelmultiview");
+  gPanelMultiView.setAttribute("mainViewId", "testMainView");
+  gPanel.appendChild(gPanelMultiView);
+
+  gMainView = document.createXULElement("panelview");
+  gMainView.id = "testMainView";
+  gPanelMultiView.appendChild(gMainView);
+  gMainButton = document.createXULElement("button");
+  gMainView.appendChild(gMainButton);
+  gMainSubButton = document.createXULElement("button");
+  gMainView.appendChild(gMainSubButton);
+  gMainSubButton.addEventListener("command", () =>
+      gPanelMultiView.showSubView("testSubView", gMainSubButton));
+
+  gSubView = document.createXULElement("panelview");
+  gSubView.id = "testSubView";
+  gPanelMultiView.appendChild(gSubView);
+  gSubButton = document.createXULElement("button");
+  gSubView.appendChild(gSubButton);
 
   registerCleanupFunction(() => {
     gAnchor.remove();
     gPanel.remove();
   });
 });
 
 // Activate the main view by pressing a key. Focus should be moved inside.
@@ -59,8 +73,61 @@ add_task(async function testMainViewByKe
 add_task(async function testMainViewByClick() {
   await gCUITestUtils.openPanelMultiView(gPanel, gMainView,
     () => gAnchor.click());
   Assert.notEqual(document.activeElement, gMainButton,
     "Focus not on button in main view");
   await gCUITestUtils.hidePanelMultiView(gPanel,
     () => PanelMultiView.hidePopup(gPanel));
 });
+
+// Activate the subview by pressing a key. Focus should be moved to the first
+// button after the Back button.
+add_task(async function testSubViewByKeypress() {
+  await gCUITestUtils.openPanelMultiView(gPanel, gMainView,
+    () => gAnchor.click());
+  while (document.activeElement != gMainSubButton) {
+    EventUtils.synthesizeKey("KEY_Tab", {shiftKey: true});
+  }
+  let shown = BrowserTestUtils.waitForEvent(gSubView, "ViewShown");
+  EventUtils.synthesizeKey(" ");
+  await shown;
+  Assert.equal(document.activeElement, gSubButton,
+    "Focus on first button after Back button in subview");
+  await gCUITestUtils.hidePanelMultiView(gPanel,
+    () => PanelMultiView.hidePopup(gPanel));
+});
+
+// Activate the subview by clicking the mouse. Focus should not be moved
+// inside.
+add_task(async function testSubViewByClick() {
+  await gCUITestUtils.openPanelMultiView(gPanel, gMainView,
+    () => gAnchor.click());
+  let shown = BrowserTestUtils.waitForEvent(gSubView, "ViewShown");
+  gMainSubButton.click();
+  await shown;
+  let backButton = gSubView.querySelector(".subviewbutton-back");
+  Assert.notEqual(document.activeElement, backButton,
+    "Focus not on Back button in subview");
+  Assert.notEqual(document.activeElement, gSubButton,
+    "Focus not on button after Back button in subview");
+  await gCUITestUtils.hidePanelMultiView(gPanel,
+    () => PanelMultiView.hidePopup(gPanel));
+});
+
+// Test that focus is restored when going back to a previous view.
+add_task(async function testBackRestoresFocus() {
+  await gCUITestUtils.openPanelMultiView(gPanel, gMainView,
+    () => gAnchor.click());
+  while (document.activeElement != gMainSubButton) {
+    EventUtils.synthesizeKey("KEY_Tab", {shiftKey: true});
+  }
+  let shown = BrowserTestUtils.waitForEvent(gSubView, "ViewShown");
+  EventUtils.synthesizeKey(" ");
+  await shown;
+  shown = BrowserTestUtils.waitForEvent(gMainView, "ViewShown");
+  EventUtils.synthesizeKey("KEY_ArrowLeft");
+  await shown;
+  Assert.equal(document.activeElement, gMainSubButton,
+    "Focus on sub button in main view");
+  await gCUITestUtils.hidePanelMultiView(gPanel,
+    () => PanelMultiView.hidePopup(gPanel));
+});
new file mode 100644
--- /dev/null
+++ b/browser/components/customizableui/test/browser_PanelMultiView_keyboard.js
@@ -0,0 +1,256 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Test the keyboard behavior of PanelViews.
+ */
+
+const {PanelMultiView} = ChromeUtils.import("resource:///modules/PanelMultiView.jsm");
+
+let gAnchor;
+let gPanel;
+let gPanelMultiView;
+let gMainView;
+let gMainButton1;
+let gMainMenulist;
+let gMainTextbox;
+let gMainButton2;
+let gMainButton3;
+let gMainTabOrder;
+let gMainArrowOrder;
+let gSubView;
+let gSubButton;
+let gSubTextarea;
+
+async function openPopup() {
+  let shown = BrowserTestUtils.waitForEvent(gMainView, "ViewShown");
+  PanelMultiView.openPopup(gPanel, gAnchor, "bottomcenter topright");
+  await shown;
+}
+
+async function hidePopup() {
+  let hidden = BrowserTestUtils.waitForEvent(gPanel, "popuphidden");
+  PanelMultiView.hidePopup(gPanel);
+  await hidden;
+}
+
+async function showSubView() {
+  let shown = BrowserTestUtils.waitForEvent(gSubView, "ViewShown");
+  gPanelMultiView.showSubView(gSubView);
+  await shown;
+}
+
+async function expectFocusAfterKey(aKey, aFocus) {
+  let res = aKey.match(/^(Shift\+)?(.+)$/);
+  let shift = Boolean(res[1]);
+  let key;
+  if (res[2].length == 1) {
+    key = res[2]; // Character.
+  } else {
+    key = "KEY_" + res[2]; // Tab, ArrowRight, etc.
+  }
+  info("Waiting for focus on " + aFocus.id);
+  let focused = BrowserTestUtils.waitForEvent(aFocus, "focus");
+  EventUtils.synthesizeKey(key, {shiftKey: shift});
+  await focused;
+  ok(true, aFocus.id + " focused after " + aKey + " pressed");
+}
+
+add_task(async function setup() {
+  let navBar = document.getElementById("nav-bar");
+  gAnchor = document.createXULElement("toolbarbutton");
+  navBar.appendChild(gAnchor);
+  gPanel = document.createXULElement("panel");
+  navBar.appendChild(gPanel);
+  gPanelMultiView = document.createXULElement("panelmultiview");
+  gPanelMultiView.setAttribute("mainViewId", "testMainView");
+  gPanel.appendChild(gPanelMultiView);
+
+  gMainView = document.createXULElement("panelview");
+  gMainView.id = "testMainView";
+  gPanelMultiView.appendChild(gMainView);
+  gMainButton1 = document.createXULElement("button");
+  gMainButton1.id = "gMainButton1";
+  gMainView.appendChild(gMainButton1);
+  gMainMenulist = document.createXULElement("menulist");
+  gMainMenulist.id = "gMainMenulist";
+  gMainView.appendChild(gMainMenulist);
+  let menuPopup = document.createXULElement("menupopup");
+  gMainMenulist.appendChild(menuPopup);
+  let item = document.createXULElement("menuitem");
+  item.setAttribute("value", "1");
+  item.setAttribute("selected", "true");
+  menuPopup.appendChild(item);
+  item = document.createXULElement("menuitem");
+  item.setAttribute("value", "2");
+  menuPopup.appendChild(item);
+  gMainTextbox = document.createXULElement("textbox");
+  gMainTextbox.id = "gMainTextbox";
+  gMainView.appendChild(gMainTextbox);
+  gMainTextbox.setAttribute("value", "value");
+  gMainButton2 = document.createXULElement("button");
+  gMainButton2.id = "gMainButton2";
+  gMainView.appendChild(gMainButton2);
+  gMainButton3 = document.createXULElement("button");
+  gMainButton3.id = "gMainButton3";
+  gMainView.appendChild(gMainButton3);
+  gMainTabOrder = [gMainButton1, gMainMenulist, gMainTextbox, gMainButton2,
+                   gMainButton3];
+  gMainArrowOrder = [gMainButton1, gMainButton2, gMainButton3];
+
+  gSubView = document.createXULElement("panelview");
+  gSubView.id = "testSubView";
+  gPanelMultiView.appendChild(gSubView);
+  gSubButton = document.createXULElement("button");
+  gSubView.appendChild(gSubButton);
+  gSubTextarea = document.createElementNS("http://www.w3.org/1999/xhtml",
+                                          "textarea");
+  gSubTextarea.id = "gSubTextarea";
+  gSubView.appendChild(gSubTextarea);
+  gSubTextarea.value = "value";
+
+  registerCleanupFunction(() => {
+    gAnchor.remove();
+    gPanel.remove();
+  });
+});
+
+// Test that the tab key focuses all expected controls.
+add_task(async function testTab() {
+  await openPopup();
+  for (let elem of gMainTabOrder) {
+    await expectFocusAfterKey("Tab", elem);
+  }
+  // Wrap around.
+  await expectFocusAfterKey("Tab", gMainTabOrder[0]);
+  await hidePopup();
+});
+
+// Test that the shift+tab key focuses all expected controls.
+add_task(async function testShiftTab() {
+  await openPopup();
+  for (let i = gMainTabOrder.length - 1; i >= 0; --i) {
+    await expectFocusAfterKey("Shift+Tab", gMainTabOrder[i]);
+  }
+  // Wrap around.
+  await expectFocusAfterKey("Shift+Tab",
+                            gMainTabOrder[gMainTabOrder.length - 1]);
+  await hidePopup();
+});
+
+// Test that the down arrow key skips menulists and textboxes.
+add_task(async function testDownArrow() {
+  await openPopup();
+  for (let elem of gMainArrowOrder) {
+    await expectFocusAfterKey("ArrowDown", elem);
+  }
+  // Wrap around.
+  await expectFocusAfterKey("ArrowDown", gMainArrowOrder[0]);
+  await hidePopup();
+});
+
+// Test that the up arrow key skips menulists and textboxes.
+add_task(async function testUpArrow() {
+  await openPopup();
+  for (let i = gMainArrowOrder.length - 1; i >= 0; --i) {
+    await expectFocusAfterKey("ArrowUp", gMainArrowOrder[i]);
+  }
+  // Wrap around.
+  await expectFocusAfterKey("ArrowUp",
+                            gMainArrowOrder[gMainArrowOrder.length - 1]);
+  await hidePopup();
+});
+
+// Test that the home/end keys move to the first/last controls.
+add_task(async function testHomeEnd() {
+  await openPopup();
+  await expectFocusAfterKey("Home", gMainArrowOrder[0]);
+  await expectFocusAfterKey("End",
+                            gMainArrowOrder[gMainArrowOrder.length - 1]);
+  await hidePopup();
+});
+
+// Test that the up/down arrow keys work as expected in menulists.
+add_task(async function testArrowsMenulist() {
+  await openPopup();
+  gMainMenulist.focus();
+  is(document.activeElement, gMainMenulist, "menulist focused");
+  is(gMainMenulist.value, "1", "menulist initial value 1");
+  if (AppConstants.platform == "macosx") {
+    // On Mac, down/up arrows just open the menulist.
+    let popup = gMainMenulist.menupopup;
+    for (let key of ["ArrowDown", "ArrowUp"]) {
+      let shown = BrowserTestUtils.waitForEvent(popup, "popupshown");
+      EventUtils.synthesizeKey("KEY_" + key);
+      await shown;
+      ok(gMainMenulist.open, "menulist open after " + key);
+      let hidden = BrowserTestUtils.waitForEvent(popup, "popuphidden");
+      EventUtils.synthesizeKey("KEY_Escape");
+      await hidden;
+      ok(!gMainMenulist.open, "menulist closed after Escape");
+    }
+  } else {
+    // On other platforms, down/up arrows change the value without opening the
+    // menulist.
+    EventUtils.synthesizeKey("KEY_ArrowDown");
+    is(document.activeElement, gMainMenulist,
+       "menulist still focused after ArrowDown");
+    is(gMainMenulist.value, "2", "menulist value 2 after ArrowDown");
+    EventUtils.synthesizeKey("KEY_ArrowUp");
+    is(document.activeElement, gMainMenulist,
+       "menulist still focused after ArrowUp");
+    is(gMainMenulist.value, "1", "menulist value 1 after ArrowUp");
+  }
+  await hidePopup();
+});
+
+// Test that pressing space in a textbox inserts a space (instead of trying to
+// activate the control).
+add_task(async function testSpaceTextbox() {
+  await openPopup();
+  gMainTextbox.focus();
+  EventUtils.synthesizeKey("KEY_Home");
+  EventUtils.synthesizeKey(" ");
+  is(gMainTextbox.value, " value", "Space typed into textbox");
+  gMainTextbox.value = "value";
+  await hidePopup();
+});
+
+// Tests that the left arrow key normally moves back to the previous view.
+add_task(async function testLeftArrow() {
+  await openPopup();
+  await showSubView();
+  let shown = BrowserTestUtils.waitForEvent(gMainView, "ViewShown");
+  EventUtils.synthesizeKey("KEY_ArrowLeft");
+  await shown;
+  ok("Moved to previous view after ArrowLeft");
+  await hidePopup();
+});
+
+// Tests that the left arrow key moves the caret in a textarea in a subview
+// (instead of going back to the previous view).
+add_task(async function testLeftArrowTextarea() {
+  await openPopup();
+  await showSubView();
+  gSubTextarea.focus();
+  is(document.activeElement, gSubTextarea, "textarea focused");
+  EventUtils.synthesizeKey("KEY_End");
+  is(gSubTextarea.selectionStart, 5, "selectionStart 5 after End");
+  EventUtils.synthesizeKey("KEY_ArrowLeft");
+  is(gSubTextarea.selectionStart, 4, "selectionStart 4 after ArrowLeft");
+  is(document.activeElement, gSubTextarea, "textarea still focused");
+  await hidePopup();
+});
+
+// Test navigation to a button which is initially disabled and later enabled.
+add_task(async function testDynamicButton() {
+  gMainButton2.disabled = true;
+  await openPopup();
+  await expectFocusAfterKey("ArrowDown", gMainButton1);
+  await expectFocusAfterKey("ArrowDown", gMainButton3);
+  gMainButton2.disabled = false;
+  await expectFocusAfterKey("ArrowUp", gMainButton2);
+  await hidePopup();
+});
--- a/browser/components/customizableui/test/browser_panel_keyboard_navigation.js
+++ b/browser/components/customizableui/test/browser_panel_keyboard_navigation.js
@@ -86,23 +86,30 @@ add_task(async function testEnterKeyBeha
   EventUtils.synthesizeKey("KEY_Enter");
   await promise;
 
   let helpButtons = getEnabledNavigableElementsForView(PanelUI.helpView);
   Assert.ok(helpButtons[0].classList.contains("subviewbutton-back"),
     "First button in help view should be a back button");
 
   // For posterity, check navigating the subview using up/ down arrow keys as well.
+  // When opening a subview, the first control *after* the Back button gets
+  // focus.
+  EventUtils.synthesizeKey("KEY_ArrowUp");
+  focusedElement = document.commandDispatcher.focusedElement;
+  Assert.equal(focusedElement, helpButtons[0],
+    "The Back button should be focused after navigating upward");
   for (let i = helpButtons.length - 1; i >= 0; --i) {
     let button = helpButtons[i];
     if (button.disabled)
       continue;
     EventUtils.synthesizeKey("KEY_ArrowUp");
     focusedElement = document.commandDispatcher.focusedElement;
-    Assert.equal(focusedElement, button, "The first button should be focused after navigating upward");
+    Assert.equal(focusedElement, button,
+      "The previous button should be focused after navigating upward");
   }
 
   // Make sure the back button is in focus again.
   while (focusedElement != helpButtons[0]) {
     EventUtils.synthesizeKey("KEY_ArrowDown");
     focusedElement = document.commandDispatcher.focusedElement;
   }
 
--- a/browser/components/downloads/content/allDownloadsView.js
+++ b/browser/components/downloads/content/allDownloadsView.js
@@ -243,17 +243,17 @@ DownloadsPlacesView.prototype = {
       if (!this._richlistbox.firstChild) {
         return;
       }
 
       let rlbRect = this._richlistbox.getBoundingClientRect();
       let winUtils = window.windowUtils;
       let nodes = winUtils.nodesFromRect(rlbRect.left, rlbRect.top,
                                          0, rlbRect.width, rlbRect.height, 0,
-                                         true, false);
+                                         true, false, false);
       // nodesFromRect returns nodes in z-index order, and for the same z-index
       // sorts them in inverted DOM order, thus starting from the one that would
       // be on top.
       let firstVisibleNode, lastVisibleNode;
       for (let node of nodes) {
         if (node.localName === "richlistitem" && node._shell) {
           node._shell.ensureActive();
           // The first visible node is the last match.
--- a/dom/base/DocumentOrShadowRoot.cpp
+++ b/dom/base/DocumentOrShadowRoot.cpp
@@ -357,16 +357,17 @@ Element* DocumentOrShadowRoot::ElementFr
   return elements.SafeElementAt(0);
 }
 
 void DocumentOrShadowRoot::NodesFromRect(float aX, float aY, float aTopSize,
                                          float aRightSize, float aBottomSize,
                                          float aLeftSize,
                                          bool aIgnoreRootScrollFrame,
                                          bool aFlushLayout,
+                                         bool aOnlyVisible,
                                          nsTArray<RefPtr<nsINode>>& aReturn) {
   // Following the same behavior of elementFromPoint,
   // we don't return anything if either coord is negative
   if (!aIgnoreRootScrollFrame && (aX < 0 || aY < 0)) {
     return;
   }
 
   nscoord x = nsPresContext::CSSPixelsToAppUnits(aX - aLeftSize);
@@ -375,16 +376,19 @@ void DocumentOrShadowRoot::NodesFromRect
   nscoord h = nsPresContext::CSSPixelsToAppUnits(aTopSize + aBottomSize) + 1;
 
   nsRect rect(x, y, w, h);
 
   EnumSet<FrameForPointOption> options;
   if (aIgnoreRootScrollFrame) {
     options += FrameForPointOption::IgnoreRootScrollFrame;
   }
+  if (aOnlyVisible) {
+    options += FrameForPointOption::OnlyVisible;
+  }
 
   auto flush = aFlushLayout ? FlushLayout::Yes : FlushLayout::No;
   QueryNodesFromRect(*this, rect, options, flush, Multiple::Yes, aReturn);
 }
 
 Element* DocumentOrShadowRoot::AddIDTargetObserver(nsAtom* aID,
                                                    IDTargetObserver aObserver,
                                                    void* aData,
--- a/dom/base/DocumentOrShadowRoot.h
+++ b/dom/base/DocumentOrShadowRoot.h
@@ -115,17 +115,17 @@ class DocumentOrShadowRoot {
    */
   Element* ElementFromPointHelper(float aX, float aY,
                                   bool aIgnoreRootScrollFrame,
                                   bool aFlushLayout);
 
   void NodesFromRect(float aX, float aY, float aTopSize, float aRightSize,
                      float aBottomSize, float aLeftSize,
                      bool aIgnoreRootScrollFrame, bool aFlushLayout,
-                     nsTArray<RefPtr<nsINode>>&);
+                     bool aOnlyVisible, nsTArray<RefPtr<nsINode>>&);
 
   /**
    * This gets fired when the element that an id refers to changes.
    * This fires at difficult times. It is generally not safe to do anything
    * which could modify the DOM in any way. Use
    * nsContentUtils::AddScriptRunner.
    * @return true to keep the callback in the callback set, false
    * to remove it.
--- a/dom/base/nsDOMWindowUtils.cpp
+++ b/dom/base/nsDOMWindowUtils.cpp
@@ -1150,27 +1150,29 @@ nsDOMWindowUtils::ElementFromPoint(float
   el.forget(aReturn);
   return NS_OK;
 }
 
 NS_IMETHODIMP
 nsDOMWindowUtils::NodesFromRect(float aX, float aY, float aTopSize,
                                 float aRightSize, float aBottomSize,
                                 float aLeftSize, bool aIgnoreRootScrollFrame,
-                                bool aFlushLayout, nsINodeList** aReturn) {
+                                bool aFlushLayout, bool aOnlyVisible,
+                                nsINodeList** aReturn) {
   nsCOMPtr<Document> doc = GetDocument();
   NS_ENSURE_STATE(doc);
 
   nsSimpleContentList* list = new nsSimpleContentList(doc);
   NS_ADDREF(list);
   *aReturn = list;
 
   AutoTArray<RefPtr<nsINode>, 8> nodes;
   doc->NodesFromRect(aX, aY, aTopSize, aRightSize, aBottomSize, aLeftSize,
-                     aIgnoreRootScrollFrame, aFlushLayout, nodes);
+                     aIgnoreRootScrollFrame, aFlushLayout, aOnlyVisible,
+                     nodes);
   list->SetCapacity(nodes.Length());
   for (auto& node : nodes) {
     list->AppendElement(node->AsContent());
   }
   return NS_OK;
 }
 
 NS_IMETHODIMP
--- a/dom/html/HTMLMediaElement.cpp
+++ b/dom/html/HTMLMediaElement.cpp
@@ -4092,29 +4092,17 @@ void HTMLMediaElement::AfterMaybeChangeA
 nsresult HTMLMediaElement::BindToTree(Document* aDocument, nsIContent* aParent,
                                       nsIContent* aBindingParent) {
   nsresult rv =
       nsGenericHTMLElement::BindToTree(aDocument, aParent, aBindingParent);
 
   if (IsInComposedDoc()) {
     // Construct Shadow Root so web content can be hidden in the DOM.
     AttachAndSetUAShadowRoot();
-#ifdef ANDROID
     NotifyUAWidgetSetupOrChange();
-#else
-    // We don't want to call into JS if the website never asks for native
-    // video controls.
-    // If controls attribute is set later, controls is constructed lazily
-    // with the UAWidgetAttributeChanged event.
-    // This only applies to Desktop because on Fennec we would need to show
-    // an UI if the video is blocked.
-    if (Controls()) {
-      NotifyUAWidgetSetupOrChange();
-    }
-#endif
   }
 
   mUnboundFromTree = false;
 
   if (aDocument) {
     // The preload action depends on the value of the autoplay attribute.
     // It's value may have changed, so update it.
     UpdatePreloadAction();
--- a/dom/html/HTMLVideoElement.cpp
+++ b/dom/html/HTMLVideoElement.cpp
@@ -543,10 +543,20 @@ void HTMLVideoElement::EndCloningVisuall
   Unused << mVisualCloneTarget->SetVisualCloneSource(nullptr);
   Unused << SetVisualCloneTarget(nullptr);
 
   if (IsInComposedDoc() && !sCloneElementVisuallyTesting) {
     NotifyUAWidgetSetupOrChange();
   }
 }
 
+void HTMLVideoElement::TogglePictureInPicture(ErrorResult& error) {
+  // The MozTogglePictureInPicture event is listen for via the
+  // PictureInPictureChild actor, which is responsible for opening the new
+  // window and starting the visual clone.
+  nsresult rv = DispatchEvent(NS_LITERAL_STRING("MozTogglePictureInPicture"));
+  if (NS_FAILED(rv)) {
+    error.Throw(rv);
+  }
+}
+
 }  // namespace dom
 }  // namespace mozilla
--- a/dom/html/HTMLVideoElement.h
+++ b/dom/html/HTMLVideoElement.h
@@ -140,16 +140,18 @@ class HTMLVideoElement final : public HT
   void SetMozIsOrientationLocked(bool aLock) { mIsOrientationLocked = aLock; }
 
   void CloneElementVisually(HTMLVideoElement& aTarget, ErrorResult& rv);
 
   void StopCloningElementVisually();
 
   bool IsCloningElementVisually() const { return !!mVisualCloneTarget; }
 
+  void TogglePictureInPicture(ErrorResult& rv);
+
  protected:
   virtual ~HTMLVideoElement();
 
   virtual JSObject* WrapNode(JSContext* aCx,
                              JS::Handle<JSObject*> aGivenProto) override;
 
   /**
    * We create video wakelock when the video is playing and release it when
--- a/dom/interfaces/base/nsIDOMWindowUtils.idl
+++ b/dom/interfaces/base/nsIDOMWindowUtils.idl
@@ -741,25 +741,28 @@ interface nsIDOMWindowUtils : nsISupport
    * @param aTopSize How much to expand up the rectangle
    * @param aRightSize How much to expand right the rectangle
    * @param aBottomSize How much to expand down the rectangle
    * @param aLeftSize How much to expand left the rectangle
    * @param aIgnoreRootScrollFrame whether or not to ignore the root scroll
    *        frame when retrieving the element. If false, this method returns
    *        null for coordinates outside of the viewport.
    * @param aFlushLayout flushes layout if true. Otherwise, no flush occurs.
+   * @param aOnlyVisible Set to true if you only want nodes that pass a visibility
+   *        hit test.
    */
   NodeList nodesFromRect(in float aX,
                          in float aY,
                          in float aTopSize,
                          in float aRightSize,
                          in float aBottomSize,
                          in float aLeftSize,
                          in boolean aIgnoreRootScrollFrame,
-                         in boolean aFlushLayout);
+                         in boolean aFlushLayout,
+                         in boolean aOnlyVisible);
 
 
   /**
    * Get a list of nodes that have meaningful textual content to
    * be translated. The implementation of this algorithm is in flux
    * as we experiment and refine which approach works best.
    *
    * This method requires chrome privileges.
--- a/dom/media/test/test_cloneElementVisually_no_suspend.html
+++ b/dom/media/test/test_cloneElementVisually_no_suspend.html
@@ -51,26 +51,33 @@ add_task(async () => {
     await waitForEventOnce(originalVideo, "seeked");
 
     let suspendTimerFired = false;
 
     let listener = () => {
       suspendTimerFired = true;
     }
     originalVideo.addEventListener("mozstartvideosuspendtimer", listener);
-    originalVideo.setVisible(false);
+
+    // Have to do this to access normally-preffed off binding methods for some
+    // reason.
+    // See bug 1544257.
+    SpecialPowers.wrap(originalVideo).setVisible(false);
 
     await waitForEventOnce(originalVideo, "ended");
 
     originalVideo.removeEventListener("mozstartvideosuspendtimer", listener);
 
     ok(!suspendTimerFired,
        "mozstartvideosuspendtimer should not have fired.");
 
-    originalVideo.setVisible(true);
+    // Have to do this to access normally-preffed off binding methods for some
+    // reason.
+    // See bug 1544257.
+    SpecialPowers.wrap(originalVideo).setVisible(true);
   });
 
   await originalVideo.play();
 
   // With the clone gone, the original video should be able to suspend now.
   await ensureVideoSuspendable(originalVideo);
 
   await setVideoSrc(originalVideo, TEST_VIDEO_1);
--- a/dom/media/tests/mochitest/test_setSinkId.html
+++ b/dom/media/tests/mochitest/test_setSinkId.html
@@ -25,16 +25,21 @@
       return;
     }
 
     const allDevices = await navigator.mediaDevices.enumerateDevices();
     const audioDevices = allDevices.filter(({kind}) => kind == 'audiooutput');
     info(`Found  ${audioDevices.length} output devices`);
     ok(audioDevices.length > 0, "More than one output device found");
 
+    // Have to do this to access normally-preffed off binding methods for some
+    // reason.
+    // See bug 1544257.
+    audio = SpecialPowers.wrap(audio);
+
     is(audio.sinkId, "", "Initial value is empty string");
 
     const p = audio.setSinkId(audioDevices[0].deviceId);
     is(audio.sinkId, "", "Value is unchanged upon function return");
     is(await p, undefined, "promise resolves with undefined");
     is(audio.sinkId, audioDevices[0].deviceId, `Sink device is set, id: ${audio.sinkId}`);
 
     await audio.setSinkId(audioDevices[0].deviceId);
--- a/dom/tests/mochitest/chrome/489127.html
+++ b/dom/tests/mochitest/chrome/489127.html
@@ -8,30 +8,18 @@
 
   var SimpleTest = window.opener.SimpleTest;
   function ok() { window.opener.ok.apply(window.opener, arguments); }
   function done() { window.opener.done.apply(window.opener, arguments); }
   let e = {};
 
   let dwu = window.windowUtils;
 
-  /*
-    NodeList nodesFromRect(in float aX,
-                           in float aY,
-                           in float aTopSize, 
-                           in float aRightSize,
-                           in float aBottomSize,
-                           in float aLeftSize,
-                           in boolean aIgnoreRootScrollFrame,
-                           in boolean aFlushLayout);
-
-  */
-
   function check(x, y, top, right, bottom, left, list) {
-    let nodes = dwu.nodesFromRect(x, y, top, right, bottom, left, true, false);
+    let nodes = dwu.nodesFromRect(x, y, top, right, bottom, left, true, false, false);
     
     list.push(e.body);
     list.push(e.html);
 
     if (nodes.length != list.length) {
       ok(false, "Different number of nodes for rect" +
                 "[" + x + "," + y + "], " + 
                 "[" + top + "," + right + "," + bottom + "," + left + "]");
--- a/dom/webidl/HTMLVideoElement.webidl
+++ b/dom/webidl/HTMLVideoElement.webidl
@@ -64,15 +64,21 @@ partial interface HTMLVideoElement {
   // wasn't cloning in the first place.
   [Func="IsChromeOrXBLOrUAWidget"]
     void stopCloningElementVisually();
 
   // Returns true if the <video> is being cloned visually to another
   // <video> element (see cloneElementVisually).
   [Func="IsChromeOrXBLOrUAWidget"]
     readonly attribute boolean isCloningElementVisually;
+
+  // Fires the privileged MozTogglePictureInPicture event to enter
+  // Picture-in-Picture. Call this when triggering Picture-in-Picture
+  // from the video controls UAWidget.
+  [Throws, Func="IsChromeOrXBLOrUAWidget"]
+    void togglePictureInPicture();
 };
 
 // https://dvcs.w3.org/hg/html-media/raw-file/default/media-source/media-source.html#idl-def-HTMLVideoElement
 partial interface HTMLVideoElement {
   [Func="mozilla::dom::MediaSource::Enabled", NewObject]
   VideoPlaybackQuality getVideoPlaybackQuality();
 };
--- a/layout/generic/TextOverflow.cpp
+++ b/layout/generic/TextOverflow.cpp
@@ -213,17 +213,17 @@ static void PaintTextShadowCallback(gfxC
                                     const nscolor& aShadowColor, void* aData) {
   reinterpret_cast<nsDisplayTextOverflowMarker*>(aData)->PaintTextToContext(
       aCtx, aShadowOffset);
 }
 
 void nsDisplayTextOverflowMarker::Paint(nsDisplayListBuilder* aBuilder,
                                         gfxContext* aCtx) {
   DrawTargetAutoDisableSubpixelAntialiasing disable(aCtx->GetDrawTarget(),
-                                                    mDisableSubpixelAA);
+                                                    IsSubpixelAADisabled());
 
   nscolor foregroundColor =
       nsLayoutUtils::GetColor(mFrame, &nsStyleText::mWebkitTextFillColor);
 
   // Paint the text-shadows for the overflow marker
   nsLayoutUtils::PaintTextShadow(mFrame, aCtx, mRect, GetPaintRect(),
                                  foregroundColor, PaintTextShadowCallback,
                                  (void*)this);
--- a/layout/generic/ViewportFrame.cpp
+++ b/layout/generic/ViewportFrame.cpp
@@ -101,18 +101,21 @@ static void BuildDisplayListForTopLayerF
     // root scroll frame.
     clipState.SetClipChainForContainingBlockDescendants(
         savedOutOfFlowData->mCombinedClipChain);
     clipState.ClipContainingBlockDescendantsExtra(
         visible + aBuilder->ToReferenceFrame(aFrame), nullptr);
     asrSetter.SetCurrentActiveScrolledRoot(
         savedOutOfFlowData->mContainingBlockActiveScrolledRoot);
   }
+  // This function jumps into random frames that may not be descendants of
+  // aBuilder->mCurrentFrame, so aBuilder->mInInvalidSubtree is unrelated.
+  // Request recalculation of mInInvalidSubtree.
   nsDisplayListBuilder::AutoBuildingDisplayList buildingForChild(
-      aBuilder, aFrame, visible, dirty);
+      aBuilder, aFrame, visible, dirty, nsDisplayListBuilder::RIIS_YES);
 
   nsDisplayList list;
   aFrame->BuildDisplayListForStackingContext(aBuilder, &list);
   aList->AppendToTop(&list);
 }
 
 void ViewportFrame::BuildDisplayListForTopLayer(nsDisplayListBuilder* aBuilder,
                                                 nsDisplayList* aList) {
@@ -148,18 +151,20 @@ void ViewportFrame::BuildDisplayListForT
               frame->GetChildList(kBackdropList).FirstChild()) {
         MOZ_ASSERT(backdropPh->IsPlaceholderFrame());
         MOZ_ASSERT(!backdropPh->GetNextSibling(), "more than one ::backdrop?");
         MOZ_ASSERT(backdropPh->HasAnyStateBits(NS_FRAME_FIRST_REFLOW),
                    "did you intend to reflow ::backdrop placeholders?");
         nsIFrame* backdropFrame =
             static_cast<nsPlaceholderFrame*>(backdropPh)->GetOutOfFlowFrame();
         MOZ_ASSERT(backdropFrame);
+
         BuildDisplayListForTopLayerFrame(aBuilder, backdropFrame, aList);
       }
+
       BuildDisplayListForTopLayerFrame(aBuilder, frame, aList);
     }
   }
 
   if (nsCanvasFrame* canvasFrame = PresShell()->GetCanvasFrame()) {
     if (Element* container = canvasFrame->GetCustomContentContainer()) {
       if (nsIFrame* frame = container->GetPrimaryFrame()) {
         MOZ_ASSERT(frame->StyleDisplay()->mTopLayer != NS_STYLE_TOP_LAYER_NONE,
--- a/layout/generic/nsBulletFrame.cpp
+++ b/layout/generic/nsBulletFrame.cpp
@@ -641,17 +641,17 @@ bool nsDisplayBullet::CreateWebRenderCom
 
 void nsDisplayBullet::Paint(nsDisplayListBuilder* aBuilder, gfxContext* aCtx) {
   uint32_t flags = imgIContainer::FLAG_NONE;
   if (aBuilder->ShouldSyncDecodeImages()) {
     flags |= imgIContainer::FLAG_SYNC_DECODE;
   }
 
   ImgDrawResult result = static_cast<nsBulletFrame*>(mFrame)->PaintBullet(
-      *aCtx, ToReferenceFrame(), GetPaintRect(), flags, mDisableSubpixelAA);
+      *aCtx, ToReferenceFrame(), GetPaintRect(), flags, IsSubpixelAADisabled());
 
   nsDisplayBulletGeometry::UpdateDrawResult(this, result);
 }
 
 void nsBulletFrame::BuildDisplayList(nsDisplayListBuilder* aBuilder,
                                      const nsDisplayListSet& aLists) {
   if (!IsVisibleForPainting()) return;
 
--- a/layout/generic/nsFrame.cpp
+++ b/layout/generic/nsFrame.cpp
@@ -3802,18 +3802,25 @@ void nsIFrame::BuildDisplayListForChild(
        IsSVGContentWithCSSClip(child))) {
     pseudoStackingContext = true;
     awayFromCommonPath = true;
   }
 
   NS_ASSERTION(!isStackingContext || pseudoStackingContext,
                "Stacking contexts must also be pseudo-stacking-contexts");
 
+  // nsBlockFrame paints pushed floats directly, rather than through their
+  // placeholder, which is why we force a recallculation of InInvalidSubtree
+  // state.
+  auto recalcInInvalidSubtree =
+      (child->GetStateBits() & NS_FRAME_IS_PUSHED_FLOAT)
+          ? nsDisplayListBuilder::RIIS_YES
+          : nsDisplayListBuilder::RIIS_NO;
   nsDisplayListBuilder::AutoBuildingDisplayList buildingForChild(
-      aBuilder, child, visible, dirty);
+      aBuilder, child, visible, dirty, recalcInInvalidSubtree);
   DisplayListClipState::AutoClipMultiple clipState(aBuilder);
   nsDisplayListBuilder::AutoCurrentActiveScrolledRootSetter asrSetter(aBuilder);
   CheckForApzAwareEventHandlers(aBuilder, child);
 
   if (savedOutOfFlowData) {
     aBuilder->SetBuildingInvisibleItems(false);
 
     clipState.SetClipChainForContainingBlockDescendants(
--- a/layout/generic/nsGfxScrollFrame.cpp
+++ b/layout/generic/nsGfxScrollFrame.cpp
@@ -3759,16 +3759,22 @@ void ScrollFrameHelper::BuildDisplayList
 
 void ScrollFrameHelper::MaybeAddTopLayerItems(nsDisplayListBuilder* aBuilder,
                                               const nsDisplayListSet& aLists) {
   if (mIsRoot) {
     if (ViewportFrame* viewportFrame = do_QueryFrame(mOuter->GetParent())) {
       nsDisplayList topLayerList;
       viewportFrame->BuildDisplayListForTopLayer(aBuilder, &topLayerList);
       if (!topLayerList.IsEmpty()) {
+        // This function jumps into random frames that may not be descendants of
+        // aBuilder->mCurrentFrame, so aBuilder->mInInvalidSubtree is unrelated.
+        // Request recalculation of mInInvalidSubtree.
+        nsDisplayListBuilder::AutoBuildingDisplayList buildingDisplayList(
+            aBuilder, viewportFrame, nsDisplayListBuilder::RIIS_YES);
+
         // Wrap the whole top layer in a single item with maximum z-index,
         // and append it at the very end, so that it stays at the topmost.
         nsDisplayWrapList* wrapList = MakeDisplayItem<nsDisplayWrapList>(
             aBuilder, viewportFrame, &topLayerList,
             aBuilder->CurrentActiveScrolledRoot(), false, 2);
         if (wrapList) {
           wrapList->SetOverrideZIndex(
               std::numeric_limits<decltype(wrapList->ZIndex())>::max());
--- a/layout/generic/nsPageFrame.cpp
+++ b/layout/generic/nsPageFrame.cpp
@@ -452,17 +452,17 @@ class nsDisplayHeaderFooter final : publ
 
   virtual void Paint(nsDisplayListBuilder* aBuilder,
                      gfxContext* aCtx) override {
 #ifdef DEBUG
     nsPageFrame* pageFrame = do_QueryFrame(mFrame);
     MOZ_ASSERT(pageFrame, "We should have an nsPageFrame");
 #endif
     static_cast<nsPageFrame*>(mFrame)->PaintHeaderFooter(
-        *aCtx, ToReferenceFrame(), mDisableSubpixelAA);
+        *aCtx, ToReferenceFrame(), IsSubpixelAADisabled());
   }
   NS_DISPLAY_DECL_NAME("HeaderFooter", TYPE_HEADER_FOOTER)
 
   virtual nsRect GetComponentAlphaBounds(
       nsDisplayListBuilder* aBuilder) const override {
     bool snap;
     return GetBounds(aBuilder, &snap);
   }
@@ -519,18 +519,22 @@ void nsPageFrame::BuildDisplayList(nsDis
     // we don't have to process earlier pages. The display lists for
     // these extra pages are pruned so that only display items for the
     // page we currently care about (which we would have reached by
     // following placeholders to their out-of-flows) end up on the list.
     nsIFrame* page = child;
     while ((page = GetNextPage(page)) != nullptr) {
       nsRect childVisible = visibleRect + child->GetOffsetTo(page);
 
+      // This function jumps into random frames that may not be descendants of
+      // aBuilder->mCurrentFrame, so aBuilder->mInInvalidSubtree is unrelated.
+      // Request recalculation of mInInvalidSubtree.
       nsDisplayListBuilder::AutoBuildingDisplayList buildingForChild(
-          aBuilder, page, childVisible, childVisible);
+          aBuilder, page, childVisible, childVisible,
+          nsDisplayListBuilder::RIIS_YES);
       BuildDisplayListForExtraPage(aBuilder, this, page, &content);
     }
 
     // Invoke AutoBuildingDisplayList to ensure that the correct visibleRect
     // is used to compute the visible rect if AddCanvasBackgroundColorItem
     // creates a display item.
     nsDisplayListBuilder::AutoBuildingDisplayList building(
         aBuilder, child, visibleRect, visibleRect);
--- a/layout/generic/nsSubDocumentFrame.cpp
+++ b/layout/generic/nsSubDocumentFrame.cpp
@@ -430,24 +430,25 @@ void nsSubDocumentFrame::BuildDisplayLis
   bool constructZoomItem = subdocRootFrame && parentAPD != subdocAPD;
   bool needsOwnLayer = false;
   if (constructResolutionItem || constructZoomItem || haveDisplayPort ||
       presContext->IsRootContentDocument() ||
       (sf && sf->IsScrollingActive(aBuilder))) {
     needsOwnLayer = true;
   }
 
-  if (aBuilder->IsRetainingDisplayList()) {
+  if (subdocRootFrame && aBuilder->IsRetainingDisplayList()) {
     // Caret frame changed, rebuild the entire subdoc.
     // We could just invalidate the old and new frame
     // areas and save some work here. RetainedDisplayListBuilder
     // does this, so we could teach it to find and check all
     // subdocs in advance.
     if (mPreviousCaret != aBuilder->GetCaretFrame()) {
       dirty = visible;
+      aBuilder->MarkFrameModifiedDuringBuilding(subdocRootFrame);
       aBuilder->RebuildAllItemsInCurrentSubtree();
       // Mark the old caret frame as invalid so that we remove the
       // old nsDisplayCaret. We don't mark the current frame as invalid
       // since we want the nsDisplaySubdocument to retain it's place
       // in the retained display list.
       if (mPreviousCaret) {
         aBuilder->MarkFrameModifiedDuringBuilding(mPreviousCaret);
       }
--- a/layout/ipc/RenderFrame.cpp
+++ b/layout/ipc/RenderFrame.cpp
@@ -190,21 +190,16 @@ mozilla::LayerState nsDisplayRemote::Get
     nsDisplayListBuilder* aBuilder, LayerManager* aManager,
     const ContainerLayerParameters& aParameters) {
   if (IsTempLayerManager(aManager)) {
     return mozilla::LAYER_NONE;
   }
   return mozilla::LAYER_ACTIVE_FORCE;
 }
 
-bool nsDisplayRemote::HasDeletedFrame() const {
-  // RenderFrame might change without invalidating nsSubDocumentFrame.
-  return !GetFrameLoader() || nsDisplayItem::HasDeletedFrame();
-}
-
 already_AddRefed<Layer> nsDisplayRemote::BuildLayer(
     nsDisplayListBuilder* aBuilder, LayerManager* aManager,
     const ContainerLayerParameters& aContainerParameters) {
   MOZ_ASSERT(mFrame, "Makes no sense to have a shadow tree without a frame");
 
   if (IsTempLayerManager(aManager)) {
     // This can happen if aManager is a "temporary" manager, or if the
     // widget's layer manager changed out from under us.  We need to
--- a/layout/ipc/RenderFrame.h
+++ b/layout/ipc/RenderFrame.h
@@ -85,31 +85,31 @@ class RenderFrame final {
 }  // namespace layout
 }  // namespace mozilla
 
 /**
  * A nsDisplayRemote will graft a remote frame's shadow layer tree (for a given
  * nsFrameLoader) into its parent frame's layer tree.
  */
 class nsDisplayRemote final : public nsDisplayItem {
+  friend class nsDisplayItem;
+
   typedef mozilla::dom::TabId TabId;
   typedef mozilla::gfx::Matrix4x4 Matrix4x4;
   typedef mozilla::layers::EventRegionsOverride EventRegionsOverride;
   typedef mozilla::layers::Layer Layer;
   typedef mozilla::layers::LayersId LayersId;
   typedef mozilla::layers::RefLayer RefLayer;
   typedef mozilla::layout::RenderFrame RenderFrame;
   typedef mozilla::LayoutDeviceRect LayoutDeviceRect;
   typedef mozilla::LayoutDeviceIntPoint LayoutDeviceIntPoint;
 
  public:
   nsDisplayRemote(nsDisplayListBuilder* aBuilder, nsSubDocumentFrame* aFrame);
 
-  bool HasDeletedFrame() const override;
-
   LayerState GetLayerState(
       nsDisplayListBuilder* aBuilder, LayerManager* aManager,
       const ContainerLayerParameters& aParameters) override;
 
   already_AddRefed<Layer> BuildLayer(
       nsDisplayListBuilder* aBuilder, LayerManager* aManager,
       const ContainerLayerParameters& aContainerParameters) override;
 
--- a/layout/painting/RetainedDisplayListBuilder.cpp
+++ b/layout/painting/RetainedDisplayListBuilder.cpp
@@ -136,21 +136,20 @@ bool RetainedDisplayListBuilder::PreProc
 
   MOZ_RELEASE_ASSERT(initializeDAG || aList->mDAG.Length() == aList->Count());
 
   nsDisplayList saved;
   aList->mOldItems.SetCapacity(aList->Count());
   MOZ_RELEASE_ASSERT(aList->mOldItems.IsEmpty());
   while (nsDisplayItem* item = aList->RemoveBottom()) {
 #ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED
-    item->mMergedItem = false;
-    item->mPreProcessedItem = true;
+    item->SetMergedPreProcessed(false, true);
 #endif
 
-    if (item->HasDeletedFrame() || !item->CanBeReused()) {
+    if (!item->CanBeReused() || item->HasDeletedFrame()) {
       size_t i = aList->mOldItems.Length();
       aList->mOldItems.AppendElement(OldItemInfo(nullptr));
       item->Destroy(&mBuilder);
 
       if (initializeDAG) {
         if (i == 0) {
           aList->mDAG.AddNode(Span<const MergedListIndex>());
         } else {
@@ -209,27 +208,32 @@ void RetainedDisplayListBuilder::Increme
   MOZ_ASSERT(subDocFrame);
 
   nsIPresShell* presShell = subDocFrame->GetSubdocumentPresShellForPainting(0);
   MOZ_ASSERT(presShell);
 
   mBuilder.IncrementPresShellPaintCount(presShell);
 }
 
-static bool AnyContentAncestorModified(nsIFrame* aFrame,
-                                       nsIFrame* aStopAtFrame = nullptr) {
-  for (nsIFrame* f = aFrame; f;
-       f = nsLayoutUtils::GetParentOrPlaceholderForCrossDoc(f)) {
+bool AnyContentAncestorModified(nsIFrame* aFrame, nsIFrame* aStopAtFrame) {
+  nsIFrame* f = aFrame;
+  while (f) {
     if (f->IsFrameModified()) {
       return true;
     }
 
     if (aStopAtFrame && f == aStopAtFrame) {
       break;
     }
+
+    if (f->GetStateBits() & NS_FRAME_IS_PUSHED_FLOAT) {
+      f = f->GetParent();
+    } else {
+      f = nsLayoutUtils::GetParentOrPlaceholderForCrossDoc(f);
+    }
   }
 
   return false;
 }
 
 static Maybe<const ActiveScrolledRoot*> SelectContainerASR(
     const DisplayItemClipChain* aClipChain, const ActiveScrolledRoot* aItemASR,
     Maybe<const ActiveScrolledRoot*>& aContainerASR) {
@@ -270,16 +274,24 @@ static void UpdateASR(nsDisplayItem* aIt
   }
 
   wrapList->SetActiveScrolledRoot(ActiveScrolledRoot::PickAncestor(
       wrapList->GetFrameActiveScrolledRoot(), *asr));
 
   wrapList->UpdateHitTestInfoActiveScrolledRoot(*asr);
 }
 
+OldItemInfo::OldItemInfo(nsDisplayItem* aItem)
+    : mItem(aItem), mUsed(false), mDiscarded(false) {
+  if (mItem) {
+    // Clear cached modified frame state when adding an item to the old list.
+    mItem->SetModifiedFrame(false);
+  }
+}
+
 void OldItemInfo::AddedMatchToMergedList(RetainedDisplayListBuilder* aBuilder,
                                          MergedListIndex aIndex) {
   AddedToMergedList(aIndex);
 }
 
 void OldItemInfo::Discard(RetainedDisplayListBuilder* aBuilder,
                           nsTArray<MergedListIndex>&& aDirectPredecessors) {
   MOZ_ASSERT(!IsUsed());
@@ -287,48 +299,50 @@ void OldItemInfo::Discard(RetainedDispla
   mDirectPredecessors = std::move(aDirectPredecessors);
   if (mItem) {
     mItem->Destroy(aBuilder->Builder());
   }
   mItem = nullptr;
 }
 
 bool OldItemInfo::IsChanged() {
-  return !mItem || mItem->HasDeletedFrame() || !mItem->CanBeReused();
+  return !mItem || !mItem->CanBeReused() || mItem->HasDeletedFrame();
 }
 
 /**
  * A C++ implementation of Markus Stange's merge-dags algorithm.
  * https://github.com/mstange/merge-dags
  *
  * MergeState handles combining a new list of display items into an existing
  * DAG and computes the new DAG in a single pass.
  * Each time we add a new item, we resolve all dependencies for it, so that the
  * resulting list and DAG are built in topological ordering.
  */
 class MergeState {
  public:
   MergeState(RetainedDisplayListBuilder* aBuilder,
-             RetainedDisplayList& aOldList, uint32_t aOuterKey)
+             RetainedDisplayList& aOldList, nsDisplayItem* aOuterItem)
       : mBuilder(aBuilder),
         mOldList(&aOldList),
         mOldItems(std::move(aOldList.mOldItems)),
         mOldDAG(
             std::move(*reinterpret_cast<DirectedAcyclicGraph<OldListUnits>*>(
                 &aOldList.mDAG))),
-        mOuterKey(aOuterKey),
+        mOuterItem(aOuterItem),
         mResultIsModified(false) {
     mMergedDAG.EnsureCapacityFor(mOldDAG);
     MOZ_RELEASE_ASSERT(mOldItems.Length() == mOldDAG.Length());
   }
 
   Maybe<MergedListIndex> ProcessItemFromNewList(
       nsDisplayItem* aNewItem, const Maybe<MergedListIndex>& aPreviousItem) {
     OldListIndex oldIndex;
-    if (!HasModifiedFrame(aNewItem) &&
+    MOZ_DIAGNOSTIC_ASSERT(aNewItem->HasModifiedFrame() ==
+                          HasModifiedFrame(aNewItem));
+    if (!aNewItem->HasModifiedFrame() &&
         HasMatchingItemInOldList(aNewItem, &oldIndex)) {
       nsDisplayItem* oldItem = mOldItems[oldIndex.val].mItem;
       MOZ_DIAGNOSTIC_ASSERT(oldItem->GetPerFrameKey() ==
                                 aNewItem->GetPerFrameKey() &&
                             oldItem->Frame() == aNewItem->Frame());
       if (!mOldItems[oldIndex.val].IsChanged()) {
         MOZ_DIAGNOSTIC_ASSERT(!mOldItems[oldIndex.val].IsUsed());
         nsDisplayItem* destItem;
@@ -458,29 +472,31 @@ class MergeState {
     return result;
   }
 
   bool HasMatchingItemInOldList(nsDisplayItem* aItem, OldListIndex* aOutIndex) {
     nsIFrame::DisplayItemArray* items =
         aItem->Frame()->GetProperty(nsIFrame::DisplayItems());
     // Look for an item that matches aItem's frame and per-frame-key, but isn't
     // the same item.
+    uint32_t outerKey = mOuterItem ? mOuterItem->GetPerFrameKey() : 0;
     for (nsDisplayItem* i : *items) {
       if (i != aItem && i->Frame() == aItem->Frame() &&
           i->GetPerFrameKey() == aItem->GetPerFrameKey()) {
-        if (i->GetOldListIndex(mOldList, mOuterKey, aOutIndex)) {
+        if (i->GetOldListIndex(mOldList, outerKey, aOutIndex)) {
           return true;
         }
       }
     }
     return false;
   }
 
   bool HasModifiedFrame(nsDisplayItem* aItem) {
-    return AnyContentAncestorModified(aItem->FrameForInvalidation());
+    nsIFrame* stopFrame = mOuterItem ? mOuterItem->Frame() : nullptr;
+    return AnyContentAncestorModified(aItem->FrameForInvalidation(), stopFrame);
   }
 
   void UpdateContainerASR(nsDisplayItem* aItem) {
     mContainerASR = SelectContainerASR(
         aItem->GetClipChain(), aItem->GetActiveScrolledRoot(), mContainerASR);
   }
 
   MergedListIndex AddNewNode(
@@ -491,22 +507,21 @@ class MergeState {
     aItem->NotifyUsed(mBuilder->Builder());
 
 #ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED
     nsIFrame::DisplayItemArray* items =
         aItem->Frame()->GetProperty(nsIFrame::DisplayItems());
     for (nsDisplayItem* i : *items) {
       if (i->Frame() == aItem->Frame() &&
           i->GetPerFrameKey() == aItem->GetPerFrameKey()) {
-        MOZ_DIAGNOSTIC_ASSERT(!i->mMergedItem);
+        MOZ_DIAGNOSTIC_ASSERT(!i->IsMergedItem());
       }
     }
 
-    aItem->mMergedItem = true;
-    aItem->mPreProcessedItem = false;
+    aItem->SetMergedPreProcessed(true, false);
 #endif
 
     mMergedItems.AppendToTop(aItem);
     MergedListIndex newIndex =
         mMergedDAG.AddNode(aDirectPredecessors, aExtraDirectPredecessor);
     return newIndex;
   }
 
@@ -614,17 +629,17 @@ class MergeState {
   Maybe<const ActiveScrolledRoot*> mContainerASR;
   nsTArray<OldItemInfo> mOldItems;
   DirectedAcyclicGraph<OldListUnits> mOldDAG;
   // Unfortunately we can't use strong typing for the hashtables
   // since they internally encode the type with the mOps pointer,
   // and assert when we try swap the contents
   nsDisplayList mMergedItems;
   DirectedAcyclicGraph<MergedListUnits> mMergedDAG;
-  uint32_t mOuterKey;
+  nsDisplayItem* mOuterItem;
   bool mResultIsModified;
 };
 
 /**
  * Takes two display lists and merges them into an output list.
  *
  * Display lists wthout an explicit DAG are interpreted as linear DAGs (with a
  * maximum of one direct predecessor and one direct successor per node). We add
@@ -636,18 +651,17 @@ class MergeState {
  */
 bool RetainedDisplayListBuilder::MergeDisplayLists(
     nsDisplayList* aNewList, RetainedDisplayList* aOldList,
     RetainedDisplayList* aOutList,
     mozilla::Maybe<const mozilla::ActiveScrolledRoot*>& aOutContainerASR,
     nsDisplayItem* aOuterItem) {
   AUTO_PROFILER_LABEL_CATEGORY_PAIR(GRAPHICS_DisplayListMerging);
 
-  MergeState merge(this, *aOldList,
-                   aOuterItem ? aOuterItem->GetPerFrameKey() : 0);
+  MergeState merge(this, *aOldList, aOuterItem);
 
   Maybe<MergedListIndex> previousItemIndex;
   while (nsDisplayItem* item = aNewList->RemoveBottom()) {
     previousItemIndex = merge.ProcessItemFromNewList(item, previousItemIndex);
   }
 
   *aOutList = merge.Finalize();
   aOutContainerASR = merge.mContainerASR;
--- a/layout/painting/RetainedDisplayListHelpers.h
+++ b/layout/painting/RetainedDisplayListHelpers.h
@@ -134,18 +134,17 @@ class DirectedAcyclicGraph {
   nsTArray<NodeInfo> mNodesInfo;
   nsTArray<Index<T>> mDirectPredecessorList;
 };
 
 struct RetainedDisplayListBuilder;
 class nsDisplayItem;
 
 struct OldItemInfo {
-  explicit OldItemInfo(nsDisplayItem* aItem)
-      : mItem(aItem), mUsed(false), mDiscarded(false) {}
+  explicit OldItemInfo(nsDisplayItem* aItem);
 
   void AddedToMergedList(MergedListIndex aIndex) {
     MOZ_ASSERT(!IsUsed());
     mUsed = true;
     mIndex = aIndex;
     mItem = nullptr;
   }
 
@@ -164,9 +163,12 @@ struct OldItemInfo {
 
   nsDisplayItem* mItem;
   bool mUsed;
   bool mDiscarded;
   MergedListIndex mIndex;
   nsTArray<MergedListIndex> mDirectPredecessors;
 };
 
+bool AnyContentAncestorModified(nsIFrame* aFrame,
+                                nsIFrame* aStopAtFrame = nullptr);
+
 #endif  // RETAINEDDISPLAYLISTHELPERS_H_
--- a/layout/painting/nsDisplayList.cpp
+++ b/layout/painting/nsDisplayList.cpp
@@ -96,16 +96,17 @@
 #include "nsSliderFrame.h"
 #include "ClientLayerManager.h"
 #include "mozilla/layers/RenderRootStateManager.h"
 #include "mozilla/layers/StackingContextHelper.h"
 #include "mozilla/layers/WebRenderBridgeChild.h"
 #include "mozilla/layers/WebRenderLayerManager.h"
 #include "mozilla/layers/WebRenderMessages.h"
 #include "mozilla/layers/WebRenderScrollData.h"
+#include "mozilla/layout/RenderFrame.h"
 
 using namespace mozilla;
 using namespace mozilla::layers;
 using namespace mozilla::dom;
 using namespace mozilla::layout;
 using namespace mozilla::gfx;
 
 typedef ScrollableLayerGuid::ViewID ViewID;
@@ -130,17 +131,17 @@ void AssertUniqueItem(nsDisplayItem* aIt
   nsIFrame::DisplayItemArray* items =
       aItem->Frame()->GetProperty(nsIFrame::DisplayItems());
   if (!items) {
     return;
   }
   for (nsDisplayItem* i : *items) {
     if (i != aItem && !i->HasDeletedFrame() && i->Frame() == aItem->Frame() &&
         i->GetPerFrameKey() == aItem->GetPerFrameKey()) {
-      if (i->mPreProcessedItem) {
+      if (i->IsPreProcessedItem()) {
         continue;
       }
       MOZ_DIAGNOSTIC_ASSERT(false, "Duplicate display item!");
     }
   }
 }
 #endif
 
@@ -3166,28 +3167,19 @@ static_assert(sizeof(nsDisplayItem) <= 1
 #endif
 
 nsDisplayItem::nsDisplayItem(nsDisplayListBuilder* aBuilder, nsIFrame* aFrame)
     : nsDisplayItem(aBuilder, aFrame, aBuilder->CurrentActiveScrolledRoot()) {}
 
 nsDisplayItem::nsDisplayItem(nsDisplayListBuilder* aBuilder, nsIFrame* aFrame,
                              const ActiveScrolledRoot* aActiveScrolledRoot)
     : mFrame(aFrame),
+      mItemFlags(),
       mActiveScrolledRoot(aActiveScrolledRoot),
-      mAnimatedGeometryRoot(nullptr),
-      mForceNotVisible(aBuilder->IsBuildingInvisibleItems()),
-      mDisableSubpixelAA(false),
-      mReusedItem(false),
-      mPaintRectValid(false),
-      mCanBeReused(true)
-#ifdef MOZ_DUMP_PAINTING
-      ,
-      mPainted(false)
-#endif
-{
+      mAnimatedGeometryRoot(nullptr) {
   MOZ_COUNT_CTOR(nsDisplayItem);
   if (aBuilder->IsRetainingDisplayList()) {
     mFrame->AddDisplayItem(this);
   }
   mReferenceFrame = aBuilder->FindReferenceFrameFor(aFrame, &mToReferenceFrame);
   // This can return the wrong result if the item override
   // ShouldFixToViewport(), the item needs to set it again in its constructor.
   mAnimatedGeometryRoot = aBuilder->FindAnimatedGeometryRootFor(aFrame);
@@ -3203,45 +3195,66 @@ nsDisplayItem::nsDisplayItem(nsDisplayLi
 
   // The visible rect is for mCurrentFrame, so we have to use
   // mCurrentOffsetToReferenceFrame
   nsRect visible = aBuilder->GetVisibleRect() +
                    aBuilder->GetCurrentFrameOffsetToReferenceFrame();
   SetBuildingRect(visible);
 
   const nsStyleDisplay* disp = mFrame->StyleDisplay();
-  mBackfaceIsHidden = mFrame->BackfaceIsHidden(disp);
-  mCombines3DTransformWithAncestors =
-      mFrame->Combines3DTransformWithAncestors(disp);
+  if (mFrame->BackfaceIsHidden(disp)) {
+    mItemFlags += ItemFlag::BackfaceHidden;
+  }
+  if (mFrame->Combines3DTransformWithAncestors(disp)) {
+    mItemFlags += ItemFlag::Combines3DTransformWithAncestors;
+  }
 }
 
 /* static */
 bool nsDisplayItem::ForceActiveLayers() {
   static bool sForce = false;
   static bool sForceCached = false;
 
   if (!sForceCached) {
     Preferences::AddBoolVarCache(&sForce, "layers.force-active", false);
     sForceCached = true;
   }
 
   return sForce;
 }
 
+bool nsDisplayItem::HasModifiedFrame() const {
+  return mItemFlags.contains(ItemFlag::ModifiedFrame);
+}
+
+void nsDisplayItem::SetModifiedFrame(bool aModified) {
+  if (aModified) {
+    mItemFlags += ItemFlag::ModifiedFrame;
+  } else {
+    mItemFlags -= ItemFlag::ModifiedFrame;
+  }
+}
+
+bool nsDisplayItem::HasDeletedFrame() const {
+  return mItemFlags.contains(ItemFlag::DeletedFrame) ||
+         (GetType() == DisplayItemType::TYPE_REMOTE &&
+          !static_cast<const nsDisplayRemote*>(this)->GetFrameLoader());
+}
+
 int32_t nsDisplayItem::ZIndex() const { return mFrame->ZIndex(); }
 
 bool nsDisplayItem::ComputeVisibility(nsDisplayListBuilder* aBuilder,
                                       nsRegion* aVisibleRegion) {
   return !GetPaintRect().IsEmpty() &&
          !IsInvisibleInRect(aVisibleRegion->GetBounds());
 }
 
 bool nsDisplayItem::RecomputeVisibility(nsDisplayListBuilder* aBuilder,
                                         nsRegion* aVisibleRegion) {
-  if (mForceNotVisible && !GetSameCoordinateSystemChildren()) {
+  if (ForceNotVisible() && !GetSameCoordinateSystemChildren()) {
     // mForceNotVisible wants to ensure that this display item doesn't render
     // anything itself. If this item has contents, then we obviously want to
     // render those, so we don't need this check in that case.
     NS_ASSERTION(GetBuildingRect().IsEmpty(),
                  "invisible items without children should have empty vis rect");
     SetPaintRect(nsRect());
   } else {
     bool snap;
@@ -3296,16 +3309,18 @@ void nsDisplayItem::FuseClipChainUpTo(ns
 
   if (mClipChain) {
     mClip = &mClipChain->mClip;
   } else {
     mClip = nullptr;
   }
 }
 
+void nsDisplayItem::SetDeletedFrame() { mItemFlags += ItemFlag::DeletedFrame; }
+
 bool nsDisplayItem::ShouldUseAdvancedLayer(LayerManager* aManager,
                                            PrefFunc aFunc) const {
   return CanUseAdvancedLayer(aManager) ? aFunc() : false;
 }
 
 bool nsDisplayItem::CanUseAdvancedLayer(LayerManager* aManager) const {
   return gfxPrefs::LayersAdvancedBasicLayerEnabled() || !aManager ||
          aManager->GetBackendType() == layers::LayersBackend::LAYERS_WR;
@@ -6693,23 +6708,20 @@ nsDisplaySubDocument::~nsDisplaySubDocum
     mSubDocFrame->RemoveDisplayItem(this);
   }
 }
 
 nsIFrame* nsDisplaySubDocument::FrameForInvalidation() const {
   return mSubDocFrame ? mSubDocFrame : mFrame;
 }
 
-bool nsDisplaySubDocument::HasDeletedFrame() const {
-  return !mSubDocFrame || nsDisplayItem::HasDeletedFrame();
-}
-
 void nsDisplaySubDocument::RemoveFrame(nsIFrame* aFrame) {
   if (aFrame == mSubDocFrame) {
     mSubDocFrame = nullptr;
+    SetDeletedFrame();
   }
   nsDisplayItem::RemoveFrame(aFrame);
 }
 
 void nsDisplaySubDocument::Disown() {
   if (mFrame) {
     mFrame->RemoveDisplayItem(this);
     mFrame = nullptr;
@@ -8939,17 +8951,17 @@ bool nsDisplayText::CanApplyOpacity() co
 
   return true;
 }
 
 void nsDisplayText::Paint(nsDisplayListBuilder* aBuilder, gfxContext* aCtx) {
   AUTO_PROFILER_LABEL("nsDisplayText::Paint", GRAPHICS);
 
   DrawTargetAutoDisableSubpixelAntialiasing disable(aCtx->GetDrawTarget(),
-                                                    mDisableSubpixelAA);
+                                                    IsSubpixelAADisabled());
   RenderToContext(aCtx, aBuilder);
 }
 
 bool nsDisplayText::CreateWebRenderCommands(
     mozilla::wr::DisplayListBuilder& aBuilder,
     mozilla::wr::IpcResourceUpdateQueue& aResources,
     const StackingContextHelper& aSc, RenderRootStateManager* aManager,
     nsDisplayListBuilder* aDisplayListBuilder) {
@@ -10267,8 +10279,91 @@ PaintTelemetry::AutoRecord::~AutoRecord(
   if (mStart.IsNull()) {
     return;
   }
 
   sMetrics[mMetric] += (TimeStamp::Now() - mStart).ToMilliseconds();
 }
 
 }  // namespace mozilla
+
+#ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED
+static nsIFrame* GetSelfOrPlaceholderFor(nsIFrame* aFrame) {
+  if (aFrame->GetStateBits() & NS_FRAME_IS_PUSHED_FLOAT) {
+    return aFrame;
+  }
+
+  if ((aFrame->GetStateBits() & NS_FRAME_OUT_OF_FLOW) &&
+      !aFrame->GetPrevInFlow()) {
+    return aFrame->GetPlaceholderFrame();
+  }
+
+  return aFrame;
+}
+
+static nsIFrame* GetAncestorFor(nsIFrame* aFrame) {
+  nsIFrame* f = GetSelfOrPlaceholderFor(aFrame);
+  MOZ_ASSERT(f);
+  return nsLayoutUtils::GetCrossDocParentFrame(f);
+}
+#endif
+
+nsDisplayListBuilder::AutoBuildingDisplayList::AutoBuildingDisplayList(
+    nsDisplayListBuilder* aBuilder, nsIFrame* aForChild,
+    const nsRect& aVisibleRect, const nsRect& aDirtyRect,
+    const bool aIsTransformed, RecalcInInvalidSubtree aRecalcInvalidSubtree)
+    : mBuilder(aBuilder),
+      mPrevFrame(aBuilder->mCurrentFrame),
+      mPrevReferenceFrame(aBuilder->mCurrentReferenceFrame),
+      mPrevHitTestArea(aBuilder->mHitTestArea),
+      mPrevHitTestInfo(aBuilder->mHitTestInfo),
+      mPrevOffset(aBuilder->mCurrentOffsetToReferenceFrame),
+      mPrevVisibleRect(aBuilder->mVisibleRect),
+      mPrevDirtyRect(aBuilder->mDirtyRect),
+      mPrevAGR(aBuilder->mCurrentAGR),
+      mPrevAncestorHasApzAwareEventHandler(
+          aBuilder->mAncestorHasApzAwareEventHandler),
+      mPrevBuildingInvisibleItems(aBuilder->mBuildingInvisibleItems),
+      mPrevInInvalidSubtree(aBuilder->mInInvalidSubtree) {
+#ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED
+  // Validate that aForChild is being visited from it's parent frame if
+  // recalculation of mInInvalidSubtree isn't requested.
+  const nsIFrame* ancestor = GetAncestorFor(aForChild);
+  MOZ_DIAGNOSTIC_ASSERT(aRecalcInvalidSubtree ==
+                            nsDisplayListBuilder::RIIS_YES ||
+                        aForChild == mPrevFrame || ancestor == mPrevFrame);
+#endif
+
+  if (aIsTransformed) {
+    aBuilder->mCurrentOffsetToReferenceFrame = nsPoint();
+    aBuilder->mCurrentReferenceFrame = aForChild;
+  } else if (aBuilder->mCurrentFrame == aForChild->GetParent()) {
+    aBuilder->mCurrentOffsetToReferenceFrame += aForChild->GetPosition();
+  } else {
+    aBuilder->mCurrentReferenceFrame = aBuilder->FindReferenceFrameFor(
+        aForChild, &aBuilder->mCurrentOffsetToReferenceFrame);
+  }
+
+  bool isAsync;
+  mCurrentAGRState = aBuilder->IsAnimatedGeometryRoot(aForChild, isAsync);
+
+  if (aBuilder->mCurrentFrame == aForChild->GetParent()) {
+    if (mCurrentAGRState == AGR_YES) {
+      aBuilder->mCurrentAGR =
+          aBuilder->WrapAGRForFrame(aForChild, isAsync, aBuilder->mCurrentAGR);
+    }
+  } else if (aBuilder->mCurrentFrame != aForChild) {
+    aBuilder->mCurrentAGR = aBuilder->FindAnimatedGeometryRootFor(aForChild);
+  }
+
+  MOZ_ASSERT(nsLayoutUtils::IsAncestorFrameCrossDoc(
+      aBuilder->RootReferenceFrame(), *aBuilder->mCurrentAGR));
+  if (!aRecalcInvalidSubtree) {
+    aBuilder->mInInvalidSubtree = aBuilder->mInInvalidSubtree ||
+      aForChild->IsFrameModified();
+  } else {
+    aBuilder->mInInvalidSubtree = AnyContentAncestorModified(aForChild);
+  }
+  aBuilder->mCurrentFrame = aForChild;
+  aBuilder->mVisibleRect = aVisibleRect;
+  aBuilder->mDirtyRect =
+      aBuilder->mInInvalidSubtree ? aVisibleRect : aDirtyRect;
+}
--- a/layout/painting/nsDisplayList.h
+++ b/layout/painting/nsDisplayList.h
@@ -437,16 +437,21 @@ class nsDisplayListBuilder {
   typedef mozilla::layers::FrameMetrics FrameMetrics;
   typedef mozilla::layers::ScrollableLayerGuid ScrollableLayerGuid;
   typedef mozilla::layers::ScrollableLayerGuid::ViewID ViewID;
   typedef mozilla::gfx::CompositorHitTestInfo CompositorHitTestInfo;
   typedef mozilla::gfx::Matrix4x4 Matrix4x4;
   typedef mozilla::Maybe<mozilla::layers::ScrollDirection> MaybeScrollDirection;
 
   /**
+   * Does InInvalidSubtree need to recalculated?
+   */
+  enum RecalcInInvalidSubtree { RIIS_NO, RIIS_YES };
+
+  /**
    * @param aReferenceFrame the frame at the root of the subtree; its origin
    * is the origin of the reference coordinate system for this display list
    * @param aMode encodes what the builder is being used for.
    * @param aBuildCaret whether or not we should include the caret in any
    * display lists that we make.
    */
   nsDisplayListBuilder(nsIFrame* aReferenceFrame,
                        nsDisplayListBuilderMode aMode, bool aBuildCaret,
@@ -1127,69 +1132,35 @@ class nsDisplayListBuilder {
   /**
    * A helper class used to temporarily set nsDisplayListBuilder properties for
    * building display items.
    * aVisibleRect and aDirtyRect are relative to aForChild.
    */
   class AutoBuildingDisplayList {
    public:
     AutoBuildingDisplayList(nsDisplayListBuilder* aBuilder, nsIFrame* aForChild,
-                            const nsRect& aVisibleRect,
-                            const nsRect& aDirtyRect)
+                            RecalcInInvalidSubtree aRecalcInvalidSubtree)
+        : AutoBuildingDisplayList(
+              aBuilder, aForChild, aBuilder->GetVisibleRect(),
+              aBuilder->GetDirtyRect(), aForChild->IsTransformed(),
+              aRecalcInvalidSubtree) {}
+
+    AutoBuildingDisplayList(
+        nsDisplayListBuilder* aBuilder, nsIFrame* aForChild,
+        const nsRect& aVisibleRect, const nsRect& aDirtyRect,
+        RecalcInInvalidSubtree aRecalcInvalidSubtree = RIIS_NO)
         : AutoBuildingDisplayList(aBuilder, aForChild, aVisibleRect, aDirtyRect,
-                                  aForChild->IsTransformed()) {}
-
-    AutoBuildingDisplayList(nsDisplayListBuilder* aBuilder, nsIFrame* aForChild,
-                            const nsRect& aVisibleRect,
-                            const nsRect& aDirtyRect, const bool aIsTransformed)
-        : mBuilder(aBuilder),
-          mPrevFrame(aBuilder->mCurrentFrame),
-          mPrevReferenceFrame(aBuilder->mCurrentReferenceFrame),
-          mPrevHitTestArea(aBuilder->mHitTestArea),
-          mPrevHitTestInfo(aBuilder->mHitTestInfo),
-          mPrevOffset(aBuilder->mCurrentOffsetToReferenceFrame),
-          mPrevVisibleRect(aBuilder->mVisibleRect),
-          mPrevDirtyRect(aBuilder->mDirtyRect),
-          mPrevAGR(aBuilder->mCurrentAGR),
-          mPrevAncestorHasApzAwareEventHandler(
-              aBuilder->mAncestorHasApzAwareEventHandler),
-          mPrevBuildingInvisibleItems(aBuilder->mBuildingInvisibleItems),
-          mPrevInInvalidSubtree(aBuilder->mInInvalidSubtree) {
-      if (aIsTransformed) {
-        aBuilder->mCurrentOffsetToReferenceFrame = nsPoint();
-        aBuilder->mCurrentReferenceFrame = aForChild;
-      } else if (aBuilder->mCurrentFrame == aForChild->GetParent()) {
-        aBuilder->mCurrentOffsetToReferenceFrame += aForChild->GetPosition();
-      } else {
-        aBuilder->mCurrentReferenceFrame = aBuilder->FindReferenceFrameFor(
-            aForChild, &aBuilder->mCurrentOffsetToReferenceFrame);
-      }
-
-      bool isAsync;
-      mCurrentAGRState = aBuilder->IsAnimatedGeometryRoot(aForChild, isAsync);
-
-      if (aBuilder->mCurrentFrame == aForChild->GetParent()) {
-        if (mCurrentAGRState == AGR_YES) {
-          aBuilder->mCurrentAGR = aBuilder->WrapAGRForFrame(
-              aForChild, isAsync, aBuilder->mCurrentAGR);
-        }
-      } else if (aBuilder->mCurrentFrame != aForChild) {
-        aBuilder->mCurrentAGR =
-            aBuilder->FindAnimatedGeometryRootFor(aForChild);
-      }
-
-      MOZ_ASSERT(nsLayoutUtils::IsAncestorFrameCrossDoc(
-          aBuilder->RootReferenceFrame(), *aBuilder->mCurrentAGR));
-      aBuilder->mInInvalidSubtree =
-          aBuilder->mInInvalidSubtree || aForChild->IsFrameModified();
-      aBuilder->mCurrentFrame = aForChild;
-      aBuilder->mVisibleRect = aVisibleRect;
-      aBuilder->mDirtyRect =
-          aBuilder->mInInvalidSubtree ? aVisibleRect : aDirtyRect;
-    }
+                                  aForChild->IsTransformed(),
+                                  aRecalcInvalidSubtree) {}
+
+    AutoBuildingDisplayList(
+        nsDisplayListBuilder* aBuilder, nsIFrame* aForChild,
+        const nsRect& aVisibleRect, const nsRect& aDirtyRect,
+        const bool aIsTransformed,
+        RecalcInInvalidSubtree aRecalcInvalidSubtree = RIIS_NO);
 
     void SetReferenceFrameAndCurrentOffset(const nsIFrame* aFrame,
                                            const nsPoint& aOffset) {
       mBuilder->mCurrentReferenceFrame = aFrame;
       mBuilder->mCurrentOffsetToReferenceFrame = aOffset;
     }
 
     bool IsAnimatedGeometryRoot() const { return mCurrentAGRState == AGR_YES; }
@@ -1755,27 +1726,17 @@ class nsDisplayListBuilder {
     if (!aFrame->IsFrameModified()) {
       mModifiedFramesDuringBuilding.AppendElement(aFrame);
       aFrame->SetFrameIsModified(true);
       return true;
     }
     return false;
   }
 
-  bool MarkCurrentFrameModifiedDuringBuilding() {
-    if (MarkFrameModifiedDuringBuilding(const_cast<nsIFrame*>(mCurrentFrame))) {
-      mInInvalidSubtree = true;
-      mDirtyRect = mVisibleRect;
-      return true;
-    }
-    return false;
-  }
-
   void RebuildAllItemsInCurrentSubtree() {
-    mInInvalidSubtree = true;
     mDirtyRect = mVisibleRect;
   }
 
   /**
    * This is a convenience function to ease the transition until AGRs and ASRs
    * are unified.
    */
   AnimatedGeometryRoot* AnimatedGeometryRootForASR(
@@ -2106,21 +2067,32 @@ MOZ_ALWAYS_INLINE T* MakeDisplayItem(nsD
         if (!did->HasMergedFrames()) {
           item->SetDisplayItemData(did, did->GetLayer()->Manager());
         }
         break;
       }
     }
   }
 
+  if (aBuilder->InInvalidSubtree() ||
+      item->FrameForInvalidation()->IsFrameModified()) {
+    item->SetModifiedFrame(true);
+  }
+
 #ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED
   if (aBuilder->IsRetainingDisplayList() && !aBuilder->IsInPageSequence() &&
       aBuilder->IsBuilding()) {
     AssertUniqueItem(item);
   }
+
+  // Verify that InInvalidSubtree matches invalidation frame's modified state.
+  if (aBuilder->InInvalidSubtree()) {
+    MOZ_DIAGNOSTIC_ASSERT(
+        AnyContentAncestorModified(item->FrameForInvalidation()));
+  }
 #endif
 
   return item;
 }
 
 /**
  * This is the unit of rendering and event testing. Each instance of this
  * class represents an entity that can be drawn on the screen, e.g., a
@@ -2178,23 +2150,24 @@ class nsDisplayItem : public nsDisplayIt
     DisplayItemType type = GetType();
     this->~nsDisplayItem();
     aBuilder->Destroy(type, this);
   }
 
   virtual void RestoreState() {
     mClipChain = mState.mClipChain;
     mClip = mState.mClip;
-    mDisableSubpixelAA = false;
+    mItemFlags -= ItemFlag::DisableSubpixelAA;
   }
 
   virtual void RemoveFrame(nsIFrame* aFrame) {
     if (mFrame && aFrame == mFrame) {
       MOZ_ASSERT(!mFrame->HasDisplayItem(this));
       mFrame = nullptr;
+      SetDeletedFrame();
       SetDisplayItemData(nullptr, nullptr);
     }
   }
 
   /**
    * Downcasts this item to nsDisplayWrapList, if possible.
    */
   virtual const nsDisplayWrapList* AsDisplayWrapList() const { return nullptr; }
@@ -2210,38 +2183,39 @@ class nsDisplayItem : public nsDisplayIt
   nsDisplayItem(const nsDisplayItem&) = delete;
   /**
    * The custom copy-constructor is implemented to prevent copying the saved
    * state of the item.
    * This is currently only used when creating temporary items for merging.
    */
   nsDisplayItem(nsDisplayListBuilder* aBuilder, const nsDisplayItem& aOther)
       : mFrame(aOther.mFrame),
+        mItemFlags(),
         mClipChain(aOther.mClipChain),
         mClip(aOther.mClip),
         mActiveScrolledRoot(aOther.mActiveScrolledRoot),
         mReferenceFrame(aOther.mReferenceFrame),
         mAnimatedGeometryRoot(aOther.mAnimatedGeometryRoot),
         mToReferenceFrame(aOther.mToReferenceFrame),
         mBuildingRect(aOther.mBuildingRect),
-        mPaintRect(aOther.mPaintRect),
-        mForceNotVisible(aOther.mForceNotVisible),
-        mDisableSubpixelAA(aOther.mDisableSubpixelAA),
-        mReusedItem(false),
-        mBackfaceIsHidden(aOther.mBackfaceIsHidden),
-        mCombines3DTransformWithAncestors(
-            aOther.mCombines3DTransformWithAncestors),
-        mPaintRectValid(false),
-        mCanBeReused(true)
-#ifdef MOZ_DUMP_PAINTING
-        ,
-        mPainted(false)
-#endif
-  {
+        mPaintRect(aOther.mPaintRect) {
     MOZ_COUNT_CTOR(nsDisplayItem);
+    // TODO: It might be better to remove the flags that aren't copied.
+    if (aOther.ForceNotVisible()) {
+      mItemFlags += ItemFlag::ForceNotVisible;
+    }
+    if (aOther.IsSubpixelAADisabled()) {
+      mItemFlags += ItemFlag::DisableSubpixelAA;
+    }
+    if (mFrame->In3DContextAndBackfaceIsHidden()) {
+      mItemFlags += ItemFlag::BackfaceHidden;
+    }
+    if (aOther.Combines3DTransformWithAncestors()) {
+      mItemFlags += ItemFlag::Combines3DTransformWithAncestors;
+    }
   }
 
   struct HitTestState {
     explicit HitTestState() : mInPreserves3D(false) {}
 
     ~HitTestState() {
       NS_ASSERTION(mItemBuffer.Length() == 0,
                    "mItemBuffer should have been cleared");
@@ -2295,17 +2269,20 @@ class nsDisplayItem : public nsDisplayIt
   }
 
   /**
    * @return the nsIFrame that provides the style data, and should
    * be checked when deciding if this display item can be reused.
    */
   virtual nsIFrame* FrameForInvalidation() const { return mFrame; }
 
-  virtual bool HasDeletedFrame() const { return !mFrame; }
+  bool HasModifiedFrame() const;
+  void SetModifiedFrame(bool aModified);
+
+  bool HasDeletedFrame() const;
 
   virtual nsIFrame* StyleFrame() const { return mFrame; }
 
   /**
    * Compute the used z-index of our frame; returns zero for elements to which
    * z-index does not apply, and for z-index:auto.
    * @note This can be overridden, @see nsDisplayWrapList::SetOverrideZIndex.
    */
@@ -2539,22 +2516,22 @@ class nsDisplayItem : public nsDisplayIt
   virtual void PaintWithClip(nsDisplayListBuilder* aBuilder, gfxContext* aCtx,
                              const DisplayItemClip& aClip) {}
 
 #ifdef MOZ_DUMP_PAINTING
   /**
    * Mark this display item as being painted via
    * FrameLayerBuilder::DrawPaintedLayer.
    */
-  bool Painted() const { return mPainted; }
+  bool Painted() const { return mItemFlags.contains(ItemFlag::Painted); }
 
   /**
    * Check if this display item has been painted.
    */
-  void SetPainted() { mPainted = true; }
+  void SetPainted() { mItemFlags += ItemFlag::Painted; }
 #endif
 
   /**
    * Get the layer drawn by this display item. Call this only if
    * GetLayerState() returns something other than LAYER_NONE.
    * If GetLayerState returned LAYER_NONE then Paint will be called
    * instead.
    * This is called while aManager is in the construction phase.
@@ -2727,24 +2704,26 @@ class nsDisplayItem : public nsDisplayIt
 
   void SetBuildingRect(const nsRect& aBuildingRect) {
     if (aBuildingRect == mBuildingRect) {
       // Avoid unnecessary paint rect recompution when the
       // building rect is staying the same.
       return;
     }
     mPaintRect = mBuildingRect = aBuildingRect;
-    mPaintRectValid = false;
+    mItemFlags -= ItemFlag::PaintRectValid;
   }
 
   void SetPaintRect(const nsRect& aPaintRect) {
     mPaintRect = aPaintRect;
-    mPaintRectValid = true;
-  }
-  bool HasPaintRect() const { return mPaintRectValid; }
+    mItemFlags += ItemFlag::PaintRectValid;
+  }
+  bool HasPaintRect() const {
+    return mItemFlags.contains(ItemFlag::PaintRectValid);
+  }
 
   /**
    * Returns the building rect for the children, relative to their
    * reference frame. Can be different from mBuildingRect for
    * nsDisplayTransform, since the reference frame for the children is different
    * from the reference frame for the item itself.
    */
   virtual const nsRect& GetBuildingRectForChildren() const {
@@ -2760,17 +2739,19 @@ class nsDisplayItem : public nsDisplayIt
     NS_ASSERTION(CanApplyOpacity(), "ApplyOpacity not supported on this type");
   }
   /**
    * Returns true if this display item would return true from ApplyOpacity
    * without actually applying the opacity. Otherwise returns false.
    */
   virtual bool CanApplyOpacity() const { return false; }
 
-  bool ForceNotVisible() const { return mForceNotVisible; }
+  bool ForceNotVisible() const {
+    return mItemFlags.contains(ItemFlag::ForceNotVisible);
+  }
 
   /**
    * For debugging and stuff
    */
   virtual const char* Name() const = 0;
 
   virtual void WriteDebugInfo(std::stringstream& aStream) {}
 
@@ -2826,19 +2807,21 @@ class nsDisplayItem : public nsDisplayIt
   virtual nsRect GetComponentAlphaBounds(nsDisplayListBuilder* aBuilder) const {
     return nsRect();
   }
 
   /**
    * Disable usage of component alpha. Currently only relevant for items that
    * have text.
    */
-  void DisableComponentAlpha() { mDisableSubpixelAA = true; }
-
-  bool IsSubpixelAADisabled() const { return mDisableSubpixelAA; }
+  void DisableComponentAlpha() { mItemFlags += ItemFlag::DisableSubpixelAA; }
+
+  bool IsSubpixelAADisabled() const {
+    return mItemFlags.contains(ItemFlag::DisableSubpixelAA);
+  }
 
   /**
    * Check if we can add async animations to the layer for this display item.
    */
   virtual bool CanUseAsyncAnimations(nsDisplayListBuilder* aBuilder) {
     return false;
   }
 
@@ -2864,47 +2847,58 @@ class nsDisplayItem : public nsDisplayIt
 
   /**
    * Intersect all clips in our clip chain up to (and including) aASR and set
    * set the intersection as this item's clip.
    */
   void FuseClipChainUpTo(nsDisplayListBuilder* aBuilder,
                          const ActiveScrolledRoot* aASR);
 
-  bool BackfaceIsHidden() const { return mBackfaceIsHidden; }
+  bool BackfaceIsHidden() const {
+    return mItemFlags.contains(ItemFlag::BackfaceHidden);
+  }
 
   bool Combines3DTransformWithAncestors() const {
-    return mCombines3DTransformWithAncestors;
+    return mItemFlags.contains(ItemFlag::Combines3DTransformWithAncestors);
   }
 
   bool In3DContextAndBackfaceIsHidden() const {
-    return mBackfaceIsHidden && mCombines3DTransformWithAncestors;
+    return mItemFlags.contains(ItemFlag::BackfaceHidden) &&
+           mItemFlags.contains(ItemFlag::Combines3DTransformWithAncestors);
   }
 
   bool HasDifferentFrame(const nsDisplayItem* aOther) const {
     return mFrame != aOther->mFrame;
   }
 
   bool HasSameTypeAndClip(const nsDisplayItem* aOther) const {
     return GetPerFrameKey() == aOther->GetPerFrameKey() &&
            GetClipChain() == aOther->GetClipChain();
   }
 
   bool HasSameContent(const nsDisplayItem* aOther) const {
     return mFrame->GetContent() == aOther->Frame()->GetContent();
   }
 
-  bool IsReused() const { return mReusedItem; }
-
-  void SetReused(bool aReused) { mReusedItem = aReused; }
-
-  bool CanBeReused() const { return mCanBeReused; }
+  bool IsReused() const { return mItemFlags.contains(ItemFlag::ReusedItem); }
+  void SetReused(bool aReused) {
+    if (aReused) {
+      mItemFlags += ItemFlag::ReusedItem;
+    } else {
+      mItemFlags -= ItemFlag::ReusedItem;
+    }
+  }
+
+  bool CanBeReused() const {
+    return !mItemFlags.contains(ItemFlag::CantBeReused);
+  }
+  void SetCantBeReused() { mItemFlags += ItemFlag::CantBeReused; }
   void DiscardIfOldItem() {
     if (mOldList) {
-      mCanBeReused = false;
+      SetCantBeReused();
     }
   }
   virtual void NotifyUsed(nsDisplayListBuilder* aBuilder) {}
 
   virtual nsIFrame* GetDependentFrame() { return nullptr; }
 
   virtual mozilla::Maybe<nsRect> GetClipWithRespectToASR(
       nsDisplayListBuilder* aBuilder, const ActiveScrolledRoot* aASR) const;
@@ -2966,21 +2960,44 @@ class nsDisplayItem : public nsDisplayIt
 
   virtual bool HasHitTestInfo() const { return false; }
 
 #ifdef DEBUG
   virtual bool IsHitTestItem() const { return false; }
 #endif
 
  protected:
+  void SetDeletedFrame();
+
   typedef bool (*PrefFunc)(void);
   bool ShouldUseAdvancedLayer(LayerManager* aManager, PrefFunc aFunc) const;
   bool CanUseAdvancedLayer(LayerManager* aManager) const;
 
+  enum class ItemFlag {
+    ModifiedFrame,
+    DeletedFrame,
+    ForceNotVisible,
+    DisableSubpixelAA,
+    CantBeReused,
+    ReusedItem,
+    BackfaceHidden,
+    Combines3DTransformWithAncestors,
+    PaintRectValid,
+#ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED
+    MergedItem,
+    PreProcessedItem,
+#endif
+#ifdef MOZ_DUMP_PAINTING
+    // True if this frame has been painted.
+    Painted,
+#endif
+  };
+
   nsIFrame* mFrame;
+  mozilla::EnumSet<ItemFlag, uint16_t> mItemFlags;
   RefPtr<const DisplayItemClipChain> mClipChain;
   const DisplayItemClip* mClip;
   RefPtr<const ActiveScrolledRoot> mActiveScrolledRoot;
   // Result of FindReferenceFrameFor(mFrame), if mFrame is non-null
   const nsIFrame* mReferenceFrame;
   RefPtr<struct AnimatedGeometryRoot> mAnimatedGeometryRoot;
   // Result of ToReferenceFrame(mFrame), if mFrame is non-null
   nsPoint mToReferenceFrame;
@@ -3004,36 +3021,41 @@ class nsDisplayItem : public nsDisplayIt
  protected:
   struct {
     RefPtr<const DisplayItemClipChain> mClipChain;
     const DisplayItemClip* mClip;
   } mState;
 
 #ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED
  public:
+  bool IsMergedItem() const {
+    return mItemFlags.contains(ItemFlag::MergedItem);
+  }
+  bool IsPreProcessedItem() const {
+    return mItemFlags.contains(ItemFlag::PreProcessedItem);
+  }
+  void SetMergedPreProcessed(bool aMerged, bool aPreProcessed) {
+    if (aMerged) {
+      mItemFlags += ItemFlag::MergedItem;
+    } else {
+      mItemFlags -= ItemFlag::MergedItem;
+    }
+
+    if (aPreProcessed) {
+      mItemFlags += ItemFlag::PreProcessedItem;
+    } else {
+      mItemFlags -= ItemFlag::PreProcessedItem;
+    }
+  }
+
   uint32_t mOldListKey = 0;
   uint32_t mOldNestingDepth = 0;
-  bool mMergedItem = false;
-  bool mPreProcessedItem = false;
 
  protected:
 #endif
-
-  bool mForceNotVisible;
-  bool mDisableSubpixelAA;
-  bool mReusedItem;
-  bool mBackfaceIsHidden;
-  bool mCombines3DTransformWithAncestors;
-  bool mPaintRectValid;
-  bool mCanBeReused;
-
-#ifdef MOZ_DUMP_PAINTING
-  // True if this frame has been painted.
-  bool mPainted;
-#endif
 };
 
 /**
  * Manages a singly-linked list of display list items.
  *
  * mSentinel is the sentinel list value, the first value in the null-terminated
  * linked list of items. mTop is the last item in the list (whose 'above'
  * pointer is null). This class has no virtual methods. So list objects are just
@@ -4156,17 +4178,19 @@ class nsDisplaySolidColor : public nsDis
  public:
   nsDisplaySolidColor(nsDisplayListBuilder* aBuilder, nsIFrame* aFrame,
                       const nsRect& aBounds, nscolor aColor,
                       bool aCanBeReused = true)
       : nsDisplaySolidColorBase(aBuilder, aFrame, aColor), mBounds(aBounds) {
     NS_ASSERTION(NS_GET_A(aColor) > 0,
                  "Don't create invisible nsDisplaySolidColors!");
     MOZ_COUNT_CTOR(nsDisplaySolidColor);
-    mCanBeReused = aCanBeReused;
+    if (!aCanBeReused) {
+      SetCantBeReused();
+    }
   }
 
 #ifdef NS_BUILD_REFCNT_LOGGING
   ~nsDisplaySolidColor() override { MOZ_COUNT_DTOR(nsDisplaySolidColor); }
 #endif
 
   NS_DISPLAY_DECL_NAME("SolidColor", TYPE_SOLID_COLOR)
 
@@ -4494,23 +4518,20 @@ class nsDisplayTableBackgroundImage : pu
            (static_cast<uint8_t>(mTableType) << TYPE_BITS) |
            nsDisplayItem::GetPerFrameKey();
   }
 
   bool IsInvalid(nsRect& aRect) const override;
 
   nsIFrame* FrameForInvalidation() const override { return mStyleFrame; }
 
-  bool HasDeletedFrame() const override {
-    return !mStyleFrame || nsDisplayBackgroundImage::HasDeletedFrame();
-  }
-
   void RemoveFrame(nsIFrame* aFrame) override {
     if (aFrame == mStyleFrame) {
       mStyleFrame = nullptr;
+      SetDeletedFrame();
     }
     nsDisplayBackgroundImage::RemoveFrame(aFrame);
   }
 
  protected:
   nsIFrame* StyleFrame() const override { return mStyleFrame; }
 
   nsIFrame* mStyleFrame;
@@ -4625,23 +4646,20 @@ class nsDisplayTableThemedBackground : p
 
   uint32_t GetPerFrameKey() const override {
     return (static_cast<uint8_t>(mTableType) << TYPE_BITS) |
            nsDisplayItem::GetPerFrameKey();
   }
 
   nsIFrame* FrameForInvalidation() const override { return mAncestorFrame; }
 
-  bool HasDeletedFrame() const override {
-    return !mAncestorFrame || nsDisplayThemedBackground::HasDeletedFrame();
-  }
-
   void RemoveFrame(nsIFrame* aFrame) override {
     if (aFrame == mAncestorFrame) {
       mAncestorFrame = nullptr;
+      SetDeletedFrame();
     }
     nsDisplayThemedBackground::RemoveFrame(aFrame);
   }
 
  protected:
   nsIFrame* StyleFrame() const override { return mAncestorFrame; }
   nsIFrame* mAncestorFrame;
   TableType mTableType;
@@ -4805,23 +4823,20 @@ class nsDisplayTableBackgroundColor : pu
       mAncestorFrame->RemoveDisplayItem(this);
     }
   }
 
   NS_DISPLAY_DECL_NAME("TableBackgroundColor", TYPE_TABLE_BACKGROUND_COLOR)
 
   nsIFrame* FrameForInvalidation() const override { return mAncestorFrame; }
 
-  bool HasDeletedFrame() const override {
-    return !mAncestorFrame || nsDisplayBackgroundColor::HasDeletedFrame();
-  }
-
   void RemoveFrame(nsIFrame* aFrame) override {
     if (aFrame == mAncestorFrame) {
       mAncestorFrame = nullptr;
+      SetDeletedFrame();
     }
     nsDisplayBackgroundColor::RemoveFrame(aFrame);
   }
 
   uint32_t GetPerFrameKey() const override {
     return (static_cast<uint8_t>(mTableType) << TYPE_BITS) |
            nsDisplayItem::GetPerFrameKey();
   }
@@ -5627,23 +5642,20 @@ class nsDisplayTableBlendMode : public n
   NS_DISPLAY_DECL_NAME("TableBlendMode", TYPE_TABLE_BLEND_MODE)
 
   nsDisplayWrapList* Clone(nsDisplayListBuilder* aBuilder) const override {
     return MakeDisplayItem<nsDisplayTableBlendMode>(aBuilder, *this);
   }
 
   nsIFrame* FrameForInvalidation() const override { return mAncestorFrame; }
 
-  bool HasDeletedFrame() const override {
-    return !mAncestorFrame || nsDisplayBlendMode::HasDeletedFrame();
-  }
-
   void RemoveFrame(nsIFrame* aFrame) override {
     if (aFrame == mAncestorFrame) {
       mAncestorFrame = nullptr;
+      SetDeletedFrame();
     }
     nsDisplayBlendMode::RemoveFrame(aFrame);
   }
 
   uint32_t GetPerFrameKey() const override {
     return (mIndex << (TYPE_BITS +
                        static_cast<uint8_t>(TableTypeBits::COUNT))) |
            (static_cast<uint8_t>(mTableType) << TYPE_BITS) |
@@ -5734,23 +5746,20 @@ class nsDisplayTableBlendContainer : pub
   NS_DISPLAY_DECL_NAME("TableBlendContainer", TYPE_TABLE_BLEND_CONTAINER)
 
   nsDisplayWrapList* Clone(nsDisplayListBuilder* aBuilder) const override {
     return MakeDisplayItem<nsDisplayTableBlendContainer>(aBuilder, *this);
   }
 
   nsIFrame* FrameForInvalidation() const override { return mAncestorFrame; }
 
-  bool HasDeletedFrame() const override {
-    return !mAncestorFrame || nsDisplayBlendContainer::HasDeletedFrame();
-  }
-
   void RemoveFrame(nsIFrame* aFrame) override {
     if (aFrame == mAncestorFrame) {
       mAncestorFrame = nullptr;
+      SetDeletedFrame();
     }
     nsDisplayBlendContainer::RemoveFrame(aFrame);
   }
 
   uint32_t GetPerFrameKey() const override {
     return (static_cast<uint8_t>(mTableType) << TYPE_BITS) |
            nsDisplayItem::GetPerFrameKey();
   }
@@ -5967,17 +5976,16 @@ class nsDisplaySubDocument : public nsDi
   nsRegion GetOpaqueRegion(nsDisplayListBuilder* aBuilder,
                            bool* aSnap) const override;
 
   mozilla::UniquePtr<ScrollMetadata> ComputeScrollMetadata(
       LayerManager* aLayerManager,
       const ContainerLayerParameters& aContainerParameters);
 
   nsIFrame* FrameForInvalidation() const override;
-  bool HasDeletedFrame() const override;
   void RemoveFrame(nsIFrame* aFrame) override;
 
   void Disown();
 
  protected:
   ViewID mScrollParentId;
   bool mForceDispatchToContentRegion;
   bool mShouldFlatten;
@@ -6149,23 +6157,20 @@ class nsDisplayTableFixedPosition : publ
   NS_DISPLAY_DECL_NAME("TableFixedPosition", TYPE_TABLE_FIXED_POSITION)
 
   nsDisplayWrapList* Clone(nsDisplayListBuilder* aBuilder) const override {
     return MakeDisplayItem<nsDisplayTableFixedPosition>(aBuilder, *this);
   }
 
   nsIFrame* FrameForInvalidation() const override { return mAncestorFrame; }
 
-  bool HasDeletedFrame() const override {
-    return !mAncestorFrame || nsDisplayFixedPosition::HasDeletedFrame();
-  }
-
   void RemoveFrame(nsIFrame* aFrame) override {
     if (aFrame == mAncestorFrame) {
       mAncestorFrame = nullptr;
+      SetDeletedFrame();
     }
     nsDisplayFixedPosition::RemoveFrame(aFrame);
   }
 
   uint32_t GetPerFrameKey() const override {
     return (mIndex << (TYPE_BITS +
                        static_cast<uint8_t>(TableTypeBits::COUNT))) |
            (static_cast<uint8_t>(mTableType) << TYPE_BITS) |
--- a/layout/style/nsCSSPropertyID.h.in
+++ b/layout/style/nsCSSPropertyID.h.in
@@ -30,16 +30,19 @@ enum nsCSSPropertyID {
   // property.
   eCSSPropertyExtra_no_properties,
   eCSSPropertyExtra_all_properties,
 
   // Extra value to represent custom properties (--*).
   eCSSPropertyExtra_variable,
 };
 
+// MOZ_DBG support is defined in nsCSSProps.h since it depends on
+// nsCSSProps::GetStringValue
+
 const nsCSSPropertyID
   eCSSProperty_COUNT_no_shorthands = $longhand_count;
 const nsCSSPropertyID
   eCSSProperty_COUNT = $shorthand_count;
 const nsCSSPropertyID
   eCSSProperty_COUNT_with_aliases = eCSSPropertyExtra_no_properties;
 
 namespace mozilla {
--- a/layout/style/nsCSSPropertyIDSet.h
+++ b/layout/style/nsCSSPropertyIDSet.h
@@ -2,25 +2,26 @@
  * 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/. */
 
 /* bit vectors for sets of CSS properties */
 
 #ifndef nsCSSPropertyIDSet_h__
 #define nsCSSPropertyIDSet_h__
 
-#include "mozilla/ArrayUtils.h"
+#include <initializer_list>
+#include <limits.h>  // for CHAR_BIT
+#include <ostream>
 
-#include "nsCSSPropertyID.h"
-#include <limits.h>  // for CHAR_BIT
-#include <initializer_list>
-
+#include "mozilla/ArrayUtils.h"
 // For COMPOSITOR_ANIMATABLE_PROPERTY_LIST and
 // COMPOSITOR_ANIMATABLE_PROPERTY_LIST_LENGTH
 #include "mozilla/CompositorAnimatableProperties.h"
+#include "nsCSSProps.h"  // For operator<< for nsCSSPropertyID
+#include "nsCSSPropertyID.h"
 
 /**
  * nsCSSPropertyIDSet maintains a set of non-shorthand CSS properties.  In
  * other words, for each longhand CSS property we support, it has a bit
  * for whether that property is in the set.
  */
 class nsCSSPropertyIDSet {
  public:
@@ -183,13 +184,102 @@ class nsCSSPropertyIDSet {
   }
   bool HasPropertyAt(size_t aChunk, size_t aBit) const {
     return (mProperties[aChunk] & (property_set_type(1) << aBit)) != 0;
   }
   static nsCSSPropertyID CSSPropertyAt(size_t aChunk, size_t aBit) {
     return nsCSSPropertyID(aChunk * kBitsInChunk + aBit);
   }
 
+  // Iterator for use in range-based for loops
+  class Iterator {
+   public:
+    Iterator(Iterator&& aOther)
+        : mPropertySet(aOther.mPropertySet),
+          mChunk(aOther.mChunk),
+          mBit(aOther.mBit) {}
+
+    static Iterator BeginIterator(const nsCSSPropertyIDSet& aPropertySet) {
+      Iterator result(aPropertySet);
+
+      // Search for the first property.
+      // Unsigned integer overflow is defined so the following is safe.
+      result.mBit = -1;
+      ++result;
+
+      return result;
+    }
+
+    static Iterator EndIterator(const nsCSSPropertyIDSet& aPropertySet) {
+      Iterator result(aPropertySet);
+      result.mChunk = kChunkCount;
+      result.mBit = 0;
+      return result;
+    }
+
+    bool operator!=(const Iterator& aOther) const {
+      return mChunk != aOther.mChunk || mBit != aOther.mBit;
+    }
+
+    Iterator& operator++() {
+      MOZ_ASSERT(mChunk < kChunkCount, "Should not iterate beyond end");
+
+      do {
+        mBit++;
+      } while (mBit < kBitsInChunk &&
+               !mPropertySet.HasPropertyAt(mChunk, mBit));
+      if (mBit != kBitsInChunk) {
+        return *this;
+      }
+
+      do {
+        mChunk++;
+      } while (mChunk < kChunkCount &&
+               !mPropertySet.HasPropertyInChunk(mChunk));
+      mBit = 0;
+      if (mChunk != kChunkCount) {
+        while (mBit < kBitsInChunk &&
+               !mPropertySet.HasPropertyAt(mChunk, mBit)) {
+          mBit++;
+        }
+      }
+
+      return *this;
+    }
+
+    nsCSSPropertyID operator*() {
+      MOZ_ASSERT(mChunk < kChunkCount, "Should not dereference beyond end");
+      return nsCSSPropertyIDSet::CSSPropertyAt(mChunk, mBit);
+    }
+
+   private:
+    explicit Iterator(const nsCSSPropertyIDSet& aPropertySet)
+        : mPropertySet(aPropertySet) {}
+
+    Iterator() = delete;
+    Iterator(const Iterator&) = delete;
+    Iterator& operator=(const Iterator&) = delete;
+    Iterator& operator=(const Iterator&&) = delete;
+
+    const nsCSSPropertyIDSet& mPropertySet;
+    size_t mChunk = 0;
+    size_t mBit = 0;
+  };
+
+  Iterator begin() const { return Iterator::BeginIterator(*this); }
+  Iterator end() const { return Iterator::EndIterator(*this); }
+
  private:
   property_set_type mProperties[kChunkCount];
 };
 
+// MOZ_DBG support
+
+inline std::ostream& operator<<(std::ostream& aOut,
+                                const nsCSSPropertyIDSet& aPropertySet) {
+  AutoTArray<nsCSSPropertyID, 16> properties;
+  for (nsCSSPropertyID property : aPropertySet) {
+    properties.AppendElement(property);
+  }
+  return aOut << properties;
+}
+
 #endif /* !defined(nsCSSPropertyIDSet_h__) */
--- a/layout/style/nsCSSProps.h
+++ b/layout/style/nsCSSProps.h
@@ -8,17 +8,19 @@
  * methods for dealing with CSS properties and tables of the keyword
  * values they accept
  */
 
 #ifndef nsCSSProps_h___
 #define nsCSSProps_h___
 
 #include <limits>
+#include <ostream>
 #include <type_traits>
+
 #include "nsString.h"
 #include "nsCSSPropertyID.h"
 #include "nsStyleStructFwd.h"
 #include "nsCSSKeywords.h"
 #include "mozilla/CSSEnabledState.h"
 #include "mozilla/CSSPropFlags.h"
 #include "mozilla/UseCounter.h"
 #include "mozilla/EnumTypeTraits.h"
@@ -307,9 +309,15 @@ class nsCSSProps {
   static const KTableEntry kLineHeightKTable[];
   static const KTableEntry kTextAlignKTable[];
   static const KTableEntry kTextDecorationStyleKTable[];
   static const KTableEntry kTextEmphasisStyleShapeKTable[];
   static const KTableEntry kTextOverflowKTable[];
   static const KTableEntry kVerticalAlignKTable[];
 };
 
+// MOZ_DBG support for nsCSSPropertyID
+
+inline std::ostream& operator<<(std::ostream& aOut, nsCSSPropertyID aProperty) {
+  return aOut << nsCSSProps::GetStringValue(aProperty);
+}
+
 #endif /* nsCSSProps_h___ */
--- a/layout/svg/SVGTextFrame.cpp
+++ b/layout/svg/SVGTextFrame.cpp
@@ -2856,17 +2856,17 @@ void nsDisplaySVGText::HitTest(nsDisplay
   nsIFrame* target = frame->GetFrameForPoint(userSpacePt);
   if (target) {
     aOutFrames->AppendElement(target);
   }
 }
 
 void nsDisplaySVGText::Paint(nsDisplayListBuilder* aBuilder, gfxContext* aCtx) {
   DrawTargetAutoDisableSubpixelAntialiasing disable(aCtx->GetDrawTarget(),
-                                                    mDisableSubpixelAA);
+                                                    IsSubpixelAADisabled());
 
   uint32_t appUnitsPerDevPixel = mFrame->PresContext()->AppUnitsPerDevPixel();
 
   // ToReferenceFrame includes our mRect offset, but painting takes
   // account of that too. To avoid double counting, we subtract that
   // here.
   nsPoint offset = ToReferenceFrame() - mFrame->GetPosition();
 
--- a/layout/xul/nsTextBoxFrame.cpp
+++ b/layout/xul/nsTextBoxFrame.cpp
@@ -273,17 +273,17 @@ static void PaintTextShadowCallback(gfxC
                                     const nscolor& aShadowColor, void* aData) {
   reinterpret_cast<nsDisplayXULTextBox*>(aData)->PaintTextToContext(
       aCtx, aShadowOffset, &aShadowColor);
 }
 
 void nsDisplayXULTextBox::Paint(nsDisplayListBuilder* aBuilder,
                                 gfxContext* aCtx) {
   DrawTargetAutoDisableSubpixelAntialiasing disable(aCtx->GetDrawTarget(),
-                                                    mDisableSubpixelAA);
+                                                    IsSubpixelAADisabled());
 
   // Paint the text shadow before doing any foreground stuff
   nsRect drawRect =
       static_cast<nsTextBoxFrame*>(mFrame)->mTextDrawRect + ToReferenceFrame();
   nsLayoutUtils::PaintTextShadow(mFrame, aCtx, drawRect, GetPaintRect(),
                                  mFrame->StyleColor()->mColor.ToColor(),
                                  PaintTextShadowCallback, (void*)this);
 
--- a/layout/xul/tree/nsTreeBodyFrame.cpp
+++ b/layout/xul/tree/nsTreeBodyFrame.cpp
@@ -2493,17 +2493,17 @@ class nsDisplayTreeBody final : public n
     nsDisplayItem::ComputeInvalidationRegion(aBuilder, aGeometry,
                                              aInvalidRegion);
   }
 
   virtual void Paint(nsDisplayListBuilder* aBuilder,
                      gfxContext* aCtx) override {
     MOZ_ASSERT(aBuilder);
     DrawTargetAutoDisableSubpixelAntialiasing disable(aCtx->GetDrawTarget(),
-                                                      mDisableSubpixelAA);
+                                                      IsSubpixelAADisabled());
 
     ImgDrawResult result = static_cast<nsTreeBodyFrame*>(mFrame)->PaintTreeBody(
         *aCtx, GetPaintRect(), ToReferenceFrame(), aBuilder);
 
     nsDisplayItemGenericImageGeometry::UpdateDrawResult(this, result);
   }
 
   NS_DISPLAY_DECL_NAME("XULTreeBody", TYPE_XUL_TREE_BODY)
--- a/toolkit/actors/PictureInPictureChild.jsm
+++ b/toolkit/actors/PictureInPictureChild.jsm
@@ -2,74 +2,51 @@
 /* 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";
 
 var EXPORTED_SYMBOLS = ["PictureInPictureChild", "PictureInPictureToggleChild"];
 
 const {ActorChild} = ChromeUtils.import("resource://gre/modules/ActorChild.jsm");
+const {XPCOMUtils} = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
 
 ChromeUtils.defineModuleGetter(this, "DeferredTask",
   "resource://gre/modules/DeferredTask.jsm");
-ChromeUtils.defineModuleGetter(this, "DOMLocalization",
-  "resource://gre/modules/DOMLocalization.jsm");
 ChromeUtils.defineModuleGetter(this, "Services",
   "resource://gre/modules/Services.jsm");
 
-const TOGGLE_STYLESHEET = "chrome://global/skin/pictureinpicture/toggle.css";
-const TOGGLE_ID = "picture-in-picture-toggle";
-const FLYOUT_TOGGLE_ID = "picture-in-picture-flyout-toggle";
-const FLYOUT_TOGGLE_CONTAINER = "picture-in-picture-flyout-container";
+XPCOMUtils.defineLazyGlobalGetters(this, ["InspectorUtils"]);
+
 const TOGGLE_ENABLED_PREF =
   "media.videocontrols.picture-in-picture.video-toggle.enabled";
-const FLYOUT_ENABLED_PREF =
-  "media.videocontrols.picture-in-picture.video-toggle.flyout-enabled";
-const FLYOUT_WAIT_MS_PREF =
-  "media.videocontrols.picture-in-picture.video-toggle.flyout-wait-ms";
-const FLYOUT_ANIMATION_RUNTIME_MS = 400;
 const MOUSEMOVE_PROCESSING_DELAY_MS = 50;
 
 // A weak reference to the most recent <video> in this content
 // process that is being viewed in Picture-in-Picture.
 var gWeakVideo = null;
 // A weak reference to the content window of the most recent
 // Picture-in-Picture window for this content process.
 var gWeakPlayerContent = null;
-// A process-global Promise that's set the first time the string for the
-// flyout toggle label is requested from Fluent.
-var gFlyoutLabelPromise = null;
-// A process-global for the width of the toggle icon. We stash this here after
-// computing it the first time to avoid repeatedly flushing styles.
-var gToggleWidth = 0;
 
 /**
  * The PictureInPictureToggleChild is responsible for displaying the overlaid
  * Picture-in-Picture toggle over top of <video> elements that the mouse is
  * hovering.
- *
- * It's also responsible for showing the "flyout" version of the toggle, which
- * currently displays on the first visible video per page.
  */
 class PictureInPictureToggleChild extends ActorChild {
   constructor(dispatcher) {
     super(dispatcher);
     // We need to maintain some state about various things related to the
     // Picture-in-Picture toggles - however, for now, the same
     // PictureInPictureToggleChild might be re-used for different documents.
     // We keep the state stashed inside of this WeakMap, keyed on the document
     // itself.
     this.weakDocStates = new WeakMap();
     this.toggleEnabled = Services.prefs.getBoolPref(TOGGLE_ENABLED_PREF);
-    this.flyoutEnabled = Services.prefs.getBoolPref(FLYOUT_ENABLED_PREF);
-    this.flyoutWaitMs = Services.prefs.getIntPref(FLYOUT_WAIT_MS_PREF);
-
-    this.l10n = new DOMLocalization([
-      "toolkit/global/videocontrols.ftl",
-    ]);
   }
 
   /**
    * Returns the state for the current document referred to via
    * this.content.document. If no such state exists, creates it, stores it
    * and returns it.
    */
   get docState() {
@@ -79,64 +56,42 @@ class PictureInPictureToggleChild extend
         // A reference to the IntersectionObserver that's monitoring for videos
         // to become visible.
         intersectionObserver: null,
         // A WeakSet of videos that are supposedly visible, according to the
         // IntersectionObserver.
         weakVisibleVideos: new WeakSet(),
         // The number of videos that are supposedly visible, according to the
         // IntersectionObserver
-        visibleVideos: 0,
+        visibleVideosCount: 0,
         // The DeferredTask that we'll arm every time a mousemove event occurs
         // on a page where we have one or more visible videos.
         mousemoveDeferredTask: null,
         // A weak reference to the last video we displayed the toggle over.
         weakOverVideo: null,
-        // A reference to the AnonymousContent returned after inserting the
-        // small toggle.
-        pipToggle: null,
-        // A reference to the AnonymousContent returned after inserting the
-        // flyout toggle.
-        flyoutToggle: null,
       };
       this.weakDocStates.set(this.content.document, state);
     }
 
     return state;
   }
 
   handleEvent(event) {
     switch (event.type) {
       case "canplay": {
         if (this.toggleEnabled &&
             event.target instanceof this.content.HTMLVideoElement &&
+            !event.target.controls &&
             event.target.ownerDocument == this.content.document) {
           this.registerVideo(event.target);
         }
         break;
       }
-      case "click": {
-        let state = this.docState;
-        let clickedFlyout = state.flyoutToggle &&
-          state.flyoutToggle.getTargetIdForEvent(event) == FLYOUT_TOGGLE_ID;
-        let clickedToggle = state.pipToggle &&
-          state.pipToggle.getTargetIdForEvent(event) == TOGGLE_ID;
-
-        if (clickedFlyout || clickedToggle) {
-          let video = state.weakOverVideo && state.weakOverVideo.get();
-          if (video) {
-            let pipEvent =
-              new this.content.CustomEvent("MozTogglePictureInPicture", {
-                bubbles: true,
-              });
-            video.dispatchEvent(pipEvent);
-            this.hideFlyout();
-            this.onMouseLeaveVideo(video);
-          }
-        }
+      case "mousedown": {
+        this.onMouseDown(event);
         break;
       }
       case "mousemove": {
         this.onMouseMove(event);
         break;
       }
     }
   }
@@ -147,17 +102,17 @@ class PictureInPictureToggleChild extend
    *
    * @param {Element} video The <video> element to register.
    */
   registerVideo(video) {
     let state = this.docState;
     if (!state.intersectionObserver) {
       let fn = this.onIntersection.bind(this);
       state.intersectionObserver = new this.content.IntersectionObserver(fn, {
-        threshold: [0.0, 1.0],
+        threshold: [0.0, 0.5],
       });
     }
 
     state.intersectionObserver.observe(video);
   }
 
   /**
    * Called by the IntersectionObserver callback once a video becomes visible.
@@ -166,66 +121,52 @@ class PictureInPictureToggleChild extend
    * that means that the entirety of the video must be in the viewport.
    *
    * @param {IntersectionEntry} intersectionEntry An IntersectionEntry passed to
    * the IntersectionObserver callback.
    * @return bool Whether or not we should start tracking mousemove events for
    * this registered video.
    */
   worthTracking(intersectionEntry) {
-    let video = intersectionEntry.target;
-    let rect = video.ownerGlobal.windowUtils.getBoundsWithoutFlushing(video);
-    let intRect = intersectionEntry.intersectionRect;
-
-    return intersectionEntry.isIntersecting &&
-           rect.width == intRect.width &&
-           rect.height == intRect.height;
+    return intersectionEntry.isIntersecting;
   }
 
   /**
    * Called by the IntersectionObserver once a video crosses one of the
    * thresholds dictated by the IntersectionObserver configuration.
    *
    * @param {Array<IntersectionEntry>} A collection of one or more
    * IntersectionEntry's for <video> elements that might have entered or exited
    * the viewport.
    */
   onIntersection(entries) {
     // The IntersectionObserver will also fire when a previously intersecting
     // element is removed from the DOM. We know, however, that the node is
     // still alive and referrable from the WeakSet because the
     // IntersectionObserverEntry holds a strong reference to the video.
     let state = this.docState;
-    let oldVisibleVideos = state.visibleVideos;
+    let oldVisibleVideosCount = state.visibleVideosCount;
     for (let entry of entries) {
       let video = entry.target;
       if (this.worthTracking(entry)) {
         if (!state.weakVisibleVideos.has(video)) {
           state.weakVisibleVideos.add(video);
-          state.visibleVideos++;
-
-          // The very first video that we notice is worth tracking, we'll show
-          // the flyout toggle on.
-          if (this.flyoutEnabled) {
-            this.content.requestIdleCallback(() => {
-              this.maybeShowFlyout(video);
-            });
-          }
+          state.visibleVideosCount++;
         }
       } else if (state.weakVisibleVideos.has(video)) {
         state.weakVisibleVideos.delete(video);
-        state.visibleVideos--;
+        state.visibleVideosCount--;
       }
     }
 
-    if (!oldVisibleVideos && state.visibleVideos) {
+    if (!oldVisibleVideosCount && state.visibleVideosCount) {
       this.content.requestIdleCallback(() => {
         this.beginTrackingMouseOverVideos();
       });
-    } else if (oldVisibleVideos && !state.visibleVideos) {
+    } else if (oldVisibleVideosCount && !state.visibleVideosCount) {
       this.content.requestIdleCallback(() => {
         this.stopTrackingMouseOverVideos();
       });
     }
   }
 
   /**
    * One of the challenges of displaying this toggle is that many sites put
@@ -243,40 +184,93 @@ class PictureInPictureToggleChild extend
   beginTrackingMouseOverVideos() {
     let state = this.docState;
     if (!state.mousemoveDeferredTask) {
       state.mousemoveDeferredTask = new DeferredTask(() => {
         this.checkLastMouseMove();
       }, MOUSEMOVE_PROCESSING_DELAY_MS);
     }
     this.content.document.addEventListener("mousemove", this,
-                                           { mozSystemGroup: true });
-    this.content.document.addEventListener("click", this,
-                                           { mozSystemGroup: true });
+                                           { mozSystemGroup: true, capture: true });
+    // We want to try to cancel the mouse events from continuing
+    // on into content if the user has clicked on the toggle, so
+    // we don't use the mozSystemGroup here.
+    this.content.document.addEventListener("mousedown", this,
+                                           { capture: true });
   }
 
   /**
    * If we no longer have any interesting videos in the viewport, we deregister
    * the mousemove and click listeners, and also remove any toggles that might
    * be on the page still.
    */
   stopTrackingMouseOverVideos() {
     let state = this.docState;
     state.mousemoveDeferredTask.disarm();
     this.content.document.removeEventListener("mousemove", this,
-                                              { mozSystemGroup: true });
-    this.content.document.removeEventListener("click", this,
-                                              { mozSystemGroup: true });
+                                              { mozSystemGroup: true, capture: true });
+    this.content.document.removeEventListener("mousedown", this,
+                                              { capture: true });
     let oldOverVideo = state.weakOverVideo && state.weakOverVideo.get();
     if (oldOverVideo) {
       this.onMouseLeaveVideo(oldOverVideo);
     }
   }
 
   /**
+   * If we're tracking <video> elements, this mousedown event handler is run anytime
+   * a mousedown occurs on the document. This function is responsible for checking
+   * if the user clicked on the Picture-in-Picture toggle. It does this by first
+   * checking if the video is visible beneath the point that was clicked. Then
+   * it tests whether or not the mousedown occurred within the rectangle of the
+   * toggle. If so, the event's default behaviour and propagation are stopped,
+   * and Picture-in-Picture is triggered.
+   *
+   * @param {Event} event The mousemove event.
+   */
+  onMouseDown(event) {
+    let state = this.docState;
+    let video = state.weakOverVideo && state.weakOverVideo.get();
+    if (!video) {
+      return;
+    }
+
+    let shadowRoot = video.openOrClosedShadowRoot;
+    if (!shadowRoot) {
+      return;
+    }
+
+    let { clientX, clientY } = event;
+    let winUtils = this.content.windowUtils;
+    // We use winUtils.nodesFromRect instead of document.elementsFromPoint,
+    // since document.elementsFromPoint always flushes layout. The 1's in that
+    // function call are for the size of the rect that we want, which is 1x1.
+    //
+    // We pass the aOnlyVisible boolean argument to check that the video isn't
+    // occluded by anything visible at the point of mousedown. If it is, we'll
+    // ignore the mousedown.
+    let elements = winUtils.nodesFromRect(clientX, clientY, 1, 1, 1, 1, true,
+                                          false, true /* aOnlyVisible */);
+    if (!Array.from(elements).includes(video)) {
+      return;
+    }
+
+    let toggle = shadowRoot.getElementById("pictureInPictureToggleButton");
+    if (this.isMouseOverToggle(toggle, event)) {
+      event.preventDefault();
+      event.stopPropagation();
+      let pipEvent =
+        new this.content.CustomEvent("MozTogglePictureInPicture", {
+          bubbles: true,
+        });
+      video.dispatchEvent(pipEvent);
+    }
+  }
+
+  /**
    * Called for each mousemove event when we're tracking those events to
    * determine if the cursor is hovering over a <video>.
    *
    * @param {Event} event The mousemove event.
    */
   onMouseMove(event) {
     let state = this.docState;
     state.lastMouseMoveEvent = event;
@@ -293,254 +287,133 @@ class PictureInPictureToggleChild extend
     let state = this.docState;
     let event = state.lastMouseMoveEvent;
     let { clientX, clientY } = event;
     let winUtils = this.content.windowUtils;
     // We use winUtils.nodesFromRect instead of document.elementsFromPoint,
     // since document.elementsFromPoint always flushes layout. The 1's in that
     // function call are for the size of the rect that we want, which is 1x1.
     let elements = winUtils.nodesFromRect(clientX, clientY, 1, 1, 1, 1, true,
-                                          false);
+                                          false, false);
 
     for (let element of elements) {
       if (state.weakVisibleVideos.has(element) &&
           !element.isCloningElementVisually) {
-        this.onMouseOverVideo(element);
+        this.onMouseOverVideo(element, event);
         return;
       }
     }
 
     let oldOverVideo = state.weakOverVideo && state.weakOverVideo.get();
     if (oldOverVideo) {
       this.onMouseLeaveVideo(oldOverVideo);
     }
   }
 
   /**
    * Called once it has been determined that the mouse is overtop of a video
    * that is in the viewport.
    *
    * @param {Element} video The video the mouse is over.
    */
-  onMouseOverVideo(video) {
+  onMouseOverVideo(video, event) {
     let state = this.docState;
     let oldOverVideo = state.weakOverVideo && state.weakOverVideo.get();
-    if (oldOverVideo && oldOverVideo == video) {
+    let shadowRoot = video.openOrClosedShadowRoot;
+
+    // It seems from automated testing that if it's still very early on in the
+    // lifecycle of a <video> element, it might not yet have a shadowRoot,
+    // in which case, we can bail out here early.
+    if (!shadowRoot) {
+      if (oldOverVideo) {
+        // We also clear the hover state on the old video we were hovering,
+        // if there was one.
+        this.onMouseLeaveVideo(oldOverVideo);
+      }
+
       return;
     }
 
+    let toggle = shadowRoot.getElementById("pictureInPictureToggleButton");
+
+    if (oldOverVideo) {
+      if (oldOverVideo == video) {
+        // If we're still hovering the old video, we might have entered or
+        // exited the toggle region.
+        this.checkHoverToggle(toggle, event);
+        return;
+      }
+
+      // We had an old video that we were hovering, and we're not hovering
+      // it anymore. Let's leave it.
+      this.onMouseLeaveVideo(oldOverVideo);
+    }
+
     state.weakOverVideo = Cu.getWeakReference(video);
-    this.moveToggleToVideo(video);
+    let controlsOverlay = shadowRoot.querySelector(".controlsOverlay");
+    InspectorUtils.addPseudoClassLock(controlsOverlay, ":hover");
+
+    // Now that we're hovering the video, we'll check to see if we're
+    // hovering the toggle too.
+    this.checkHoverToggle(toggle, event);
+  }
+
+  /**
+   * Checks if a mouse event is happening over a toggle element. If it is,
+   * sets the :hover pseudoclass on it. Otherwise, it clears the :hover
+   * pseudoclass.
+   *
+   * @param {Element} toggle The Picture-in-Picture toggle to check.
+   * @param {MouseEvent} event A MouseEvent to test.
+   */
+  checkHoverToggle(toggle, event) {
+    if (this.isMouseOverToggle(toggle, event)) {
+      InspectorUtils.addPseudoClassLock(toggle, ":hover");
+    } else {
+      InspectorUtils.removePseudoClassLock(toggle, ":hover");
+    }
   }
 
   /**
    * Called once it has been determined that the mouse is no longer overlapping
    * a video that we'd previously called onMouseOverVideo with.
    *
    * @param {Element} video The video that the mouse left.
    */
   onMouseLeaveVideo(video) {
     let state = this.docState;
-    state.weakOverVideo = null;
-    state.pipToggle.setAttributeForElement(TOGGLE_ID, "hidden", "true");
-  }
+    let shadowRoot = video.openOrClosedShadowRoot;
 
-  /**
-   * The toggle is injected as AnonymousContent that is positioned absolutely.
-   * This method takes the <video> that we want to display the toggle on and
-   * calculates where exactly we need to position the AnonymousContent in
-   * absolute coordinates.
-   *
-   * @param {Element} video The video to display the toggle on.
-   * @param {AnonymousContent} anonymousContent The anonymousContent associated
-   * with the toggle about to be shown.
-   * @param {String} toggleID The ID of the toggle element with the CSS
-   * variables defining the toggle width and padding.
-   *
-   * @return {Object} with the following properties:
-   *   {Number} top The top / y coordinate.
-   *   {Number} left The left / x coordinate.
-   *   {Number} width The width of the toggle icon, including padding.
-   */
-  calculateTogglePosition(video, anonymousContent, toggleID) {
-    let winUtils = this.content.windowUtils;
-
-    let scrollX = {}, scrollY = {};
-    winUtils.getScrollXY(false, scrollX, scrollY);
-
-    let rect = winUtils.getBoundsWithoutFlushing(video);
-
-    // For now, using AnonymousContent.getComputedStylePropertyValue causes
-    // a style flush, so we'll cache the value in this content process the
-    // first time we read it. See bug 1541207.
-    if (!gToggleWidth) {
-      let widthStr = anonymousContent.getComputedStylePropertyValue(toggleID,
-        "--pip-toggle-icon-width-height");
-      let paddingStr = anonymousContent.getComputedStylePropertyValue(toggleID,
-        "--pip-toggle-padding");
-      let iconWidth = parseInt(widthStr, 0);
-      let iconPadding = parseInt(paddingStr, 0);
-      gToggleWidth = iconWidth + (2 * iconPadding);
+    if (shadowRoot) {
+      let controlsOverlay = shadowRoot.querySelector(".controlsOverlay");
+      let toggle = shadowRoot.getElementById("pictureInPictureToggleButton");
+      InspectorUtils.removePseudoClassLock(controlsOverlay, ":hover");
+      InspectorUtils.removePseudoClassLock(toggle, ":hover");
     }
 
-    let originY = rect.top + scrollY.value;
-    let originX = rect.left + scrollX.value;
-
-    let top = originY + (rect.height / 2 - Math.round(gToggleWidth / 2));
-    let left = originX + (rect.width - gToggleWidth);
-
-    return { top, left, width: gToggleWidth };
-  }
-
-  /**
-   * Puts the small "Picture-in-Picture" toggle onto the passed in video.
-   *
-   * @param {Element} video The video to display the toggle on.
-   */
-  moveToggleToVideo(video) {
-    let state = this.docState;
-    let winUtils = this.content.windowUtils;
-
-    if (!state.pipToggle) {
-      try {
-        winUtils.loadSheetUsingURIString(TOGGLE_STYLESHEET,
-                                         winUtils.AGENT_SHEET);
-      } catch (e) {
-        // This method can fail with NS_ERROR_INVALID_ARG if the sheet is
-        // already loaded - for example, from the flyout toggle.
-        if (e.result != Cr.NS_ERROR_INVALID_ARG) {
-          throw e;
-        }
-      }
-      let toggle = this.content.document.createElement("button");
-      toggle.classList.add("picture-in-picture-toggle-button");
-      toggle.id = TOGGLE_ID;
-      let icon = this.content.document.createElement("div");
-      icon.classList.add("icon");
-      toggle.appendChild(icon);
-
-      state.pipToggle = this.content.document.insertAnonymousContent(toggle);
-    }
-
-    let { top, left } = this.calculateTogglePosition(video, state.pipToggle,
-                                                     TOGGLE_ID);
-
-    let styles = `
-      top: ${top}px;
-      left: ${left}px;
-    `;
-
-    let toggle = state.pipToggle;
-    toggle.setAttributeForElement(TOGGLE_ID, "style", styles);
-    // The toggle might have been hidden after a previous appearance.
-    toggle.removeAttributeForElement(TOGGLE_ID, "hidden");
+    state.weakOverVideo = null;
   }
 
   /**
-   * Lazy getter that returns a Promise that resolves to the flyout toggle
-   * label string. Sets a process-global variable to the Promise so that
-   * subsequent calls within the same process don't cause us to go through
-   * the Fluent look-up path again.
-   */
-  get flyoutLabel() {
-    if (gFlyoutLabelPromise) {
-      return gFlyoutLabelPromise;
-    }
-
-    gFlyoutLabelPromise =
-      this.l10n.formatValue("picture-in-picture-flyout-toggle");
-    return gFlyoutLabelPromise;
-  }
-
-  /**
-   * If configured to, will display the "Picture-in-Picture" flyout toggle on
-   * the passed-in video. This is an asynchronous function that handles the
-   * entire lifecycle of the flyout animation. If a flyout toggle has already
-   * been seen on this page, this function does nothing.
+   * Given a reference to a Picture-in-Picture toggle element, determines
+   * if a MouseEvent event is occurring within its bounds.
    *
-   * @param {Element} video The video to display the flyout on.
+   * @param {Element} toggle The Picture-in-Picture toggle.
+   * @param {MouseEvent} event A MouseEvent to test.
    *
-   * @return {Promise}
-   * @resolves {undefined} Once the flyout toggle animation has completed.
+   * @return {Boolean}
    */
-  async maybeShowFlyout(video) {
-    let state = this.docState;
-
-    if (state.flyoutToggle) {
-      return;
-    }
-
-    let winUtils = this.content.windowUtils;
-
-    try {
-      winUtils.loadSheetUsingURIString(TOGGLE_STYLESHEET, winUtils.AGENT_SHEET);
-    } catch (e) {
-      // This method can fail with NS_ERROR_INVALID_ARG if the sheet is
-      // already loaded.
-      if (e.result != Cr.NS_ERROR_INVALID_ARG) {
-        throw e;
-      }
-    }
-
-    let container = this.content.document.createElement("div");
-    container.id = FLYOUT_TOGGLE_CONTAINER;
-
-    let toggle = this.content.document.createElement("button");
-    toggle.classList.add("picture-in-picture-toggle-button");
-    toggle.id = FLYOUT_TOGGLE_ID;
-
-    let icon = this.content.document.createElement("div");
-    icon.classList.add("icon");
-    toggle.appendChild(icon);
-
-    let label = this.content.document.createElement("span");
-    label.classList.add("label");
-    label.textContent = await this.flyoutLabel;
-    toggle.appendChild(label);
-    container.appendChild(toggle);
-    state.flyoutToggle =
-      this.content.document.insertAnonymousContent(container);
-
-    let { top, left, width } =
-      this.calculateTogglePosition(video, state.flyoutToggle, FLYOUT_TOGGLE_ID);
-
-    let styles = `
-      top: ${top}px;
-      left: ${left}px;
-    `;
-
-    let flyout = state.flyoutToggle;
-    flyout.setAttributeForElement(FLYOUT_TOGGLE_CONTAINER, "style", styles);
-    let flyoutAnim = flyout.setAnimationForElement(FLYOUT_TOGGLE_ID, [
-      { transform: `translateX(calc(100% - ${width}px))`, opacity: "0.2" },
-      { transform: `translateX(calc(100% - ${width}px))`, opacity: "0.8" },
-      { transform: "translateX(0)", opacity: "1" },
-    ], FLYOUT_ANIMATION_RUNTIME_MS);
-
-    await flyoutAnim.finished;
-
-    await new Promise(resolve => this.content.setTimeout(resolve,
-                                                         this.flyoutWaitMs));
-
-    flyoutAnim.reverse();
-    await flyoutAnim.finished;
-
-    this.hideFlyout();
-  }
-
-  /**
-   * Once the flyout has finished animating, or Picture-in-Picture has been
-   * requested, this function can be called to hide it.
-   */
-  hideFlyout() {
-    let state = this.docState;
-    let flyout = state.flyoutToggle;
-    if (flyout) {
-      flyout.setAttributeForElement(FLYOUT_TOGGLE_CONTAINER, "hidden", "true");
-    }
+  isMouseOverToggle(toggle, event) {
+    let toggleRect =
+      toggle.ownerGlobal.windowUtils.getBoundsWithoutFlushing(toggle);
+    let { clientX, clientY } = event;
+    return clientX >= toggleRect.left &&
+           clientX <= toggleRect.right &&
+           clientY >= toggleRect.top &&
+           clientY <= toggleRect.bottom;
   }
 }
 
 class PictureInPictureChild extends ActorChild {
   static videoIsPlaying(video) {
     return !!(video.currentTime > 0 && !video.paused && !video.ended && video.readyState > 2);
   }
 
--- a/toolkit/actors/UAWidgetsChild.jsm
+++ b/toolkit/actors/UAWidgetsChild.jsm
@@ -9,16 +9,17 @@ var EXPORTED_SYMBOLS = ["UAWidgetsChild"
 const {ActorChild} = ChromeUtils.import("resource://gre/modules/ActorChild.jsm");
 const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
 
 class UAWidgetsChild extends ActorChild {
   constructor(dispatcher) {
     super(dispatcher);
 
     this.widgets = new WeakMap();
+    this.prefsCache = new Map();
   }
 
   handleEvent(aEvent) {
     switch (aEvent.type) {
       case "UAWidgetSetupOrChange":
         this.setupOrNotifyWidget(aEvent.target);
         break;
       case "UAWidgetTeardown":
@@ -44,21 +45,25 @@ class UAWidgetsChild extends ActorChild 
         Cu.reportError(ex);
       }
     }
   }
 
   setupWidget(aElement) {
     let uri;
     let widgetName;
+    let prefKeys = [];
     switch (aElement.localName) {
       case "video":
       case "audio":
         uri = "chrome://global/content/elements/videocontrols.js";
         widgetName = "VideoControlsWidget";
+        prefKeys = [
+          "media.videocontrols.picture-in-picture.video-toggle.enabled",
+        ];
         break;
       case "input":
         uri = "chrome://global/content/elements/datetimebox.js";
         widgetName = "DateTimeBoxWidget";
         break;
       case "embed":
       case "object":
         uri = "chrome://global/content/elements/pluginProblem.js";
@@ -84,17 +89,19 @@ class UAWidgetsChild extends ActorChild 
     let isSystemPrincipal = aElement.nodePrincipal.isSystemPrincipal;
     let sandbox = isSystemPrincipal ?
       Object.create(null) : Cu.getUAWidgetScope(aElement.nodePrincipal);
 
     if (!sandbox[widgetName]) {
       Services.scriptloader.loadSubScript(uri, sandbox);
     }
 
-    let widget = new sandbox[widgetName](shadowRoot);
+    let prefs = Cu.cloneInto(this.getPrefsForUAWidget(widgetName, prefKeys), sandbox);
+
+    let widget = new sandbox[widgetName](shadowRoot, prefs);
     if (!isSystemPrincipal) {
       widget = widget.wrappedJSObject;
     }
     this.widgets.set(aElement, widget);
     try {
       widget.onsetup();
     } catch (ex) {
       Cu.reportError(ex);
@@ -110,9 +117,37 @@ class UAWidgetsChild extends ActorChild 
       try {
         widget.destructor();
       } catch (ex) {
         Cu.reportError(ex);
       }
     }
     this.widgets.delete(aElement);
   }
+
+  getPrefsForUAWidget(aWidgetName, aPrefKeys) {
+    let result = this.prefsCache.get(aWidgetName);
+    if (result) {
+      return result;
+    }
+
+    result = {};
+    for (let key of aPrefKeys) {
+      switch (Services.prefs.getPrefType(key)) {
+        case Ci.nsIPrefBranch.PREF_BOOL: {
+          result[key] = Services.prefs.getBoolPref(key);
+          break;
+        }
+        case Ci.nsIPrefBranch.PREF_INT: {
+          result[key] = Services.prefs.getIntPref(key);
+          break;
+        }
+        case Ci.nsIPrefBranch.PREF_STRING: {
+          result[key] = Services.prefs.getStringPref(key);
+          break;
+        }
+      }
+    }
+
+    this.prefsCache.set(aWidgetName, result);
+    return result;
+  }
 }
--- a/toolkit/content/widgets/videocontrols.js
+++ b/toolkit/content/widgets/videocontrols.js
@@ -7,18 +7,19 @@
 // This is a UA widget. It runs in per-origin UA widget scope,
 // to be loaded by UAWidgetsChild.jsm.
 
 /*
  * This is the class of entry. It will construct the actual implementation
  * according to the value of the "controls" property.
  */
 this.VideoControlsWidget = class {
-  constructor(shadowRoot) {
+  constructor(shadowRoot, prefs) {
     this.shadowRoot = shadowRoot;
+    this.prefs = prefs;
     this.element = shadowRoot.host;
     this.document = this.element.ownerDocument;
     this.window = this.document.defaultView;
 
     this.isMobile = this.window.navigator.appVersion.includes("Android");
   }
 
   /*
@@ -40,39 +41,42 @@ this.VideoControlsWidget = class {
    * - With "controls" set, the VideoControlsImplWidget controls should load.
    * - Without it, on mobile, the NoControlsMobileImplWidget should load, so
    *   the user could see the click-to-play button when the video/audio is blocked.
    * - Without it, on desktop, the NoControlsPictureInPictureImpleWidget should load
    *   if the video is being viewed in Picture-in-Picture.
    */
   switchImpl() {
     let newImpl;
+    let pageURI = this.document.documentURI;
     if (this.element.controls) {
       newImpl = VideoControlsImplWidget;
     } else if (this.isMobile) {
       newImpl = NoControlsMobileImplWidget;
     } else if (VideoControlsWidget.isPictureInPictureVideo(this.element)) {
       newImpl = NoControlsPictureInPictureImplWidget;
+    } else if (pageURI.startsWith("http://") || pageURI.startsWith("https://")) {
+      newImpl = NoControlsDesktopImplWidget;
     }
 
     // Skip if we are asked to load the same implementation, and
     // the underlying element state hasn't changed in ways that we
     // care about. This can happen if the property is set again
     // without a value change.
     if (this.impl &&
         this.impl.constructor == newImpl &&
         this.impl.elementStateMatches(this.element)) {
       return;
     }
     if (this.impl) {
       this.impl.destructor();
       this.shadowRoot.firstChild.remove();
     }
     if (newImpl) {
-      this.impl = new newImpl(this.shadowRoot);
+      this.impl = new newImpl(this.shadowRoot, this.prefs);
       this.impl.onsetup();
     } else {
       this.impl = undefined;
     }
   }
 
   destructor() {
     if (!this.impl) {
@@ -84,18 +88,19 @@ this.VideoControlsWidget = class {
   }
 
   static isPictureInPictureVideo(someVideo) {
     return someVideo.isCloningElementVisually;
   }
 };
 
 this.VideoControlsImplWidget = class {
-  constructor(shadowRoot) {
+  constructor(shadowRoot, prefs) {
     this.shadowRoot = shadowRoot;
+    this.prefs = prefs;
     this.element = shadowRoot.host;
     this.document = this.element.ownerDocument;
     this.window = this.document.defaultView;
   }
 
   onsetup() {
     this.generateContent();
 
@@ -260,16 +265,20 @@ this.VideoControlsImplWidget = class {
           this.startFadeOut(this.clickToPlay, true);
           this.statusIcon.setAttribute("type", "error");
           this.updateErrorText();
           this.setupStatusFader(true);
         } else if (VideoControlsWidget.isPictureInPictureVideo(this.video)) {
           this.setShowPictureInPictureMessage(true);
         }
 
+        if (!this.pipToggleEnabled || this.isShowingPictureInPictureMessage) {
+          this.pictureInPictureToggleButton.setAttribute("hidden", true);
+        }
+
         let adjustableControls = [
           ...this.prioritizedControls,
           this.controlBar,
           this.clickToPlay,
         ];
 
         let throwOnGet = {
           get() {
@@ -662,16 +671,19 @@ this.VideoControlsImplWidget = class {
               case this.textTrackList:
                 const index = +aEvent.originalTarget.getAttribute("index");
                 this.changeTextTrack(index);
                 break;
               case this.videocontrols:
                 // Prevent any click event within media controls from dispatching through to video.
                 aEvent.stopPropagation();
                 break;
+              case this.pictureInPictureToggleButton:
+                this.video.togglePictureInPicture();
+                break;
             }
             break;
           case "dblclick":
             this.toggleFullscreen();
             break;
           case "resizevideocontrols":
             // Since this event come from the layout, this is the only place
             // we are sure of that probing into layout won't trigger or force
@@ -1938,23 +1950,28 @@ this.VideoControlsImplWidget = class {
           if (this.clickToPlay.hidden && !this.video.played.length && this.video.paused) {
             this.clickToPlay.hiddenByAdjustment = false;
           }
           this.clickToPlay.style.width = `${clickToPlayScaledSize}px`;
           this.clickToPlay.style.height = `${clickToPlayScaledSize}px`;
         }
       },
 
-      init(shadowRoot) {
+      get pipToggleEnabled() {
+        return this.prefs["media.videocontrols.picture-in-picture.video-toggle.enabled"];
+      },
+
+      init(shadowRoot, prefs) {
         this.shadowRoot = shadowRoot;
         this.video = this.installReflowCallValidator(shadowRoot.host);
         this.videocontrols = this.installReflowCallValidator(shadowRoot.firstChild);
         this.document = this.videocontrols.ownerDocument;
         this.window = this.document.defaultView;
         this.shadowRoot = shadowRoot;
+        this.prefs = prefs;
 
         this.controlsContainer = this.shadowRoot.getElementById("controlsContainer");
         this.statusIcon = this.shadowRoot.getElementById("statusIcon");
         this.controlBar = this.shadowRoot.getElementById("controlBar");
         this.playButton = this.shadowRoot.getElementById("playButton");
         this.controlBarSpacer = this.shadowRoot.getElementById("controlBarSpacer");
         this.muteButton = this.shadowRoot.getElementById("muteButton");
         this.volumeStack = this.shadowRoot.getElementById("volumeStack");
@@ -1970,16 +1987,18 @@ this.VideoControlsImplWidget = class {
         this.controlsOverlay = this.shadowRoot.getElementById("controlsOverlay");
         this.pictureInPictureOverlay = this.shadowRoot.getElementById("pictureInPictureOverlay");
         this.controlsSpacer = this.shadowRoot.getElementById("controlsSpacer");
         this.clickToPlay = this.shadowRoot.getElementById("clickToPlay");
         this.fullscreenButton = this.shadowRoot.getElementById("fullscreenButton");
         this.castingButton = this.shadowRoot.getElementById("castingButton");
         this.closedCaptionButton = this.shadowRoot.getElementById("closedCaptionButton");
         this.textTrackList = this.shadowRoot.getElementById("textTrackList");
+        this.pictureInPictureToggleButton =
+          this.shadowRoot.getElementById("pictureInPictureToggleButton");
 
         if (this.positionDurationBox) {
           this.durationSpan = this.positionDurationBox.getElementsByTagName("span")[0];
         }
 
         let isMobile = this.window.navigator.appVersion.includes("Android");
         if (isMobile) {
           this.controlsContainer.classList.add("mobile");
@@ -2057,16 +2076,18 @@ this.VideoControlsImplWidget = class {
           // isn't fired when the input value before/after dragging are the same. (bug 1328061)
           { el: this.scrubber, type: "mouseup" },
           { el: this.volumeControl, type: "input" },
           { el: this.video.textTracks, type: "addtrack" },
           { el: this.video.textTracks, type: "removetrack" },
           { el: this.video.textTracks, type: "change" },
 
           { el: this.video, type: "media-videoCasting", touchOnly: true },
+
+          { el: this.pictureInPictureToggleButton, type: "click" },
         ];
 
         for (let { el, type, nonTouchOnly = false, touchOnly = false,
                    mozSystemGroup = true, capture = false } of this.controlsEvents) {
           if ((this.isTouchControls && nonTouchOnly) ||
               (!this.isTouchControls && touchOnly)) {
             continue;
           }
@@ -2205,17 +2226,17 @@ this.VideoControlsImplWidget = class {
         // the controls to remain visible. this.controlsTimeout is a full
         // 5s, which feels too long after the transition.
         if (this.video.currentTime !== 0) {
           this.delayHideControls(this.Utils.HIDE_CONTROLS_TIMEOUT_MS);
         }
       },
     };
 
-    this.Utils.init(this.shadowRoot);
+    this.Utils.init(this.shadowRoot, this.prefs);
     if (this.Utils.isTouchControls) {
       this.TouchUtils.init(this.shadowRoot, this.Utils);
     }
     this.shadowRoot.firstChild.dispatchEvent(new this.window.CustomEvent("VideoBindingAttached"));
 
     this._setupEventListeners();
   }
 
@@ -2248,16 +2269,20 @@ this.VideoControlsImplWidget = class {
           </div>
 
           <div id="controlsOverlay" class="controlsOverlay stackItem" role="none">
             <div class="controlsSpacerStack">
               <div id="controlsSpacer" class="controlsSpacer stackItem" role="none"></div>
               <div id="clickToPlay" class="clickToPlay" hidden="true"></div>
             </div>
 
+            <button id="pictureInPictureToggleButton" class="pictureInPictureToggleButton">
+              <div id="pictureInPictureToggleIcon" class="pictureInPictureToggleIcon"></div>
+            </button>
+
             <div id="controlBar" class="controlBar" role="none" hidden="true">
               <button id="playButton"
                       class="button playButton"
                       playlabel="&playButton.playLabel;"
                       pauselabel="&playButton.pauseLabel;"
                       tabindex="-1"/>
               <div id="scrubberStack" class="scrubberStack progressContainer" role="none">
                 <div class="progressBackgroundBar stackItem" role="none">
@@ -2463,18 +2488,19 @@ this.NoControlsMobileImplWidget = class 
           </div>
         </div>
       </div>`, "application/xml");
     this.shadowRoot.importNodeAndAppendChildAt(this.shadowRoot, parserDoc.documentElement, true);
   }
 };
 
 this.NoControlsPictureInPictureImplWidget = class {
-  constructor(shadowRoot) {
+  constructor(shadowRoot, prefs) {
     this.shadowRoot = shadowRoot;
+    this.prefs = prefs;
     this.element = shadowRoot.host;
     this.document = this.element.ownerDocument;
     this.window = this.document.defaultView;
   }
 
   onsetup() {
     this.generateContent();
   }
@@ -2504,8 +2530,76 @@ this.NoControlsPictureInPictureImplWidge
             <span class="statusLabel" id="pictureInPicture">&status.pictureInPicture;</span>
           </div>
           <div class="controlsOverlay stackItem"></div>
         </div>
       </div>`, "application/xml");
     this.shadowRoot.importNodeAndAppendChildAt(this.shadowRoot, parserDoc.documentElement, true);
   }
 };
+
+this.NoControlsDesktopImplWidget = class {
+  constructor(shadowRoot, prefs) {
+    this.shadowRoot = shadowRoot;
+    this.element = shadowRoot.host;
+    this.document = this.element.ownerDocument;
+    this.window = this.document.defaultView;
+    this.prefs = prefs;
+  }
+
+  onsetup() {
+    this.generateContent();
+
+    this.Utils = {
+      init(shadowRoot, prefs) {
+        this.shadowRoot = shadowRoot;
+        this.prefs = prefs;
+        this.video = shadowRoot.host;
+        this.videocontrols = shadowRoot.firstChild;
+        this.document = this.videocontrols.ownerDocument;
+        this.window = this.document.defaultView;
+        this.shadowRoot = shadowRoot;
+
+        this.pictureInPictureToggleButton =
+          this.shadowRoot.getElementById("pictureInPictureToggleButton");
+
+        if (!this.pipToggleEnabled) {
+          this.pictureInPictureToggleButton.setAttribute("hidden", true);
+        }
+      },
+
+      get pipToggleEnabled() {
+        return this.prefs["media.videocontrols.picture-in-picture.video-toggle.enabled"];
+      },
+    };
+    this.Utils.init(this.shadowRoot, this.prefs);
+  }
+
+  elementStateMatches(element) {
+    return true;
+  }
+
+  destructor() {
+  }
+
+  generateContent() {
+    /*
+     * Pass the markup through XML parser purely for the reason of loading the localization DTD.
+     * Remove it when migrate to Fluent.
+     */
+    const parser = new this.window.DOMParser();
+    let parserDoc = parser.parseFromString(`<!DOCTYPE bindings [
+      <!ENTITY % videocontrolsDTD SYSTEM "chrome://global/locale/videocontrols.dtd">
+      %videocontrolsDTD;
+      ]>
+      <div class="videocontrols" xmlns="http://www.w3.org/1999/xhtml" role="none">
+        <link rel="stylesheet" type="text/css" href="chrome://global/skin/media/videocontrols.css" />
+        <div id="controlsContainer" class="controlsContainer" role="none">
+          <div class="controlsOverlay stackItem">
+            <button id="pictureInPictureToggleButton" class="pictureInPictureToggleButton">
+              <div id="pictureInPictureToggleIcon" class="pictureInPictureToggleIcon"></div>
+            </button>
+          </div>
+        </div>
+      </div>`, "application/xml");
+    this.shadowRoot.importNodeAndAppendChildAt(this.shadowRoot, parserDoc.documentElement, true);
+  }
+};
deleted file mode 100644
--- a/toolkit/locales/en-US/toolkit/global/videocontrols.ftl
+++ /dev/null
@@ -1,13 +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/.
-
-### These strings are used in the video controls.
-
-# This string is used when displaying the Picture-in-Picture "flyout" toggle.
-# The "flyout" toggle is a variation of the Picture-in-Picture video toggle that
-# appears in a ribbon over top of <video> elements when Picture-in-Picture is
-# enabled. This variation only appears on the first <video> that's displayed to
-# a user on a page. It animates out, displaying this string, and after 5
-# seconds, animates away again.
-picture-in-picture-flyout-toggle = Picture-in-Picture
--- a/toolkit/themes/shared/jar.inc.mn
+++ b/toolkit/themes/shared/jar.inc.mn
@@ -107,11 +107,10 @@ toolkit.jar:
   skin/classic/global/plugins/plugin.svg                    (../../shared/plugins/plugin.svg)
   skin/classic/global/plugins/plugin-blocked.svg            (../../shared/plugins/plugin-blocked.svg)
   skin/classic/global/plugins/pluginGeneric.svg             (../../shared/extensions/category-plugins.svg)
   skin/classic/global/plugins/pluginProblem.css             (../../shared/plugins/pluginProblem.css)
   skin/classic/global/plugins/contentPluginBlocked.png      (../../shared/plugins/contentPluginBlocked.png)
   skin/classic/global/plugins/contentPluginCrashed.png      (../../shared/plugins/contentPluginCrashed.png)
   skin/classic/global/plugins/contentPluginStripe.png       (../../shared/plugins/contentPluginStripe.png)
   skin/classic/global/pictureinpicture/player.css           (../../shared/pictureinpicture/player.css)
-  skin/classic/global/pictureinpicture/toggle.css           (../../shared/pictureinpicture/toggle.css)
   skin/classic/global/media/pictureinpicture.svg            (../../shared/media/pictureinpicture.svg)
 
--- a/toolkit/themes/shared/media/videocontrols.css
+++ b/toolkit/themes/shared/media/videocontrols.css
@@ -23,16 +23,20 @@
 .controlsContainer {
   --clickToPlay-size: 48px;
   --button-size: 30px;
   --timer-size: 40px;
   --timer-long-size: 60px;
   --track-size: 5px;
   --thumb-size: 13px;
   --label-font-size: 13px;
+  --pip-toggle-bgcolor: rgb(0, 96, 223);
+  --pip-toggle-text-and-icon-color: rgb(255, 255, 255);
+  --pip-toggle-padding: 5px;
+  --pip-toggle-icon-width-height: 16px;
 }
 .controlsContainer.touch {
   --clickToPlay-size: 64px;
   --button-size: 40px;
   --timer-size: 52px;
   --timer-long-size: 78px;
   --track-size: 7px;
   --thumb-size: 16px;
@@ -60,17 +64,18 @@
 .touch .controlBar {
   /* Do not delete: these variables are accessed by JavaScript directly.
      see videocontrols.js and search for |-width|. */
   --scrubberStack-width: 84px;
   --volumeStack-width: 64px;
 }
 
 .controlsContainer [hidden],
-.controlBar[hidden] {
+.controlBar[hidden],
+.pictureInPictureToggleButton[hidden] {
   display: none;
 }
 
 .controlBar[size="hidden"] {
   display: none;
 }
 
 .controlsSpacer[hideCursor] {
@@ -428,16 +433,54 @@
   min-width: 84px;
   min-height: 84px;
   background-image: url(chrome://global/skin/media/pictureinpicture.svg);
   -moz-context-properties: fill, stroke;
   fill: #fff;
   stroke: #fff;
 }
 
+.pictureInPictureToggleButton {
+  display: flex;
+  -moz-appearance: none;
+  position: absolute;
+  background-color: var(--pip-toggle-bgcolor);
+  color: var(--pip-toggle-text-and-icon-color);
+  border: 0;
+  padding: var(--pip-toggle-padding);
+  right: 0;
+  top: 50%;
+  transform: translateY(-50%);
+  transition: opacity 160ms linear;
+  min-width: max-content;
+  pointer-events: auto;
+  opacity: 0;
+}
+
+.pictureInPictureToggleIcon {
+  display: inline-block;
+  background-image: url(chrome://global/skin/media/pictureinpicture.svg);
+  background-position: center left;
+  background-repeat: no-repeat;
+  -moz-context-properties: fill, stroke;
+  fill: var(--pip-toggle-text-and-icon-color);
+  stroke: var(--pip-toggle-text-and-icon-color);
+  width: var(--pip-toggle-icon-width-height);
+  height: var(--pip-toggle-icon-width-height);
+  min-width: max-content;
+}
+
+.controlsOverlay:hover > .pictureInPictureToggleButton {
+  opacity: 0.8;
+}
+
+.controlsOverlay:hover > .pictureInPictureToggleButton:hover {
+  opacity: 1;
+}
+
 /* Overlay Play button */
 .clickToPlay {
   min-width: var(--clickToPlay-size);
   min-height: var(--clickToPlay-size);
   border-radius: 50%;
   background-image: url(chrome://global/skin/media/playButton.svg);
   background-repeat: no-repeat;
   background-position: 54% 50%;
deleted file mode 100644
--- a/toolkit/themes/shared/pictureinpicture/toggle.css
+++ /dev/null
@@ -1,83 +0,0 @@
-/**
- * We add the #picture-in-picture-flyout-container and
- * #picture-in-picture-toggle IDs here so that it's easier to read these
- * property values in script, since they're AnonymousContent, and we need
- * IDs and can't use classes to query AnonymousContent property values.
- */
-#picture-in-picture-flyout-container:-moz-native-anonymous,
-#picture-in-picture-toggle:-moz-native-anonymous,
-.picture-in-picture-toggle-button:-moz-native-anonymous {
-  --pip-toggle-bgcolor: rgb(0, 96, 223);
-  --pip-toggle-text-and-icon-color: rgb(255, 255, 255);
-  --pip-toggle-padding: 5px;
-  --pip-toggle-icon-width-height: 16px;
-}
-
-.picture-in-picture-toggle-button:-moz-native-anonymous {
-  -moz-appearance: none;
-  display: flex;
-  position: absolute;
-  background-color: var(--pip-toggle-bgcolor);
-  border: 0;
-  padding: var(--pip-toggle-padding);
-  color: var(--pip-toggle-text-and-icon-color);
-  transform: translateX(0);
-  transition: transform 350ms linear;
-  min-width: max-content;
-  pointer-events: auto;
-  opacity: 0.8;
-}
-
-.picture-in-picture-toggle-button:-moz-native-anonymous:hover,
-.picture-in-picture-toggle-button:-moz-native-anonymous:active {
-  opacity: 1;
-  background-color: var(--pip-toggle-bgcolor);
-  color: var(--pip-toggle-text-and-icon-color);
-  padding: var(--pip-toggle-padding);
-}
-
-#picture-in-picture-flyout-container[hidden]:-moz-native-anonymous,
-.picture-in-picture-toggle-button[hidden]:-moz-native-anonymous {
-  display: none;
-}
-
-.picture-in-picture-toggle-button:-moz-native-anonymous > .icon {
-  display: inline-block;
-  background-image: url(chrome://global/skin/media/pictureinpicture.svg);
-  background-position: center left;
-  background-repeat: no-repeat;
-  -moz-context-properties: fill, stroke;
-  fill: var(--pip-toggle-text-and-icon-color);
-  stroke: var(--pip-toggle-text-and-icon-color);
-  width: var(--pip-toggle-icon-width-height);
-  height: var(--pip-toggle-icon-width-height);
-  min-width: max-content;
-  pointer-events: none;
-}
-
-.picture-in-picture-toggle-button:-moz-native-anonymous > .label {
-  margin-left: var(--pip-toggle-padding);
-  min-width: max-content;
-  pointer-events: none;
-}
-
-#picture-in-picture-flyout-container:-moz-native-anonymous {
-  position: absolute;
-  /**
-   * A higher z-index makes sure that the flyout always appears on top of the
-   * other toggle, so that we avoid seeing double-toggles.
-   */
-  z-index: 2;
-  overflow: hidden;
-  /**
-   * This places the container for the flyout in the position where the flyout
-   * eventually ends up. This, coupled with the overflow: hidden, gives the
-   * effect that the flyout is sliding out from the edge of the video.
-   */
-  transform: translateX(calc(-100% + var(--pip-toggle-icon-width-height) + 2 * var(--pip-toggle-padding)));
-}
-
-#picture-in-picture-flyout-container:-moz-native-anonymous > .picture-in-picture-toggle-button {
-  position: relative;
-  opacity: 1;
-}
--- a/widget/windows/WinCompositorWindowThread.cpp
+++ b/widget/windows/WinCompositorWindowThread.cpp
@@ -154,18 +154,17 @@ WinCompositorWnds WinCompositorWindowThr
         // as parent window, so instead create it with a temporary placeholder
         // parent. Its parent is set as main window in UI process.
         initialParentWnd =
             ::CreateWindowEx(WS_EX_TOOLWINDOW, kClassNameCompositorInitalParent,
                              nullptr, WS_POPUP | WS_DISABLED, 0, 0, 1, 1,
                              nullptr, 0, GetModuleHandle(nullptr), 0);
 
         compositorWnd = ::CreateWindowEx(
-            WS_EX_NOPARENTNOTIFY | WS_EX_LAYERED | WS_EX_TRANSPARENT |
-                WS_EX_NOREDIRECTIONBITMAP,
+            WS_EX_NOPARENTNOTIFY | WS_EX_NOREDIRECTIONBITMAP,
             kClassNameCompositor, nullptr,
             WS_CHILDWINDOW | WS_DISABLED | WS_VISIBLE, 0, 0, 1, 1,
             initialParentWnd, 0, GetModuleHandle(nullptr), 0);
       });
 
   Loop()->PostTask(runnable.forget());
 
   task.Wait();