Loading browser/app/profile/001-base-profile.js +4 −0 Original line number Diff line number Diff line Loading @@ -508,6 +508,10 @@ pref("security.remote_settings.intermediates.enabled", false); pref("dom.use_components_shim", false); // Enable letterboxing pref("privacy.resistFingerprinting.letterboxing", true); // tor-browser#41917: Center letterboxed area vertically pref("privacy.resistFingerprinting.letterboxing.vcenter", true); // tor-browser#41917: Letterboxing gradient background pref("privacy.resistFingerprinting.letterboxing.gradient", true); // tor-browser#43402: Avoid a resize from the skeleton to the newwin size. // Should be fixed in ESR-140 with Bug 1448423. pref("browser.startup.blankWindow", false); Loading browser/components/tabbrowser/content/tabbrowser.js +4 −0 Original line number Diff line number Diff line Loading @@ -2410,6 +2410,10 @@ stack.className = "browserStack"; stack.appendChild(b); let decorator = document.createXULElement("hbox"); decorator.className = "browserDecorator"; stack.appendChild(decorator); let browserContainer = document.createXULElement("vbox"); browserContainer.className = "browserContainer"; browserContainer.appendChild(stack); Loading browser/themes/shared/tabbrowser/content-area.css +5 −0 Original line number Diff line number Diff line Loading @@ -380,6 +380,11 @@ split-view-footer { position: absolute; inset: 0; /* Override the <stack> `grid-area: 1 / 1` rule with an `auto` placement. * Otherwise the .dialogStack start edges are placed relative to the * center-aligned grid items, rather than the grid's padding area. */ grid-area: auto; /* Hide tab-modal dialogs when a window-modal one is up. */ :root[window-modal-open] .browserStack > &, /* For some printing use cases we need to visually hide the dialog before Loading toolkit/components/resistfingerprinting/RFPHelper.sys.mjs +320 −23 Original line number Diff line number Diff line Loading @@ -15,12 +15,21 @@ const kPrefLetterboxingDimensions = "privacy.resistFingerprinting.letterboxing.dimensions"; const kPrefLetterboxingTesting = "privacy.resistFingerprinting.letterboxing.testing"; const kPrefLetterboxingVcenter = "privacy.resistFingerprinting.letterboxing.vcenter"; const kTopicDOMWindowOpened = "domwindowopened"; const kTopicDOMWindowClosed = "domwindowclosed"; const kTopicFullscreenNavToolbox = "fullscreen-nav-toolbox"; const kPrefVerticalTabs = "sidebar.verticalTabs"; const lazy = {}; ChromeUtils.defineESModuleGetters(lazy, { Color: "resource://gre/modules/Color.sys.mjs", }); ChromeUtils.defineLazyGetter(lazy, "logConsole", () => console.createInstance({ prefix: "RFPHelper", Loading Loading @@ -56,6 +65,10 @@ class _RFPHelper { // Add unconditional observers Services.prefs.addObserver(kPrefResistFingerprinting, this); Services.prefs.addObserver(kPrefLetterboxing, this); Services.prefs.addObserver(kPrefLetterboxingVcenter, this); Services.prefs.addObserver(kPrefVerticalTabs, this); Services.obs.addObserver(this, kTopicFullscreenNavToolbox); XPCOMUtils.defineLazyPreferenceGetter( this, "_letterboxingDimensions", Loading Loading @@ -87,7 +100,10 @@ class _RFPHelper { // Remove unconditional observers Services.prefs.removeObserver(kPrefResistFingerprinting, this); Services.prefs.removeObserver(kPrefLetterboxingVcenter, this); Services.prefs.removeObserver(kPrefLetterboxing, this); Services.prefs.removeObserver(kPrefVerticalTabs, this); Services.obs.removeObserver(this, kTopicFullscreenNavToolbox); // Remove the RFP observers, swallowing exceptions if they weren't present this._removeLanguagePrefObservers(); } Loading @@ -109,6 +125,15 @@ class _RFPHelper { case kTopicDOMWindowClosed: this._handleDOMWindowClosed(subject); break; case kTopicFullscreenNavToolbox: // The `subject` is the gNavToolbox. // Record whether the toobox has been hidden when the browser (not // content) is in fullscreen. subject.ownerGlobal.gBrowser.tabbox.classList.toggle( "letterboxing-nav-toolbox-hidden", data === "hidden" ); break; default: break; } Loading @@ -123,6 +148,13 @@ class _RFPHelper { resizeObserver.observe(browser.parentElement); break; } case "nativethemechange": // NOTE: "nativethemechange" seems to always be sent after // "windowlwthemeupdate". So all the lwtheme CSS properties should be // set to the new theme's values already, so we don't need to wait for // windowlwthemeupdate. this._updateLetterboxingColors(aMessage.currentTarget, true); break; default: break; } Loading @@ -138,8 +170,14 @@ class _RFPHelper { this._handleSpoofEnglishChanged(); break; case kPrefLetterboxing: case kPrefLetterboxingVcenter: this._handleLetterboxingPrefChanged(); break; case kPrefVerticalTabs: if (this.letterboxingEnabled) { forEachWindow(win => this._updateLetterboxingColors(win)); } break; default: break; } Loading Loading @@ -346,7 +384,7 @@ class _RFPHelper { // If not already cached on the document object, traverse the CSSOM and // find the rule applying the default letterboxing styles to browsers // preemptively in order to beat race conditions on tab/window creation return (document._letterboxingMarginsRule ||= (() => { return (document._letterboxingDefaultRule ||= (() => { const LETTERBOX_CSS_SELECTOR = ".letterboxing"; const LETTERBOX_CSS_URL = "chrome://global/content/resistfingerprinting/letterboxing.css"; Loading Loading @@ -553,26 +591,22 @@ class _RFPHelper { if (lastRoundedSize) { // Check whether the letterboxing margin is less than the border radius, // and if so flatten the borders. let borderRadius = parseInt( win .getComputedStyle(browserContainer) .getPropertyValue("--letterboxing-border-radius") // and if so do not show an outline. const gapVertical = parentHeight - lastRoundedSize.height; const gapHorizontal = parentWidth - lastRoundedSize.width; browserParent.classList.toggle( "letterboxing-show-outline", gapVertical >= this._letterboxingBorderRadius || gapHorizontal >= this._letterboxingBorderRadius ); // When the Letterboxing area is top-aligned, only show the sidebar corner // if there is enough horizontal space. // The factor of 4 is from the horizontal centre-alignment and wanting // enough space for twice the corner radius. browserParent.classList.toggle( "letterboxing-show-sidebar-corner", gapHorizontal >= 4 * this._letterboxingBorderRadius ); if ( borderRadius && parentWidth - lastRoundedSize.width < borderRadius && parentHeight - lastRoundedSize.height < borderRadius ) { borderRadius = 0; } else { borderRadius = ""; } styleChanges.queueIfNeeded(browserParent, { "--letterboxing-decorator-visibility": borderRadius === 0 ? "hidden" : "", "--letterboxing-border-radius": borderRadius, }); } // If the size of the content is already quantized, we do nothing. Loading Loading @@ -605,12 +639,31 @@ class _RFPHelper { _resetContentSize(aBrowser) { aBrowser.parentElement.classList.add("exclude-letterboxing"); aBrowser.parentElement.classList.remove( "letterboxing-show-outline", "letterboxing-show-sidebar-corner" ); } _updateSizeForTabsInWindow(aWindow) { let tabBrowser = aWindow.gBrowser; tabBrowser.tabpanels?.classList.add("letterboxing"); tabBrowser.tabbox.classList.add("letterboxing"); tabBrowser.tabbox.classList.toggle( "letterboxing-vcenter", Services.prefs.getBoolPref(kPrefLetterboxingVcenter, false) ); if (this._letterboxingBorderRadius === undefined && tabBrowser.tabbox) { // Cache the value since it is not expected to change in a session for any // window. this._letterboxingBorderRadius = Math.ceil( parseFloat( aWindow .getComputedStyle(tabBrowser.tabbox) .getPropertyValue("--letterboxing-border-radius") ) ); } for (let tab of tabBrowser.tabs) { let browser = tab.linkedBrowser; Loading @@ -619,7 +672,7 @@ class _RFPHelper { // We need to add this class late because otherwise new windows get // maximized. aWindow.setTimeout(() => { tabBrowser.tabpanels?.classList.add("letterboxing-ready"); tabBrowser.tabbox.classList.add("letterboxing-ready"); }); } Loading @@ -639,6 +692,247 @@ class _RFPHelper { this._resizeObservers.set(aWindow, resizeObserver); // Rounding the content viewport. this._updateSizeForTabsInWindow(aWindow); this._updateLetterboxingColors(aWindow, true); aWindow.addEventListener("nativethemechange", this); } /** * Convert a CSS property to its RGBA value. * * @param {Window} win - The window for the element. * @param {CSSStyleDeclaration} style - The computed style for the element we * want to grab the color from. * @param {string} property - The name of the property we want. * * @returns {InspectorRGBATuple} - The RGBA color. The "r", "g", "b" fields * are relative to the 0-255 color range. The "a" field is in the 0-1 range. */ _convertToRGBA(win, style, property) { let cssColor = style.getPropertyValue(property); if (!cssColor) { lazy.logConsole.error(`Missing color "${property}"`); return { r: 0, g: 0, b: 0, a: 0 }; } const currentColorRegex = /(^|[^a-zA-Z0-9_-])currentColor($|[^a-zA-Z0-9_-])/g; if (currentColorRegex.test(cssColor)) { const currentColor = style.color; cssColor = cssColor.replace(currentColorRegex, (_, pre, post) => { return pre + currentColor + post; }); lazy.logConsole.debug( "Replaced currentColor.", property, currentColor, cssColor ); } /* Can drop the document argument after bugzilla bug 1973684 (142). */ const colorRGBA = win.InspectorUtils.colorToRGBA(cssColor, win.document); if (!colorRGBA) { lazy.logConsole.error( `Failed to convert "${property}" color (${cssColor}) to RGBA` ); return { r: 0, g: 0, b: 0, a: 0 }; } return colorRGBA; } /** * Compose two colors with alpha values on top of each other. * * @param {InspectorRGBATuple} topRGBA - The color to place on the top. * @param {InspectorRGBATuple} bottomRGBA - The color to place on the bottom. * * @returns {InspectorRGBATuple} - The composed color. */ _composeRGBA(topRGBA, bottomRGBA) { const topA = Math.max(0, Math.min(1, topRGBA.a)); const bottomA = Math.max(0, Math.min(1, bottomRGBA.a)); const a = topA + bottomA - topA * bottomA; // Should be 1 if either is 1. if (a === 0) { return { r: 0, g: 0, b: 0, a }; } const ret = { a }; for (const field of ["r", "g", "b"]) { ret[field] = (topRGBA[field] * topA + bottomRGBA[field] * bottomA * (1 - topA)) / a; } return ret; } /** * Calculate the urlbar's container opaque background color, removing any * transparency. * * @param {Window} win - The window to calculate the color for. * @param {CSSStyleDeclaration} style - The computed style for the #nav-bar * element. * * @returns {InspectorRGBATuple} - The calculated color, which will be opaque. */ _calculateUrlbarContainerColor(win, style) { let colorRGBA; if (!Services.prefs.getBoolPref(kPrefVerticalTabs)) { lazy.logConsole.debug("Toolbar background used."); colorRGBA = this._convertToRGBA(win, style, "--toolbar-bgcolor"); if (colorRGBA.a === 1) { return colorRGBA; } } else { // The urlbar only has the toolbox colour. colorRGBA = { r: 0, g: 0, b: 0, a: 0 }; } let toolboxHasBackgroundImage = false; const isLwTheme = win.document.documentElement.hasAttribute("lwtheme"); if (isLwTheme) { for (const prop of ["--lwt-header-image", "--lwt-additional-images"]) { const headerImage = style.getPropertyValue(prop); if (headerImage && headerImage !== "none") { // The theme sets a background image behind the urlbar. No easy way to // derive a single colour from this. toolboxHasBackgroundImage = true; lazy.logConsole.debug( "Toolbox has background image.", prop, headerImage ); break; } } } if (!toolboxHasBackgroundImage) { lazy.logConsole.debug("Toolbox background used."); colorRGBA = this._composeRGBA( colorRGBA, this._convertToRGBA(win, style, "--toolbox-bgcolor") ); if (colorRGBA.a === 1) { return colorRGBA; } } // Determine whether the urlbar is dark. // At this point, the urlbar background has some transparency, likely on top // of an image. // We use the theme's text colour to figure out whether the urlbar // background is overall meant to be light or dark. Unlike the urlbar, we // expect this colour to be (almost) opaque. const textRGBA = this._convertToRGBA(win, style, "--toolbar-field-color"); const textColor = new lazy.Color(textRGBA.r, textRGBA.g, textRGBA.b); if (textColor.relativeLuminance >= 0.5) { // Light text, so assume it has a dark background. // Combine with a generic opaque dark colour. Copied from "frame" for the // built-in dark theme. lazy.logConsole.debug("Generic dark background used."); const darkFrameRGBA = { r: 28, g: 27, b: 34, a: 1 }; return this._composeRGBA(colorRGBA, darkFrameRGBA); } // Combine with an opaque light colour. Copied from "frame" for the built-in // light theme. lazy.logConsole.debug("Generic light background used."); const lightFrameRGBA = { r: 234, g: 234, b: 237, a: 1 }; return this._composeRGBA(colorRGBA, lightFrameRGBA); } /** * Update the Letterboxing colors and related classes, or clear them if * Letterboxing is not enabled. * * @param {Window} win - The window to update the colors for. * @param {boolean} letterboxingEnabled - Whether Letterboxing is enabled. */ _updateLetterboxingColors(win, letterboxingEnabled) { let urlbarBackgroundRGBA; let urlbarTextRGBA; let contentSeparatorRGBA; let urlbarBackgroundDark = false; let lowBackgroundOutlineContrast = false; if (letterboxingEnabled) { // Want the effective colour of various elements without any alpha values // so they can be used consistently. const navbarStyle = win.getComputedStyle( win.document.getElementById("nav-bar") ); const containerRGBA = this._calculateUrlbarContainerColor( win, navbarStyle ); urlbarBackgroundRGBA = this._composeRGBA( this._convertToRGBA( win, navbarStyle, "--toolbar-field-background-color" ), containerRGBA ); urlbarTextRGBA = this._composeRGBA( this._convertToRGBA(win, navbarStyle, "--toolbar-field-color"), urlbarBackgroundRGBA ); /* Separator between the urlbar container #nav-bar and the tabbox. */ const tabboxStyle = win.getComputedStyle(win.gBrowser.tabbox); contentSeparatorRGBA = this._composeRGBA( this._convertToRGBA( win, tabboxStyle, "--chrome-content-separator-color" ), containerRGBA ); const bgColor = new lazy.Color( urlbarBackgroundRGBA.r, urlbarBackgroundRGBA.g, urlbarBackgroundRGBA.b ); const outlineColor = new lazy.Color( contentSeparatorRGBA.r, contentSeparatorRGBA.g, contentSeparatorRGBA.b ); const contrastRatio = bgColor.contrastRatio(outlineColor); lazy.logConsole.debug( "Outline-background contrast ratio.", contrastRatio ); urlbarBackgroundDark = bgColor.relativeLuminance < 0.5; /* Very low contrast ratio. For reference the default light theme has * a contrast ratio of ~1.1. */ lowBackgroundOutlineContrast = contrastRatio < 1.05; } for (const { name, colorRGBA } of [ { name: "--letterboxing-urlbar-text-color", colorRGBA: urlbarTextRGBA, }, { name: "--letterboxing-urlbar-background-color", colorRGBA: urlbarBackgroundRGBA, }, { name: "--letterboxing-content-separator-color", colorRGBA: contentSeparatorRGBA, }, ]) { if (letterboxingEnabled) { win.gBrowser.tabbox.style.setProperty( name, `rgb(${colorRGBA.r}, ${colorRGBA.g}, ${colorRGBA.b})` ); } else { win.gBrowser.tabbox.style.removeProperty(name); } } win.gBrowser.tabbox.classList.toggle( "letterboxing-urlbar-background-dark", urlbarBackgroundDark ); win.gBrowser.tabbox.classList.toggle( "letterboxing-low-background-outline-contrast", lowBackgroundOutlineContrast ); } _attachAllWindows() { Loading Loading @@ -670,13 +964,16 @@ class _RFPHelper { aWindow.removeEventListener("TabOpen", this); // revert tabpanel's style to default tabBrowser.tabpanels?.classList.remove("letterboxing"); tabBrowser.tabbox.classList.remove("letterboxing"); // and restore default size on each browser element for (let tab of tabBrowser.tabs) { let browser = tab.linkedBrowser; this._resetContentSize(browser); } aWindow.removeEventListener("nativethemechange", this); this._updateLetterboxingColors(aWindow, false); } _detachAllWindows() { Loading toolkit/components/resistfingerprinting/content/letterboxing.css +164 −7 Original line number Diff line number Diff line Loading @@ -7,9 +7,106 @@ * RFPHelper.sys.mjs (LETTERBOX_CSS_SELECTOR and LETTERBOX_CSS_URL, * respectively), where --letterboxing-width & --letterboxing-height are * actually set. * Keep this block first and separate to the rules that do not necessarily * require --letterboxing-width or --letterboxing-height. */ .letterboxing { --letterboxing-bgcolor: var(--tabpanel-background-color); .browserContainer:not(.responsive-mode) > .browserStack:not(.exclude-letterboxing) > browser { width: var(--letterboxing-width) !important; height: var(--letterboxing-height) !important; } } #tabbrowser-tabbox.letterboxing { --letterboxing-bgcolor: var(--background-color-canvas); /* Match the border radius used for the sidebar. */ --letterboxing-border-radius: var(--border-radius-medium); --letterboxing-border-radius-top: 0; --letterboxing-vertical-alignment: start; --letterboxing-shadow: none; --letterboxing-outline-color: var(--border-color); --letterboxing-outline-width: 1px; @media not ((prefers-contrast) or (forced-colors)) { /* Match the #sidebar outline width. */ --letterboxing-outline-width: 0.5px; --letterboxing-shadow-color: rgba(58, 57, 68, 0.2); --letterboxing-shadow: 0 2px 14px 0 var(--letterboxing-shadow-color); /* Match the effective urlbar background colour. */ --letterboxing-bgcolor: var(--letterboxing-urlbar-background-color); /* Match the effective colour of the separator between the urlbar container * and the content. */ --letterboxing-outline-color: var(--letterboxing-content-separator-color); &.letterboxing-urlbar-background-dark { --letterboxing-shadow-color: #15141a; } &.letterboxing-low-background-outline-contrast { /* The default content separator colour has insufficient contrast. */ --letterboxing-outline-color: color-mix(in srgb, var(--letterboxing-content-separator-color) 90%, black); &.letterboxing-urlbar-background-dark { /* Lighten the colour. */ --letterboxing-outline-color: color-mix(in srgb, var(--letterboxing-content-separator-color) 90%, white); } } } @media (prefers-contrast) and (not (forced-colors)) { :root[lwtheme] & { /* User with prefers-contrast coming from the system settings, but also an * installed theme. */ --letterboxing-bgcolor: var(--letterboxing-urlbar-background-color); /* Presumably a user with prefers-contrast and a custom theme has chosen * a theme where the contrast between the urlbar and the text is * sufficiently high or low. */ --letterboxing-outline-color: var(--letterboxing-urlbar-text-color); } } background: var(--letterboxing-bgcolor); &:has(.deck-selected .browserContainer:not(.responsive-mode) > .browserStack:not(.exclude-letterboxing).letterboxing-show-outline) { /* Letterboxing outline is visible for the current tab. Replace the usual * outline to match the Letterboxing outline. For most scenarios, this * should be mostly the same colour as when Letterboxing is not visible. But * it may make a difference for some theme combinations. */ outline-color: var(--letterboxing-outline-color); outline-width: var(--letterboxing-outline-width); } #tabbrowser-tabpanels { /* Override the --tabpanel-background-color. * Also, make sure this remains transparent, otherwise it will overlap the * parent's corner's border-radius due to it's "position: relative" rule. */ /* TODO: FIX this for newtab pages. tor-browser#44085 */ background: transparent; } /* stylelint-disable-next-line media-query-no-invalid */ @media -moz-pref("sidebar.revamp") { :root:not([inDOMFullscreen]) &[sidebar-shown]:not(.letterboxing-nav-toolbox-hidden):is( /* When the Letterboxing area is aligned to the top, show the rounded * corner if there is enough vertical space between the sidebar and the * browser element, which is not rounded at the top. */ :not(.letterboxing-vcenter):has(.deck-selected .browserContainer:not(.responsive-mode) > .browserStack:not(.exclude-letterboxing).letterboxing-show-sidebar-corner), /* When the Letterboxing area is aligned to the centre, show the rounded * corner if the Letterboxing border is shown. */ .letterboxing-vcenter:has(.deck-selected .browserContainer:not(.responsive-mode) > .browserStack:not(.exclude-letterboxing).letterboxing-show-outline) ) { /* stylelint-disable-next-line media-query-no-invalid */ @media -moz-pref("sidebar.position_start") { border-start-start-radius: var(--letterboxing-border-radius); } /* stylelint-disable-next-line media-query-no-invalid */ @media not -moz-pref("sidebar.position_start") { border-start-end-radius: var(--letterboxing-border-radius); } } } .browserContainer { /* Loading @@ -18,15 +115,75 @@ * doesn't get notified on horizontal shrinking. */ overflow: hidden; background: var(--letterboxing-bgcolor); } .browserContainer:not(.responsive-mode) > .browserStack:not(.exclude-letterboxing) > browser { width: var(--letterboxing-width) !important; height: var(--letterboxing-height) !important; &.letterboxing-vcenter { --letterboxing-border-radius-top: var(--letterboxing-border-radius); --letterboxing-vertical-alignment: center; } } .browserContainer:not(.responsive-mode) > .browserStack:not(.exclude-letterboxing) { :root:not([inDOMFullscreen]) .letterboxing.letterboxing-ready & { place-content: var(--letterboxing-vertical-alignment) center; } :root:not([inDOMFullscreen]) .letterboxing &.letterboxing-show-outline { browser { /* We use clip-path rather than border-radius because border-radius on its * own leads to rendering artefacts in the corners (tested with GNOME). * See tor-browser#44214 (comment 3262962). */ /* TODO: Use border-radius once bugzilla bug 1991874 is resolved. */ clip-path: rect( auto auto auto auto round var(--letterboxing-border-radius-top) var(--letterboxing-border-radius-top) var(--letterboxing-border-radius) var(--letterboxing-border-radius) ); } .browserDecorator { /* Need a separate browserDecorator element because the clip-path on the * browser would exclude the outline and box-shadow. */ /* TODO: Move these rules to the browser element once bugzilla bug 1991874 * is resolved, and drop browserDecorator. */ display: block; border-radius: var(--letterboxing-border-radius-top) var(--letterboxing-border-radius-top) var(--letterboxing-border-radius) var(--letterboxing-border-radius); /* NOTE: The top outline will not be visible when this is aligned to the * top. */ outline: var(--letterboxing-outline-width) solid var(--letterboxing-outline-color); box-shadow: var(--letterboxing-shadow); } #statuspanel:not([mirror]) #statuspanel-label { border-end-start-radius: var(--letterboxing-border-radius); } #statuspanel[mirror] #statuspanel-label { border-end-end-radius: var(--letterboxing-border-radius); } } #statuspanel { position: relative; place-self: end start; z-index: 2; &[mirror] { justify-self: end; } } #statuspanel-label { margin: 0; outline: var(--letterboxing-outline-width) solid var(--letterboxing-outline-color); max-width: calc(var(--letterboxing-width) * 0.5); } } :root:not([inDOMFullscreen]) .letterboxing.letterboxing-ready .browserContainer:not(.responsive-mode) > .browserStack:not(.exclude-letterboxing) { place-content: start center; .browserDecorator { display: none; pointer-events: none; background: transparent; position: relative; z-index: 1; } Loading
browser/app/profile/001-base-profile.js +4 −0 Original line number Diff line number Diff line Loading @@ -508,6 +508,10 @@ pref("security.remote_settings.intermediates.enabled", false); pref("dom.use_components_shim", false); // Enable letterboxing pref("privacy.resistFingerprinting.letterboxing", true); // tor-browser#41917: Center letterboxed area vertically pref("privacy.resistFingerprinting.letterboxing.vcenter", true); // tor-browser#41917: Letterboxing gradient background pref("privacy.resistFingerprinting.letterboxing.gradient", true); // tor-browser#43402: Avoid a resize from the skeleton to the newwin size. // Should be fixed in ESR-140 with Bug 1448423. pref("browser.startup.blankWindow", false); Loading
browser/components/tabbrowser/content/tabbrowser.js +4 −0 Original line number Diff line number Diff line Loading @@ -2410,6 +2410,10 @@ stack.className = "browserStack"; stack.appendChild(b); let decorator = document.createXULElement("hbox"); decorator.className = "browserDecorator"; stack.appendChild(decorator); let browserContainer = document.createXULElement("vbox"); browserContainer.className = "browserContainer"; browserContainer.appendChild(stack); Loading
browser/themes/shared/tabbrowser/content-area.css +5 −0 Original line number Diff line number Diff line Loading @@ -380,6 +380,11 @@ split-view-footer { position: absolute; inset: 0; /* Override the <stack> `grid-area: 1 / 1` rule with an `auto` placement. * Otherwise the .dialogStack start edges are placed relative to the * center-aligned grid items, rather than the grid's padding area. */ grid-area: auto; /* Hide tab-modal dialogs when a window-modal one is up. */ :root[window-modal-open] .browserStack > &, /* For some printing use cases we need to visually hide the dialog before Loading
toolkit/components/resistfingerprinting/RFPHelper.sys.mjs +320 −23 Original line number Diff line number Diff line Loading @@ -15,12 +15,21 @@ const kPrefLetterboxingDimensions = "privacy.resistFingerprinting.letterboxing.dimensions"; const kPrefLetterboxingTesting = "privacy.resistFingerprinting.letterboxing.testing"; const kPrefLetterboxingVcenter = "privacy.resistFingerprinting.letterboxing.vcenter"; const kTopicDOMWindowOpened = "domwindowopened"; const kTopicDOMWindowClosed = "domwindowclosed"; const kTopicFullscreenNavToolbox = "fullscreen-nav-toolbox"; const kPrefVerticalTabs = "sidebar.verticalTabs"; const lazy = {}; ChromeUtils.defineESModuleGetters(lazy, { Color: "resource://gre/modules/Color.sys.mjs", }); ChromeUtils.defineLazyGetter(lazy, "logConsole", () => console.createInstance({ prefix: "RFPHelper", Loading Loading @@ -56,6 +65,10 @@ class _RFPHelper { // Add unconditional observers Services.prefs.addObserver(kPrefResistFingerprinting, this); Services.prefs.addObserver(kPrefLetterboxing, this); Services.prefs.addObserver(kPrefLetterboxingVcenter, this); Services.prefs.addObserver(kPrefVerticalTabs, this); Services.obs.addObserver(this, kTopicFullscreenNavToolbox); XPCOMUtils.defineLazyPreferenceGetter( this, "_letterboxingDimensions", Loading Loading @@ -87,7 +100,10 @@ class _RFPHelper { // Remove unconditional observers Services.prefs.removeObserver(kPrefResistFingerprinting, this); Services.prefs.removeObserver(kPrefLetterboxingVcenter, this); Services.prefs.removeObserver(kPrefLetterboxing, this); Services.prefs.removeObserver(kPrefVerticalTabs, this); Services.obs.removeObserver(this, kTopicFullscreenNavToolbox); // Remove the RFP observers, swallowing exceptions if they weren't present this._removeLanguagePrefObservers(); } Loading @@ -109,6 +125,15 @@ class _RFPHelper { case kTopicDOMWindowClosed: this._handleDOMWindowClosed(subject); break; case kTopicFullscreenNavToolbox: // The `subject` is the gNavToolbox. // Record whether the toobox has been hidden when the browser (not // content) is in fullscreen. subject.ownerGlobal.gBrowser.tabbox.classList.toggle( "letterboxing-nav-toolbox-hidden", data === "hidden" ); break; default: break; } Loading @@ -123,6 +148,13 @@ class _RFPHelper { resizeObserver.observe(browser.parentElement); break; } case "nativethemechange": // NOTE: "nativethemechange" seems to always be sent after // "windowlwthemeupdate". So all the lwtheme CSS properties should be // set to the new theme's values already, so we don't need to wait for // windowlwthemeupdate. this._updateLetterboxingColors(aMessage.currentTarget, true); break; default: break; } Loading @@ -138,8 +170,14 @@ class _RFPHelper { this._handleSpoofEnglishChanged(); break; case kPrefLetterboxing: case kPrefLetterboxingVcenter: this._handleLetterboxingPrefChanged(); break; case kPrefVerticalTabs: if (this.letterboxingEnabled) { forEachWindow(win => this._updateLetterboxingColors(win)); } break; default: break; } Loading Loading @@ -346,7 +384,7 @@ class _RFPHelper { // If not already cached on the document object, traverse the CSSOM and // find the rule applying the default letterboxing styles to browsers // preemptively in order to beat race conditions on tab/window creation return (document._letterboxingMarginsRule ||= (() => { return (document._letterboxingDefaultRule ||= (() => { const LETTERBOX_CSS_SELECTOR = ".letterboxing"; const LETTERBOX_CSS_URL = "chrome://global/content/resistfingerprinting/letterboxing.css"; Loading Loading @@ -553,26 +591,22 @@ class _RFPHelper { if (lastRoundedSize) { // Check whether the letterboxing margin is less than the border radius, // and if so flatten the borders. let borderRadius = parseInt( win .getComputedStyle(browserContainer) .getPropertyValue("--letterboxing-border-radius") // and if so do not show an outline. const gapVertical = parentHeight - lastRoundedSize.height; const gapHorizontal = parentWidth - lastRoundedSize.width; browserParent.classList.toggle( "letterboxing-show-outline", gapVertical >= this._letterboxingBorderRadius || gapHorizontal >= this._letterboxingBorderRadius ); // When the Letterboxing area is top-aligned, only show the sidebar corner // if there is enough horizontal space. // The factor of 4 is from the horizontal centre-alignment and wanting // enough space for twice the corner radius. browserParent.classList.toggle( "letterboxing-show-sidebar-corner", gapHorizontal >= 4 * this._letterboxingBorderRadius ); if ( borderRadius && parentWidth - lastRoundedSize.width < borderRadius && parentHeight - lastRoundedSize.height < borderRadius ) { borderRadius = 0; } else { borderRadius = ""; } styleChanges.queueIfNeeded(browserParent, { "--letterboxing-decorator-visibility": borderRadius === 0 ? "hidden" : "", "--letterboxing-border-radius": borderRadius, }); } // If the size of the content is already quantized, we do nothing. Loading Loading @@ -605,12 +639,31 @@ class _RFPHelper { _resetContentSize(aBrowser) { aBrowser.parentElement.classList.add("exclude-letterboxing"); aBrowser.parentElement.classList.remove( "letterboxing-show-outline", "letterboxing-show-sidebar-corner" ); } _updateSizeForTabsInWindow(aWindow) { let tabBrowser = aWindow.gBrowser; tabBrowser.tabpanels?.classList.add("letterboxing"); tabBrowser.tabbox.classList.add("letterboxing"); tabBrowser.tabbox.classList.toggle( "letterboxing-vcenter", Services.prefs.getBoolPref(kPrefLetterboxingVcenter, false) ); if (this._letterboxingBorderRadius === undefined && tabBrowser.tabbox) { // Cache the value since it is not expected to change in a session for any // window. this._letterboxingBorderRadius = Math.ceil( parseFloat( aWindow .getComputedStyle(tabBrowser.tabbox) .getPropertyValue("--letterboxing-border-radius") ) ); } for (let tab of tabBrowser.tabs) { let browser = tab.linkedBrowser; Loading @@ -619,7 +672,7 @@ class _RFPHelper { // We need to add this class late because otherwise new windows get // maximized. aWindow.setTimeout(() => { tabBrowser.tabpanels?.classList.add("letterboxing-ready"); tabBrowser.tabbox.classList.add("letterboxing-ready"); }); } Loading @@ -639,6 +692,247 @@ class _RFPHelper { this._resizeObservers.set(aWindow, resizeObserver); // Rounding the content viewport. this._updateSizeForTabsInWindow(aWindow); this._updateLetterboxingColors(aWindow, true); aWindow.addEventListener("nativethemechange", this); } /** * Convert a CSS property to its RGBA value. * * @param {Window} win - The window for the element. * @param {CSSStyleDeclaration} style - The computed style for the element we * want to grab the color from. * @param {string} property - The name of the property we want. * * @returns {InspectorRGBATuple} - The RGBA color. The "r", "g", "b" fields * are relative to the 0-255 color range. The "a" field is in the 0-1 range. */ _convertToRGBA(win, style, property) { let cssColor = style.getPropertyValue(property); if (!cssColor) { lazy.logConsole.error(`Missing color "${property}"`); return { r: 0, g: 0, b: 0, a: 0 }; } const currentColorRegex = /(^|[^a-zA-Z0-9_-])currentColor($|[^a-zA-Z0-9_-])/g; if (currentColorRegex.test(cssColor)) { const currentColor = style.color; cssColor = cssColor.replace(currentColorRegex, (_, pre, post) => { return pre + currentColor + post; }); lazy.logConsole.debug( "Replaced currentColor.", property, currentColor, cssColor ); } /* Can drop the document argument after bugzilla bug 1973684 (142). */ const colorRGBA = win.InspectorUtils.colorToRGBA(cssColor, win.document); if (!colorRGBA) { lazy.logConsole.error( `Failed to convert "${property}" color (${cssColor}) to RGBA` ); return { r: 0, g: 0, b: 0, a: 0 }; } return colorRGBA; } /** * Compose two colors with alpha values on top of each other. * * @param {InspectorRGBATuple} topRGBA - The color to place on the top. * @param {InspectorRGBATuple} bottomRGBA - The color to place on the bottom. * * @returns {InspectorRGBATuple} - The composed color. */ _composeRGBA(topRGBA, bottomRGBA) { const topA = Math.max(0, Math.min(1, topRGBA.a)); const bottomA = Math.max(0, Math.min(1, bottomRGBA.a)); const a = topA + bottomA - topA * bottomA; // Should be 1 if either is 1. if (a === 0) { return { r: 0, g: 0, b: 0, a }; } const ret = { a }; for (const field of ["r", "g", "b"]) { ret[field] = (topRGBA[field] * topA + bottomRGBA[field] * bottomA * (1 - topA)) / a; } return ret; } /** * Calculate the urlbar's container opaque background color, removing any * transparency. * * @param {Window} win - The window to calculate the color for. * @param {CSSStyleDeclaration} style - The computed style for the #nav-bar * element. * * @returns {InspectorRGBATuple} - The calculated color, which will be opaque. */ _calculateUrlbarContainerColor(win, style) { let colorRGBA; if (!Services.prefs.getBoolPref(kPrefVerticalTabs)) { lazy.logConsole.debug("Toolbar background used."); colorRGBA = this._convertToRGBA(win, style, "--toolbar-bgcolor"); if (colorRGBA.a === 1) { return colorRGBA; } } else { // The urlbar only has the toolbox colour. colorRGBA = { r: 0, g: 0, b: 0, a: 0 }; } let toolboxHasBackgroundImage = false; const isLwTheme = win.document.documentElement.hasAttribute("lwtheme"); if (isLwTheme) { for (const prop of ["--lwt-header-image", "--lwt-additional-images"]) { const headerImage = style.getPropertyValue(prop); if (headerImage && headerImage !== "none") { // The theme sets a background image behind the urlbar. No easy way to // derive a single colour from this. toolboxHasBackgroundImage = true; lazy.logConsole.debug( "Toolbox has background image.", prop, headerImage ); break; } } } if (!toolboxHasBackgroundImage) { lazy.logConsole.debug("Toolbox background used."); colorRGBA = this._composeRGBA( colorRGBA, this._convertToRGBA(win, style, "--toolbox-bgcolor") ); if (colorRGBA.a === 1) { return colorRGBA; } } // Determine whether the urlbar is dark. // At this point, the urlbar background has some transparency, likely on top // of an image. // We use the theme's text colour to figure out whether the urlbar // background is overall meant to be light or dark. Unlike the urlbar, we // expect this colour to be (almost) opaque. const textRGBA = this._convertToRGBA(win, style, "--toolbar-field-color"); const textColor = new lazy.Color(textRGBA.r, textRGBA.g, textRGBA.b); if (textColor.relativeLuminance >= 0.5) { // Light text, so assume it has a dark background. // Combine with a generic opaque dark colour. Copied from "frame" for the // built-in dark theme. lazy.logConsole.debug("Generic dark background used."); const darkFrameRGBA = { r: 28, g: 27, b: 34, a: 1 }; return this._composeRGBA(colorRGBA, darkFrameRGBA); } // Combine with an opaque light colour. Copied from "frame" for the built-in // light theme. lazy.logConsole.debug("Generic light background used."); const lightFrameRGBA = { r: 234, g: 234, b: 237, a: 1 }; return this._composeRGBA(colorRGBA, lightFrameRGBA); } /** * Update the Letterboxing colors and related classes, or clear them if * Letterboxing is not enabled. * * @param {Window} win - The window to update the colors for. * @param {boolean} letterboxingEnabled - Whether Letterboxing is enabled. */ _updateLetterboxingColors(win, letterboxingEnabled) { let urlbarBackgroundRGBA; let urlbarTextRGBA; let contentSeparatorRGBA; let urlbarBackgroundDark = false; let lowBackgroundOutlineContrast = false; if (letterboxingEnabled) { // Want the effective colour of various elements without any alpha values // so they can be used consistently. const navbarStyle = win.getComputedStyle( win.document.getElementById("nav-bar") ); const containerRGBA = this._calculateUrlbarContainerColor( win, navbarStyle ); urlbarBackgroundRGBA = this._composeRGBA( this._convertToRGBA( win, navbarStyle, "--toolbar-field-background-color" ), containerRGBA ); urlbarTextRGBA = this._composeRGBA( this._convertToRGBA(win, navbarStyle, "--toolbar-field-color"), urlbarBackgroundRGBA ); /* Separator between the urlbar container #nav-bar and the tabbox. */ const tabboxStyle = win.getComputedStyle(win.gBrowser.tabbox); contentSeparatorRGBA = this._composeRGBA( this._convertToRGBA( win, tabboxStyle, "--chrome-content-separator-color" ), containerRGBA ); const bgColor = new lazy.Color( urlbarBackgroundRGBA.r, urlbarBackgroundRGBA.g, urlbarBackgroundRGBA.b ); const outlineColor = new lazy.Color( contentSeparatorRGBA.r, contentSeparatorRGBA.g, contentSeparatorRGBA.b ); const contrastRatio = bgColor.contrastRatio(outlineColor); lazy.logConsole.debug( "Outline-background contrast ratio.", contrastRatio ); urlbarBackgroundDark = bgColor.relativeLuminance < 0.5; /* Very low contrast ratio. For reference the default light theme has * a contrast ratio of ~1.1. */ lowBackgroundOutlineContrast = contrastRatio < 1.05; } for (const { name, colorRGBA } of [ { name: "--letterboxing-urlbar-text-color", colorRGBA: urlbarTextRGBA, }, { name: "--letterboxing-urlbar-background-color", colorRGBA: urlbarBackgroundRGBA, }, { name: "--letterboxing-content-separator-color", colorRGBA: contentSeparatorRGBA, }, ]) { if (letterboxingEnabled) { win.gBrowser.tabbox.style.setProperty( name, `rgb(${colorRGBA.r}, ${colorRGBA.g}, ${colorRGBA.b})` ); } else { win.gBrowser.tabbox.style.removeProperty(name); } } win.gBrowser.tabbox.classList.toggle( "letterboxing-urlbar-background-dark", urlbarBackgroundDark ); win.gBrowser.tabbox.classList.toggle( "letterboxing-low-background-outline-contrast", lowBackgroundOutlineContrast ); } _attachAllWindows() { Loading Loading @@ -670,13 +964,16 @@ class _RFPHelper { aWindow.removeEventListener("TabOpen", this); // revert tabpanel's style to default tabBrowser.tabpanels?.classList.remove("letterboxing"); tabBrowser.tabbox.classList.remove("letterboxing"); // and restore default size on each browser element for (let tab of tabBrowser.tabs) { let browser = tab.linkedBrowser; this._resetContentSize(browser); } aWindow.removeEventListener("nativethemechange", this); this._updateLetterboxingColors(aWindow, false); } _detachAllWindows() { Loading
toolkit/components/resistfingerprinting/content/letterboxing.css +164 −7 Original line number Diff line number Diff line Loading @@ -7,9 +7,106 @@ * RFPHelper.sys.mjs (LETTERBOX_CSS_SELECTOR and LETTERBOX_CSS_URL, * respectively), where --letterboxing-width & --letterboxing-height are * actually set. * Keep this block first and separate to the rules that do not necessarily * require --letterboxing-width or --letterboxing-height. */ .letterboxing { --letterboxing-bgcolor: var(--tabpanel-background-color); .browserContainer:not(.responsive-mode) > .browserStack:not(.exclude-letterboxing) > browser { width: var(--letterboxing-width) !important; height: var(--letterboxing-height) !important; } } #tabbrowser-tabbox.letterboxing { --letterboxing-bgcolor: var(--background-color-canvas); /* Match the border radius used for the sidebar. */ --letterboxing-border-radius: var(--border-radius-medium); --letterboxing-border-radius-top: 0; --letterboxing-vertical-alignment: start; --letterboxing-shadow: none; --letterboxing-outline-color: var(--border-color); --letterboxing-outline-width: 1px; @media not ((prefers-contrast) or (forced-colors)) { /* Match the #sidebar outline width. */ --letterboxing-outline-width: 0.5px; --letterboxing-shadow-color: rgba(58, 57, 68, 0.2); --letterboxing-shadow: 0 2px 14px 0 var(--letterboxing-shadow-color); /* Match the effective urlbar background colour. */ --letterboxing-bgcolor: var(--letterboxing-urlbar-background-color); /* Match the effective colour of the separator between the urlbar container * and the content. */ --letterboxing-outline-color: var(--letterboxing-content-separator-color); &.letterboxing-urlbar-background-dark { --letterboxing-shadow-color: #15141a; } &.letterboxing-low-background-outline-contrast { /* The default content separator colour has insufficient contrast. */ --letterboxing-outline-color: color-mix(in srgb, var(--letterboxing-content-separator-color) 90%, black); &.letterboxing-urlbar-background-dark { /* Lighten the colour. */ --letterboxing-outline-color: color-mix(in srgb, var(--letterboxing-content-separator-color) 90%, white); } } } @media (prefers-contrast) and (not (forced-colors)) { :root[lwtheme] & { /* User with prefers-contrast coming from the system settings, but also an * installed theme. */ --letterboxing-bgcolor: var(--letterboxing-urlbar-background-color); /* Presumably a user with prefers-contrast and a custom theme has chosen * a theme where the contrast between the urlbar and the text is * sufficiently high or low. */ --letterboxing-outline-color: var(--letterboxing-urlbar-text-color); } } background: var(--letterboxing-bgcolor); &:has(.deck-selected .browserContainer:not(.responsive-mode) > .browserStack:not(.exclude-letterboxing).letterboxing-show-outline) { /* Letterboxing outline is visible for the current tab. Replace the usual * outline to match the Letterboxing outline. For most scenarios, this * should be mostly the same colour as when Letterboxing is not visible. But * it may make a difference for some theme combinations. */ outline-color: var(--letterboxing-outline-color); outline-width: var(--letterboxing-outline-width); } #tabbrowser-tabpanels { /* Override the --tabpanel-background-color. * Also, make sure this remains transparent, otherwise it will overlap the * parent's corner's border-radius due to it's "position: relative" rule. */ /* TODO: FIX this for newtab pages. tor-browser#44085 */ background: transparent; } /* stylelint-disable-next-line media-query-no-invalid */ @media -moz-pref("sidebar.revamp") { :root:not([inDOMFullscreen]) &[sidebar-shown]:not(.letterboxing-nav-toolbox-hidden):is( /* When the Letterboxing area is aligned to the top, show the rounded * corner if there is enough vertical space between the sidebar and the * browser element, which is not rounded at the top. */ :not(.letterboxing-vcenter):has(.deck-selected .browserContainer:not(.responsive-mode) > .browserStack:not(.exclude-letterboxing).letterboxing-show-sidebar-corner), /* When the Letterboxing area is aligned to the centre, show the rounded * corner if the Letterboxing border is shown. */ .letterboxing-vcenter:has(.deck-selected .browserContainer:not(.responsive-mode) > .browserStack:not(.exclude-letterboxing).letterboxing-show-outline) ) { /* stylelint-disable-next-line media-query-no-invalid */ @media -moz-pref("sidebar.position_start") { border-start-start-radius: var(--letterboxing-border-radius); } /* stylelint-disable-next-line media-query-no-invalid */ @media not -moz-pref("sidebar.position_start") { border-start-end-radius: var(--letterboxing-border-radius); } } } .browserContainer { /* Loading @@ -18,15 +115,75 @@ * doesn't get notified on horizontal shrinking. */ overflow: hidden; background: var(--letterboxing-bgcolor); } .browserContainer:not(.responsive-mode) > .browserStack:not(.exclude-letterboxing) > browser { width: var(--letterboxing-width) !important; height: var(--letterboxing-height) !important; &.letterboxing-vcenter { --letterboxing-border-radius-top: var(--letterboxing-border-radius); --letterboxing-vertical-alignment: center; } } .browserContainer:not(.responsive-mode) > .browserStack:not(.exclude-letterboxing) { :root:not([inDOMFullscreen]) .letterboxing.letterboxing-ready & { place-content: var(--letterboxing-vertical-alignment) center; } :root:not([inDOMFullscreen]) .letterboxing &.letterboxing-show-outline { browser { /* We use clip-path rather than border-radius because border-radius on its * own leads to rendering artefacts in the corners (tested with GNOME). * See tor-browser#44214 (comment 3262962). */ /* TODO: Use border-radius once bugzilla bug 1991874 is resolved. */ clip-path: rect( auto auto auto auto round var(--letterboxing-border-radius-top) var(--letterboxing-border-radius-top) var(--letterboxing-border-radius) var(--letterboxing-border-radius) ); } .browserDecorator { /* Need a separate browserDecorator element because the clip-path on the * browser would exclude the outline and box-shadow. */ /* TODO: Move these rules to the browser element once bugzilla bug 1991874 * is resolved, and drop browserDecorator. */ display: block; border-radius: var(--letterboxing-border-radius-top) var(--letterboxing-border-radius-top) var(--letterboxing-border-radius) var(--letterboxing-border-radius); /* NOTE: The top outline will not be visible when this is aligned to the * top. */ outline: var(--letterboxing-outline-width) solid var(--letterboxing-outline-color); box-shadow: var(--letterboxing-shadow); } #statuspanel:not([mirror]) #statuspanel-label { border-end-start-radius: var(--letterboxing-border-radius); } #statuspanel[mirror] #statuspanel-label { border-end-end-radius: var(--letterboxing-border-radius); } } #statuspanel { position: relative; place-self: end start; z-index: 2; &[mirror] { justify-self: end; } } #statuspanel-label { margin: 0; outline: var(--letterboxing-outline-width) solid var(--letterboxing-outline-color); max-width: calc(var(--letterboxing-width) * 0.5); } } :root:not([inDOMFullscreen]) .letterboxing.letterboxing-ready .browserContainer:not(.responsive-mode) > .browserStack:not(.exclude-letterboxing) { place-content: start center; .browserDecorator { display: none; pointer-events: none; background: transparent; position: relative; z-index: 1; }