chromium/ash/system/notification_center/notification_style_utils.cc

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

#include "ash/system/notification_center/notification_style_utils.h"
#include <memory>

#include "ash/strings/grit/ash_strings.h"
#include "ash/style/ash_color_id.h"
#include "ash/style/ash_color_provider.h"
#include "ash/style/dark_light_mode_controller_impl.h"
#include "ash/style/pill_button.h"
#include "ash/system/notification_center/message_center_constants.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/chromeos/styles/cros_tokens_color_mappings.h"
#include "ui/color/color_id.h"
#include "ui/color/color_provider.h"
#include "ui/color/color_provider_manager.h"
#include "ui/compositor/layer.h"
#include "ui/gfx/geometry/rounded_corners_f.h"
#include "ui/gfx/image/image.h"
#include "ui/gfx/image/image_skia.h"
#include "ui/gfx/image/image_skia_operations.h"
#include "ui/gfx/paint_vector_icon.h"
#include "ui/message_center/public/cpp/notification.h"
#include "ui/message_center/vector_icons.h"
#include "ui/message_center/views/message_view.h"
#include "ui/native_theme/native_theme.h"
#include "ui/views/background.h"
#include "ui/views/controls/button/label_button.h"
#include "ui/views/controls/label.h"
#include "ui/views/highlight_border.h"
#include "ui/views/layout/flex_layout_view.h"

