chromium/chromeos/ui/frame/default_frame_header.cc

// Copyright 2014 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/default_frame_header.h"

#include "base/check.h"
#include "chromeos/ui/base/app_types.h"
#include "chromeos/ui/base/chromeos_ui_constants.h"
#include "chromeos/ui/base/window_properties.h"
#include "chromeos/ui/frame/caption_buttons/frame_caption_button_container_view.h"
#include "chromeos/ui/wm/window_util.h"
#include "third_party/skia/include/core/SkPath.h"
#include "ui/color/color_id.h"
#include "ui/color/color_provider.h"
#include "ui/compositor/layer.h"
#include "ui/gfx/canvas.h"
#include "ui/gfx/color_utils.h"
#include "ui/gfx/geometry/rect.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"

using views::Widget;

namespace {

// Tiles an image into an area, rounding the top corners.
void TileRoundRect(gfx::Canvas* canvas,
                   const cc::PaintFlags& flags,
                   const gfx::Rect& bounds,
                   int corner_radius) {
  SkRect rect = gfx::RectToSkRect(bounds);
  const SkScalar corner_radius_scalar = SkIntToScalar(corner_radius);
  SkScalar radii[8] = {corner_radius_scalar,
                       corner_radius_scalar,  // top-left
                       corner_radius_scalar,
                       corner_radius_scalar,  // top-right
                       0,
                       0,  // bottom-right
                       0,
                       0};  // bottom-left
  // Antialiasing can result in blending a transparent pixel and
  // leave non opaque alpha between the frame and the client area.
  // Extend 1dp to make sure it's fully opaque.
  rect.fBottom += 1;
  SkPath path;
  path.addRoundRect(rect, radii, SkPathDirection::kCW);
  canvas->DrawPath(path, flags);
}

// For now, we should only apply dynamic color to the default frame header if
// the window is a system web app.
bool ShouldApplyDynamicColor(aura::Window* window) {
#if BUILDFLAG(IS_CHROMEOS_ASH)
  return window->GetProperty(chromeos::kAppTypeKey) ==
         chromeos::AppType::SYSTEM_APP;
#else
  // Default frame is used for non-browser frames in Lacros. In Lacros, we
  // never need dynamic colors as we don't display SWAs. This will need to be
  // redesigned if SWAs run in Lacros.
  return false;
#endif
}

}  // namespace

namespace chromeos {

///////////////////////////////////////////////////////////////////////////////
// DefaultFrameHeader, public:

DefaultFrameHeader::DefaultFrameHeader(
    views::Widget* target_widget,
    views::View* header_view,
    chromeos::FrameCaptionButtonContainerView* caption_button_container)
    : FrameHeader(target_widget, header_view) {
  DCHECK(caption_button_container);
  SetCaptionButtonContainer(caption_button_container);
  InitializeFrameColorMetricsHelper();
}

DefaultFrameHeader::~DefaultFrameHeader() = default;

void DefaultFrameHeader::SetWidthInPixels(int width_in_pixels) {
  if (width_in_pixels_ == width_in_pixels)
    return;
  width_in_pixels_ = width_in_pixels;
  SchedulePaintForTitle();
}

void DefaultFrameHeader::UpdateFrameColors() {
  aura::Window* target_window = GetTargetWindow();
  if (!target_window) {
    // b/302708285: This codepath is run during Widget teardown. In that
    // situation, `target_window` might be null and we won't display the
    // updated colors anyway.
    return;
  }

  const SkColor active_frame_color =
      target_window->GetProperty(kFrameActiveColorKey);
  const SkColor inactive_frame_color =
      target_window->GetProperty(kFrameInactiveColorKey);

  bool updated = false;
  // Update the frame if the frame color for the current active state changes.
  if (active_frame_color_ != active_frame_color) {
    active_frame_color_ = active_frame_color;
    updated = mode() == Mode::MODE_ACTIVE;
  }
  if (inactive_frame_color_ != inactive_frame_color) {
    inactive_frame_color_ = inactive_frame_color;
    updated |= mode() == Mode::MODE_INACTIVE;
  }

  if (updated) {
    StartTransitionAnimation(kDefaultFrameColorChangeAnimationDuration);
#if BUILDFLAG(IS_CHROMEOS_ASH)
    frame_color_metrics_helper_->UpdateFrameColorChangesCount();
#endif
  }

  if (ShouldApplyDynamicColor(GetTargetWindow())) {
    UpdateCaptionButtonColors(mode() == MODE_ACTIVE
                                  ? ui::kColorSysPrimary
                                  : ui::kColorFrameCaptionButtonUnfocused);
  } else {
    UpdateCaptionButtonColors(std::nullopt);
  }
}

///////////////////////////////////////////////////////////////////////////////
// DefaultFrameHeader, protected:

void DefaultFrameHeader::DoPaintHeader(gfx::Canvas* canvas) {
  cc::PaintFlags flags;

  if (ShouldApplyDynamicColor(GetTargetWindow())) {
    flags.setColor(target_widget()->GetColorProvider()->GetColor(
        GetColorIdForCurrentMode()));
  } else {
    flags.setColor(mode() == Mode::MODE_ACTIVE ? active_frame_color_
                                               : inactive_frame_color_);
  }

  const int corner_radius = header_corner_radius();
  flags.setAntiAlias(corner_radius > 0);
  if (width_in_pixels_ > 0) {
    canvas->Save();
    float layer_scale =
        target_widget()->GetNativeWindow()->layer()->device_scale_factor();
    float canvas_scale = canvas->UndoDeviceScaleFactor();
    gfx::Rect rect =
        ScaleToEnclosingRect(GetPaintedBounds(), canvas_scale, canvas_scale);
    rect.set_width(width_in_pixels_ * canvas_scale / layer_scale);
    TileRoundRect(canvas, flags, rect,
                  static_cast<int>(corner_radius * canvas_scale));
    canvas->Restore();
  } else {
    TileRoundRect(canvas, flags, GetPaintedBounds(), corner_radius);
  }
  PaintTitleBar(canvas);
}

views::CaptionButtonLayoutSize DefaultFrameHeader::GetButtonLayoutSize() const {
  return views::CaptionButtonLayoutSize::kNonBrowserCaption;
}

SkColor DefaultFrameHeader::GetTitleColor() const {
  // Use IsDark() to change target colors instead of PickContrastingColor(), so
  // that FrameCaptionButton::GetButtonColor() (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 frame_color = GetCurrentFrameColor();
  const SkColor desired_color = color_utils::IsDark(frame_color)
                                    ? SK_ColorWHITE
                                    : SkColorSetRGB(40, 40, 40);
  return color_utils::BlendForMinContrast(desired_color, frame_color).color;
}

///////////////////////////////////////////////////////////////////////////////
// DefaultFrameHeader, private:

aura::Window* DefaultFrameHeader::GetTargetWindow() {
  return target_widget()->GetNativeWindow();
}

SkColor DefaultFrameHeader::GetCurrentFrameColor() const {
  return mode() == MODE_ACTIVE ? active_frame_color_ : inactive_frame_color_;
}

void DefaultFrameHeader::InitializeFrameColorMetricsHelper() {
#if BUILDFLAG(IS_CHROMEOS_ASH)
  aura::Window* window = GetTargetWindow();
  CHECK(window);
  frame_color_metrics_helper_ = std::make_unique<FrameColorMetricsHelper>(
      window->GetProperty(chromeos::kAppTypeKey));
#endif
}

}  // namespace chromeos