| // 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 "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/caption_button_layout_constants.h" |
| #include "ui/views/window/hit_test_utils.h" |
| |
| namespace views { |
| |
| namespace { |
| |
| // Ink drop parameters. |
| constexpr float kInkDropVisibleOpacity = 0.06f; |
| |
| // 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; |
| |
| } // 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), |
| paint_as_active_(false), |
| alpha_(255), |
| ink_drop_corner_radius_(kCaptionButtonInkDropDefaultCornerRadius), |
| 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(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 source = color_utils::IsDark(background_color) |
| ? gfx::kGoogleGrey200 |
| : gfx::kGoogleGrey700; |
| const SkColor target = color_utils::GetColorWithMaxContrast(background_color); |
| // Guarantee the caption buttons reach at least contrast ratio 3; this ratio |
| // matches that used for focus indicators, large text, and other "have to see |
| // it but perhaps don't have to read fine detail" cases. |
| const SkAlpha alpha = color_utils::GetBlendValueWithMinimumContrast( |
| source, target, background_color, 3.0f); |
| return color_utils::AlphaBlend(target, source, alpha); |
| } |
| |
| // 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_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()), ink_drop_corner_radius_); |
| } |
| |
| 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::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, ink_drop_corner_radius_, 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; |
| } |
| |
| gfx::Insets FrameCaptionButton::GetInkdropInsets( |
| const gfx::Size& button_size) const { |
| const gfx::Size kInkDropHighlightSize{2 * ink_drop_corner_radius_, |
| 2 * ink_drop_corner_radius_}; |
| return gfx::Insets( |
| (button_size.height() - kInkDropHighlightSize.height()) / 2, |
| (button_size.width() - kInkDropHighlightSize.width()) / 2); |
| } |
| |
| 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_); |
| set_ink_drop_base_color( |
| GetColorWithMaxContrast(GetColorWithMaxContrast(button_color))); |
| } |
| |
| } // namespace views |