namespace ash::notification_style_utils {

gfx::ImageSkia CreateNotificationAppIcon(
    const message_center::Notification* notification) {
  SkColor icon_color = GetColorProviderForNativeTheme()->GetColor(
      cros_tokens::kCrosSysOnPrimary);
  SkColor icon_background_color = CalculateIconBackgroundColor(notification);

  // TODO(crbug.com/40541732): figure out if this has a performance impact and
  // cache images if so.
  gfx::Image masked_small_icon = notification->GenerateMaskedSmallIcon(
      kNotificationAppIconImageSize, icon_color, icon_background_color,
      icon_color);

  gfx::ImageSkia app_icon =
      masked_small_icon.IsEmpty()
          ? gfx::CreateVectorIcon(message_center::kProductIcon,
                                  kNotificationAppIconImageSize, icon_color)
          : masked_small_icon.AsImageSkia();

  return gfx::ImageSkiaOperations::CreateImageWithCircleBackground(
      kNotificationAppIconViewSize / 2, icon_background_color, app_icon);
}

gfx::ImageSkia CreateNotificationItemIcon(
    const message_center::NotificationItem* item) {
  if (item->icon().has_value()) {
    gfx::ImageSkia resized = gfx::ImageSkiaOperations::CreateResizedImage(
        item->icon().value().GetImage().AsImageSkia(),
        skia::ImageOperations::ResizeMethod::RESIZE_BEST,
        gfx::Size(kNotificationAppIconViewSize, kNotificationAppIconViewSize));

    return resized;
  }
  // TODO(b/284512022): Remove the temporary implementation returning a
  // hardcoded chrome icon as a default icon.
  return gfx::ImageSkiaOperations::CreateImageWithCircleBackground(
      kNotificationAppIconViewSize / 2, SK_ColorRED,
      gfx::CreateVectorIcon(message_center::kProductIcon,
                            kNotificationAppIconImageSize, SK_ColorBLACK));
}

SkColor CalculateIconBackgroundColor(
    const message_center::Notification* notification) {
  SkColor default_color = AshColorProvider::Get()->GetControlsLayerColor(
      AshColorProvider::ControlsLayerType::kControlBackgroundColorActive);

  if (!notification) {
    return default_color;
  }

  auto color_id = notification->accent_color_id();
  std::optional<SkColor> accent_color = notification->accent_color();

  if (!color_id || !accent_color.has_value()) {
    return default_color;
  }

  SkColor fg_color;
  // ColorProvider needs widget to be created.
  if (color_id) {
    fg_color = GetColorProviderForNativeTheme()->GetColor(color_id.value());
  } else {
    fg_color = accent_color.value();
  }

  // TODO(crbug/1351205): move color calculation logic to color mixer.
  // TODO(crbug/1294459): re-evaluate contrast, maybe increase or use fixed HSL
  float minContrastRatio =
      DarkLightModeControllerImpl::Get()->IsDarkModeEnabled()
          ? minContrastRatio = kDarkModeMinContrastRatio
          : color_utils::kMinimumReadableContrastRatio;

  // Actual color is kTransparent80, but BlendForMinContrast requires opaque.
  // GetColorProvider might be nullptr in tests.
  const auto* color_provider = GetColorProviderForNativeTheme();
  const SkColor bg_color =
      color_provider ? color_provider->GetColor(kColorAshShieldAndBaseOpaque)
                     : gfx::kPlaceholderColor;
  return color_utils::BlendForMinContrast(
             fg_color, bg_color,
             /*high_contrast_foreground=*/std::nullopt, minContrastRatio)
      .color;
}

void ConfigureLabelStyle(views::Label* label,
                         int size,
                         bool is_color_primary,
                         gfx::Font::Weight font_weight) {
  label->SetAutoColorReadabilityEnabled(false);
  label->SetFontList(
      gfx::FontList({kGoogleSansFont}, gfx::Font::NORMAL, size, font_weight));
  auto layer_type =
      is_color_primary
          ? ash::AshColorProvider::ContentLayerType::kTextColorPrimary
          : ash::AshColorProvider::ContentLayerType::kTextColorSecondary;
  label->SetEnabledColor(
      ash::AshColorProvider::Get()->GetContentLayerColor(layer_type));
}

ui::ColorProvider* GetColorProviderForNativeTheme() {
  auto* native_theme = ui::NativeTheme::GetInstanceForNativeUi();
  return ui::ColorProviderManager::Get().GetColorProviderFor(
      native_theme->GetColorProviderKey(nullptr));
}

std::unique_ptr<views::Background> CreateNotificationBackground(
    int top_radius,
    int bottom_radius,
    bool is_popup_notification,
    bool is_grouped_child_notification) {
  ui::ColorId background_color_id =
      is_popup_notification ? static_cast<ui::ColorId>(kColorAshShieldAndBase80)
                            : cros_tokens::kCrosSysSystemOnBase;

  const gfx::RoundedCornersF background_radii(top_radius, top_radius,
                                              bottom_radius, bottom_radius);
  if (is_grouped_child_notification) {
    // Grouped children are always transparent. Handle them separately.
    return views::CreateRoundedRectBackground(SK_ColorTRANSPARENT,
                                              background_radii);
  }

  return views::CreateThemedRoundedRectBackground(background_color_id,
                                                  background_radii);
}

void StyleNotificationPopup(message_center::MessageView* notification_view) {
  notification_view->SetPaintToLayer();
  auto* layer = notification_view->layer();
  layer->SetFillsBoundsOpaquely(false);
  layer->SetBackgroundBlur(ColorProvider::kBackgroundBlurSigma);
  layer->SetBackdropFilterQuality(ColorProvider::kBackgroundBlurQuality);
  layer->SetRoundedCornerRadius(
      gfx::RoundedCornersF{kMessagePopupCornerRadius});
  layer->SetIsFastRoundedCorner(true);

  notification_view->SetBackground(CreateNotificationBackground(
      kMessagePopupCornerRadius, kMessagePopupCornerRadius,
      /*is_popup_notification=*/true, /*is_grouped_child_notification=*/false));
  notification_view->SetBorder(std::make_unique<views::HighlightBorder>(
      kMessagePopupCornerRadius,
      views::HighlightBorder::Type::kHighlightBorderOnShadow));
  notification_view->UpdateCornerRadius(kMessagePopupCornerRadius,
                                        kMessagePopupCornerRadius);
}

std::unique_ptr<views::LabelButton> GenerateNotificationLabelButton(
    views::Button::PressedCallback callback,
    const std::u16string& label) {
  std::unique_ptr<PillButton> actions_button = std::make_unique<PillButton>(
      std::move(callback), label, PillButton::Type::kFloatingWithoutIcon,
      /*icon=*/nullptr, kNotificationPillButtonHorizontalSpacing);
  actions_button->SetButtonTextColorId(cros_tokens::kCrosSysOnSurface);

  return actions_button;
}

std::unique_ptr<views::FlexLayoutView> CreateInlineSettingsViewForMessageView(
    message_center::MessageView* message_view) {
  auto inline_settings_view = std::make_unique<views::FlexLayoutView>();
  inline_settings_view->SetOrientation(views::LayoutOrientation::kHorizontal);

  // base::Unretained(message_view) is safe here because `inline_settings_view`
  // and it's children must be owned by the provided `message_view`
  auto turn_off_notifications_button = GenerateNotificationLabelButton(
      base::BindRepeating(&message_center::MessageView::DisableNotification,
                          base::Unretained(message_view)),
      l10n_util::GetStringUTF16(
          IDS_ASH_NOTIFICATION_INLINE_SETTINGS_TURN_OFF_BUTTON_TEXT));
  turn_off_notifications_button->SetID(kNotificationTurnOffNotificationsButton);
  inline_settings_view->AddChildView(std::move(turn_off_notifications_button));
  auto cancel_button =
      notification_style_utils::GenerateNotificationLabelButton(
          base::BindRepeating(
              &message_center::MessageView::ToggleInlineSettings,
              base::Unretained(message_view)),
          l10n_util::GetStringUTF16(
              IDS_ASH_NOTIFICATION_INLINE_SETTINGS_CANCEL_BUTTON_TEXT));
  cancel_button->SetID(kNotificationInlineSettingsCancelButton);
  inline_settings_view->AddChildView(std::move(cancel_button));
  return inline_settings_view;
}

}  // namespace ash::notification_style_utils