tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

commit 182bb259bfc7444872b0c83d7ca09d14a6dc6512
parent 8bf1985cc5d177d80cf0ec3da21bc19c957f341d
Author: Vincent Hilla <vhilla@mozilla.com>
Date:   Tue, 16 Dec 2025 18:12:06 +0000

Bug 1858562 - Part 4: Browser Chrome Document Picture-in-Picture adjustments. r=mconley,smaug,sthompson,dom-core,webidl,sessionstore-reviewers,dao,urlbar-reviewers

Implement a basic UI/UX for Document Picture-in-Picture.
- The opener URI is shown in the PiP window's location bar.
- Fullscreen via F11 and session restore are blocked for PiP windows.

Differential Revision: https://phabricator.services.mozilla.com/D267248

Diffstat:
Mbrowser/base/content/browser.js | 30++++++++++++++++++++----------
Mbrowser/components/sessionstore/SessionStore.sys.mjs | 6++++++
Mbrowser/components/urlbar/content/UrlbarInput.mjs | 8++++++++
Mdom/base/nsGlobalWindowOuter.cpp | 6++++++
Mdom/chrome-webidl/BrowsingContext.webidl | 6++++++
Mdom/documentpip/moz.build | 2++
Mdom/documentpip/tests/browser/browser.toml | 8++++++++
Adom/documentpip/tests/browser/browser_parent_has_pip_flag.js | 19+++++++++++++++++++
Mdom/documentpip/tests/browser/browser_pip_closes_once.js | 21+++++++++++++++++++--
Adom/documentpip/tests/browser/browser_pip_fullscreen_disallowed.js | 34++++++++++++++++++++++++++++++++++
Adom/documentpip/tests/browser/browser_pip_ui.js | 52++++++++++++++++++++++++++++++++++++++++++++++++++++
Adom/documentpip/tests/browser/browser_sessionstore_ignore_dpip.js | 35+++++++++++++++++++++++++++++++++++
Mxpfe/appshell/AppWindow.cpp | 18+++++++++++++++---
13 files changed, 230 insertions(+), 15 deletions(-)

