blob: 825104daba67793418ff77d6f92e2074c826db83 [file] [log] [blame]
// Copyright 2013 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "ui/views/window/frame_caption_button.h"
#include <memory>
#include <utility>
#include "base/bind.h"
#include "ui/base/hit_test.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/gfx/animation/slide_animation.h"
#include "ui/gfx/canvas.h"
#include "ui/gfx/color_utils.h"
#include "ui/gfx/geometry/rect_conversions.h"
#include "ui/gfx/paint_vector_icon.h"
#include "ui/gfx/rrect_f.h"
#include "ui/views/animation/flood_fill_ink_drop_ripple.h"
#include "ui/views/animation/ink_drop.h"
#include "ui/views/animation/ink_drop_highlight.h"
#include "ui/views/animation/ink_drop_impl.h"
#include "ui/views/animation/ink_drop_ripple.h"
#include "ui/views/controls/highlight_path_generator.h"
#include "ui/views/window/hit_test_utils.h"
namespace views {
namespace {
// Ink drop parameters.
constexpr float kInkDropVisibleOpacity = 0.06f;
// The duration of the fade out animation of the old icon during a crossfade
// animation as a ratio of the duration of |swap_images_animation_|.
constexpr float kFadeOutRatio = 0.5f;
// The ratio applied to the button's alpha when the button is disabled.
constexpr float kDisabledButtonAlphaRatio = 0.5f;
} // namespace
// Custom highlight path generator for clipping ink drops and drawing focus
// rings.
class FrameCaptionButton::HighlightPathGenerator
: public views::HighlightPathGenerator {
public:
explicit HighlightPathGenerator(FrameCaptionButton* frame_caption_button)
: frame_caption_button_(frame_caption_button) {}
HighlightPathGenerator(const HighlightPathGenerator&) = delete;
HighlightPathGenerator& operator=(const HighlightPathGenerator&) = delete;
~HighlightPathGenerator() override = default;
// views::HighlightPathGenerator:
absl::optional<gfx::RRectF> GetRoundRect(const gfx::RectF& rect) override {
gfx::Rect bounds = gfx::ToRoundedRect(rect);
bounds.Inset(frame_caption_button_->GetInkdropInsets(bounds.size()));
return gfx::RRectF(gfx::RectF(bounds),
frame_caption_button_->GetInkDropCornerRadius());
}
private:
FrameCaptionButton* const frame_caption_button_;
};
FrameCaptionButton::FrameCaptionButton(PressedCallback callback,
CaptionButtonIcon icon,
int hit_test_type)
: Button(std::move(callback)),
icon_(icon),
swap_images_animation_(std::make_unique<gfx::SlideAnimation>(this)) {
views::SetHitTestComponent(this, hit_test_type);
// Not focusable by default, only for accessibility.
SetFocusBehavior(FocusBehavior::ACCESSIBLE_ONLY);
SetAnimateOnStateChange(true);
swap_images_animation_->Reset(1);
SetHasInkDropActionOnClick(true);
InkDrop::Get(this)->SetMode(views::InkDropHost::InkDropMode::ON);
InkDrop::Get(this)->SetVisibleOpacity(kInkDropVisibleOpacity);
UpdateInkDropBaseColor();
InkDrop::UseInkDropWithoutAutoHighlight(InkDrop::Get(this),
/*highlight_on_hover=*/false);
InkDrop::Get(this)->SetCreateRippleCallback(base::BindRepeating(
[](FrameCaptionButton* host) -> std::unique_ptr<views::InkDropRipple> {
return std::make_unique<views::FloodFillInkDropRipple>(
host->size(), host->GetInkdropInsets(host->size()),
InkDrop::Get(host)->GetInkDropCenterBasedOnLastEvent(),
InkDrop::Get(host)->GetBaseColor(),
InkDrop::Get(host)->GetVisibleOpacity());
},
this));
views::HighlightPathGenerator::Install(
this, std::make_unique<HighlightPathGenerator>(this));
// Do not flip the gfx::Canvas passed to the OnPaint() method. The snap left
// and snap right button icons should not be flipped. The other icons are
// horizontally symmetrical.
}
FrameCaptionButton::~FrameCaptionButton() = default;
// static
SkColor FrameCaptionButton::GetButtonColor(SkColor background_color) {
// Use IsDark() to change target colors instead of PickContrastingColor(), so
// that DefaultFrameHeader::GetTitleColor() (which uses different target
// colors) can change between light/dark targets at the same time. It looks
// bad when the title and caption buttons disagree about whether to be light
// or dark.
const SkColor default_foreground = color_utils::IsDark(background_color)
? gfx::kGoogleGrey200
: gfx::kGoogleGrey700;
const SkColor high_contrast_foreground =
color_utils::GetColorWithMaxContrast(background_color);
return color_utils::BlendForMinContrast(
default_foreground, background_color, high_contrast_foreground,
color_utils::kMinimumVisibleContrastRatio)
.color;
}
// static
float FrameCaptionButton::GetInactiveButtonColorAlphaRatio() {
return 0.38f;
}
void FrameCaptionButton::SetImage(CaptionButtonIcon icon,
Animate animate,
const gfx::VectorIcon& icon_definition) {
gfx::ImageSkia new_icon_image =
gfx::CreateVectorIcon(icon_definition, GetButtonColor(background_color_));
// The early return is dependent on |animate| because callers use SetImage()
// with Animate::kNo to progress the crossfade animation to the end.
if (icon == icon_ &&
(animate == Animate::kYes || !swap_images_animation_->is_animating()) &&
new_icon_image.BackedBySameObjectAs(icon_image_)) {
return;
}
if (animate == Animate::kYes)
crossfade_icon_image_ = icon_image_;
icon_ = icon;
icon_definition_ = &icon_definition;
icon_image_ = new_icon_image;
if (animate == Animate::kYes) {
swap_images_animation_->Reset(0);
swap_images_animation_->SetSlideDuration(
base::TimeDelta::FromMilliseconds(200));
swap_images_animation_->Show();
} else {
swap_images_animation_->Reset(1);
}
SchedulePaint();
}
bool FrameCaptionButton::IsAnimatingImageSwap() const {
return swap_images_animation_->is_animating();
}
void FrameCaptionButton::SetAlpha(int alpha) {
if (alpha_ != alpha) {
alpha_ = alpha;
SchedulePaint();
}
}
void FrameCaptionButton::OnGestureEvent(ui::GestureEvent* event) {
// Button does not become pressed when the user drags off and then back
// onto the button. Make FrameCaptionButton pressed in this case because this
// behavior is more consistent with AlternateFrameSizeButton.
if (event->type() == ui::ET_GESTURE_SCROLL_BEGIN ||
event->type() == ui::ET_GESTURE_SCROLL_UPDATE) {
if (HitTestPoint(event->location())) {
SetState(STATE_PRESSED);
RequestFocus();
event->StopPropagation();
} else {
SetState(STATE_NORMAL);
}
} else if (event->type() == ui::ET_GESTURE_SCROLL_END) {
if (HitTestPoint(event->location())) {
SetState(STATE_HOVERED);
NotifyClick(*event);
event->StopPropagation();
}
}
if (!event->handled())
Button::OnGestureEvent(event);
}
views::PaintInfo::ScaleType FrameCaptionButton::GetPaintScaleType() const {
return views::PaintInfo::ScaleType::kUniformScaling;
}
void FrameCaptionButton::SetBackgroundColor(SkColor background_color) {
if (background_color_ == background_color)
return;
background_color_ = background_color;
// Refresh the icon since the color may have changed.
if (icon_definition_)
SetImage(icon_, Animate::kNo, *icon_definition_);
UpdateInkDropBaseColor();
OnPropertyChanged(&background_color_, kPropertyEffectsPaint);
}
SkColor FrameCaptionButton::GetBackgroundColor() const {
return background_color_;
}
void FrameCaptionButton::SetInkDropCornerRadius(int ink_drop_corner_radius) {
ink_drop_corner_radius_ = ink_drop_corner_radius;
// Changes to |ink_drop_corner_radius| will affect the ink drop. Therefore
// this effect is handled by the ink drop.
OnPropertyChanged(&ink_drop_corner_radius_, kPropertyEffectsNone);
}
int FrameCaptionButton::GetInkDropCornerRadius() const {
return ink_drop_corner_radius_;
}
base::CallbackListSubscription
FrameCaptionButton::AddBackgroundColorChangedCallback(
PropertyChangedCallback callback) {
return AddPropertyChangedCallback(&background_color_, callback);
}
void FrameCaptionButton::SetPaintAsActive(bool paint_as_active) {
if (paint_as_active == paint_as_active_)
return;
paint_as_active_ = paint_as_active;
OnPropertyChanged(&paint_as_active_, kPropertyEffectsPaint);
}
bool FrameCaptionButton::GetPaintAsActive() const {
return paint_as_active_;
}
void FrameCaptionButton::DrawHighlight(gfx::Canvas* canvas,
cc::PaintFlags flags) {
const gfx::Point center(GetMirroredRect(GetContentsBounds()).CenterPoint());
canvas->DrawCircle(center, ink_drop_corner_radius_, flags);
}
void FrameCaptionButton::DrawIconContents(gfx::Canvas* canvas,
gfx::ImageSkia image,
int x,
int y,
cc::PaintFlags flags) {
canvas->DrawImageInt(image, x, y, flags);
}
gfx::Size FrameCaptionButton::GetInkDropSize() const {
return gfx::Size(2 * GetInkDropCornerRadius(), 2 * GetInkDropCornerRadius());
}
gfx::Insets FrameCaptionButton::GetInkdropInsets(
const gfx::Size& button_size) const {
return gfx::Insets((button_size.height() - GetInkDropSize().height()) / 2,
(button_size.width() - GetInkDropSize().width()) / 2);
}
void FrameCaptionButton::PaintButtonContents(gfx::Canvas* canvas) {
constexpr SkAlpha kHighlightVisibleOpacity = 0x14;
SkAlpha highlight_alpha = SK_AlphaTRANSPARENT;
if (hover_animation().is_animating()) {
highlight_alpha = hover_animation().CurrentValueBetween(
SK_AlphaTRANSPARENT, kHighlightVisibleOpacity);
} else if (GetState() == STATE_HOVERED || GetState() == STATE_PRESSED) {
// Painting a circular highlight in both "hovered" and "pressed" states
// simulates and ink drop highlight mode of
// AutoHighlightMode::SHOW_ON_RIPPLE.
highlight_alpha = kHighlightVisibleOpacity;
}
if (highlight_alpha != SK_AlphaTRANSPARENT) {
// We paint the highlight manually here rather than relying on the ink drop
// highlight as it doesn't work well when the button size is changing while
// the window is moving as a result of the animation from normal to
// maximized state or vice versa. https://crbug.com/840901.
cc::PaintFlags flags;
flags.setColor(InkDrop::Get(this)->GetBaseColor());
flags.setAlpha(highlight_alpha);
DrawHighlight(canvas, flags);
}
int icon_alpha = swap_images_animation_->CurrentValueBetween(0, 255);
int crossfade_icon_alpha = 0;
if (icon_alpha < static_cast<int>(kFadeOutRatio * 255))
crossfade_icon_alpha = static_cast<int>(255 - icon_alpha / kFadeOutRatio);
int centered_origin_x = (width() - icon_image_.width()) / 2;
int centered_origin_y = (height() - icon_image_.height()) / 2;
if (crossfade_icon_alpha > 0 && !crossfade_icon_image_.isNull()) {
canvas->SaveLayerAlpha(GetAlphaForIcon(alpha_));
cc::PaintFlags flags;
flags.setAlpha(icon_alpha);
DrawIconContents(canvas, icon_image_, centered_origin_x, centered_origin_y,
flags);
flags.setAlpha(crossfade_icon_alpha);
flags.setBlendMode(SkBlendMode::kPlus);
DrawIconContents(canvas, crossfade_icon_image_, centered_origin_x,
centered_origin_y, flags);
canvas->Restore();
} else {
if (!swap_images_animation_->is_animating())
icon_alpha = alpha_;
cc::PaintFlags flags;
flags.setAlpha(GetAlphaForIcon(icon_alpha));
DrawIconContents(canvas, icon_image_, centered_origin_x, centered_origin_y,
flags);
}
}
int FrameCaptionButton::GetAlphaForIcon(int base_alpha) const {
if (!GetEnabled())
return base_alpha * kDisabledButtonAlphaRatio;
if (paint_as_active_)
return base_alpha;
// Paint icons as active when they are hovered over or pressed.
double inactive_alpha = GetInactiveButtonColorAlphaRatio();
if (hover_animation().is_animating()) {
inactive_alpha =
hover_animation().CurrentValueBetween(inactive_alpha, 1.0f);
} else if (GetState() == STATE_PRESSED || GetState() == STATE_HOVERED) {
inactive_alpha = 1.0f;
}
return base_alpha * inactive_alpha;
}
void FrameCaptionButton::UpdateInkDropBaseColor() {
using color_utils::GetColorWithMaxContrast;
// A typical implementation would simply do
// GetColorWithMaxContrast(background_color_). However, this could look odd
// if we use a light button glyph and dark ink drop or vice versa. So
// instead, use the lightest/darkest color in the same direction as the button
// glyph color.
// TODO(pkasting): It would likely be better to make the button glyph always
// be an alpha-blended version of GetColorWithMaxContrast(background_color_).
const SkColor button_color = GetButtonColor(background_color_);
InkDrop::Get(this)->SetBaseColor(
GetColorWithMaxContrast(GetColorWithMaxContrast(button_color)));
}
BEGIN_METADATA(FrameCaptionButton, Button)
ADD_PROPERTY_METADATA(SkColor, BackgroundColor, ui::metadata::SkColorConverter)
ADD_PROPERTY_METADATA(int, InkDropCornerRadius)
ADD_READONLY_PROPERTY_METADATA(CaptionButtonIcon, Icon)
ADD_PROPERTY_METADATA(bool, PaintAsActive)
END_METADATA
} // namespace views
DEFINE_ENUM_CONVERTERS(
views::CaptionButtonIcon,
{views::CaptionButtonIcon::CAPTION_BUTTON_ICON_MINIMIZE,
u"CAPTION_BUTTON_ICON_MINIMIZE"},
{views::CaptionButtonIcon::CAPTION_BUTTON_ICON_MAXIMIZE_RESTORE,
u"CAPTION_BUTTON_ICON_MAXIMIZE_RESTORE"},
{views::CaptionButtonIcon::CAPTION_BUTTON_ICON_CLOSE,
u"CAPTION_BUTTON_ICON_CLOSE"},
{views::CaptionButtonIcon::CAPTION_BUTTON_ICON_LEFT_SNAPPED,
u"CAPTION_BUTTON_ICON_LEFT_SNAPPED"},
{views::CaptionButtonIcon::CAPTION_BUTTON_ICON_RIGHT_SNAPPED,
u"CAPTION_BUTTON_ICON_RIGHT_SNAPPED"},
{views::CaptionButtonIcon::CAPTION_BUTTON_ICON_BACK,
u"CAPTION_BUTTON_ICON_BACK"},
{views::CaptionButtonIcon::CAPTION_BUTTON_ICON_LOCATION,
u"CAPTION_BUTTON_ICON_LOCATION"},
{views::CaptionButtonIcon::CAPTION_BUTTON_ICON_MENU,
u"CAPTION_BUTTON_ICON_MENU"},
{views::CaptionButtonIcon::CAPTION_BUTTON_ICON_ZOOM,
u"CAPTION_BUTTON_ICON_ZOOM"},
{views::CaptionButtonIcon::CAPTION_BUTTON_ICON_CENTER,
u"CAPTION_BUTTON_ICON_CENTER"},
{views::CaptionButtonIcon::CAPTION_BUTTON_ICON_CUSTOM,
u"CAPTION_BUTTON_ICON_CUSTOM"},
{views::CaptionButtonIcon::CAPTION_BUTTON_ICON_COUNT,
u"CAPTION_BUTTON_ICON_COUNT"})