blob: 59213985f3ca3dd1eb70f5759a3f97a9f10b14ed [file] [log] [blame]
// 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