diff --git a/browser/base/content/browser.js b/browser/base/content/browser.js @@ -2215,16 +2215,19 @@ var XULBrowserWindow = { aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SESSION_STORE ); - // We want to update the popup visibility if we received this notification - // via simulated locationchange events such as switching between tabs, however - // if this is a document navigation then PopupNotifications will be updated - // via TabsProgressListener.onLocationChange and we do not want it called twice - gURLBar.setURI({ - uri: aLocationURI, - dueToTabSwitch: aIsSimulated, - dueToSessionRestore: isSessionRestore, - isSameDocument, - }); + // Don't update URL for document PiP window as it shows its opener url + if (!window.browsingContext.isDocumentPiP) { + // We want to update the popup visibility if we received this notification + // via simulated locationchange events such as switching between tabs, however + // if this is a document navigation then PopupNotifications will be updated + // via TabsProgressListener.onLocationChange and we do not want it called twice + gURLBar.setURI({ + uri: aLocationURI, + dueToTabSwitch: aIsSimulated, + dueToSessionRestore: isSessionRestore, + isSameDocument, + }); + } BookmarkingUI.onLocationChange(); // If we've actually changed document, update the toolbar visibility. @@ -2542,6 +2545,13 @@ var XULBrowserWindow = { aState |= Ci.nsIWebProgressListener.STATE_IDENTITY_ASSOCIATED; } + if (window.browsingContext.isDocumentPiP) { + gURLBar.setURI({ + uri, + isSameDocument: true, + }); + } + try { uri = Services.io.createExposableURI(uri); } catch (e) {} diff --git a/browser/components/sessionstore/SessionStore.sys.mjs b/browser/components/sessionstore/SessionStore.sys.mjs @@ -2260,6 +2260,12 @@ var SessionStoreInternal = { * Window reference */ onBeforeBrowserWindowShown(aWindow) { + // Do not track Document Picture-in-Picture windows since these are + // ephemeral and tied to a specific tab's browser document. + if (aWindow.browsingContext.isDocumentPiP) { + return; + } + // Register the window. this.onLoad(aWindow); diff --git a/browser/components/urlbar/content/UrlbarInput.mjs b/browser/components/urlbar/content/UrlbarInput.mjs @@ -717,6 +717,14 @@ export class UrlbarInput extends HTMLElement { "Cannot set URI for UrlbarInput that is not an address bar" ); } + if ( + this.window.browsingContext.isDocumentPiP && + uri.spec.startsWith("about:blank") + ) { + // If this is a Document PiP, its url will be about:blank while + // the opener will be a secure context, i.e. no about:blank + throw new Error("Document PiP should show its opener URL"); + } // We only need to update the searchModeUI on tab switch conditionally // as we only persist searchMode with ScotchBonnet enabled. if ( diff --git a/dom/base/nsGlobalWindowOuter.cpp b/dom/base/nsGlobalWindowOuter.cpp @@ -4223,6 +4223,12 @@ nsresult nsGlobalWindowOuter::SetFullscreenInternal(FullscreenReason aReason, return NS_OK; } + // Element.requestFullscreen() is already blocked, but also block + // fullscreening for other callers, especially the chrome window. + if (GetBrowsingContext()->Top()->GetIsDocumentPiP()) { + return NS_OK; + } + // SetFullscreen needs to be called on the root window, so get that // via the DocShell tree, and if we are not already the root, // call SetFullscreen on that window instead. diff --git a/dom/chrome-webidl/BrowsingContext.webidl b/dom/chrome-webidl/BrowsingContext.webidl @@ -301,6 +301,12 @@ interface BrowsingContext { undefined resetNavigationRateLimit(); readonly attribute long childOffset; + + // https://wicg.github.io/document-picture-in-picture/ + // This is true both for the top-level BC of the content and chrome window + // of a Document Picture-in-Picture window. + [BinaryName="GetIsDocumentPiP"] + readonly attribute boolean isDocumentPiP; }; BrowsingContext includes LoadContextMixin; diff --git a/dom/documentpip/moz.build b/dom/documentpip/moz.build @@ -13,6 +13,8 @@ UNIFIED_SOURCES += [ "DocumentPictureInPicture.cpp", ] +BROWSER_CHROME_MANIFESTS += ["tests/browser/browser.toml"] + include("/ipc/chromium/chromium-config.mozbuild") FINAL_LIBRARY = "xul" diff --git a/dom/documentpip/tests/browser/browser.toml b/dom/documentpip/tests/browser/browser.toml @@ -2,4 +2,12 @@ support-files = ["head.js"] prefs = ["dom.documentpip.enabled=true"] +["browser_parent_has_pip_flag.js"] + ["browser_pip_closes_once.js"] + +["browser_pip_fullscreen_disallowed.js"] + +["browser_pip_ui.js"] + +["browser_sessionstore_ignore_dpip.js"] diff --git a/dom/documentpip/tests/browser/browser_parent_has_pip_flag.js b/dom/documentpip/tests/browser/browser_parent_has_pip_flag.js @@ -0,0 +1,19 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function check_chrome_window_has_pip_flag() { + const [tab, chromePiP] = await newTabWithPiP(); + + is( + chromePiP.location.href, + "chrome://browser/content/browser.xhtml", + "Got the chrome window of the PiP" + ); + ok(chromePiP.browsingContext.isDocumentPiP, "Chrome window has pip flag set"); + + // Cleanup. + await BrowserTestUtils.closeWindow(chromePiP); + BrowserTestUtils.removeTab(tab); +}); diff --git a/dom/documentpip/tests/browser/browser_pip_closes_once.js b/dom/documentpip/tests/browser/browser_pip_closes_once.js @@ -13,14 +13,31 @@ add_task(async function closing_pip_sends_exactly_one_DOMWindowClosed() { const [tab, chromePiP] = await newTabWithPiP(); - let closeCount = 0; - chromePiP.addEventListener("DOMWindowClose", () => closeCount++); + // Note: Counting DOMWindowClose in the parent process isn't the same. + // - The parent might have multiple, i.e. for closing tab and native window + // - Sending the second event up to the parent might fail if closing has progressed far enough + await SpecialPowers.spawn(chromePiP.gBrowser.selectedBrowser, [], () => { + content.opener.closeCount = 0; + SpecialPowers.addChromeEventListener( + "DOMWindowClose", + () => content.opener.closeCount++, + true, + false + ); + }); + // close PiP await SpecialPowers.spawn(tab.linkedBrowser, [], () => { content.documentPictureInPicture.window.close(); }); await BrowserTestUtils.windowClosed(chromePiP); + const closeCount = await SpecialPowers.spawn( + tab.linkedBrowser, + [], + () => content.closeCount + ); is(closeCount, 1, "Received a single DOMWindowClosed"); + BrowserTestUtils.removeTab(tab); }); diff --git a/dom/documentpip/tests/browser/browser_pip_fullscreen_disallowed.js b/dom/documentpip/tests/browser/browser_pip_fullscreen_disallowed.js @@ -0,0 +1,34 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function check_chrome_window_of_pip_cannot_fullscreen() { + const [tab, chromePiP] = await newTabWithPiP(); + + // Sanity check: VK_F11 will set window.fullScreen via View:Fullscreen command + const fullScreenEntered = BrowserTestUtils.waitForEvent(window, "fullscreen"); + window.fullScreen = true; + ok(window.fullScreen, "FS works as expected on window"); + await fullScreenEntered; + ok(true, "Got fullscreen event"); + + const fullScreenExited = BrowserTestUtils.waitForEvent(window, "fullscreen"); + window.fullScreen = false; + ok(!window.fullScreen, "FS works as expected on window"); + await fullScreenExited; + ok(true, "Got fullscreen event"); + + // Check PiP cannot be fullscreened by chrome + is( + chromePiP.location.href, + "chrome://browser/content/browser.xhtml", + "Got the chrome window of the PiP" + ); + chromePiP.fullScreen = true; + ok(!chromePiP.fullScreen, "Didn't enter fullscreen on PiP"); + + // Cleanup. + await BrowserTestUtils.closeWindow(chromePiP); + BrowserTestUtils.removeTab(tab); +}); diff --git a/dom/documentpip/tests/browser/browser_pip_ui.js b/dom/documentpip/tests/browser/browser_pip_ui.js @@ -0,0 +1,52 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { UrlbarTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/UrlbarTestUtils.sys.mjs" +); + +add_task(async function pip_urlbar_shows_readonly_opener_url() { + const [tab, chromePiP] = await newTabWithPiP(); + + // correct URL at the beginning + const expectedURL = UrlbarTestUtils.trimURL( + tab.linkedBrowser.currentURI.spec + ); + is(chromePiP.gURLBar.value, expectedURL, "PiP urlbar shows opener url"); + ok(chromePiP.gURLBar.readOnly, "Location bar is read-only in PiP"); + + // correct URL after PiP location change + const onLocationChange = BrowserTestUtils.waitForLocationChange( + chromePiP.gBrowser, + "about:blank#0" + ); + await SpecialPowers.spawn(chromePiP.gBrowser.selectedBrowser, [], () => { + content.location.href = "about:blank#0"; + }); + await onLocationChange; + is(chromePiP.gURLBar.value, expectedURL, "PiP urlbar shows opener url"); + + // Cleanup. + await BrowserTestUtils.closeWindow(chromePiP); + BrowserTestUtils.removeTab(tab); +}); + +add_task(async function pip_alwaysontop_chromeFlag() { + const [tab, chromePiP] = await newTabWithPiP(); + + // Currently, we cannot check the widget is actually alwaysontop. But we can check + // that the respective chromeFlag is set. + const chromeFlags = chromePiP.docShell.treeOwner + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIAppWindow).chromeFlags; + ok( + chromeFlags & Ci.nsIWebBrowserChrome.CHROME_ALWAYS_ON_TOP, + "PiP has alwaysontop chrome flag" + ); + + // Cleanup. + await BrowserTestUtils.closeWindow(chromePiP); + BrowserTestUtils.removeTab(tab); +}); diff --git a/dom/documentpip/tests/browser/browser_sessionstore_ignore_dpip.js b/dom/documentpip/tests/browser/browser_sessionstore_ignore_dpip.js @@ -0,0 +1,35 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { TabStateFlusher } = ChromeUtils.importESModule( + "resource:///modules/sessionstore/TabStateFlusher.sys.mjs" +); + +add_task(async function check_document_pip_not_saved() { + const [tab, chromePiP] = await newTabWithPiP(); + + await TabStateFlusher.flushWindow(window); + await TabStateFlusher.flushWindow(chromePiP); + let state = SessionStore.getCurrentState(true); + // Don't keepOnlyWorthSavingTabs as Ctrl+Shift+T can restore regardless of that + // and the about:blank in chromePiP might not be worth saving. + + is(state.windows.length, 1, "Only one window in session store"); + + // Just to be extra sure, check the only tracked window is for |tab| + const urls = state.windows[0].tabs + .flatMap(ssTab => ssTab.entries.map(entry => entry.url)) + .filter(url => !!url); + // |urls| contains an unexpected about:blank (bug 2006027). + // But if example.com is in there, the only tacked window is |tab| + ok( + urls.includes("https://example.com/"), + "Window tracked in sessionstore is tab not PiP" + ); + + // Cleanup. + await BrowserTestUtils.closeWindow(chromePiP); + BrowserTestUtils.removeTab(tab); +}); diff --git a/xpfe/appshell/AppWindow.cpp b/xpfe/appshell/AppWindow.cpp @@ -2320,8 +2320,8 @@ void AppWindow::SetContentScrollbarVisibility(bool aVisible) { } void AppWindow::ApplyChromeFlags() { - nsCOMPtr<dom::Element> window = GetWindowDOMElement(); - if (!window) { + nsCOMPtr<dom::Element> root = GetWindowDOMElement(); + if (!root) { return; } @@ -2362,7 +2362,19 @@ void AppWindow::ApplyChromeFlags() { // Note that if we're not actually changing the value this will be a no-op, // so no need to compare to the old value. IgnoredErrorResult rv; - window->SetAttribute(u"chromehidden"_ns, newvalue, rv); + root->SetAttribute(u"chromehidden"_ns, newvalue, rv); + + // Also set the IsDocumentPiP on the chrome browsing context + if ((mChromeFlags & + nsIWebBrowserChrome::CHROME_DOCUMENT_PICTURE_IN_PICTURE) == + nsIWebBrowserChrome::CHROME_DOCUMENT_PICTURE_IN_PICTURE) { + nsCOMPtr<mozIDOMWindowProxy> windowProxy; + GetWindowDOMWindow(getter_AddRefs(windowProxy)); + if (nsCOMPtr<nsPIDOMWindowOuter> window = do_QueryInterface(windowProxy)) { + nsresult rv = window->GetBrowsingContext()->SetIsDocumentPiP(true); + NS_ENSURE_SUCCESS_VOID(rv); + } + } } NS_IMETHODIMP