chromium/chromeos/ui/frame/frame_header.cc

// 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 <vector>

#include "base/logging.h"  // DCHECK
#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/frame/frame_utils.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/color/color_id.h"
#include "ui/compositor/layer.h"
#include "ui/compositor/layer_animation_observer.h"
#include "ui/compositor/layer_tree_owner.h"
#include "ui/compositor/scoped_layer_animation_settings.h"
#include "ui/gfx/canvas.h"
#include "ui/gfx/color_utils.h"
#include "ui/gfx/font_list.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/gfx/scoped_canvas.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/non_client_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.
  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::NonClientFrameView* 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::WindowShowState show_state) {
  if (show_state == ui::SHOW_STATE_MINIMIZED)
    return;

  LayoutHeaderInternal();
}

void FrameHeader::OnFloatStateChanged() {
  LayoutHeaderInternal();
}

void FrameHeader::SetHeaderCornerRadius(int radius) {
  if (radius == corner_radius_) {
    return;
  }

  corner_radius_ = radius;
  view_->SchedulePaint();
}

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();
}

SkPath FrameHeader::GetWindowMaskForFrameHeader(const gfx::Size& size) {
  return SkPath();
}

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