chromium/chrome/browser/ui/color/win/native_chrome_color_mixer_win.cc

// Copyright 2021 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chrome/browser/ui/color/native_chrome_color_mixer.h"

#include <optional>

#include "base/callback_list.h"
#include "base/functional/bind.h"
#include "base/no_destructor.h"
#include "base/win/windows_version.h"
#include "chrome/browser/themes/theme_properties.h"
#include "chrome/browser/ui/color/chrome_color_id.h"
#include "chrome/browser/win/mica_titlebar.h"
#include "chrome/grit/theme_resources.h"
#include "ui/color/color_id.h"
#include "ui/color/color_mixer.h"
#include "ui/color/color_provider.h"
#include "ui/color/color_provider_key.h"
#include "ui/color/color_provider_utils.h"
#include "ui/color/color_recipe.h"
#include "ui/color/color_transform.h"
#include "ui/color/win/accent_color_observer.h"
#include "ui/gfx/color_utils.h"
#include "ui/native_theme/native_theme.h"
#include "ui/views/views_features.h"

namespace {
// This class encapsulates much of the same logic from ThemeHelperWin pertaining
// to the calculation of frame colors on Windows 8, 10 and up. Once the
// ColorProvider is permanently switched on, all the relevant code from
// ThemeHelperWin can be deleted.
class FrameColorHelper {
 public:
  FrameColorHelper();
  FrameColorHelper(const FrameColorHelper&) = delete;
  FrameColorHelper& operator=(const FrameColorHelper&) = delete;
  ~FrameColorHelper() = default;

  void AddNativeChromeColors(ui::ColorMixer& mixer,
                             const ui::ColorProviderKey& key) const;
  void AddBorderAccentColors(ui::ColorMixer& mixer) const;

  static FrameColorHelper* Get();

 private:
  // Returns the Tint for the given |id|. If there is no tint, the identity tint
  // {-1, -1, -1} is returned and won't tint the color on which it is used.
  color_utils::HSL GetTint(int id, const ui::ColorProviderKey& key) const;

  // Callback executed when the accent color is updated. This re-reads the
  // accent color and updates |dwm_frame_color_| and
  // |dwm_inactive_frame_color_|.
  void OnAccentColorUpdated();

  // Re-reads the accent colors and updates member variables.
  void FetchAccentColors();

  base::CallbackListSubscription subscription_ =
      ui::AccentColorObserver::Get()->Subscribe(
          base::BindRepeating(&FrameColorHelper::OnAccentColorUpdated,
                              base::Unretained(this)));

  // The frame color when active. If empty the default colors should be used.
  std::optional<SkColor> dwm_frame_color_;

  // The frame color when inactive. If empty the default colors should be used.
  std::optional<SkColor> dwm_inactive_frame_color_;

