| // 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/768748): 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(); |
| absl::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=*/absl::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 |