Bug 1543128 - Have PictureInPictureToggleChild set hover states on the no-controls <video> widget manually. r=jaws
authorMike Conley <mconley@mozilla.com>
Mon, 15 Apr 2019 01:09:26 +0000
changeset 469463 47c6930938a779db5d239c1e94a26e5e381d2612
parent 469462 1f09366dd6bd135b45779af7c43a5c5184f747ec
child 469464 932333a30c717e7933a4d9d5edc91bce68d2d2b8
push id112792
push userncsoregi@mozilla.com
push dateMon, 15 Apr 2019 09:49:11 +0000
treeherdermozilla-inbound@a57f27d3ccd0 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjaws
bugs1543128
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
Bug 1543128 - Have PictureInPictureToggleChild set hover states on the no-controls <video> widget manually. r=jaws Depends on D26805 Differential Revision: https://phabricator.services.mozilla.com/D26806
toolkit/actors/PictureInPictureChild.jsm
--- a/toolkit/actors/PictureInPictureChild.jsm
+++ b/toolkit/actors/PictureInPictureChild.jsm
@@ -2,22 +2,25 @@
 /* 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, "Services",
   "resource://gre/modules/Services.jsm");
 
+XPCOMUtils.defineLazyGlobalGetters(this, ["InspectorUtils"]);
+
 const TOGGLE_ENABLED_PREF =
   "media.videocontrols.picture-in-picture.video-toggle.enabled";
 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
@@ -99,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.
@@ -118,23 +121,17 @@ 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
@@ -242,52 +239,128 @@ class PictureInPictureToggleChild extend
     // 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);
 
     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);
-    // Stubbed out for a later patch in this series.
+    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) {
-    // Stubbed out for a later patch in this series.
+    let state = this.docState;
+    let shadowRoot = video.openOrClosedShadowRoot;
+
+    if (shadowRoot) {
+      let controlsOverlay = shadowRoot.querySelector(".controlsOverlay");
+      let toggle = shadowRoot.getElementById("pictureInPictureToggleButton");
+      InspectorUtils.removePseudoClassLock(controlsOverlay, ":hover");
+      InspectorUtils.removePseudoClassLock(toggle, ":hover");
+    }
+
+    state.weakOverVideo = null;
+  }
+
+  /**
+   * Given a reference to a Picture-in-Picture toggle element, determines
+   * if a MouseEvent event is occurring within its bounds.
+   *
+   * @param {Element} toggle The Picture-in-Picture toggle.
+   * @param {MouseEvent} event A MouseEvent to test.
+   *
+   * @return {Boolean}
+   */
+  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);
   }