| // Copyright 2018 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "chromeos/ui/frame/frame_header.h" |
| |
| #include <algorithm> |
| #include <vector> |
| |
| #include "chromeos/ui/frame/caption_buttons/frame_caption_button_container_view.h" |
| #include "chromeos/ui/frame/caption_buttons/frame_center_button.h" |
| #include "chromeos/ui/vector_icons/vector_icons.h" |
| #include "ui/base/class_property.h" |
| #include "ui/base/metadata/metadata_header_macros.h" |
| #include "ui/base/metadata/metadata_impl_macros.h" |
| #include "ui/base/mojom/window_show_state.mojom.h" |
| #include "ui/color/color_id.h" |
| #include "ui/compositor/layer.h" |
| #include "ui/compositor/layer_tree_owner.h" |
| #include "ui/compositor/layer_type.h" |
| #include "ui/compositor/scoped_layer_animation_settings.h" |
| #include "ui/gfx/canvas.h" |
| #include "ui/gfx/font_list.h" |
| #include "ui/gfx/geometry/point.h" |
| #include "ui/gfx/geometry/rect.h" |
| #include "ui/gfx/geometry/size.h" |
| #include "ui/views/background.h" |
| #include "ui/views/view.h" |
| #include "ui/views/widget/native_widget_aura.h" |
| #include "ui/views/widget/widget.h" |
| #include "ui/views/widget/widget_delegate.h" |
| #include "ui/views/window/caption_button_layout_constants.h" |
| #include "ui/views/window/frame_view.h" |
| |
| DEFINE_UI_CLASS_PROPERTY_TYPE(chromeos::FrameHeader*) |
| |
| namespace chromeos { |
| |
| namespace { |
| |
| constexpr base::TimeDelta kFrameActivationAnimationDuration = |
| base::Milliseconds(200); |
| |
| DEFINE_UI_CLASS_PROPERTY_KEY(FrameHeader*, kFrameHeaderKey, nullptr) |
| |
| // Returns the available bounds for the header's title given the views to the |
| // left and right of the title, and the font used. |left_view| should be null |
| // if there is no view to the left of the title. |
| gfx::Rect GetAvailableTitleBounds(const views::View* left_view, |
| const views::View* right_view, |
| int header_height) { |
| // Space between the title text and the caption buttons. |
| constexpr int kTitleCaptionButtonSpacing = 5; |
| // Space between window icon and title text. |
| constexpr int kTitleIconOffsetX = 5; |
| // Space between window edge and title text, when there is no icon. |
| constexpr int kTitleNoIconOffsetX = 8; |
| |
| const int x = left_view ? left_view->bounds().right() + kTitleIconOffsetX |
| : kTitleNoIconOffsetX; |
| const int title_height = gfx::FontList().GetHeight(); |
| DCHECK_LE(right_view->height(), header_height); |
| // We want to align the center points of the header and title vertically. |
| // Note that we can't just do (header_height - title_height) / 2, since this |
| // won't make the center points align perfectly vertically due to rounding. |
| // Floor when computing the center of |header_height| and when computing the |
| // center of the text. |
| const int header_center_y = header_height / 2; |
| const int title_center_y = title_height / 2; |
| const int y = std::max(0, header_center_y - title_center_y); |
| const int width = |
| std::max(0, right_view->x() - kTitleCaptionButtonSpacing - x); |
| return gfx::Rect(x, y, width, title_height); |
| } |
| |
| } // namespace |
| |
| FrameHeader::FrameAnimatorView::FrameAnimatorView(views::View* parent) |
| : parent_(parent) { |
| SetPaintToLayer(ui::LAYER_NOT_DRAWN); |
| parent_->AddChildViewAt(this, 0); |
| parent_->AddObserver(this); |
| } |
| |
| FrameHeader::FrameAnimatorView::~FrameAnimatorView() { |
| StopAnimation(); |
| // A child view should always be removed first. |
| parent_->RemoveObserver(this); |
| } |
| |
| void FrameHeader::FrameAnimatorView::StartAnimation(base::TimeDelta duration) { |
| aura::Window* window = |
| parent_->GetWidget() ? parent_->GetWidget()->GetNativeWindow() : nullptr; |
| if (layer_owner_ || !window || |
| window->layer()->GetAnimator()->is_animating()) { |
| // If the frame animation is already running or the widget |
| // hasn't been initialized yet, just update the content of the |
| // new layer. |
| parent_->SchedulePaint(); |
| return; |
| } |
| |
| // Make sure the this view is at the bottom of root view's children. |
| parent_->ReorderChildView(this, 0); |
| |
| std::unique_ptr<ui::LayerTreeOwner> old_layer_owner = |
| std::make_unique<ui::LayerTreeOwner>(window->RecreateLayer()); |
| ui::Layer* old_layer = old_layer_owner->root(); |
| ui::Layer* new_layer = window->layer(); |
| new_layer->SetName(old_layer->name()); |
| old_layer->SetName(old_layer->name() + ":Old"); |
| old_layer->SetTransform(gfx::Transform()); |
| // Layer in maximized / fullscreen / snapped state is set to |
| // opaque, which can prevent resterizing the new layer immediately. |
| if (old_layer->type() != ui::LAYER_SOLID_COLOR) { |
| old_layer->SetFillsBoundsOpaquely(false); |
| } |
| |
| layer_owner_ = std::move(old_layer_owner); |
| |
| AddLayerToRegion(old_layer, views::LayerRegion::kBelow); |
| |
| // The old layer is on top and should fade out. The new layer is given the |
| // opacity as the old layer is currently targeting. This ensures that we don't |
| // change the overall opacity, since it may have been set by something else. |
| new_layer->SetOpacity(old_layer->GetTargetOpacity()); |
| { |
| ui::ScopedLayerAnimationSettings settings(old_layer->GetAnimator()); |
| settings.SetPreemptionStrategy( |
| ui::LayerAnimator::IMMEDIATELY_ANIMATE_TO_NEW_TARGET); |
| settings.AddObserver(this); |
| settings.SetTransitionDuration(duration); |
| old_layer->SetOpacity(0.f); |
| settings.SetTweenType(gfx::Tween::EASE_OUT); |
| } |
| } |
| |
| std::unique_ptr<ui::Layer> FrameHeader::FrameAnimatorView::RecreateLayer() { |
| // A layer may be recreated for another animation (maximize/restore). |
| // Just cancel the animation if that happens during animation. |
| StopAnimation(); |
| return views::View::RecreateLayer(); |
| } |
| |
| void FrameHeader::FrameAnimatorView::OnChildViewReordered( |
| views::View* observed_view, |
| views::View* child) { |
| // Stop animation if the child view order has changed during animation. |
| StopAnimation(); |
| } |
| |
| void FrameHeader::FrameAnimatorView::OnViewBoundsChanged( |
| views::View* observed_view) { |
| // Stop animation if the frame size changed during animation. |
| StopAnimation(); |
| SetBoundsRect(parent_->GetLocalBounds()); |
| } |
| |
| void FrameHeader::FrameAnimatorView::LayerDestroyed(ui::Layer* layer) { |
| CHECK(!layer_owner_ || layer_owner_->root() != layer); |
| views::View::LayerDestroyed(layer); |
| } |
| |
| void FrameHeader::FrameAnimatorView::OnImplicitAnimationsCompleted() { |
| // TODO(crbug.com/40054632): Remove this DCHECK if this is indeed the cause. |
| DCHECK(layer_owner_); |
| if (layer_owner_) { |
| RemoveLayerFromRegions(layer_owner_->root()); |
| layer_owner_.reset(); |
| } |
| } |
| |
| void FrameHeader::FrameAnimatorView::StopAnimation() { |
| if (layer_owner_) { |
| layer_owner_->root()->GetAnimator()->StopAnimating(); |
| layer_owner_.reset(); |
| } |
| } |
| |
| BEGIN_METADATA(FrameHeader, FrameAnimatorView) |
| END_METADATA |
| |
| /////////////////////////////////////////////////////////////////////////////// |
| // FrameHeader, public: |
| |
| // static |
| FrameHeader* FrameHeader::Get(views::Widget* widget) { |
| return widget->GetNativeView()->GetProperty(kFrameHeaderKey); |
| } |
| |
| // static |
| views::View::Views FrameHeader::GetAdjustedChildrenInZOrder( |
| views::FrameView* frame_view) { |
| views::View::Views paint_order = frame_view->children(); |
| views::ClientView* client_view = frame_view->GetWidget() |
| ? frame_view->GetWidget()->client_view() |
| : nullptr; |
| |
| if (client_view && std::erase(paint_order, client_view)) { |
| paint_order.insert(std::next(paint_order.begin(), 1), client_view); |
| } |
| |
| return paint_order; |
| } |
| |
| FrameHeader::~FrameHeader() { |
| if (center_button_ && !center_button_->parent()) { |
| delete center_button_; |
| center_button_ = nullptr; |
| } |
| |
| if (underneath_layer_owner_) { |
| underneath_layer_owner_->RemoveObserver(this); |
| underneath_layer_owner_ = nullptr; |
| } |
| |
| auto* target_window = target_widget_->GetNativeView(); |
| if (target_window && target_window->GetProperty(kFrameHeaderKey) == this) |
| target_window->ClearProperty(kFrameHeaderKey); |
| } |
| |
| int FrameHeader::GetMinimumHeaderWidth() const { |
| // Ensure we have enough space for the window icon and buttons. We allow |
| // the title string to collapse to zero width. |
| return GetTitleBounds().x() + |
| caption_button_container_->GetMinimumSize().width() + |
| (GetCenterButton() ? GetCenterButton()->GetMinimumSize().width() : 0); |
| } |
| |
| void FrameHeader::PaintHeader(gfx::Canvas* canvas) { |
| painted_ = true; |
| DoPaintHeader(canvas); |
| } |
| |
| void FrameHeader::LayoutHeader() { |
| LayoutHeaderInternal(); |
| // Default to the header height; owning code may override via |
| // SetHeaderHeightForPainting(). |
| painted_height_ = GetHeaderHeight(); |
| } |
| |
| void FrameHeader::InvalidateLayout() { |
| view_->InvalidateLayout(); |
| } |
| |
| int FrameHeader::GetHeaderHeight() const { |
| return caption_button_container_->height(); |
| } |
| |
| int FrameHeader::GetHeaderHeightForPainting() const { |
| return painted_height_; |
| } |
| |
| void FrameHeader::SetHeaderHeightForPainting(int height) { |
| painted_height_ = height; |
| } |
| |
| void FrameHeader::SchedulePaintForTitle() { |
| view_->SchedulePaintInRect(view_->GetMirroredRect(GetTitleBounds())); |
| } |
| |
| void FrameHeader::SetPaintAsActive(bool paint_as_active) { |
| // No need to animate if already active. |
| const bool already_active = (mode_ == Mode::MODE_ACTIVE); |
| |
| if (already_active == paint_as_active) |
| return; |
| |
| mode_ = paint_as_active ? MODE_ACTIVE : MODE_INACTIVE; |
| |
| // The frame has no content yet to animatie. |
| if (painted_) |
| StartTransitionAnimation(kFrameActivationAnimationDuration); |
| |
| caption_button_container_->SetPaintAsActive(paint_as_active); |
| if (back_button_) |
| back_button_->SetPaintAsActive(paint_as_active); |
| if (center_button_) |
| center_button_->SetPaintAsActive(paint_as_active); |
| |
| UpdateFrameColors(); |
| } |
| |
| void FrameHeader::OnShowStateChanged(ui::mojom::WindowShowState show_state) { |
| if (show_state == ui::mojom::WindowShowState::kMinimized) { |
| return; |
| } |
| |
| LayoutHeaderInternal(); |
| } |
| |
| void FrameHeader::OnFloatStateChanged() { |
| LayoutHeaderInternal(); |
| } |
| |
| void FrameHeader::SetHeaderCornerRadius(int radius) { |
| if (radius == corner_radius_) { |
| return; |
| } |
| |
| // If the frame header's radius is reduced, the area encompassing the |
| // curvature of both corners must be repainted. |
| const gfx::Size repaint_area(std::max(radius, corner_radius_), |
| std::max(radius, corner_radius_)); |
| corner_radius_ = radius; |
| |
| view_->SchedulePaintInRect(gfx::Rect(repaint_area)); |
| view_->SchedulePaintInRect( |
| gfx::Rect(gfx::Point(view_->width() - corner_radius_, 0), repaint_area)); |
| } |
| |
| void FrameHeader::SetLeftHeaderView(views::View* left_header_view) { |
| left_header_view_ = left_header_view; |
| } |
| |
| void FrameHeader::SetBackButton(views::FrameCaptionButton* back_button) { |
| back_button_ = back_button; |
| if (back_button_) { |
| back_button_->SetBackgroundColor(GetCurrentFrameColor()); |
| back_button_->SetImage(views::CAPTION_BUTTON_ICON_BACK, |
| views::FrameCaptionButton::Animate::kNo, |
| chromeos::kWindowControlBackIcon); |
| } |
| } |
| |
| void FrameHeader::SetCenterButton(chromeos::FrameCenterButton* center_button) { |
| DCHECK(!center_button_); |
| center_button_ = center_button; |
| if (center_button_) |
| center_button_->SetBackgroundColor(GetCurrentFrameColor()); |
| } |
| |
| views::FrameCaptionButton* FrameHeader::GetBackButton() const { |
| return back_button_; |
| } |
| |
| chromeos::FrameCenterButton* FrameHeader::GetCenterButton() const { |
| return center_button_; |
| } |
| |
| const chromeos::CaptionButtonModel* FrameHeader::GetCaptionButtonModel() const { |
| return caption_button_container_->model(); |
| } |
| |
| void FrameHeader::SetFrameTextOverride( |
| const std::u16string& frame_text_override) { |
| frame_text_override_ = frame_text_override; |
| SchedulePaintForTitle(); |
| } |
| |
| ui::ColorId FrameHeader::GetColorIdForCurrentMode() const { |
| return mode_ == MODE_ACTIVE ? ui::kColorFrameActive : ui::kColorFrameInactive; |
| } |
| |
| /////////////////////////////////////////////////////////////////////////////// |
| // FrameHeader, protected: |
| |
| FrameHeader::FrameHeader(views::Widget* target_widget, views::View* view) |
| : target_widget_(target_widget), view_(view) { |
| DCHECK(target_widget); |
| DCHECK(view); |
| UpdateFrameHeaderKey(); |
| frame_animator_ = new FrameAnimatorView(view); |
| } |
| |
| void FrameHeader::UpdateFrameHeaderKey() { |
| target_widget_->GetNativeView()->SetProperty(kFrameHeaderKey, this); |
| } |
| |
| void FrameHeader::OnLayerRecreated(ui::Layer* old_layer) { |
| if (underneath_layer_owner_) { |
| frame_animator_->RemoveLayerFromRegionsKeepInLayerTree(old_layer); |
| frame_animator_->AddLayerToRegion(underneath_layer_owner_->layer(), |
| views::LayerRegion::kBelow); |
| } |
| } |
| |
| void FrameHeader::AddLayerBeneath(ui::LayerOwner* layer_owner) { |
| if (layer_owner) { |
| underneath_layer_owner_ = layer_owner; |
| // A relationship between the layer_owner's layer and animation view is |
| // created, we need to observe the layer_owner in case of the layer gets |
| // recreated. |
| layer_owner->AddObserver(this); |
| frame_animator_->AddLayerToRegion(layer_owner->layer(), |
| views::LayerRegion::kBelow); |
| } |
| } |
| |
| void FrameHeader::RemoveLayerBeneath() { |
| if (underneath_layer_owner_) { |
| frame_animator_->RemoveLayerFromRegionsKeepInLayerTree( |
| underneath_layer_owner_->layer()); |
| underneath_layer_owner_->RemoveObserver(this); |
| underneath_layer_owner_ = nullptr; |
| } |
| } |
| |
| gfx::Rect FrameHeader::GetPaintedBounds() const { |
| return gfx::Rect(view_->width(), painted_height_); |
| } |
| |
| void FrameHeader::UpdateCaptionButtonColors( |
| std::optional<ui::ColorId> icon_color_id) { |
| const SkColor frame_color = GetCurrentFrameColor(); |
| if (caption_button_container_->window_controls_overlay_enabled()) { |
| caption_button_container_->SetBackground( |
| views::CreateSolidBackground(frame_color)); |
| } |
| |
| if (icon_color_id.has_value()) { |
| caption_button_container_->SetButtonIconColor(*icon_color_id); |
| if (back_button_) { |
| back_button_->SetIconColorId(*icon_color_id); |
| } |
| if (center_button_) { |
| center_button_->SetIconColorId(*icon_color_id); |
| } |
| return; |
| } |
| caption_button_container_->SetButtonBackgroundColor(frame_color); |
| if (back_button_) { |
| back_button_->SetBackgroundColor(frame_color); |
| } |
| if (center_button_) { |
| center_button_->SetBackgroundColor(frame_color); |
| } |
| } |
| |
| void FrameHeader::PaintTitleBar(gfx::Canvas* canvas) { |
| std::u16string text = frame_text_override_; |
| views::WidgetDelegate* target_widget_delegate = |
| target_widget_->widget_delegate(); |
| if (text.empty() && target_widget_delegate && |
| target_widget_delegate->ShouldShowWindowTitle()) { |
| text = target_widget_delegate->GetWindowTitle(); |
| } |
| |
| if (!text.empty()) { |
| int flags = gfx::Canvas::NO_SUBPIXEL_RENDERING; |
| if (target_widget_delegate->ShouldCenterWindowTitleText()) |
| flags |= gfx::Canvas::TEXT_ALIGN_CENTER; |
| canvas->DrawStringRectWithFlags(text, gfx::FontList(), GetTitleColor(), |
| view_->GetMirroredRect(GetTitleBounds()), |
| flags); |
| } |
| } |
| |
| void FrameHeader::SetCaptionButtonContainer( |
| chromeos::FrameCaptionButtonContainerView* caption_button_container) { |
| caption_button_container_ = caption_button_container; |
| |
| // Perform layout to ensure the container height is correct. |
| LayoutHeaderInternal(); |
| } |
| |
| void FrameHeader::StartTransitionAnimation(base::TimeDelta duration) { |
| frame_animator_->StartAnimation(duration); |
| |
| frame_animator_->SchedulePaint(); |
| } |
| |
| /////////////////////////////////////////////////////////////////////////////// |
| // FrameHeader, private: |
| |
| void FrameHeader::LayoutHeaderInternal() { |
| // The animator's position can change when the frame is moved from overlay. |
| // Make sure the animator view is at the bottom. |
| view_->ReorderChildView(frame_animator_, 0); |
| |
| caption_button_container()->UpdateButtonsImageAndTooltip(); |
| |
| caption_button_container()->SetButtonSize( |
| views::GetCaptionButtonLayoutSize(GetButtonLayoutSize())); |
| |
| const gfx::Size caption_button_container_size = |
| caption_button_container()->GetPreferredSize({}); |
| caption_button_container()->SetBounds( |
| view_->width() - caption_button_container_size.width(), 0, |
| caption_button_container_size.width(), |
| caption_button_container_size.height()); |
| |
| caption_button_container()->DeprecatedLayoutImmediately(); |
| |
| int origin = 0; |
| if (back_button_) { |
| gfx::Size size = back_button_->GetPreferredSize({}); |
| back_button_->SetBounds(0, 0, size.width(), |
| caption_button_container_size.height()); |
| origin = back_button_->bounds().right(); |
| } |
| |
| if (left_header_view_) { |
| // Vertically center the left header view (typically the window icon) with |
| // respect to the caption button container. |
| const gfx::Size icon_size(left_header_view_->GetPreferredSize({})); |
| const int icon_offset_y = (GetHeaderHeight() - icon_size.height()) / 2; |
| constexpr int kLeftViewXInset = 9; |
| left_header_view_->SetBounds(kLeftViewXInset + origin, icon_offset_y, |
| icon_size.width(), icon_size.height()); |
| origin = left_header_view_->bounds().right(); |
| } |
| |
| if (center_button_) { |
| constexpr int kCenterButtonSpacing = 5; |
| const int full_width = center_button_->GetPreferredSize({}).width(); |
| const int begin = std::max((view_->width() - full_width) / 2, |
| origin + kCenterButtonSpacing); |
| const int end = std::max( |
| begin, std::min((view_->width() + full_width) / 2, |
| caption_button_container_->x() - kCenterButtonSpacing)); |
| center_button_->SetBounds(begin, 0, end - begin, |
| caption_button_container_size.height()); |
| } |
| } |
| |
| gfx::Rect FrameHeader::GetTitleBounds() const { |
| views::View* left_view = |
| left_header_view_ ? left_header_view_.get() : back_button_.get(); |
| return GetAvailableTitleBounds(left_view, caption_button_container_, |
| GetHeaderHeight()); |
| } |
| |
| } // namespace chromeos |