blob: b429e1cffe06f12d70bda62394c783487de4e9da [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 "ash/frame/caption_buttons/frame_caption_button.h"
#include "ash/public/cpp/ash_constants.h"
#include "ui/base/hit_test.h"
#include "ui/gfx/animation/slide_animation.h"
#include "ui/gfx/animation/throb_animation.h"
#include "ui/gfx/canvas.h"
#include "ui/gfx/color_palette.h"
#include "ui/gfx/color_utils.h"
#include "ui/gfx/paint_vector_icon.h"
#include "ui/views/animation/flood_fill_ink_drop_ripple.h"
#include "ui/views/animation/ink_drop_highlight.h"
#include "ui/views/animation/ink_drop_impl.h"
#include "ui/views/animation/ink_drop_mask.h"
#include "ui/views/animation/ink_drop_ripple.h"
#include "ui/views/window/hit_test_utils.h"
namespace ash {
namespace {
// Ink drop parameters.
constexpr float kInkDropVisibleOpacity = 0.06f;
constexpr int kInkDropCornerRadius = 14;
// The duration of the crossfade animation when swapping the button's images.
const int kSwapImagesAnimationDurationMs = 200;
// The duration of the fade out animation of the old icon during a crossfade
// animation as a ratio of |kSwapImagesAnimationDurationMs|.
const float kFadeOutRatio = 0.5f;
// The ratio applied to the button's alpha when the button is disabled.
const float kDisabledButtonAlphaRatio = 0.5f;
// Minimum theme light color contrast.
const float kContrastLightItemThreshold = 3;
// The amount to darken a light theme color by for use as foreground color.
const float kThemedForegroundBlackFraction = 0.64;
// This mimics |shouldUseLightForegroundOnBackground| in ColorUtils.java.
bool UseLightColor(FrameCaptionButton::ColorMode color_mode,
SkColor background_color) {
if (color_mode == FrameCaptionButton::ColorMode::kThemed) {
return color_utils::GetContrastRatio(SK_ColorWHITE, background_color) >=
kContrastLightItemThreshold;
}
DCHECK_EQ(color_mode, FrameCaptionButton::ColorMode::kDefault);
return color_utils::IsDark(background_color);
}
// Returns the amount by which the inkdrop ripple and mask should be insetted
// from the button size in order to achieve a circular inkdrop with a size
// equals to kInkDropHighlightSize.
gfx::Insets GetInkdropInsets(const gfx::Size& button_size) {
constexpr gfx::Size kInkDropHighlightSize{2 * kInkDropCornerRadius,
2 * kInkDropCornerRadius};
return gfx::Insets(
(button_size.height() - kInkDropHighlightSize.height()) / 2,
(button_size.width() - kInkDropHighlightSize.width()) / 2);
}
} // namespace
// static
const char FrameCaptionButton::kViewClassName[] = "FrameCaptionButton";
FrameCaptionButton::FrameCaptionButton(views::ButtonListener* listener,
CaptionButtonIcon icon,
int hit_test_type)
: Button(listener),
icon_(icon),
background_color_(SK_ColorWHITE),
color_mode_(ColorMode::kDefault),
paint_as_active_(false),
alpha_(255),
swap_images_animation_(new gfx::SlideAnimation(this)) {
views::SetHitTestComponent(this, hit_test_type);
set_animate_on_state_change(true);
swap_images_animation_->Reset(1);
set_has_ink_drop_action_on_click(true);
SetInkDropMode(InkDropMode::ON);
set_ink_drop_visible_opacity(kInkDropVisibleOpacity);
UpdateInkDropBaseColor();
// 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(ColorMode color_mode,
SkColor background_color) {
bool use_light_color = UseLightColor(color_mode, background_color);
if (color_mode == ColorMode::kThemed) {
// This mimics |getThemedAssetColor| in ColorUtils.java.
return use_light_color
? SK_ColorWHITE
: color_utils::AlphaBlend(SK_ColorBLACK, background_color,
255 * kThemedForegroundBlackFraction);
}
DCHECK_EQ(color_mode, ColorMode::kDefault);
return use_light_color ? gfx::kGoogleGrey200 : gfx::kGoogleGrey700;
}
// 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(color_mode_, background_color_));
// The early return is dependent on |animate| because callers use SetImage()
// with ANIMATE_NO to progress the crossfade animation to the end.
if (icon == icon_ &&
(animate == ANIMATE_YES || !swap_images_animation_->is_animating()) &&
new_icon_image.BackedBySameObjectAs(icon_image_)) {
return;
}
if (animate == ANIMATE_YES)
crossfade_icon_image_ = icon_image_;
icon_ = icon;
icon_definition_ = &icon_definition;
icon_image_ = new_icon_image;
if (animate == ANIMATE_YES) {
swap_images_animation_->Reset(0);
swap_images_animation_->SetSlideDuration(kSwapImagesAnimationDurationMs);
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();
}
}
const char* FrameCaptionButton::GetClassName() const {
return kViewClassName;
}
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();
}
}
Button::OnGestureEvent(event);
}
views::PaintInfo::ScaleType FrameCaptionButton::GetPaintScaleType() const {
return views::PaintInfo::ScaleType::kUniformScaling;
}
std::unique_ptr<views::InkDrop> FrameCaptionButton::CreateInkDrop() {
auto ink_drop = std::make_unique<views::InkDropImpl>(this, size());
ink_drop->SetAutoHighlightMode(views::InkDropImpl::AutoHighlightMode::NONE);
ink_drop->SetShowHighlightOnHover(false);
return ink_drop;
}
std::unique_ptr<views::InkDropRipple> FrameCaptionButton::CreateInkDropRipple()
const {
return std::make_unique<views::FloodFillInkDropRipple>(
size(), GetInkdropInsets(size()), GetInkDropCenterBasedOnLastEvent(),
GetInkDropBaseColor(), ink_drop_visible_opacity());
}
std::unique_ptr<views::InkDropMask> FrameCaptionButton::CreateInkDropMask()
const {
return std::make_unique<views::RoundRectInkDropMask>(
size(), GetInkdropInsets(size()), kInkDropCornerRadius);
}
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_NO, *icon_definition_);
UpdateInkDropBaseColor();
}
void FrameCaptionButton::SetColorMode(ColorMode color_mode) {
color_mode_ = color_mode;
UpdateInkDropBaseColor();
}
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 (state() == STATE_HOVERED || state() == 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(GetInkDropBaseColor());
flags.setAlpha(highlight_alpha);
const gfx::Point center(GetMirroredRect(GetContentsBounds()).CenterPoint());
canvas->DrawCircle(center, kInkDropCornerRadius, 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);
canvas->DrawImageInt(icon_image_, centered_origin_x, centered_origin_y,
flags);
flags.setAlpha(crossfade_icon_alpha);
flags.setBlendMode(SkBlendMode::kPlus);
canvas->DrawImageInt(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));
canvas->DrawImageInt(icon_image_, centered_origin_x, centered_origin_y,
flags);
}
}
int FrameCaptionButton::GetAlphaForIcon(int base_alpha) const {
if (!enabled())
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 (state() == STATE_PRESSED || state() == STATE_HOVERED) {
inactive_alpha = 1.0f;
}
return base_alpha * inactive_alpha;
}
void FrameCaptionButton::UpdateInkDropBaseColor() {
set_ink_drop_base_color(UseLightColor(color_mode_, background_color_)
? SK_ColorWHITE
: SK_ColorBLACK);
}
} // namespace ash