blob: 410c16c974d3a273232bf71f706992fb2f73891b [file] [log] [blame]
// Copyright 2025 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/views/tabs/groups/avatar_container_view.h"
#include <memory>
#include "base/functional/bind.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/utf_string_conversions.h"
#include "chrome/app/vector_icons/vector_icons.h"
#include "chrome/browser/data_sharing/data_sharing_service_factory.h"
#include "chrome/browser/image_fetcher/image_fetcher_service_factory.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/profiles/profile_key.h"
#include "chrome/browser/ui/tabs/saved_tab_groups/saved_tab_group_utils.h"
#include "components/data_sharing/public/data_sharing_service.h"
#include "components/image_fetcher/core/image_fetcher_service.h"
#include "components/saved_tab_groups/public/types.h"
#include "components/signin/public/base/avatar_icon_util.h"
#include "ui/base/metadata/metadata_header_macros.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/compositor/compositor.h"
#include "ui/gfx/canvas.h"
#include "ui/gfx/image/image.h"
#include "ui/gfx/paint_vector_icon.h"
#include "ui/views/layout/box_layout.h"
#include "ui/views/style/typography.h"
#include "ui/views/style/typography_provider.h"
#include "ui/views/view_class_properties.h"
#include "ui/views/widget/widget.h"
namespace {
constexpr int kCircleSize = 20;
constexpr int kIconSize = 12;
constexpr int kTextSize = 12;
constexpr int kCircleOverlap = 4;
class AvatarImageView : public views::ImageView {
METADATA_HEADER(AvatarImageView, views::ImageView)
gfx::Size GetImageSize() const override {
return image_size_.value_or(GetImageModel().Size());
}
};
BEGIN_METADATA(AvatarImageView)
END_METADATA
void DrawClippedCircle(gfx::Canvas& canvas,
int diameter,
SkColor circle_color,
bool clip_right_of_circle) {
if (clip_right_of_circle) {
const SkPath right_clip_circle =
SkPath::Circle(diameter + (diameter / 3.0f), diameter / 2.0f,
diameter / 2.0f)
.makeFillType(SkPathFillType::kInverseWinding);
canvas.ClipPath(right_clip_circle, /*do_anti_alias=*/true);
}
cc::PaintFlags flags;
flags.setAntiAlias(true);
flags.setColor(circle_color);
canvas.DrawCircle(gfx::PointF(diameter / 2.0f, diameter / 2.0f),
diameter / 2.0f, flags);
}
void DrawFallbackIcon(gfx::Canvas& canvas, int diameter, SkColor icon_color) {
int icon_offset = (diameter - kIconSize) / 2;
canvas.Save();
canvas.Translate({icon_offset, icon_offset});
gfx::PaintVectorIcon(&canvas, kTabGroupSharingIcon, kIconSize, icon_color);
canvas.Restore();
}
void DrawNumberText(gfx::Canvas& canvas,
int diameter,
int number,
SkColor text_color,
int font_size) {
const auto& font_list = views::TypographyProvider::Get().GetFont(
views::style::TextContext::CONTEXT_DIALOG_BODY_TEXT,
views::style::TextStyle::STYLE_CAPTION_MEDIUM);
gfx::Rect text_bounds(0, 0, diameter, diameter);
canvas.DrawStringRectWithFlags(
base::UTF8ToUTF16("+" + base::NumberToString(number)), font_list,
text_color, text_bounds, gfx::Canvas::TEXT_ALIGN_CENTER);
}
std::unique_ptr<AvatarImageView> CreateCircleFallbackAvatarImageView(
int diameter,
SkColor circle_color,
SkColor icon_color,
bool clip_right_of_circle,
float scale_factor) {
gfx::Canvas canvas(gfx::Size(diameter, diameter), scale_factor,
/*is_opaque=*/false);
canvas.Save();
DrawClippedCircle(canvas, diameter, circle_color, clip_right_of_circle);
DrawFallbackIcon(canvas, diameter, icon_color);
canvas.Restore();
gfx::ImageSkia circle_image =
gfx::ImageSkia::CreateFromBitmap(canvas.GetBitmap(), scale_factor);
auto image_view = std::make_unique<AvatarImageView>();
image_view->SetImage(ui::ImageModel::FromImageSkia(circle_image));
return image_view;
}
std::unique_ptr<AvatarImageView> CreateCircleNumberImageView(
int number,
int diameter,
SkColor circle_color,
SkColor text_color,
int font_size,
bool clip_right_of_circle,
float scale_factor) {
gfx::Canvas canvas(gfx::Size(diameter, diameter), scale_factor,
/*is_opaque=*/false);
canvas.Save();
DrawClippedCircle(canvas, diameter, circle_color, clip_right_of_circle);
DrawNumberText(canvas, diameter, number, text_color, font_size);
canvas.Restore();
gfx::ImageSkia circle_image =
gfx::ImageSkia::CreateFromBitmap(canvas.GetBitmap(), scale_factor);
auto image_view = std::make_unique<AvatarImageView>();
image_view->SetImage(ui::ImageModel::FromImageSkia(circle_image));
return image_view;
}
std::unique_ptr<AvatarImageView> CreateCircleAvatarImageView(
const gfx::Image& image,
int diameter,
SkColor circle_color,
SkColor icon_color,
bool clip_right_of_circle,
float scale_factor) {
gfx::ImageSkia image_skia = image.AsImageSkia();
if (image_skia.isNull()) {
return CreateCircleFallbackAvatarImageView(
diameter, circle_color, icon_color, clip_right_of_circle, scale_factor);
}
gfx::Canvas canvas(gfx::Size(diameter, diameter), scale_factor,
/*is_opaque=*/false);
canvas.Save();
const SkPath circle_path =
SkPath::Circle(diameter / 2.0f, diameter / 2.0f, diameter / 2.0f);
canvas.ClipPath(circle_path, /*do_anti_alias=*/true);
const int img_width = image_skia.width();
const int img_height = image_skia.height();
float scale = std::max(static_cast<float>(diameter) / img_width,
static_cast<float>(diameter) / img_height);
const int new_width = static_cast<int>(img_width * scale);
const int new_height = static_cast<int>(img_height * scale);
const int offset_x = (diameter - new_width) / 2;
const int offset_y = (diameter - new_height) / 2;
cc::PaintFlags paint_flags;
paint_flags.setFilterQuality(cc::PaintFlags::FilterQuality::kHigh);
paint_flags.setAntiAlias(true);
canvas.DrawImageInt(image_skia, 0, 0, img_width, img_height, offset_x,
offset_y, new_width, new_height,
/*filter=*/false, paint_flags);
canvas.Restore();
gfx::ImageSkia circle_image =
gfx::ImageSkia::CreateFromBitmap(canvas.GetBitmap(), scale_factor);
auto image_view = std::make_unique<AvatarImageView>();
image_view->SetImage(ui::ImageModel::FromImageSkia(circle_image));
return image_view;
}
} // namespace
void ManageSharingAvatarContainer::UpdateMemberGfxImage(
size_t index,
const gfx::Image& image) {
CHECK_LT(index, member_gfx_images_.size());
member_gfx_images_[index] = image;
RebuildChildren();
}
ManageSharingAvatarContainer::ManageSharingAvatarContainer(
Profile* profile,
const syncer::CollaborationId& collaboration_id)
: data_sharing_service_(
data_sharing::DataSharingServiceFactory::GetForProfile(profile)),
profile_(profile),
collaboration_id_(collaboration_id) {
SetLayoutManager(std::make_unique<views::BoxLayout>());
RequeryMemberInfo();
RebuildChildren();
}
ManageSharingAvatarContainer::~ManageSharingAvatarContainer() = default;
void ManageSharingAvatarContainer::OnDeviceScaleFactorChanged(
float old_device_scale_factor,
float new_device_scale_factor) {
views::View::OnDeviceScaleFactorChanged(old_device_scale_factor,
new_device_scale_factor);
RequeryMemberInfo();
RebuildChildren();
}
void ManageSharingAvatarContainer::RequeryMemberInfo() {
// This action cant be performed if there is no DataSharingService.
if (!data_sharing_service_) {
return;
}
members_for_display_ =
tab_groups::SavedTabGroupUtils::GetMembersOfSharedTabGroup(
profile_, collaboration_id_);
image_fetcher::ImageFetcherService* image_fetcher_service =
ImageFetcherServiceFactory::GetForKey(profile_->GetProfileKey());
image_fetcher::ImageFetcher* image_fetcher =
image_fetcher_service->GetImageFetcher(
image_fetcher::ImageFetcherConfig::kDiskCacheOnly);
// Attempt to get the exact scaled avatar image, default to a overscaled by 2
// to support HiDPI displays.
auto image_size = kCircleSize * 2;
if (GetWidget() && GetWidget()->GetCompositor()) {
image_size =
GetWidget()->GetCompositor()->device_scale_factor() * kCircleSize;
}
// If we have up to three members, initiate fetch for each.
if (members_for_display_.size() > 0) {
data_sharing_service_->GetAvatarImageForURL(
members_for_display_[0].avatar_url, image_size,
base::BindOnce(&ManageSharingAvatarContainer::UpdateMemberGfxImage,
weak_ptr_factory_.GetWeakPtr(), 0),
image_fetcher);
}
if (members_for_display_.size() > 1) {
data_sharing_service_->GetAvatarImageForURL(
members_for_display_[1].avatar_url, image_size,
base::BindOnce(&ManageSharingAvatarContainer::UpdateMemberGfxImage,
weak_ptr_factory_.GetWeakPtr(), 1),
image_fetcher);
}
// Only fetch the 3rd member if we have exactly 3 if there are more than 3 and
// we show the overflow.
if (members_for_display_.size() == 3) {
data_sharing_service_->GetAvatarImageForURL(
members_for_display_[2].avatar_url, image_size,
base::BindOnce(&ManageSharingAvatarContainer::UpdateMemberGfxImage,
weak_ptr_factory_.GetWeakPtr(), 2),
image_fetcher);
}
}
void ManageSharingAvatarContainer::RebuildChildren() {
member_1_image_view_ = nullptr;
member_2_image_view_ = nullptr;
member_3_or_overflow_image_view_ = nullptr;
RemoveAllChildViews();
const ui::ColorProvider* color_provider = GetColorProvider();
if (!color_provider) {
return;
}
// Get devices scale factor for scaling the bitmaps.
float scale_factor = 1.0f;
if (GetWidget() && GetWidget()->GetCompositor()) {
scale_factor = GetWidget()->GetCompositor()->device_scale_factor();
}
const int member_count = members_for_display_.size();
const auto circle_color =
color_provider->GetColor(ui::kColorSysNeutralContainer);
const auto icon_color = color_provider->GetColor(ui::kColorSysOnSurface);
// For each of the members, try to add the member image, otherwise fallback
// the circle with the avatar image.
if (member_count >= 1) {
if (member_gfx_images_[0].has_value()) {
member_1_image_view_ = AddChildView(CreateCircleAvatarImageView(
member_gfx_images_[0].value(), kCircleSize, circle_color, icon_color,
/*clip_right_of_circle=*/(member_count > 1), scale_factor));
} else {
member_1_image_view_ = AddChildView(CreateCircleFallbackAvatarImageView(
kCircleSize, circle_color, icon_color,
/*clip_right_of_circle=*/(member_count > 1), scale_factor));
}
member_1_image_view_->SetProperty(
views::kMarginsKey, gfx::Insets::TLBR(0, -1 * kCircleOverlap, 0, 0));
}
if (member_count >= 2) {
if (member_gfx_images_[1].has_value()) {
member_2_image_view_ = AddChildView(CreateCircleAvatarImageView(
member_gfx_images_[1].value(), kCircleSize, circle_color, icon_color,
/*clip_right_of_circle=*/(member_count > 2), scale_factor));
} else {
member_2_image_view_ = AddChildView(CreateCircleFallbackAvatarImageView(
kCircleSize, circle_color, icon_color,
/*clip_right_of_circle=*/(member_count > 2), scale_factor));
}
member_2_image_view_->SetProperty(
views::kMarginsKey, gfx::Insets::TLBR(0, -1 * kCircleOverlap, 0, 0));
}
if (member_count == 3) {
if (member_gfx_images_[2].has_value()) {
member_3_or_overflow_image_view_ =
AddChildView(CreateCircleAvatarImageView(
member_gfx_images_[2].value(), kCircleSize, circle_color,
icon_color,
/*clip_right_of_circle=*/false, scale_factor));
} else {
member_3_or_overflow_image_view_ =
AddChildView(CreateCircleFallbackAvatarImageView(
kCircleSize, circle_color, icon_color,
/*clip_right_of_circle=*/false, scale_factor));
}
} else if (member_count > 3) {
member_3_or_overflow_image_view_ = AddChildView(CreateCircleNumberImageView(
member_count - 2, kCircleSize, circle_color, icon_color, kTextSize,
/*clip_right_of_circle=*/false, scale_factor));
}
if (member_3_or_overflow_image_view_) {
member_3_or_overflow_image_view_->SetProperty(
views::kMarginsKey, gfx::Insets::TLBR(0, -1 * kCircleOverlap, 0, 0));
}
}
void ManageSharingAvatarContainer::AddedToWidget() {
views::View::AddedToWidget();
RebuildChildren();
SchedulePaint();
}
void ManageSharingAvatarContainer::OnThemeChanged() {
views::View::OnThemeChanged();
RebuildChildren();
SchedulePaint();
}
BEGIN_METADATA(ManageSharingAvatarContainer)
END_METADATA