blob: 5253acd8463a81e0571b76babaae5845e0ae24e9 [file] [log] [blame]
// Copyright 2019 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 "chrome/browser/ui/views/toolbar/toolbar_icon_container_view.h"
#include <memory>
#include "base/bind.h"
#include "base/containers/contains.h"
#include "base/scoped_observation.h"
#include "chrome/browser/themes/theme_properties.h"
#include "chrome/browser/ui/layout_constants.h"
#include "chrome/browser/ui/views/chrome_layout_provider.h"
#include "chrome/browser/ui/views/toolbar/toolbar_button.h"
#include "chrome/browser/ui/views/toolbar/toolbar_ink_drop_util.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/compositor/paint_recorder.h"
#include "ui/compositor/scoped_layer_animation_settings.h"
#include "ui/gfx/canvas.h"
#include "ui/native_theme/native_theme.h"
#include "ui/views/animation/ink_drop.h"
#include "ui/views/background.h"
#include "ui/views/layout/animating_layout_manager.h"
#include "ui/views/layout/flex_layout.h"
#include "ui/views/view_class_properties.h"
#include "ui/views/view_observer.h"
ToolbarIconContainerView::RoundRectBorder::RoundRectBorder(views::View* parent)
: parent_(parent) {
layer_.set_delegate(this);
layer_.SetFillsBoundsOpaquely(false);
layer_.SetFillsBoundsCompletely(false);
layer_.SetOpacity(0);
layer_.SetAnimator(ui::LayerAnimator::CreateImplicitAnimator());
layer_.GetAnimator()->set_tween_type(gfx::Tween::EASE_OUT);
layer_.SetVisible(true);
}
void ToolbarIconContainerView::RoundRectBorder::OnPaintLayer(
const ui::PaintContext& context) {
ui::PaintRecorder paint_recorder(context, layer_.size());
gfx::Canvas* canvas = paint_recorder.canvas();
const int radius = ChromeLayoutProvider::Get()->GetCornerRadiusMetric(
views::Emphasis::kMaximum, layer_.size());
cc::PaintFlags flags;
flags.setAntiAlias(true);
flags.setStyle(cc::PaintFlags::kStroke_Style);
flags.setStrokeWidth(1);
flags.setColor(ToolbarButton::GetDefaultBorderColor(parent_));
gfx::RectF rect(gfx::SizeF(layer_.size()));
rect.Inset(0.5f, 0.5f); // Pixel edges -> pixel centers.
canvas->DrawRoundRect(rect, radius, flags);
}
void ToolbarIconContainerView::RoundRectBorder::OnDeviceScaleFactorChanged(
float old_device_scale_factor,
float new_device_scale_factor) {}
// Watches for widget restore (or first show) and resets the animation so icons
// don't spuriously "animate in" when a window is shown or restored. See
// crbug.com/1106506 for more details.
//
// There is currently no signal that is consistent across platforms and
// accessible from the View hierarchy that can tell us if, e.g., a window has
// been restored from a minimized state. While we could theoretically plumb
// state changes through from NativeWidget, we can observe the specific set of
// cases we want by observing the size of the window.
//
// We *cannot* observe the size of the widget itself, as at least on Windows,
// minimizing a window does not set the widget to 0x0, but rather a small,
// Windows 3.1-esque tile (~160x28) and moves it to [-32000, -32000], so far off
// the screen it can't appear on any monitor.
//
// What we can observe is the widget's root view, which is (a) always present
// after the toolbar has been added to its widget and through its entire
// lifetime, and (b) is actually set to zero size when the window is zero size
// or minimized on Windows.
class ToolbarIconContainerView::WidgetRestoreObserver
: public views::ViewObserver {
public:
explicit WidgetRestoreObserver(
ToolbarIconContainerView* toolbar_icon_container_view)
: toolbar_icon_container_view_(toolbar_icon_container_view) {
scoped_observation_.Observe(
toolbar_icon_container_view->GetWidget()->GetRootView());
}
void OnViewBoundsChanged(views::View* observed_view) override {
const bool is_collapsed = observed_view->bounds().IsEmpty();
if (is_collapsed != was_collapsed_) {
was_collapsed_ = is_collapsed;
if (!is_collapsed) {
toolbar_icon_container_view_->GetAnimatingLayoutManager()
->ResetLayout();
}
}
}
private:
bool was_collapsed_ = true;
ToolbarIconContainerView* const toolbar_icon_container_view_;
base::ScopedObservation<views::View, views::ViewObserver> scoped_observation_{
this};
};
ToolbarIconContainerView::ToolbarIconContainerView(bool uses_highlight)
: uses_highlight_(uses_highlight) {
SetPaintToLayer();
layer()->SetFillsBoundsOpaquely(false);
layer()->SetFillsBoundsCompletely(false);
AddLayerBeneathView(border_.layer());
views::AnimatingLayoutManager* animating_layout =
SetLayoutManager(std::make_unique<views::AnimatingLayoutManager>());
animating_layout->SetBoundsAnimationMode(
views::AnimatingLayoutManager::BoundsAnimationMode::kAnimateBothAxes);
animating_layout->SetDefaultFadeMode(
views::AnimatingLayoutManager::FadeInOutMode::kSlideFromTrailingEdge);
auto* flex_layout = animating_layout->SetTargetLayoutManager(
std::make_unique<views::FlexLayout>());
flex_layout->SetCollapseMargins(true)
.SetIgnoreDefaultMainAxisMargins(true)
.SetDefault(views::kMarginsKey,
gfx::Insets(0, GetLayoutConstant(TOOLBAR_ELEMENT_PADDING)));
}
ToolbarIconContainerView::~ToolbarIconContainerView() {
// As childred might be Observers of |this|, we need to destroy them before
// destroying |observers_|.
RemoveAllChildViews(true);
}
void ToolbarIconContainerView::AddMainButton(views::Button* main_button) {
DCHECK(!main_button_);
main_button_ = main_button;
ObserveButton(main_button_);
AddChildView(main_button_);
}
void ToolbarIconContainerView::ObserveButton(views::Button* button) {
// We don't care about the main button being highlighted.
if (button != main_button_) {
subscriptions_.push_back(
views::InkDrop::Get(button)->AddHighlightedChangedCallback(
base::BindRepeating(
&ToolbarIconContainerView::OnButtonHighlightedChanged,
base::Unretained(this), base::Unretained(button))));
}
subscriptions_.push_back(button->AddStateChangedCallback(base::BindRepeating(
&ToolbarIconContainerView::UpdateHighlight, base::Unretained(this))));
button->AddObserver(this);
}
void ToolbarIconContainerView::AddObserver(Observer* obs) {
observers_.AddObserver(obs);
}
void ToolbarIconContainerView::RemoveObserver(const Observer* obs) {
observers_.RemoveObserver(obs);
}
void ToolbarIconContainerView::SetIconColor(SkColor color) {
if (icon_color_ == color)
return;
icon_color_ = color;
UpdateAllIcons();
OnPropertyChanged(&icon_color_, views::kPropertyEffectsNone);
}
SkColor ToolbarIconContainerView::GetIconColor() const {
return icon_color_.value_or(
GetThemeProvider()->GetColor(ThemeProperties::COLOR_TOOLBAR_BUTTON_ICON));
}
bool ToolbarIconContainerView::GetHighlighted() const {
if (!uses_highlight_)
return false;
if (IsMouseHovered() && (!main_button_ || !main_button_->IsMouseHovered()))
return true;
// Focused, pressed or hovered children should trigger the highlight.
for (const views::View* child : children()) {
if (child == main_button_)
continue;
if (child->HasFocus())
return true;
const views::Button* button = views::Button::AsButton(child);
if (!button)
continue;
if (button->GetState() == views::Button::ButtonState::STATE_PRESSED ||
button->GetState() == views::Button::ButtonState::STATE_HOVERED) {
return true;
}
// The container should also be highlighted if a dialog is anchored to.
if (base::Contains(highlighted_buttons_, button))
return true;
}
return false;
}
void ToolbarIconContainerView::OnThemeChanged() {
views::View::OnThemeChanged();
border_.layer()->SchedulePaint(GetLocalBounds());
}
void ToolbarIconContainerView::OnViewFocused(views::View* observed_view) {
UpdateHighlight();
}
void ToolbarIconContainerView::OnViewBlurred(views::View* observed_view) {
UpdateHighlight();
}
views::AnimatingLayoutManager*
ToolbarIconContainerView::GetAnimatingLayoutManager() {
return static_cast<views::AnimatingLayoutManager*>(GetLayoutManager());
}
const views::AnimatingLayoutManager*
ToolbarIconContainerView::GetAnimatingLayoutManager() const {
return static_cast<const views::AnimatingLayoutManager*>(GetLayoutManager());
}
views::FlexLayout* ToolbarIconContainerView::GetTargetLayoutManager() {
return static_cast<views::FlexLayout*>(
GetAnimatingLayoutManager()->target_layout_manager());
}
void ToolbarIconContainerView::OnBoundsChanged(
const gfx::Rect& previous_bounds) {
const gfx::Rect bounds = GetLocalBounds();
border_.layer()->SetBounds(ConvertRectToWidget(bounds));
border_.layer()->SchedulePaint(bounds);
}
void ToolbarIconContainerView::OnMouseEntered(const ui::MouseEvent& event) {
UpdateHighlight();
}
void ToolbarIconContainerView::OnMouseExited(const ui::MouseEvent& event) {
UpdateHighlight();
}
void ToolbarIconContainerView::AddedToWidget() {
// Add an observer to reset the animation if the browser window is restored,
// preventing spurious animation. (See crbug.com/1106506)
restore_observer_ = std::make_unique<WidgetRestoreObserver>(this);
}
void ToolbarIconContainerView::UpdateHighlight() {
bool showing_before = border_.layer()->GetTargetOpacity() == 1;
{
ui::ScopedLayerAnimationSettings settings(border_.layer()->GetAnimator());
border_.layer()->SetOpacity(GetHighlighted() ? 1 : 0);
}
// TODO(crbug.com/1194150): For some reason, the SchedulePaint() calls that
// happen initially -- in OnThemeChanged() and OnBoundsChanged() -- do not
// result in the layer getting painted for the first time. Calling
// SchedulePaint() here works. Without this, the highlight will not appear
// until an extension icon is added or removed or the theme is changed.
if (!ever_painted_highlight_ && GetHighlighted()) {
ever_painted_highlight_ = true;
border_.layer()->SchedulePaint(GetLocalBounds());
}
if (showing_before == (border_.layer()->GetTargetOpacity() == 1))
return;
for (Observer& observer : observers_)
observer.OnHighlightChanged();
}
void ToolbarIconContainerView::OnButtonHighlightedChanged(
views::Button* button) {
if (views::InkDrop::Get(button)->GetHighlighted())
highlighted_buttons_.insert(button);
else
highlighted_buttons_.erase(button);
UpdateHighlight();
}
BEGIN_METADATA(ToolbarIconContainerView, views::View)
ADD_PROPERTY_METADATA(SkColor, IconColor, ui::metadata::SkColorConverter)
ADD_READONLY_PROPERTY_METADATA(bool, Highlighted)
END_METADATA