Commit 081a2921 authored by ma1's avatar ma1 Committed by Pier Angelo Vendrame
Browse files

BB 32308: Use direct browser sizing for letterboxing.

Bug 30556: align letterboxing with 200x100 new win width stepping
parent 3701ca1c
Loading
Loading
Loading
Loading
+4 −0
Original line number Diff line number Diff line
@@ -549,6 +549,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);
+5 −0
Original line number Diff line number Diff line
@@ -673,6 +673,11 @@ split-view-footer {
  width: 100%;
  height: 100%;

  /* 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
+348 −23
Original line number Diff line number Diff line
@@ -14,12 +14,19 @@ 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 kPrefVerticalTabs = "sidebar.verticalTabs";

const lazy = {};

ChromeUtils.defineESModuleGetters(lazy, {
  Color: "resource://gre/modules/Color.sys.mjs",
});
ChromeUtils.defineLazyGetter(lazy, "logConsole", () =>
  console.createInstance({
    prefix: "RFPHelper",
@@ -55,6 +62,9 @@ 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);

    XPCOMUtils.defineLazyPreferenceGetter(
      this,
      "_letterboxingDimensions",
@@ -86,7 +96,9 @@ 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);
    // Remove the RFP observers, swallowing exceptions if they weren't present
    this._removeLanguagePrefObservers();
  }
@@ -122,6 +134,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;
    }
@@ -137,8 +156,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;
    }
@@ -345,7 +370,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";
@@ -552,26 +577,14 @@ 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
      );
      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.
@@ -604,12 +617,28 @@ class _RFPHelper {

  _resetContentSize(aBrowser) {
    aBrowser.parentElement.classList.add("exclude-letterboxing");
    aBrowser.parentElement.classList.remove("letterboxing-show-outline");
  }

  _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;
@@ -618,7 +647,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");
    });
  }

@@ -638,6 +667,299 @@ 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.
   * @param {object} [options] - Optional details.
   * @param {string} [options.fallbackProperty] - A fallback to use instead if
   *   the property doesn't have a computed value.
   * @param {string} [options.currentColorProperty] - The name of a property to
   *   use as the currentColor.
   *
   * @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, options) {
    let cssColor = style.getPropertyValue(property);
    if (!cssColor) {
      if (options?.fallbackProperty) {
        lazy.logConsole.debug(
          "Using fallback property for RGBA.",
          property,
          options.fallbackProperty
        );
        return this._convertToRGBA(win, style, options.fallbackProperty);
      }
      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)) {
      let currentColor;
      if (options?.currentColorProperty) {
        const currRGBA = this._convertToRGBA(
          win,
          style,
          options.currentColorProperty
        );
        currentColor = `rgba(${currRGBA.r}, ${currRGBA.g}, ${currRGBA.b}, ${currRGBA.a})`;
      } else {
        lazy.logConsole.warning(
          "Missing a specification for the currentColor, using computed color."
        );
        // Use the current "color" value. NOTE: this may not be exactly what we
        // want since it's current value may be effected by :hover, :active,
        // :focus, etc. But we want this to be a stable colour for the theme.
        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.
   * @param {boolean} verticalTabs - Whether vertical tabs are enabled.
   *
   * @returns {InspectorRGBATuple} - The calculated color, which will be opaque.
   */
  _calculateUrlbarContainerColor(win, style, verticalTabs) {
    let colorRGBA;
    if (!verticalTabs) {
      lazy.logConsole.debug("Toolbar background used.");
      colorRGBA = this._convertToRGBA(win, style, "--toolbar-background-color");
      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-background-color")
      );
      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-text-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 verticalTabs = Services.prefs.getBoolPref(kPrefVerticalTabs);
      const chromeTextColorProperty = verticalTabs
        ? "--toolbox-text-color"
        : "--toolbar-text-color";

      const navbarStyle = win.getComputedStyle(
        win.document.getElementById("nav-bar")
      );
      const containerRGBA = this._calculateUrlbarContainerColor(
        win,
        navbarStyle,
        verticalTabs
      );
      urlbarBackgroundRGBA = this._composeRGBA(
        this._convertToRGBA(
          win,
          navbarStyle,
          "--toolbar-field-background-color"
        ),
        containerRGBA
      );
      // NOTE: In the default theme (no "lwtheme" attribute) with
      // browser.theme.native-theme set to false, --toolbar-field-color can be
      // set to "inherit", which means it will have a blank computed value. We
      // fallback to --toolbar-text-color or --toolbox-text-color in this case.
      // Similarly, for windows OS, it can be set to "currentColor".
      urlbarTextRGBA = this._composeRGBA(
        this._convertToRGBA(win, navbarStyle, "--toolbar-field-text-color", {
          fallbackProperty: chromeTextColorProperty,
          currentColorProperty: chromeTextColorProperty,
        }),
        urlbarBackgroundRGBA
      );
      // Separator between the urlbar container #nav-bar and the tabbox.
      // For the default theme, this can be set to --card-border-color, which
      // can use "currentColor".
      const tabboxStyle = win.getComputedStyle(win.gBrowser.tabbox);
      contentSeparatorRGBA = this._composeRGBA(
        this._convertToRGBA(
          win,
          tabboxStyle,
          "--chrome-content-separator-color",
          { currentColorProperty: chromeTextColorProperty }
        ),
        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() {
@@ -669,13 +991,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() {
+112 −7
Original line number Diff line number Diff line
@@ -7,26 +7,131 @@
 * 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: var(--border-width);

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

  /* Override the --tabpanel-background-color. */
  /* TODO: FIX this for newtab pages. tor-browser#44085 */
  --tabpanel-background-color: transparent;

  .browserContainer {
    background: var(--letterboxing-bgcolor);
    /*
     * From Firefox 115 on, .browserContainer layout is flex / column,
     * and without this trick the .browserStack's resize observer
     * 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;
  }
}

.letterboxing .deck-selected .browserContainer:not(.responsive-mode):has(> .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);
}

.letterboxing .browserContainer:not(.responsive-mode) > .browserStack:not(.exclude-letterboxing) {
  :root:not([inDOMFullscreen]) .letterboxing-ready & {
    place-content: var(--letterboxing-vertical-alignment) center;
  }

  :root:not([inDOMFullscreen]) &.letterboxing-show-outline {
    browser {
      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);
    }
  }

:root:not([inDOMFullscreen]) .letterboxing.letterboxing-ready .browserContainer:not(.responsive-mode) > .browserStack:not(.exclude-letterboxing) {
  place-content: start center;
  #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);
  }
}