blob: f421295f7060af14eb3a3ed017f6cf389f103e0b [file] [log] [blame]
// 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 "chrome/browser/ui/views/permissions/chip/permission_chip_view.h"
#include <memory>
#include "base/numerics/safe_conversions.h"
#include "base/time/time.h"
#include "chrome/browser/themes/theme_properties.h"
#include "chrome/browser/ui/color/chrome_color_id.h"
#include "chrome/browser/ui/layout_constants.h"
#include "chrome/browser/ui/views/location_bar/location_bar_util.h"
#include "chrome/browser/ui/views/permissions/chip/multi_image_container.h"
#include "chrome/browser/ui/views/permissions/chip/permission_chip_theme.h"
#include "chrome/browser/ui/views/permissions/permission_prompt_style.h"
#include "components/permissions/permission_uma_util.h"
#include "components/vector_icons/vector_icons.h"
#include "third_party/skia/include/core/SkColor.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/base/theme_provider.h"
#include "ui/base/ui_base_features.h"
#include "ui/color/color_id.h"
#include "ui/color/color_provider.h"
#include "ui/gfx/color_utils.h"
#include "ui/gfx/vector_icon_types.h"
#include "ui/views/background.h"
#include "ui/views/controls/highlight_path_generator.h"
#include "ui/views/painter.h"
#include "ui/views/view_class_properties.h"
DEFINE_CLASS_ELEMENT_IDENTIFIER_VALUE(PermissionChipView, kElementIdForTesting);
PermissionChipView::PermissionChipView(PressedCallback callback)
: MdTextButton(std::move(callback),
std::u16string(),
views::style::CONTEXT_BUTTON_MD,
/*use_text_color_for_icon=*/true,
std::make_unique<MultiImageContainer>()) {
views::InstallPillHighlightPathGenerator(this);
SetHorizontalAlignment(gfx::ALIGN_LEFT);
SetElideBehavior(gfx::ElideBehavior::FADE_TAIL);
SetFocusBehavior(views::View::FocusBehavior::ALWAYS);
// Equalizing padding on the left, right and between icon and label.
SetImageLabelSpacing(GetLayoutConstant(LOCATION_BAR_CHIP_PADDING));
SetCustomPadding(GetPadding());
label()->SetTextStyle(views::style::STYLE_BODY_4_EMPHASIS);
SetCornerRadius(GetCornerRadius());
animation_ = std::make_unique<gfx::SlideAnimation>(this);
SetProperty(views::kElementIdentifierKey, kElementIdForTesting);
UpdateIconAndColors();
}
PermissionChipView::~PermissionChipView() = default;
void PermissionChipView::VisibilityChanged(views::View* starting_from,
bool is_visible) {
observers_.Notify(&Observer::OnChipVisibilityChanged, is_visible);
}
void PermissionChipView::AnimateCollapse(base::TimeDelta duration) {
base_width_ = 0;
animation_->SetSlideDuration(duration);
ForceAnimateCollapse();
}
void PermissionChipView::AnimateExpand(base::TimeDelta duration) {
base_width_ = 0;
animation_->SetSlideDuration(duration);
ForceAnimateExpand();
}
void PermissionChipView::AnimateToFit(base::TimeDelta duration) {
animation_->SetSlideDuration(duration);
if (label()
->GetPreferredSize(views::SizeBounds(label()->width(), {}))
.width() < width()) {
base_width_ =
label()
->GetPreferredSize(views::SizeBounds(label()->width(), {}))
.width();
// As we're collapsing, we need to make sure that the padding is not
// animated away.
base_width_ += GetPadding().width();
ForceAnimateCollapse();
} else {
ForceAnimateExpand();
}
}
void PermissionChipView::ResetAnimation(double value) {
// `base_width_` is used regardless of the animation value. When animation is
// reset, e.g. on a tab switch, `base_width_` may hold obsolete values that
// should be reset as well.
if (value == 0.0) {
base_width_ = 0;
}
animation_->Reset(value);
OnAnimationValueMaybeChanged();
}
gfx::Size PermissionChipView::CalculatePreferredSize(
const views::SizeBounds& available_size) const {
const int icon_width = GetIconViewWidth();
const int extra_width = GetPadding().width() + icon_width;
views::SizeBound available_width =
std::max<views::SizeBound>(0, available_size.width() - extra_width);
const int label_width =
label()->GetPreferredSize(views::SizeBounds(available_width, {})).width();
const int collapsable_width = label_width + GetPadding().width();
const int width =
base_width_ + icon_width +
base::ClampRound(collapsable_width * animation_->GetCurrentValue());
return gfx::Size(width, views::LabelButton::CalculatePreferredSize(
views::SizeBounds(width, {}))
.height());
}
bool PermissionChipView::OnMousePressed(const ui::MouseEvent& event) {
observers_.Notify(&Observer::OnMousePressed);
return MdTextButton::OnMousePressed(event);
}
void PermissionChipView::OnThemeChanged() {
MdTextButton::OnThemeChanged();
UpdateIconAndColors();
}
void PermissionChipView::UpdateBackgroundColor() {
SetBackground(views::CreateBackgroundFromPainter(
views::Painter::CreateSolidRoundRectPainterWithVariableRadius(
GetBackgroundColor(), GetCornerRadii())));
}
void PermissionChipView::AnimationEnded(const gfx::Animation* animation) {
if (animation != animation_.get()) {
return;
}
OnAnimationValueMaybeChanged();
const double value = animation_->GetCurrentValue();
if (value == 1.0) {
observers_.Notify(&Observer::OnExpandAnimationEnded);
} else if (value == 0.0) {
observers_.Notify(&Observer::OnCollapseAnimationEnded);
}
}
void PermissionChipView::AnimationProgressed(const gfx::Animation* animation) {
if (animation != animation_.get()) {
return;
}
OnAnimationValueMaybeChanged();
PreferredSizeChanged();
}
void PermissionChipView::SetUserDecision(
permissions::PermissionAction user_decision) {
user_decision_ = user_decision;
UpdateIconAndColors();
}
void PermissionChipView::SetTheme(PermissionChipTheme theme) {
theme_ = theme;
UpdateIconAndColors();
}
void PermissionChipView::SetBlockedIconShowing(bool should_show_blocked_icon) {
should_show_blocked_icon_ = should_show_blocked_icon;
UpdateIconAndColors();
}
void PermissionChipView::SetPermissionPromptStyle(
PermissionPromptStyle prompt_style) {
prompt_style_ = prompt_style;
UpdateIconAndColors();
}
void PermissionChipView::SetMessage(std::u16string message) {
SetText(message);
UpdateIconAndColors();
}
MultiImageContainer* PermissionChipView::multi_image_container() {
return static_cast<MultiImageContainer*>(image_container());
}
ui::ImageModel PermissionChipView::GetIconImageModel() const {
return ui::ImageModel::FromVectorIcon(GetIcon(), GetForegroundColor(),
GetIconSize(), nullptr);
}
const gfx::VectorIcon& PermissionChipView::GetIcon() const {
if (icon_) {
return const_cast<decltype(*icon_)>(*icon_);
}
return gfx::VectorIcon::EmptyIcon();
}
SkColor PermissionChipView::GetForegroundColor() const {
if (theme() == PermissionChipTheme::kInUseActivityIndicator) {
return GetColorProvider()->GetColor(
kColorOmniboxChipInUseActivityIndicatorForeground);
}
if (theme() == PermissionChipTheme::kBlockedActivityIndicator) {
return GetColorProvider()->GetColor(
kColorOmniboxChipBlockedActivityIndicatorForeground);
}
if (theme() == PermissionChipTheme::kOnSystemBlockedActivityIndicator) {
return GetColorProvider()->GetColor(
kColorOmniboxChipOnSystemBlockedActivityIndicatorForeground);
}
// 1. Default to the system primary color.
SkColor text_and_icon_color =
GetColorProvider()->GetColor(kColorOmniboxChipForegroundNormalVisibility);
// 2. Then update the color if the quiet chip is showing.
if (GetPermissionPromptStyle() == PermissionPromptStyle::kQuietChip) {
text_and_icon_color =
GetColorProvider()->GetColor(kColorOmniboxChipForegroundLowVisibility);
}
// 3. Then update the color based on the user decision.
// TODO(dljames): There is potentially a bug here if there exists a case
// where a quiet chip can be shown on a GRANTED_ONCE permission action.
// In that case the color should stay kColorOmniboxChipTextDefaultCR23.
switch (GetUserDecision()) {
case permissions::PermissionAction::GRANTED:
case permissions::PermissionAction::GRANTED_ONCE:
text_and_icon_color = GetColorProvider()->GetColor(
kColorOmniboxChipForegroundNormalVisibility);
break;
case permissions::PermissionAction::DENIED:
case permissions::PermissionAction::DISMISSED:
case permissions::PermissionAction::IGNORED:
case permissions::PermissionAction::REVOKED:
text_and_icon_color = GetColorProvider()->GetColor(
kColorOmniboxChipForegroundLowVisibility);
break;
case permissions::PermissionAction::NUM:
break;
}
// 4. Then update the color based on if the icon is blocked or not.
if (ShouldShowBlockedIcon()) {
text_and_icon_color =
GetColorProvider()->GetColor(kColorOmniboxChipForegroundLowVisibility);
}
return text_and_icon_color;
}
SkColor PermissionChipView::GetBackgroundColor() const {
if (theme() == PermissionChipTheme::kInUseActivityIndicator) {
return GetColorProvider()->GetColor(
kColorOmniboxChipInUseActivityIndicatorBackground);
}
if (theme() == PermissionChipTheme::kBlockedActivityIndicator) {
return GetColorProvider()->GetColor(
kColorOmniboxChipBlockedActivityIndicatorBackground);
}
if (theme() == PermissionChipTheme::kOnSystemBlockedActivityIndicator) {
return GetColorProvider()->GetColor(
kColorOmniboxChipOnSystemBlockedActivityIndicatorBackground);
}
return GetColorProvider()->GetColor(kColorOmniboxChipBackground);
}
void PermissionChipView::UpdateIconAndColors() {
if (!GetWidget()) {
return;
}
SkColor foreground_color = GetForegroundColor();
SetEnabledTextColors(foreground_color);
// On macOS the chip view could get disabled if a browser's window lost its
// focus. This can lead to low contrast between the chip's text and background
// color.
SetTextColor(views::Button::STATE_DISABLED, foreground_color);
ui::ImageModel image_model = GetIconImageModel();
SetImageModel(views::Button::STATE_NORMAL, image_model);
SetImageModel(views::Button::STATE_DISABLED, image_model);
ConfigureInkDropForRefresh2023(this, kColorOmniboxChipInkDropHover,
kColorOmniboxChipInkDropRipple);
}
void PermissionChipView::ForceAnimateExpand() {
ResetAnimation(0.0);
animation_->Show();
}
void PermissionChipView::ForceAnimateCollapse() {
ResetAnimation(1.0);
animation_->Hide();
}
void PermissionChipView::OnAnimationValueMaybeChanged() {
fully_collapsed_ = animation_->GetCurrentValue() == 0.0;
}
int PermissionChipView::GetIconSize() const {
return GetLayoutConstant(LOCATION_BAR_CHIP_ICON_SIZE);
}
int PermissionChipView::GetCornerRadius() const {
return GetLayoutConstant(LOCATION_BAR_CHILD_CORNER_RADIUS);
}
gfx::RoundedCornersF PermissionChipView::GetCornerRadii() const {
const int leading_radius = GetCornerRadius();
// If the chips' divider is visible, the left/trailing side of the request
// chip should be rectangular.
const int trailing_radius = is_divider_visible_ ? 0 : leading_radius;
return gfx::RoundedCornersF(trailing_radius, leading_radius, leading_radius,
trailing_radius);
}
gfx::Insets PermissionChipView::GetPadding() const {
return gfx::Insets(GetLayoutConstant(LOCATION_BAR_CHIP_PADDING));
}
void PermissionChipView::SetChipIcon(const gfx::VectorIcon& icon) {
icon_ = &icon;
UpdateIconAndColors();
}
void PermissionChipView::SetChipIcon(const gfx::VectorIcon* icon) {
icon_ = icon;
UpdateIconAndColors();
}
bool PermissionChipView::GetIsRequestForTesting() const {
switch (theme()) {
case PermissionChipTheme::kNormalVisibility:
case PermissionChipTheme::kLowVisibility:
return true;
case PermissionChipTheme::kBlockedActivityIndicator:
case PermissionChipTheme::kOnSystemBlockedActivityIndicator:
case PermissionChipTheme::kInUseActivityIndicator:
return false;
}
}
void PermissionChipView::AddObserver(Observer* observer) {
observers_.AddObserver(observer);
}
void PermissionChipView::RemoveObserver(Observer* observer) {
observers_.RemoveObserver(observer);
}
void PermissionChipView::UpdateForDividerVisibility(bool is_divider_visible,
int divider_arc_width) {
is_divider_visible_ = is_divider_visible;
UpdateBackgroundColor();
// The request chip should move under the divider arc if the divider is
// visible.
gfx::Insets margin = is_divider_visible
? gfx::Insets::TLBR(0, -divider_arc_width, 0, 0)
: gfx::Insets();
SetProperty(views::kMarginsKey, margin);
gfx::Insets padding = GetPadding();
if (is_divider_visible) {
// Set a left padding to move the request chip's icon to the right.
padding += gfx::Insets::TLBR(0, divider_arc_width, 0, 0);
}
SetCustomPadding(padding);
views::HighlightPathGenerator::Install(
this, std::make_unique<views::RoundRectHighlightPathGenerator>(
gfx::Insets(), GetCornerRadii()));
}
int PermissionChipView::GetIconViewWidth() const {
return GetIconSize() + GetInsets().width();
}
BEGIN_METADATA(PermissionChipView)
ADD_READONLY_PROPERTY_METADATA(int, IconSize)
END_METADATA