  // The DWM accent border color, if available; white otherwise.
  SkColor dwm_accent_border_color_ = SK_ColorWHITE;
};

FrameColorHelper::FrameColorHelper() {
  FetchAccentColors();
}

void FrameColorHelper::AddNativeChromeColors(
    ui::ColorMixer& mixer,
    const ui::ColorProviderKey& key) const {
  using TP = ThemeProperties;
  using ColorMode = ui::ColorProviderKey::ColorMode;

  auto get_theme_color = [key](int id) -> std::optional<SkColor> {
    SkColor theme_color;
    if (key.custom_theme && key.custom_theme->GetColor(id, &theme_color))
      return theme_color;
    return std::nullopt;
  };

  // When we're custom-drawing the titlebar we want to use either the colors
  // we calculated in OnDwmKeyUpdated() or the default colors. When we're not
  // custom-drawing the titlebar we want to match the color Windows actually
  // uses because some things (like the incognito icon) use this color to
  // decide whether they should draw in light or dark mode. Incognito colors
  // should be the same as non-incognito in all cases here.

  constexpr SkColor kSystemMicaLightFrameColor =
      SkColorSetRGB(0xE8, 0xE8, 0xE8);
  constexpr SkColor kSystemMicaDarkFrameColor = SkColorSetRGB(0x20, 0x20, 0x20);

  // We should only attempt to paint system-style frames if configured to do so
  // in the key.
  const bool use_native_colors =
      (key.frame_type == ui::ColorProviderKey::FrameType::kChromium &&
       key.frame_style == ui::ColorProviderKey::FrameStyle::kSystem);

  std::optional<ui::ColorTransform> active_frame_transform;
  if (auto color = get_theme_color(TP::COLOR_FRAME_ACTIVE)) {
    active_frame_transform = {color.value()};
  } else if (use_native_colors) {
    if (dwm_frame_color_) {
      active_frame_transform = {dwm_frame_color_.value()};
    } else if (ShouldDefaultThemeUseMicaTitlebar()) {
      active_frame_transform = {key.color_mode == ColorMode::kDark
                                    ? kSystemMicaDarkFrameColor
                                    : kSystemMicaLightFrameColor};
    }
  }

  std::optional<ui::ColorTransform> inactive_frame_transform;
  if (auto color = get_theme_color(TP::COLOR_FRAME_INACTIVE)) {
    inactive_frame_transform = {color.value()};
  } else if (use_native_colors) {
    if (dwm_inactive_frame_color_) {
      inactive_frame_transform = {dwm_inactive_frame_color_.value()};
    } else if (dwm_frame_color_) {
      inactive_frame_transform =
          ui::HSLShift({dwm_frame_color_.value()},
                       GetTint(ThemeProperties::TINT_FRAME_INACTIVE, key));
    } else if (ShouldDefaultThemeUseMicaTitlebar()) {
      inactive_frame_transform = {key.color_mode == ColorMode::kDark
                                      ? kSystemMicaDarkFrameColor
                                      : kSystemMicaLightFrameColor};
    }
  }

  // If setting custom window frame colors ensure we also update the
  // corresponding sys header colors. Although this diverges from chrome's
  // material spec these overrides are necessary to ensure UI assigned to these
  // color roles can continue to work as expected while respecting platform
  // frame overrides.
  if (active_frame_transform) {
    mixer[ui::kColorFrameActive] = active_frame_transform.value();
    mixer[ui::kColorSysHeader] = active_frame_transform.value();
    mixer[ui::kColorSysOnHeaderDivider] =
        GetColorWithMaxContrast(ui::kColorSysHeader);
    mixer[ui::kColorSysOnHeaderPrimary] =
        GetColorWithMaxContrast(ui::kColorSysHeader);
    mixer[ui::kColorSysStateHeaderHover] =
        ui::AlphaBlend(ui::kColorSysBase, ui::kColorSysHeader,
                       /* 40% opacity */ 0.4 * SK_AlphaOPAQUE);
    mixer[ui::kColorSysHeaderContainer] = {ui::kColorSysBase};
  }
  if (inactive_frame_transform) {
    mixer[ui::kColorFrameInactive] = inactive_frame_transform.value();
    mixer[ui::kColorSysHeaderInactive] = inactive_frame_transform.value();
    mixer[ui::kColorSysOnHeaderDividerInactive] =
        GetColorWithMaxContrast(ui::kColorSysHeaderInactive);
    mixer[ui::kColorSysOnHeaderPrimaryInactive] =
        GetColorWithMaxContrast(ui::kColorSysHeaderInactive);
    mixer[ui::kColorSysStateHeaderHoverInactive] =
        ui::AlphaBlend(ui::kColorSysBase, ui::kColorSysHeaderInactive,
                       /* 40% opacity */ 0.4 * SK_AlphaOPAQUE);
    mixer[ui::kColorSysHeaderContainerInactive] = {ui::kColorSysBase};
  }

  if (ShouldDefaultThemeUseMicaTitlebar() && !key.app_controller) {
    mixer[kColorNewTabButtonBackgroundFrameActive] = {SK_ColorTRANSPARENT};
    mixer[kColorNewTabButtonBackgroundFrameInactive] = {SK_ColorTRANSPARENT};
    mixer[kColorNewTabButtonInkDropFrameActive] =
        ui::GetColorWithMaxContrast(ui::kColorFrameActive);
    mixer[kColorNewTabButtonInkDropFrameInactive] =
        ui::GetColorWithMaxContrast(ui::kColorFrameInactive);
  }
}

void FrameColorHelper::AddBorderAccentColors(ui::ColorMixer& mixer) const {
  // In Windows 10, native inactive borders are #555555 with 50% alpha.
  // Prior to version 1809, native active borders use the accent color.
  // In version 1809 and following, the active border is #262626 with 66%
  // alpha unless the accent color is also used for the frame.
  mixer[kColorAccentBorderActive] = {
      (base::win::GetVersion() >= base::win::Version::WIN10_RS5 &&
       !dwm_frame_color_)
          ? SkColorSetARGB(0xa8, 0x26, 0x26, 0x26)
          : dwm_accent_border_color_};
  mixer[kColorAccentBorderInactive] = {SkColorSetARGB(0x80, 0x55, 0x55, 0x55)};
}

// static
FrameColorHelper* FrameColorHelper::Get() {
  static base::NoDestructor<FrameColorHelper> g_frame_color_helper;
  return g_frame_color_helper.get();
}

color_utils::HSL FrameColorHelper::GetTint(
    int id,
    const ui::ColorProviderKey& key) const {
  color_utils::HSL hsl;
  if (key.custom_theme && key.custom_theme->GetTint(id, &hsl))
    return hsl;
  // Always pass false for |incognito| here since the ColorProvider is treating
  // incognito mode as dark mode. If this needs to change, that information will
  // need to propagate into the ColorProviderKey.
  return ThemeProperties::GetDefaultTint(
      id, false, key.color_mode == ui::ColorProviderKey::ColorMode::kDark);
}

void FrameColorHelper::OnAccentColorUpdated() {
  FetchAccentColors();
  ui::NativeTheme::GetInstanceForNativeUi()->NotifyOnNativeThemeUpdated();
  ui::NativeTheme::GetInstanceForDarkUI()->NotifyOnNativeThemeUpdated();
  ui::NativeTheme::GetInstanceForWeb()->NotifyOnNativeThemeUpdated();
}

void FrameColorHelper::FetchAccentColors() {
  // Update the NativeTheme's user_color to reflect the system accent color.
  // TODO(crbug.com/40280436): Explore moving FrameColorHelper logic into
  // NativeThemeWin.
  const auto* accent_color_observer = ui::AccentColorObserver::Get();
  const auto accent_color = accent_color_observer->accent_color();
  ui::NativeTheme::GetInstanceForNativeUi()->set_user_color(accent_color);
  ui::NativeTheme::GetInstanceForDarkUI()->set_user_color(accent_color);
  ui::NativeTheme::GetInstanceForWeb()->set_user_color(accent_color);

  if (!accent_color_observer->use_dwm_frame_color()) {
    dwm_accent_border_color_ = SK_ColorWHITE;
    dwm_frame_color_.reset();
    dwm_inactive_frame_color_.reset();
    return;
  }

  dwm_accent_border_color_ =
      accent_color_observer->accent_border_color().value_or(SK_ColorWHITE);

  dwm_frame_color_ = accent_color;
  dwm_inactive_frame_color_ = accent_color_observer->accent_color_inactive();
}

ui::ColorTransform GetCaptionForegroundColor(
    ui::ColorTransform input_transform) {
  const auto generator = [](ui::ColorTransform input_transform,
                            SkColor input_color, const ui::ColorMixer& mixer) {
    const SkColor background_color = input_transform.Run(input_color, mixer);
    const float windows_luma = 0.25f * SkColorGetR(background_color) +
                               0.625f * SkColorGetG(background_color) +
                               0.125f * SkColorGetB(background_color);
    const SkColor result_color =
        (windows_luma <= 128.0f) ? SK_ColorWHITE : SK_ColorBLACK;
    DVLOG(2) << "ColorTransform GetCaptionForegroundColor:"
             << " Background Color: " << ui::SkColorName(background_color)
             << " Result Color: " << ui::SkColorName(result_color);
    return result_color;
  };
  return base::BindRepeating(generator, std::move(input_transform));
}

}  // namespace

