зеркало из https://github.com/mozilla/gecko-dev.git
Merge mozilla-central to mozilla-inbound. CLOSED TREE
This commit is contained in:
Коммит
0a57edb23d
|
@ -65,9 +65,6 @@ let whitelist = [
|
|||
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")) {
|
||||
|
|
|
@ -13,8 +13,7 @@
|
|||
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">
|
||||
|
|
|
@ -644,6 +644,10 @@ var PanelMultiView = class extends AssociatedToNode {
|
|||
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.
|
||||
|
@ -692,6 +696,7 @@ var PanelMultiView = class extends AssociatedToNode {
|
|||
}
|
||||
}
|
||||
|
||||
nextPanelView.focusWhenActive = doingKeyboardActivation;
|
||||
this._activateView(nextPanelView);
|
||||
}
|
||||
|
||||
|
@ -814,7 +819,7 @@ var PanelMultiView = class extends AssociatedToNode {
|
|||
if (panelView.isOpenIn(this)) {
|
||||
panelView.active = true;
|
||||
if (panelView.focusWhenActive) {
|
||||
panelView.focusFirstNavigableElement();
|
||||
panelView.focusFirstNavigableElement(false, true);
|
||||
panelView.focusWhenActive = false;
|
||||
}
|
||||
panelView.dispatchCustomEvent("ViewShown");
|
||||
|
@ -1400,38 +1405,70 @@ var PanelView = class extends AssociatedToNode {
|
|||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
if (element.hasAttribute("disabled")) {
|
||||
return false;
|
||||
let bounds = this._getBoundsWithoutFlushing(node);
|
||||
if (bounds.width == 0 || bounds.height == 0) {
|
||||
return NodeFilter.FILTER_REJECT;
|
||||
}
|
||||
let bounds = this._getBoundsWithoutFlushing(element);
|
||||
return bounds.width > 0 && bounds.height > 0;
|
||||
});
|
||||
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;
|
||||
}
|
||||
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();
|
||||
|
@ -1447,18 +1484,36 @@ var PanelView = class extends AssociatedToNode {
|
|||
/**
|
||||
* 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();
|
||||
}
|
||||
|
||||
|
@ -1466,54 +1521,26 @@ var PanelView = class extends AssociatedToNode {
|
|||
* 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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1538,38 +1565,70 @@ var PanelView = class extends AssociatedToNode {
|
|||
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")) {
|
||||
|
@ -1586,11 +1645,15 @@ var PanelView = class extends AssociatedToNode {
|
|||
}
|
||||
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.
|
||||
|
@ -1599,6 +1662,7 @@ var PanelView = class extends AssociatedToNode {
|
|||
button.doCommand();
|
||||
let clickEvent = new event.target.ownerGlobal.MouseEvent("click", {"bubbles": true});
|
||||
button.dispatchEvent(clickEvent);
|
||||
this._doingKeyboardActivation = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@ -1618,7 +1682,6 @@ var PanelView = class extends AssociatedToNode {
|
|||
* Clear all traces of keyboard navigation happening right now.
|
||||
*/
|
||||
clearNavigation() {
|
||||
delete this.__navigableElements;
|
||||
let selected = this.selectedElement;
|
||||
if (selected) {
|
||||
selected.blur();
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
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">
|
||||
|
|
|
@ -175,6 +175,7 @@ subsuite = clipboard
|
|||
[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
|
||||
|
|
|
@ -11,8 +11,12 @@ const {PanelMultiView} = ChromeUtils.import("resource:///modules/PanelMultiView.
|
|||
|
||||
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");
|
||||
|
@ -20,22 +24,32 @@ add_task(async function setup() {
|
|||
// 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();
|
||||
|
@ -64,3 +78,56 @@ add_task(async function testMainViewByClick() {
|
|||
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));
|
||||
});
|
||||
|
|
|
@ -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();
|
||||
});
|
|
@ -91,13 +91,20 @@ add_task(async function testEnterKeyBehaviors() {
|
|||
"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.
|
||||
|
|
|
@ -248,7 +248,7 @@ DownloadsPlacesView.prototype = {
|
|||
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.
|
||||
|
|
|
@ -362,6 +362,7 @@ void DocumentOrShadowRoot::NodesFromRect(float aX, float aY, float aTopSize,
|
|||
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
|
||||
|
@ -380,6 +381,9 @@ void DocumentOrShadowRoot::NodesFromRect(float aX, float aY, float aTopSize,
|
|||
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);
|
||||
|
|
|
@ -120,7 +120,7 @@ class DocumentOrShadowRoot {
|
|||
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.
|
||||
|
|
|
@ -1155,7 +1155,8 @@ 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);
|
||||
|
||||
|
@ -1165,7 +1166,8 @@ nsDOMWindowUtils::NodesFromRect(float aX, float aY, float aTopSize,
|
|||
|
||||
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());
|
||||
|
|
|
@ -4097,19 +4097,7 @@ nsresult HTMLMediaElement::BindToTree(Document* aDocument, nsIContent* aParent,
|
|||
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;
|
||||
|
|
|
@ -548,5 +548,15 @@ void HTMLVideoElement::EndCloningVisually() {
|
|||
}
|
||||
}
|
||||
|
||||
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
|
||||
|
|
|
@ -145,6 +145,8 @@ class HTMLVideoElement final : public HTMLMediaElement {
|
|||
|
||||
bool IsCloningElementVisually() const { return !!mVisualCloneTarget; }
|
||||
|
||||
void TogglePictureInPicture(ErrorResult& rv);
|
||||
|
||||
protected:
|
||||
virtual ~HTMLVideoElement();
|
||||
|
||||
|
|
|
@ -746,6 +746,8 @@ interface nsIDOMWindowUtils : nsISupports {
|
|||
* 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,
|
||||
|
@ -754,7 +756,8 @@ interface nsIDOMWindowUtils : nsISupports {
|
|||
in float aBottomSize,
|
||||
in float aLeftSize,
|
||||
in boolean aIgnoreRootScrollFrame,
|
||||
in boolean aFlushLayout);
|
||||
in boolean aFlushLayout,
|
||||
in boolean aOnlyVisible);
|
||||
|
||||
|
||||
/**
|
||||
|
|
|
@ -56,7 +56,11 @@ add_task(async () => {
|
|||
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");
|
||||
|
||||
|
@ -65,7 +69,10 @@ add_task(async () => {
|
|||
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();
|
||||
|
|
|
@ -30,6 +30,11 @@
|
|||
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);
|
||||
|
|
|
@ -13,20 +13,8 @@
|
|||
|
||||
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);
|
||||
|
|
|
@ -69,6 +69,12 @@ partial interface HTMLVideoElement {
|
|||
// <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
|
||||
|
|
|
@ -218,7 +218,7 @@ static void PaintTextShadowCallback(gfxContext* aCtx, nsPoint aShadowOffset,
|
|||
void nsDisplayTextOverflowMarker::Paint(nsDisplayListBuilder* aBuilder,
|
||||
gfxContext* aCtx) {
|
||||
DrawTargetAutoDisableSubpixelAntialiasing disable(aCtx->GetDrawTarget(),
|
||||
mDisableSubpixelAA);
|
||||
IsSubpixelAADisabled());
|
||||
|
||||
nscolor foregroundColor =
|
||||
nsLayoutUtils::GetColor(mFrame, &nsStyleText::mWebkitTextFillColor);
|
||||
|
|
|
@ -106,8 +106,11 @@ static void BuildDisplayListForTopLayerFrame(nsDisplayListBuilder* aBuilder,
|
|||
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);
|
||||
|
@ -153,8 +156,10 @@ void ViewportFrame::BuildDisplayListForTopLayer(nsDisplayListBuilder* aBuilder,
|
|||
nsIFrame* backdropFrame =
|
||||
static_cast<nsPlaceholderFrame*>(backdropPh)->GetOutOfFlowFrame();
|
||||
MOZ_ASSERT(backdropFrame);
|
||||
|
||||
BuildDisplayListForTopLayerFrame(aBuilder, backdropFrame, aList);
|
||||
}
|
||||
|
||||
BuildDisplayListForTopLayerFrame(aBuilder, frame, aList);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -646,7 +646,7 @@ void nsDisplayBullet::Paint(nsDisplayListBuilder* aBuilder, gfxContext* aCtx) {
|
|||
}
|
||||
|
||||
ImgDrawResult result = static_cast<nsBulletFrame*>(mFrame)->PaintBullet(
|
||||
*aCtx, ToReferenceFrame(), GetPaintRect(), flags, mDisableSubpixelAA);
|
||||
*aCtx, ToReferenceFrame(), GetPaintRect(), flags, IsSubpixelAADisabled());
|
||||
|
||||
nsDisplayBulletGeometry::UpdateDrawResult(this, result);
|
||||
}
|
||||
|
|
|
@ -3807,8 +3807,15 @@ void nsIFrame::BuildDisplayListForChild(nsDisplayListBuilder* aBuilder,
|
|||
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);
|
||||
|
|
|
@ -3764,6 +3764,12 @@ void ScrollFrameHelper::MaybeAddTopLayerItems(nsDisplayListBuilder* aBuilder,
|
|||
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>(
|
||||
|
|
|
@ -457,7 +457,7 @@ class nsDisplayHeaderFooter final : public nsDisplayItem {
|
|||
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)
|
||||
|
||||
|
@ -524,8 +524,12 @@ void nsPageFrame::BuildDisplayList(nsDisplayListBuilder* aBuilder,
|
|||
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);
|
||||
}
|
||||
|
||||
|
|
|
@ -435,7 +435,7 @@ void nsSubDocumentFrame::BuildDisplayList(nsDisplayListBuilder* 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
|
||||
|
@ -443,6 +443,7 @@ void nsSubDocumentFrame::BuildDisplayList(nsDisplayListBuilder* aBuilder,
|
|||
// 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
|
||||
|
|
|
@ -195,11 +195,6 @@ mozilla::LayerState nsDisplayRemote::GetLayerState(
|
|||
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) {
|
||||
|
|
|
@ -90,6 +90,8 @@ class RenderFrame final {
|
|||
* 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;
|
||||
|
@ -103,8 +105,6 @@ class nsDisplayRemote final : public nsDisplayItem {
|
|||
public:
|
||||
nsDisplayRemote(nsDisplayListBuilder* aBuilder, nsSubDocumentFrame* aFrame);
|
||||
|
||||
bool HasDeletedFrame() const override;
|
||||
|
||||
LayerState GetLayerState(
|
||||
nsDisplayListBuilder* aBuilder, LayerManager* aManager,
|
||||
const ContainerLayerParameters& aParameters) override;
|
||||
|
|
|
@ -141,11 +141,10 @@ bool RetainedDisplayListBuilder::PreProcessDisplayList(
|
|||
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);
|
||||
|
@ -214,10 +213,9 @@ void RetainedDisplayListBuilder::IncrementSubDocPresShellPaintCount(
|
|||
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;
|
||||
}
|
||||
|
@ -225,6 +223,12 @@ static bool AnyContentAncestorModified(nsIFrame* aFrame,
|
|||
if (aStopAtFrame && f == aStopAtFrame) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (f->GetStateBits() & NS_FRAME_IS_PUSHED_FLOAT) {
|
||||
f = f->GetParent();
|
||||
} else {
|
||||
f = nsLayoutUtils::GetParentOrPlaceholderForCrossDoc(f);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
|
@ -275,6 +279,14 @@ static void UpdateASR(nsDisplayItem* aItem,
|
|||
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);
|
||||
|
@ -292,7 +304,7 @@ void OldItemInfo::Discard(RetainedDisplayListBuilder* aBuilder,
|
|||
}
|
||||
|
||||
bool OldItemInfo::IsChanged() {
|
||||
return !mItem || mItem->HasDeletedFrame() || !mItem->CanBeReused();
|
||||
return !mItem || !mItem->CanBeReused() || mItem->HasDeletedFrame();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -307,14 +319,14 @@ bool OldItemInfo::IsChanged() {
|
|||
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());
|
||||
|
@ -323,7 +335,9 @@ class MergeState {
|
|||
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() ==
|
||||
|
@ -463,10 +477,11 @@ class MergeState {
|
|||
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;
|
||||
}
|
||||
}
|
||||
|
@ -475,7 +490,8 @@ class MergeState {
|
|||
}
|
||||
|
||||
bool HasModifiedFrame(nsDisplayItem* aItem) {
|
||||
return AnyContentAncestorModified(aItem->FrameForInvalidation());
|
||||
nsIFrame* stopFrame = mOuterItem ? mOuterItem->Frame() : nullptr;
|
||||
return AnyContentAncestorModified(aItem->FrameForInvalidation(), stopFrame);
|
||||
}
|
||||
|
||||
void UpdateContainerASR(nsDisplayItem* aItem) {
|
||||
|
@ -496,12 +512,11 @@ class MergeState {
|
|||
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);
|
||||
|
@ -619,7 +634,7 @@ class MergeState {
|
|||
// and assert when we try swap the contents
|
||||
nsDisplayList mMergedItems;
|
||||
DirectedAcyclicGraph<MergedListUnits> mMergedDAG;
|
||||
uint32_t mOuterKey;
|
||||
nsDisplayItem* mOuterItem;
|
||||
bool mResultIsModified;
|
||||
};
|
||||
|
||||
|
@ -641,8 +656,7 @@ bool RetainedDisplayListBuilder::MergeDisplayLists(
|
|||
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()) {
|
||||
|
|
|
@ -139,8 +139,7 @@ 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());
|
||||
|
@ -169,4 +168,7 @@ struct OldItemInfo {
|
|||
nsTArray<MergedListIndex> mDirectPredecessors;
|
||||
};
|
||||
|
||||
bool AnyContentAncestorModified(nsIFrame* aFrame,
|
||||
nsIFrame* aStopAtFrame = nullptr);
|
||||
|
||||
#endif // RETAINEDDISPLAYLISTHELPERS_H_
|
||||
|
|
|
@ -101,6 +101,7 @@
|
|||
#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;
|
||||
|
@ -135,7 +136,7 @@ void AssertUniqueItem(nsDisplayItem* aItem) {
|
|||
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!");
|
||||
|
@ -3171,18 +3172,9 @@ nsDisplayItem::nsDisplayItem(nsDisplayListBuilder* aBuilder, nsIFrame* aFrame)
|
|||
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);
|
||||
|
@ -3208,9 +3200,12 @@ nsDisplayItem::nsDisplayItem(nsDisplayListBuilder* aBuilder, nsIFrame* aFrame,
|
|||
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 */
|
||||
|
@ -3226,6 +3221,24 @@ bool nsDisplayItem::ForceActiveLayers() {
|
|||
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,
|
||||
|
@ -3236,7 +3249,7 @@ bool nsDisplayItem::ComputeVisibility(nsDisplayListBuilder* aBuilder,
|
|||
|
||||
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.
|
||||
|
@ -3301,6 +3314,8 @@ void nsDisplayItem::FuseClipChainUpTo(nsDisplayListBuilder* aBuilder,
|
|||
}
|
||||
}
|
||||
|
||||
void nsDisplayItem::SetDeletedFrame() { mItemFlags += ItemFlag::DeletedFrame; }
|
||||
|
||||
bool nsDisplayItem::ShouldUseAdvancedLayer(LayerManager* aManager,
|
||||
PrefFunc aFunc) const {
|
||||
return CanUseAdvancedLayer(aManager) ? aFunc() : false;
|
||||
|
@ -6698,13 +6713,10 @@ 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);
|
||||
}
|
||||
|
@ -8944,7 +8956,7 @@ void nsDisplayText::Paint(nsDisplayListBuilder* aBuilder, gfxContext* aCtx) {
|
|||
AUTO_PROFILER_LABEL("nsDisplayText::Paint", GRAPHICS);
|
||||
|
||||
DrawTargetAutoDisableSubpixelAntialiasing disable(aCtx->GetDrawTarget(),
|
||||
mDisableSubpixelAA);
|
||||
IsSubpixelAADisabled());
|
||||
RenderToContext(aCtx, aBuilder);
|
||||
}
|
||||
|
||||
|
@ -10272,3 +10284,86 @@ PaintTelemetry::AutoRecord::~AutoRecord() {
|
|||
}
|
||||
|
||||
} // 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;
|
||||
}
|
||||
|
|
|
@ -441,6 +441,11 @@ class nsDisplayListBuilder {
|
|||
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
|
||||
|
@ -1132,59 +1137,25 @@ class nsDisplayListBuilder {
|
|||
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()) {}
|
||||
aForChild->IsTransformed(),
|
||||
aRecalcInvalidSubtree) {}
|
||||
|
||||
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;
|
||||
}
|
||||
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) {
|
||||
|
@ -1760,17 +1731,7 @@ class nsDisplayListBuilder {
|
|||
return false;
|
||||
}
|
||||
|
||||
bool MarkCurrentFrameModifiedDuringBuilding() {
|
||||
if (MarkFrameModifiedDuringBuilding(const_cast<nsIFrame*>(mCurrentFrame))) {
|
||||
mInInvalidSubtree = true;
|
||||
mDirtyRect = mVisibleRect;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void RebuildAllItemsInCurrentSubtree() {
|
||||
mInInvalidSubtree = true;
|
||||
mDirtyRect = mVisibleRect;
|
||||
}
|
||||
|
||||
|
@ -2111,11 +2072,22 @@ MOZ_ALWAYS_INLINE T* MakeDisplayItem(nsDisplayListBuilder* aBuilder,
|
|||
}
|
||||
}
|
||||
|
||||
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;
|
||||
|
@ -2183,13 +2155,14 @@ class nsDisplayItem : public nsDisplayItemLink {
|
|||
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);
|
||||
}
|
||||
}
|
||||
|
@ -2215,6 +2188,7 @@ class nsDisplayItem : public nsDisplayItemLink {
|
|||
*/
|
||||
nsDisplayItem(nsDisplayListBuilder* aBuilder, const nsDisplayItem& aOther)
|
||||
: mFrame(aOther.mFrame),
|
||||
mItemFlags(),
|
||||
mClipChain(aOther.mClipChain),
|
||||
mClip(aOther.mClip),
|
||||
mActiveScrolledRoot(aOther.mActiveScrolledRoot),
|
||||
|
@ -2222,21 +2196,21 @@ class nsDisplayItem : public nsDisplayItemLink {
|
|||
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 {
|
||||
|
@ -2300,7 +2274,10 @@ class nsDisplayItem : public nsDisplayItemLink {
|
|||
*/
|
||||
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; }
|
||||
|
||||
|
@ -2544,12 +2521,12 @@ class nsDisplayItem : public nsDisplayItemLink {
|
|||
* 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
|
||||
|
||||
/**
|
||||
|
@ -2732,14 +2709,16 @@ class nsDisplayItem : public nsDisplayItemLink {
|
|||
return;
|
||||
}
|
||||
mPaintRect = mBuildingRect = aBuildingRect;
|
||||
mPaintRectValid = false;
|
||||
mItemFlags -= ItemFlag::PaintRectValid;
|
||||
}
|
||||
|
||||
void SetPaintRect(const nsRect& aPaintRect) {
|
||||
mPaintRect = aPaintRect;
|
||||
mPaintRectValid = true;
|
||||
mItemFlags += ItemFlag::PaintRectValid;
|
||||
}
|
||||
bool HasPaintRect() const {
|
||||
return mItemFlags.contains(ItemFlag::PaintRectValid);
|
||||
}
|
||||
bool HasPaintRect() const { return mPaintRectValid; }
|
||||
|
||||
/**
|
||||
* Returns the building rect for the children, relative to their
|
||||
|
@ -2765,7 +2744,9 @@ class nsDisplayItem : public nsDisplayItemLink {
|
|||
*/
|
||||
virtual bool CanApplyOpacity() const { return false; }
|
||||
|
||||
bool ForceNotVisible() const { return mForceNotVisible; }
|
||||
bool ForceNotVisible() const {
|
||||
return mItemFlags.contains(ItemFlag::ForceNotVisible);
|
||||
}
|
||||
|
||||
/**
|
||||
* For debugging and stuff
|
||||
|
@ -2831,9 +2812,11 @@ class nsDisplayItem : public nsDisplayItemLink {
|
|||
* Disable usage of component alpha. Currently only relevant for items that
|
||||
* have text.
|
||||
*/
|
||||
void DisableComponentAlpha() { mDisableSubpixelAA = true; }
|
||||
void DisableComponentAlpha() { mItemFlags += ItemFlag::DisableSubpixelAA; }
|
||||
|
||||
bool IsSubpixelAADisabled() const { return mDisableSubpixelAA; }
|
||||
bool IsSubpixelAADisabled() const {
|
||||
return mItemFlags.contains(ItemFlag::DisableSubpixelAA);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if we can add async animations to the layer for this display item.
|
||||
|
@ -2869,14 +2852,17 @@ class nsDisplayItem : public nsDisplayItemLink {
|
|||
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 {
|
||||
|
@ -2892,14 +2878,22 @@ class nsDisplayItem : public nsDisplayItemLink {
|
|||
return mFrame->GetContent() == aOther->Frame()->GetContent();
|
||||
}
|
||||
|
||||
bool IsReused() const { return mReusedItem; }
|
||||
bool IsReused() const { return mItemFlags.contains(ItemFlag::ReusedItem); }
|
||||
void SetReused(bool aReused) {
|
||||
if (aReused) {
|
||||
mItemFlags += ItemFlag::ReusedItem;
|
||||
} else {
|
||||
mItemFlags -= ItemFlag::ReusedItem;
|
||||
}
|
||||
}
|
||||
|
||||
void SetReused(bool aReused) { mReusedItem = aReused; }
|
||||
|
||||
bool CanBeReused() const { return mCanBeReused; }
|
||||
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) {}
|
||||
|
@ -2971,11 +2965,34 @@ class nsDisplayItem : public nsDisplayItemLink {
|
|||
#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;
|
||||
|
@ -3009,26 +3026,31 @@ class nsDisplayItem : public nsDisplayItemLink {
|
|||
|
||||
#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
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -4161,7 +4183,9 @@ class nsDisplaySolidColor : public nsDisplaySolidColorBase {
|
|||
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
|
||||
|
@ -4499,13 +4523,10 @@ class nsDisplayTableBackgroundImage : public nsDisplayBackgroundImage {
|
|||
|
||||
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);
|
||||
}
|
||||
|
@ -4630,13 +4651,10 @@ class nsDisplayTableThemedBackground : public nsDisplayThemedBackground {
|
|||
|
||||
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);
|
||||
}
|
||||
|
@ -4810,13 +4828,10 @@ class nsDisplayTableBackgroundColor : public nsDisplayBackgroundColor {
|
|||
|
||||
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);
|
||||
}
|
||||
|
@ -5632,13 +5647,10 @@ class nsDisplayTableBlendMode : public nsDisplayBlendMode {
|
|||
|
||||
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);
|
||||
}
|
||||
|
@ -5739,13 +5751,10 @@ class nsDisplayTableBlendContainer : public nsDisplayBlendContainer {
|
|||
|
||||
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);
|
||||
}
|
||||
|
@ -5972,7 +5981,6 @@ class nsDisplaySubDocument : public nsDisplayOwnLayer {
|
|||
const ContainerLayerParameters& aContainerParameters);
|
||||
|
||||
nsIFrame* FrameForInvalidation() const override;
|
||||
bool HasDeletedFrame() const override;
|
||||
void RemoveFrame(nsIFrame* aFrame) override;
|
||||
|
||||
void Disown();
|
||||
|
@ -6154,13 +6162,10 @@ class nsDisplayTableFixedPosition : public nsDisplayFixedPosition {
|
|||
|
||||
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);
|
||||
}
|
||||
|
|
|
@ -35,6 +35,9 @@ $property_ids
|
|||
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
|
||||
|
|
|
@ -7,15 +7,16 @@
|
|||
#ifndef nsCSSPropertyIDSet_h__
|
||||
#define nsCSSPropertyIDSet_h__
|
||||
|
||||
#include "mozilla/ArrayUtils.h"
|
||||
|
||||
#include "nsCSSPropertyID.h"
|
||||
#include <limits.h> // for CHAR_BIT
|
||||
#include <initializer_list>
|
||||
#include <limits.h> // for CHAR_BIT
|
||||
#include <ostream>
|
||||
|
||||
#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
|
||||
|
@ -188,8 +189,97 @@ class nsCSSPropertyIDSet {
|
|||
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__) */
|
||||
|
|
|
@ -13,7 +13,9 @@
|
|||
#define nsCSSProps_h___
|
||||
|
||||
#include <limits>
|
||||
#include <ostream>
|
||||
#include <type_traits>
|
||||
|
||||
#include "nsString.h"
|
||||
#include "nsCSSPropertyID.h"
|
||||
#include "nsStyleStructFwd.h"
|
||||
|
@ -312,4 +314,10 @@ class nsCSSProps {
|
|||
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___ */
|
||||
|
|
|
@ -2861,7 +2861,7 @@ void nsDisplaySVGText::HitTest(nsDisplayListBuilder* aBuilder,
|
|||
|
||||
void nsDisplaySVGText::Paint(nsDisplayListBuilder* aBuilder, gfxContext* aCtx) {
|
||||
DrawTargetAutoDisableSubpixelAntialiasing disable(aCtx->GetDrawTarget(),
|
||||
mDisableSubpixelAA);
|
||||
IsSubpixelAADisabled());
|
||||
|
||||
uint32_t appUnitsPerDevPixel = mFrame->PresContext()->AppUnitsPerDevPixel();
|
||||
|
||||
|
|
|
@ -278,7 +278,7 @@ static void PaintTextShadowCallback(gfxContext* aCtx, nsPoint aShadowOffset,
|
|||
void nsDisplayXULTextBox::Paint(nsDisplayListBuilder* aBuilder,
|
||||
gfxContext* aCtx) {
|
||||
DrawTargetAutoDisableSubpixelAntialiasing disable(aCtx->GetDrawTarget(),
|
||||
mDisableSubpixelAA);
|
||||
IsSubpixelAADisabled());
|
||||
|
||||
// Paint the text shadow before doing any foreground stuff
|
||||
nsRect drawRect =
|
||||
|
|
|
@ -2498,7 +2498,7 @@ class nsDisplayTreeBody final : public nsDisplayItem {
|
|||
gfxContext* aCtx) override {
|
||||
MOZ_ASSERT(aBuilder);
|
||||
DrawTargetAutoDisableSubpixelAntialiasing disable(aCtx->GetDrawTarget(),
|
||||
mDisableSubpixelAA);
|
||||
IsSubpixelAADisabled());
|
||||
|
||||
ImgDrawResult result = static_cast<nsTreeBodyFrame*>(mFrame)->PaintTreeBody(
|
||||
*aCtx, GetPaintRect(), ToReferenceFrame(), aBuilder);
|
||||
|
|
|
@ -7,25 +7,17 @@
|
|||
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
|
||||
|
@ -34,20 +26,11 @@ 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) {
|
||||
|
@ -59,12 +42,6 @@ class PictureInPictureToggleChild extends ActorChild {
|
|||
// 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",
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -84,18 +61,12 @@ class PictureInPictureToggleChild extends ActorChild {
|
|||
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);
|
||||
}
|
||||
|
@ -108,30 +79,14 @@ class PictureInPictureToggleChild extends ActorChild {
|
|||
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": {
|
||||
|
@ -152,7 +107,7 @@ class PictureInPictureToggleChild extends ActorChild {
|
|||
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],
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -171,13 +126,7 @@ class PictureInPictureToggleChild extends ActorChild {
|
|||
* 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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -194,33 +143,25 @@ class PictureInPictureToggleChild extends ActorChild {
|
|||
// 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();
|
||||
});
|
||||
|
@ -248,9 +189,12 @@ class PictureInPictureToggleChild extends ActorChild {
|
|||
}, 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 });
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -262,15 +206,65 @@ class PictureInPictureToggleChild extends ActorChild {
|
|||
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>.
|
||||
|
@ -298,12 +292,12 @@ class PictureInPictureToggleChild extends ActorChild {
|
|||
// 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;
|
||||
}
|
||||
}
|
||||
|
@ -320,15 +314,62 @@ class PictureInPictureToggleChild extends ActorChild {
|
|||
*
|
||||
* @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");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -339,203 +380,35 @@ class PictureInPictureToggleChild extends ActorChild {
|
|||
*/
|
||||
onMouseLeaveVideo(video) {
|
||||
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;
|
||||
state.pipToggle.setAttributeForElement(TOGGLE_ID, "hidden", "true");
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* 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 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.
|
||||
* @param {Element} toggle The Picture-in-Picture toggle.
|
||||
* @param {MouseEvent} event A MouseEvent to test.
|
||||
*
|
||||
* @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.
|
||||
* @return {Boolean}
|
||||
*/
|
||||
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);
|
||||
}
|
||||
|
||||
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");
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @param {Element} video The video to display the flyout on.
|
||||
*
|
||||
* @return {Promise}
|
||||
* @resolves {undefined} Once the flyout toggle animation has completed.
|
||||
*/
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -14,6 +14,7 @@ class UAWidgetsChild extends ActorChild {
|
|||
super(dispatcher);
|
||||
|
||||
this.widgets = new WeakMap();
|
||||
this.prefsCache = new Map();
|
||||
}
|
||||
|
||||
handleEvent(aEvent) {
|
||||
|
@ -49,11 +50,15 @@ class UAWidgetsChild extends ActorChild {
|
|||
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";
|
||||
|
@ -89,7 +94,9 @@ class UAWidgetsChild extends ActorChild {
|
|||
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;
|
||||
}
|
||||
|
@ -115,4 +122,32 @@ class UAWidgetsChild extends ActorChild {
|
|||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,8 +12,9 @@
|
|||
* 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;
|
||||
|
@ -45,12 +46,15 @@ this.VideoControlsWidget = class {
|
|||
*/
|
||||
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
|
||||
|
@ -67,7 +71,7 @@ this.VideoControlsWidget = class {
|
|||
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;
|
||||
|
@ -89,8 +93,9 @@ this.VideoControlsWidget = class {
|
|||
};
|
||||
|
||||
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;
|
||||
|
@ -265,6 +270,10 @@ this.VideoControlsImplWidget = class {
|
|||
this.setShowPictureInPictureMessage(true);
|
||||
}
|
||||
|
||||
if (!this.pipToggleEnabled || this.isShowingPictureInPictureMessage) {
|
||||
this.pictureInPictureToggleButton.setAttribute("hidden", true);
|
||||
}
|
||||
|
||||
let adjustableControls = [
|
||||
...this.prioritizedControls,
|
||||
this.controlBar,
|
||||
|
@ -667,6 +676,9 @@ this.VideoControlsImplWidget = class {
|
|||
// 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":
|
||||
|
@ -1943,13 +1955,18 @@ this.VideoControlsImplWidget = class {
|
|||
}
|
||||
},
|
||||
|
||||
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");
|
||||
|
@ -1975,6 +1992,8 @@ this.VideoControlsImplWidget = class {
|
|||
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];
|
||||
|
@ -2062,6 +2081,8 @@ this.VideoControlsImplWidget = class {
|
|||
{ 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,
|
||||
|
@ -2210,7 +2231,7 @@ this.VideoControlsImplWidget = class {
|
|||
},
|
||||
};
|
||||
|
||||
this.Utils.init(this.shadowRoot);
|
||||
this.Utils.init(this.shadowRoot, this.prefs);
|
||||
if (this.Utils.isTouchControls) {
|
||||
this.TouchUtils.init(this.shadowRoot, this.Utils);
|
||||
}
|
||||
|
@ -2253,6 +2274,10 @@ this.VideoControlsImplWidget = class {
|
|||
<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"
|
||||
|
@ -2468,8 +2493,9 @@ this.NoControlsMobileImplWidget = class {
|
|||
};
|
||||
|
||||
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;
|
||||
|
@ -2509,3 +2535,71 @@ this.NoControlsPictureInPictureImplWidget = class {
|
|||
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);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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
|
|
@ -112,6 +112,5 @@ toolkit.jar:
|
|||
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)
|
||||
|
||||
|
|
|
@ -28,6 +28,10 @@
|
|||
--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;
|
||||
|
@ -65,7 +69,8 @@
|
|||
}
|
||||
|
||||
.controlsContainer [hidden],
|
||||
.controlBar[hidden] {
|
||||
.controlBar[hidden],
|
||||
.pictureInPictureToggleButton[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
@ -433,6 +438,44 @@
|
|||
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);
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -159,8 +159,7 @@ WinCompositorWnds WinCompositorWindowThread::CreateCompositorWindow() {
|
|||
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);
|
||||
|
|
Загрузка…
Ссылка в новой задаче