// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "ui/native_theme/native_theme_fluent.h"

#include "base/no_destructor.h"
#include "cc/paint/paint_canvas.h"
#include "cc/paint/paint_flags.h"
#include "third_party/skia/include/core/SkFont.h"
#include "third_party/skia/include/core/SkFontMgr.h"
#include "third_party/skia/include/core/SkTextBlob.h"
#include "ui/color/color_provider.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/gfx/geometry/rect_conversions.h"
#include "ui/gfx/geometry/rect_f.h"
#include "ui/gfx/geometry/rrect_f.h"
#include "ui/native_theme/native_theme_constants_fluent.h"

namespace ui {

NativeThemeFluent::NativeThemeFluent(bool should_only_use_dark_colors)
    : NativeThemeBase(should_only_use_dark_colors) {
  scrollbar_width_ = kFluentScrollbarThickness;

  const sk_sp<SkFontMgr> font_manager(SkFontMgr::RefDefault());
  typeface_ = sk_sp<SkTypeface>(
      font_manager->matchFamilyStyle(kFluentScrollbarFont, SkFontStyle()));
}

NativeThemeFluent::~NativeThemeFluent() = default;

// static
NativeThemeFluent* NativeThemeFluent::web_instance() {
  static base::NoDestructor<NativeThemeFluent> s_native_theme_for_web(
      /*should_only_use_dark_colors=*/false);
  return s_native_theme_for_web.get();
}

void NativeThemeFluent::PaintArrowButton(
    cc::PaintCanvas* canvas,
    const ColorProvider* color_provider,
    const gfx::Rect& rect,
    Part direction,
    State state,
    ColorScheme color_scheme,
    const ScrollbarArrowExtraParams& arrow) const {
  PaintButton(canvas, color_provider, rect, color_scheme);
  PaintArrow(canvas, color_provider, rect, direction, state, color_scheme);
}

void NativeThemeFluent::PaintScrollbarTrack(
    cc::PaintCanvas* canvas,
    const ColorProvider* color_provider,
    Part part,
    State state,
    const ScrollbarTrackExtraParams& extra_params,
    const gfx::Rect& rect,
    ColorScheme color_scheme) const {
  const SkColor track_color = color_provider->GetColor(kColorScrollbarTrack);
  cc::PaintFlags flags;
  flags.setColor(track_color);
  canvas->drawIRect(gfx::RectToSkIRect(rect), flags);
}

void NativeThemeFluent::PaintScrollbarThumb(cc::PaintCanvas* canvas,
                                            const ColorProvider* color_provider,
                                            Part part,
                                            State state,
                                            const gfx::Rect& rect,
                                            ScrollbarOverlayColorTheme theme,
                                            ColorScheme color_scheme) const {
  DCHECK_NE(state, NativeTheme::kDisabled);

  cc::PaintCanvasAutoRestore auto_restore(canvas, true);
  SkRRect rrect =
      SkRRect::MakeRectXY(gfx::RectToSkRect(rect), kFluentScrollbarThumbRadius,
                          kFluentScrollbarThumbRadius);

  // Clip the canvas to match the round rect and create round corners.
  SkPath path;
  path.addRRect(rrect);
  canvas->clipPath(path, true);

  ColorId thumb_color_id = kColorScrollbarThumb;
  if (state == NativeTheme::kPressed) {
    thumb_color_id = kColorScrollbarThumbPressed;
  } else if (state == NativeTheme::kHovered) {
    thumb_color_id = kColorScrollbarThumbHovered;
  }
  const SkColor thumb_color = color_provider->GetColor(thumb_color_id);
  cc::PaintFlags flags;
  flags.setAntiAlias(true);
  flags.setColor(thumb_color);
  canvas->drawRect(gfx::RectToSkRect(rect), flags);
}

void NativeThemeFluent::PaintScrollbarCorner(
    cc::PaintCanvas* canvas,
    const ColorProvider* color_provider,
    State state,
    const gfx::Rect& rect,
    ColorScheme color_scheme) const {
  const SkColor corner_color = color_provider->GetColor(kColorScrollbarTrack);

  cc::PaintFlags flags;
  flags.setColor(corner_color);
  canvas->drawIRect(RectToSkIRect(rect), flags);
}

gfx::Size NativeThemeFluent::GetPartSize(Part part,
                                         State state,
                                         const ExtraParams& extra) const {
  switch (part) {
    case kScrollbarHorizontalThumb:
      return gfx::Size(kFluentScrollbarMinimalThumbLength,
                       kFluentScrollbarThumbThickness);
    case kScrollbarVerticalThumb:
      return gfx::Size(kFluentScrollbarThumbThickness,
                       kFluentScrollbarMinimalThumbLength);
    case kScrollbarHorizontalTrack:
      return gfx::Size(0, scrollbar_width_);
    case kScrollbarVerticalTrack:
      return gfx::Size(scrollbar_width_, 0);
    case kScrollbarUpArrow:
    case kScrollbarDownArrow:
      return gfx::Size(scrollbar_width_, kFluentScrollbarButtonSideLength);
    case kScrollbarLeftArrow:
    case kScrollbarRightArrow:
      return gfx::Size(kFluentScrollbarButtonSideLength, scrollbar_width_);
    default:
      break;
  }

  return NativeThemeBase::GetPartSize(part, state, extra);
}

void NativeThemeFluent::PaintButton(cc::PaintCanvas* canvas,
                                    const ColorProvider* color_provider,
                                    const gfx::Rect& rect,
                                    ColorScheme color_scheme) const {
  const SkColor button_color = color_provider->GetColor(kColorScrollbarTrack);
  cc::PaintFlags flags;
  flags.setColor(button_color);
  canvas->drawIRect(gfx::RectToSkIRect(rect), flags);
}

void NativeThemeFluent::PaintArrow(cc::PaintCanvas* canvas,
                                   const ColorProvider* color_provider,
                                   const gfx::Rect& rect,
                                   Part part,
                                   State state,
                                   ColorScheme color_scheme) const {
  const ColorId arrow_color_id =
      state == NativeTheme::kPressed || state == NativeTheme::kHovered
          ? kColorScrollbarArrowForegroundPressed
          : kColorScrollbarArrowForeground;
  const SkColor arrow_color = color_provider->GetColor(arrow_color_id);
  cc::PaintFlags flags;
  flags.setColor(arrow_color);

  if (!ArrowIconsAvailable()) {
    // Paint regular triangular arrows if the font with arrow icons is not
    // available. GetArrowRect() returns the float rect but it is expected to be
    // the integer rect in this case.
    const SkPath path =
        PathForArrow(ToNearestRect(GetArrowRect(rect, part, state)), part);
    canvas->drawPath(path, flags);
    return;
  }

  const gfx::RectF bounding_rect = GetArrowRect(rect, part, state);
  // The bounding rect for an arrow is a square, so that we can use the width
  // despite the arrow direction.
  DCHECK(typeface_);
  SkFont font(typeface_, bounding_rect.width());
  font.setEdging(SkFont::Edging::kSubpixelAntiAlias);
  font.setSubpixel(true);
  flags.setAntiAlias(true);
  const char* arrow_code_point = GetArrowCodePointForScrollbarPart(part);
  canvas->drawTextBlob(SkTextBlob::MakeFromString(arrow_code_point, font),
                       bounding_rect.x(), bounding_rect.bottom(), flags);
}

gfx::RectF NativeThemeFluent::GetArrowRect(const gfx::Rect& rect,
                                           Part part,
                                           State state) const {
  int min_rect_side, max_rect_side;
  std::tie(min_rect_side, max_rect_side) =
      std::minmax(rect.width(), rect.height());
  const int arrow_side = GetArrowSideLength(state);

  // Calculates the scaling ratio used to determine the arrow rect side length.
  const float arrow_to_button_side_scale_ratio =
      arrow_side / static_cast<float>(kFluentScrollbarButtonSideLength);
  int side_length =
      base::ClampCeil(max_rect_side * arrow_to_button_side_scale_ratio);

  gfx::RectF arrow_rect(rect);
  if (ArrowIconsAvailable()) {
    arrow_rect.ClampToCenteredSize(gfx::SizeF(side_length, side_length));
  } else {
    // Add 1px to the side length if the difference between smaller button rect
    // and arrow side length is odd to keep the arrow rect in the center as well
    // as use int coordinates. This avoids the usage of anti-aliasing.
    side_length += (min_rect_side - side_length) % 2;
    arrow_rect.ClampToCenteredSize(gfx::SizeF(side_length, side_length));
    arrow_rect.set_origin(
        gfx::PointF(std::floor(arrow_rect.x()), std::floor(arrow_rect.y())));
  }

  // The end result is a centered arrow rect within the button rect with the
  // applied offset.
  OffsetArrowRect(arrow_rect, part, max_rect_side);
  return arrow_rect;
}

int NativeThemeFluent::GetArrowSideLength(State state) const {
  if (state == NativeTheme::kPressed)
    return ArrowIconsAvailable()
               ? kFluentScrollbarPressedArrowRectLength
               : kFluentScrollbarPressedArrowRectFallbackLength;

  return kFluentScrollbarArrowRectLength;
}

void NativeThemeFluent::OffsetArrowRect(gfx::RectF& arrow_rect,
                                        Part part,
                                        int max_rect_side) const {
  const float scaled_offset =
      std::round(kFluentScrollbarArrowOffset * max_rect_side /
                 static_cast<float>(kFluentScrollbarButtonSideLength));
  switch (part) {
    case kScrollbarUpArrow:
      arrow_rect.Offset(0, -scaled_offset);
      break;
    case kScrollbarDownArrow:
      arrow_rect.Offset(0, scaled_offset);
      break;
    case kScrollbarLeftArrow:
      arrow_rect.Offset(-scaled_offset, 0);
      break;
    case kScrollbarRightArrow:
      arrow_rect.Offset(scaled_offset, 0);
      break;
    default:
      NOTREACHED();
  }
}

const char* NativeThemeFluent::GetArrowCodePointForScrollbarPart(
    Part part) const {
  switch (part) {
    case Part::kScrollbarUpArrow:
      return kFluentScrollbarUpArrow;
    case Part::kScrollbarDownArrow:
      return kFluentScrollbarDownArrow;
    case Part::kScrollbarLeftArrow:
      return kFluentScrollbarLeftArrow;
    case Part::kScrollbarRightArrow:
      return kFluentScrollbarRightArrow;
    default:
      NOTREACHED();
      return nullptr;
  }
}

}  // namespace ui