910e62b5创建于 1月15日历史提交
// Copyright 2025 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_base.h"

#include <stddef.h>
#include <stdint.h>

#include <array>
#include <optional>
#include <utility>

#include "base/containers/span.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/skia/include/core/SkColor.h"
#include "third_party/skia/include/core/SkPath.h"
#include "third_party/skia/include/core/SkPoint.h"
#include "ui/gfx/color_utils.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/gfx/geometry/rect_f.h"
#include "ui/native_theme/native_theme.h"

namespace ui {
namespace {

void VerifyTriangle(SkPath path, base::span<const SkPoint> expected_points) {
  ASSERT_EQ(static_cast<size_t>(path.countPoints()), expected_points.size());
  for (int i = 0; const SkPoint& expected_point : expected_points) {
    const SkPoint actual_point = path.getPoint(i++);
    // TODO(pkasting): Move `cc::PaintOpHelper::ToString(const SkPoint&)` to a
    // more general location and use `EXPECT_EQ(actual_point, expected_point)`.
    EXPECT_EQ(actual_point.x(), expected_point.x());
    EXPECT_EQ(actual_point.y(), expected_point.y());
  }
}

}  // namespace

class NativeThemeBaseTest : public ::testing::Test {
 protected:
  gfx::RectF GetArrowRect(const gfx::Rect& rect) const {
    return theme_.GetArrowRect(rect, NativeTheme::kScrollbarDownArrow,
                               NativeTheme::kNormal);
  }

  SkPath PathForArrow(const gfx::Rect& rect, NativeTheme::Part part) const {
    return NativeThemeBase::PathForArrow(GetArrowRect(rect), part);
  }

  float GetScrollbarPartContrastRatio() const {
    return theme_.GetScrollbarPartContrastRatioForState(
        NativeTheme::State::kHovered);
  }

  SkColor GetContrastingColorForScrollbarPart(
      std::optional<SkColor> color,
      std::optional<SkColor> bg_color) const {
    return theme_
        .GetContrastingColorForScrollbarPart(
            std::move(color), std::move(bg_color), NativeTheme::State::kHovered)
        .value();
  }