void AddNativeChromeColorMixer(ui::ColorProvider* provider,
                               const ui::ColorProviderKey& key) {
  ui::ColorMixer& mixer = provider->AddMixer();

  // NOTE: These cases are always handled, even on Win7, in order to ensure the
  // the color provider redirection tests function. Win7 callers should never
  // actually pass in these IDs.
  FrameColorHelper::Get()->AddBorderAccentColors(mixer);

  mixer[kColorCaptionButtonForegroundActive] =
      GetCaptionForegroundColor(kColorWindowControlButtonBackgroundActive);
  mixer[kColorCaptionButtonForegroundInactive] =
      GetCaptionForegroundColor(kColorWindowControlButtonBackgroundInactive);
  mixer[kColorCaptionCloseButtonBackgroundHovered] = {
      SkColorSetRGB(0xE8, 0x11, 0x23)};
  mixer[kColorCaptionCloseButtonForegroundHovered] = {SK_ColorWHITE};
  mixer[kColorCaptionForegroundActive] =
      GetCaptionForegroundColor(ui::kColorFrameActive);
  mixer[kColorCaptionForegroundInactive] =
      SetAlpha(GetCaptionForegroundColor(ui::kColorFrameInactive), 0x66);
  mixer[kColorTabSearchCaptionButtonFocusRing] = ui::PickGoogleColor(
      ui::kColorFocusableBorderFocused, ui::kColorFrameActive,
      color_utils::kMinimumVisibleContrastRatio);

  if (key.color_mode == ui::ColorProviderKey::ColorMode::kLight) {
    mixer[kColorNewTabPageBackground] = {ui::kColorNativeWindow};
    mixer[kColorNewTabPageLink] = {ui::kColorNativeHotlight};
    mixer[kColorNewTabPageText] = {ui::kColorNativeWindowText};
  }

  if (key.contrast_mode != ui::ColorProviderKey::ContrastMode::kHigh) {
    FrameColorHelper::Get()->AddNativeChromeColors(mixer, key);
    return;
  }

  // High contrast uses system colors.
  mixer[kColorDownloadShelfContentAreaSeparator] = {
      kColorToolbarContentAreaSeparator};
  mixer[kColorInfoBarContentAreaSeparator] = {
      kColorToolbarContentAreaSeparator};
  mixer[kColorLocationBarBorder] = {ui::kColorNativeWindowText};
  mixer[kColorToolbarBackgroundSubtleEmphasis] = {ui::kColorNativeBtnFace};
  mixer[kColorToolbarBackgroundSubtleEmphasisHovered] = {
      kColorToolbarBackgroundSubtleEmphasis};
  mixer[kColorOmniboxBubbleOutline] = {kColorOmniboxText};
  mixer[kColorOmniboxKeywordSelected] = {kColorOmniboxText};
  mixer[kColorOmniboxResultsBackground] = {
      kColorToolbarBackgroundSubtleEmphasis};
  mixer[kColorOmniboxResultsBackgroundHovered] = {ui::kColorNativeHighlight};
  mixer[kColorOmniboxResultsBackgroundSelected] = {ui::kColorNativeHighlight};
  mixer[kColorOmniboxResultsIcon] = {kColorOmniboxText};
  mixer[kColorOmniboxResultsIconSelected] = {kColorOmniboxResultsTextSelected};
  mixer[kColorOmniboxResultsTextDimmed] = {kColorOmniboxText};
  mixer[kColorOmniboxResultsTextDimmedSelected] = {
      kColorOmniboxResultsTextSelected};
  mixer[kColorOmniboxResultsTextNegative] = {kColorOmniboxText};
  mixer[kColorOmniboxResultsTextNegativeSelected] = {
      kColorOmniboxResultsTextSelected};
  mixer[kColorOmniboxResultsTextPositive] = {kColorOmniboxText};
  mixer[kColorOmniboxResultsTextPositiveSelected] = {
      kColorOmniboxResultsTextSelected};
  mixer[kColorOmniboxResultsTextSecondary] = {kColorOmniboxText};
  mixer[kColorOmniboxResultsTextSecondarySelected] = {
      kColorOmniboxResultsTextSelected};
  mixer[kColorOmniboxResultsTextSelected] = {ui::kColorNativeHighlightText};
  mixer[kColorOmniboxResultsUrl] = {kColorOmniboxText};
  mixer[kColorOmniboxResultsUrlSelected] = {kColorOmniboxResultsTextSelected};
  mixer[kColorOmniboxSecurityChipDangerous] = {kColorOmniboxText};
  mixer[kColorOmniboxSecurityChipDefault] = {kColorOmniboxText};
  mixer[kColorOmniboxSecurityChipSecure] = {kColorOmniboxText};
  mixer[kColorOmniboxText] = {ui::kColorTextfieldForeground};
  mixer[kColorOmniboxTextDimmed] = {kColorOmniboxText};
  mixer[kColorTabBackgroundActiveFrameActive] = {ui::kColorNativeHighlight};
  mixer[kColorTabBackgroundActiveFrameInactive] = {
      kColorTabBackgroundActiveFrameActive};
  mixer[kColorTabForegroundActiveFrameActive] = {ui::kColorNativeHighlightText};
  mixer[kColorNewTabButtonCRBackgroundFrameActive] = {
      kColorTabBackgroundActiveFrameActive};
  mixer[kColorNewTabButtonCRBackgroundFrameInactive] = {
      kColorTabBackgroundActiveFrameInactive};
  mixer[kColorNewTabButtonForegroundFrameActive] = {
      kColorTabForegroundActiveFrameActive};
  mixer[kColorNewTabButtonForegroundFrameInactive] = {
      kColorTabForegroundActiveFrameActive};
  mixer[kColorToolbar] = {ui::kColorNativeWindow};
  mixer[kColorToolbarButtonIcon] = {kColorToolbarText};
  const bool platform_high_contrast_ink_drop = base::FeatureList::IsEnabled(
      views::features::kEnablePlatformHighContrastInkDrop);
  if (platform_high_contrast_ink_drop)
    mixer[kColorToolbarButtonIconHovered] = {ui::kColorNativeHighlightText};
  else
    mixer[kColorToolbarButtonIconHovered] = {kColorToolbarText};
  mixer[kColorToolbarButtonIconInactive] = {ui::kColorNativeGrayText};
  mixer[kColorToolbarContentAreaSeparator] = {kColorToolbarText};
  if (platform_high_contrast_ink_drop)
    mixer[kColorToolbarInkDrop] = {ui::kColorNativeHighlight};
  mixer[kColorToolbarSeparator] = {ui::kColorNativeWindowText};
  mixer[kColorToolbarText] = {ui::kColorNativeBtnText};
  mixer[kColorToolbarTopSeparatorFrameActive] = {kColorToolbarSeparator};
  mixer[kColorToolbarTopSeparatorFrameInactive] = {
      kColorToolbarTopSeparatorFrameActive};
}