Merge mozilla-central to mozilla-inbound. CLOSED TREE

This commit is contained in:
Csoregi Natalia 2019-04-15 12:48:34 +03:00
Родитель 4ec909364e 1b2636e851
Коммит 0a57edb23d
47 изменённых файлов: 1293 добавлений и 706 удалений

Просмотреть файл

@ -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);