 private:
  NativeThemeBase theme_;
};

TEST_F(NativeThemeBaseTest, BoundingRectSquare) {
  EXPECT_EQ(GetArrowRect(gfx::Rect(42, 61, 21, 21)), gfx::RectF(48, 67, 9, 9));
}

TEST_F(NativeThemeBaseTest, BoundingRectSlightlyRectangular) {
  // Stretched horizontally.
  EXPECT_EQ(GetArrowRect(gfx::Rect(42, 61, 25, 20)),
            gfx::RectF(49, 66, 11, 11));

  // Stretched vertically.
  EXPECT_EQ(GetArrowRect(gfx::Rect(42, 61, 14, 10)), gfx::RectF(46, 63, 6, 6));
}

TEST_F(NativeThemeBaseTest, BoundingRectVeryRectangular) {
  // Stretched horizontally.
  EXPECT_EQ(GetArrowRect(gfx::Rect(42, 61, 30, 8)), gfx::RectF(53, 61, 8, 8));

  // Stretched vertically.
  EXPECT_EQ(GetArrowRect(gfx::Rect(42, 61, 6, 44)), gfx::RectF(42, 80, 6, 6));
}

TEST_F(NativeThemeBaseTest, BoundingRectSnappedToWholePixels) {
  EXPECT_EQ(GetArrowRect(gfx::Rect(0, 0, 9, 10)).x(), 3);
  EXPECT_EQ(GetArrowRect(gfx::Rect(0, 0, 10, 9)).y(), 3);
}

TEST_F(NativeThemeBaseTest, VerticalArrows) {
  // Up arrow, sized for 1x.
  VerifyTriangle(
      PathForArrow(gfx::Rect(100, 200, 17, 17), NativeTheme::kScrollbarUpArrow),
      {SkPoint::Make(105, 211), SkPoint::Make(112, 211),
       SkPoint::Make(108.5, 207)});

  // 1.25x, should be larger.
  VerifyTriangle(
      PathForArrow(gfx::Rect(50, 70, 21, 21), NativeTheme::kScrollbarUpArrow),
      {SkPoint::Make(56, 84), SkPoint::Make(65, 84), SkPoint::Make(60.5, 79)});

  // Down arrow is just a flipped up arrow.
  VerifyTriangle(
      PathForArrow(gfx::Rect(20, 80, 17, 17), NativeTheme::kScrollbarDownArrow),
      {SkPoint::Make(25, 86), SkPoint::Make(32, 86), SkPoint::Make(28.5, 90)});
}

TEST_F(NativeThemeBaseTest, HorizontalArrows) {
  // Right arrow, sized for 1x.
  VerifyTriangle(PathForArrow(gfx::Rect(100, 200, 17, 17),
                              NativeTheme::kScrollbarRightArrow),
                 {SkPoint::Make(107, 205), SkPoint::Make(107, 212),
                  SkPoint::Make(111, 208.5)});

  // Button size for 1.25x, should be larger.
  VerifyTriangle(
      PathForArrow(gfx::Rect(50, 70, 21, 21),
                   NativeTheme::kScrollbarRightArrow),
      {SkPoint::Make(58, 76), SkPoint::Make(58, 85), SkPoint::Make(63, 80.5)});

  // Left arrow is just a flipped right arrow.
  VerifyTriangle(
      PathForArrow(gfx::Rect(20, 80, 17, 17), NativeTheme::kScrollbarLeftArrow),
      {SkPoint::Make(30, 85), SkPoint::Make(30, 92), SkPoint::Make(26, 88.5)});
}

TEST_F(NativeThemeBaseTest, ArrowForNonSquareButton) {
  VerifyTriangle(
      PathForArrow(gfx::Rect(90, 80, 42, 37), NativeTheme::kScrollbarLeftArrow),
      {SkPoint::Make(116, 89), SkPoint::Make(116, 109),
       SkPoint::Make(105, 99)});
}

// Checks that `GetContrastingColorForScrollbarPart()` doesn't modify
// fully-transparent colors.
TEST_F(NativeThemeBaseTest, GetContrastingColorTransparent) {
  static constexpr auto kOpaqueColor = SkColorSetRGB(0xBA, 0x74, 0x74);
  static constexpr auto kTransparentColor = SkColorSetA(kOpaqueColor, 0x00);
  EXPECT_EQ(kTransparentColor, GetContrastingColorForScrollbarPart(
                                   kTransparentColor, kOpaqueColor));
}

// Checks that `GetContrastingColorForScrollbarPart` can adapt to a whole range
// of luminosity for the colors to modify.
TEST_F(NativeThemeBaseTest, GetContrastingColorLuminosity) {
  for (unsigned i = 0; i < 255; i++) {
    const SkColor color = SkColorSetRGB(i, i, i);
    const float luminance = color_utils::GetRelativeLuminance(color);
    const float adjusted_luminance = color_utils::GetRelativeLuminance(
        GetContrastingColorForScrollbarPart(color, std::nullopt));
    if (color_utils::IsDark(color)) {
      EXPECT_GT(adjusted_luminance, luminance);
    } else {
      EXPECT_LT(adjusted_luminance, luminance);
    }
  }
}

// Checks that the returned color never loses contrast against the background
// color.
TEST_F(NativeThemeBaseTest, GetContrastingColorBackgroundContrast) {
  for (size_t c = 0; c < 24; c += 8) {
    // Test with Green, Blue and Red.
    const SkColor bg_color =
        SkColorSetA(SkColor(uint32_t{0x80} << c), SK_AlphaOPAQUE);
    for (uint32_t i = 0; i < 255; ++i) {
      const SkColor adjusted_color = GetContrastingColorForScrollbarPart(
          SkColorSetA(SkColor(i << c), SK_AlphaOPAQUE), bg_color);
      EXPECT_GT(color_utils::GetContrastRatio(bg_color, adjusted_color),
                GetScrollbarPartContrastRatio());
    }
  }
}

// Checks that grayscale colors never lose contrast with the background when it
// is different shades of gray.
TEST_F(NativeThemeBaseTest, GetContrastingColorGrayScales) {
  static constexpr auto kColors =
      std::to_array({SK_ColorWHITE, SK_ColorLTGRAY, SK_ColorGRAY,
                     SK_ColorDKGRAY, SK_ColorBLACK});
  for (const auto foreground : kColors) {
    for (const auto background : kColors) {
      const float background_contrast_ratio = color_utils::GetContrastRatio(
          background,
          GetContrastingColorForScrollbarPart(foreground, background));
      const float base_contrast_ratio = GetScrollbarPartContrastRatio();
      EXPECT_GE(background_contrast_ratio, base_contrast_ratio);
    }
  }
}

// Checks that transparent foreground colors don't lose contrast against the
// background when altered.
TEST_F(NativeThemeBaseTest, GetContrastingColorTransparencies) {
  for (const auto background : {SK_ColorBLACK, SK_ColorWHITE}) {
    for (auto foreground : {SK_ColorRED, SK_ColorGREEN, SK_ColorBLUE}) {
      for (int i = 1; i < 4; ++i) {
        foreground = SkColorSetA(foreground, SK_AlphaOPAQUE * i / 4);
        const float background_contrast_ratio = color_utils::GetContrastRatio(
            background,
            GetContrastingColorForScrollbarPart(foreground, background));
        const float base_contrast_ratio = GetScrollbarPartContrastRatio();
        EXPECT_GE(background_contrast_ratio, base_contrast_ratio);
      }
    }
  }
}

}  // namespace ui