blob: 86475f5f0920ef0a46c8dc605710e2fc8c6e90a4 [file] [log] [blame]
// 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 <algorithm>
#include <array>
#include <cmath>
#include <memory>
#include <string_view>
#include <utility>
#include "base/strings/strcat.h"
#include "cc/paint/paint_op.h"
#include "cc/paint/paint_record.h"
#include "cc/paint/record_paint_canvas.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/skia/include/core/SkColor.h"
#include "ui/color/color_id.h"
#include "ui/color/color_provider.h"
#include "ui/color/color_provider_utils.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/gfx/geometry/rect_conversions.h"
#include "ui/gfx/geometry/rect_f.h"
#include "ui/native_theme/native_theme.h"
namespace ui {
class NativeThemeFluentTest : public ::testing::Test,
public ::testing::WithParamInterface<float> {
protected:
const NativeThemeFluent& theme() const { return theme_; }
// Mocks the availability of the font for drawing arrow icons.
void SetArrowIconsAvailable(bool available) {
theme_.SetArrowIconsAvailableForTesting(available);
EXPECT_EQ(theme().GetArrowIconsAvailable(), available);
}
void VerifyArrowRect() const {
SCOPED_TRACE(::testing::Message() << "Scale: " << ScaleFromDIP());
for (const auto& parts_elem :
std::to_array<std::pair<NativeTheme::Part, std::string_view>>(
{{NativeTheme::kScrollbarDownArrow, "down"},
{NativeTheme::kScrollbarLeftArrow, "left"},
{NativeTheme::kScrollbarRightArrow, "right"},
{NativeTheme::kScrollbarUpArrow, "up"}})) {
const NativeTheme::Part part = parts_elem.first;
SCOPED_TRACE(base::StrCat({"Arrow direction: ", parts_elem.second}));
const gfx::RectF button_rect = ButtonRect(part);
for (const auto& states_elem :
std::to_array<std::pair<NativeTheme::State, std::string_view>>(
{{NativeTheme::kDisabled, "disabled"},
{NativeTheme::kHovered, "hovered"},
{NativeTheme::kNormal, "normal"},
{NativeTheme::kPressed, "pressed"}})) {
const NativeTheme::State state = states_elem.first;
SCOPED_TRACE(base::StrCat({"Arrow state: ", states_elem.second}));
const gfx::RectF arrow_rect =
theme().GetArrowRect(gfx::ToNearestRect(button_rect), part, state);
VerifyArrowRectCommonDimensions(arrow_rect);
if (!theme().GetArrowIconsAvailable()) {
VerifyArrowRectIsIntRect(arrow_rect);
}
VerifyArrowRectIsCentered(button_rect, arrow_rect, part);
VerifyArrowRectLengthRatio(button_rect, arrow_rect, state);
}
}
}
void PaintScrollbarThumb(cc::PaintCanvas* canvas) const {
ColorProvider color_provider;
theme_.PaintScrollbarThumb(canvas, &color_provider,
NativeTheme::kScrollbarVerticalThumb,
NativeTheme::kNormal, gfx::Rect(15, 100), {});
}
private:
static float ScaleFromDIP() { return GetParam(); }
static void VerifyArrowRectCommonDimensions(const gfx::RectF& arrow_rect) {
EXPECT_FALSE(arrow_rect.IsEmpty());
EXPECT_EQ(arrow_rect.width(), arrow_rect.height());
EXPECT_EQ(arrow_rect.width(), std::floor(arrow_rect.width()));
}
void VerifyArrowRectIsCentered(const gfx::RectF& button_rect,
const gfx::RectF& arrow_rect,
NativeTheme::Part part) const {
const gfx::PointF button_center = button_rect.CenterPoint();
const gfx::PointF arrow_center = arrow_rect.CenterPoint();
// The arrow is shifted away from center along the length axis by one dp,
// rounded to integral px.
float expected_shift = std::round(ScaleFromDIP());
if (!theme().GetArrowIconsAvailable()) {
// For triangular arrows, rect coordinates are snapped to integers, which
// may introduce an additional half pixel shift.
expected_shift += 0.5f;
}
if (part == NativeTheme::kScrollbarUpArrow ||
part == NativeTheme::kScrollbarDownArrow) {
EXPECT_EQ(button_center.x(), arrow_center.x());
EXPECT_NEAR(button_center.y(), arrow_center.y(), expected_shift);
} else {
EXPECT_NEAR(button_center.x(), arrow_center.x(), expected_shift);
EXPECT_EQ(button_center.y(), arrow_center.y());
}
}
static void VerifyArrowRectIsIntRect(const gfx::RectF& arrow_rect) {
EXPECT_TRUE(gfx::IsNearestRectWithinDistance(arrow_rect, 0.01f));
}
static void VerifyArrowRectLengthRatio(const gfx::RectF& button_rect,
const gfx::RectF& arrow_rect,
NativeTheme::State state) {
const float thickness = std::min(button_rect.width(), button_rect.height());
const float arrow_side = arrow_rect.width(); // The arrow is square.
if (state == NativeTheme::kPressed) {
// Pressed icons are ~0.5 times as thick as the button (precise value
// depends on zoom and whether arrow icons are available).
EXPECT_GT(arrow_side, thickness / 3.0f);
EXPECT_LT(arrow_side, thickness / 1.5f);
} else {
// Non-pressed arrows are ~0.6 times as thick as the button.
EXPECT_GT(arrow_side, thickness / 2.0f);
EXPECT_LT(arrow_side, thickness);
}
}
gfx::RectF ButtonRect(NativeTheme::Part part) const {
gfx::Rect rect({}, theme().GetVerticalScrollbarButtonSize());
if (part == NativeTheme::kScrollbarLeftArrow ||
part == NativeTheme::kScrollbarRightArrow) {
rect.Transpose();
}
return gfx::RectF(gfx::ScaleToEnclosedRect(rect, ScaleFromDIP()));
}
private:
NativeThemeFluent theme_;
};
// Verify the dimensions of an arrow rect with triangular arrows for a given
// button rect depending on the arrow direction and state.
TEST_P(NativeThemeFluentTest, VerifyArrowRectWithTriangularArrows) {
SetArrowIconsAvailable(false);
VerifyArrowRect();
}
// Verify the dimensions of an arrow rect with arrow icons for a given button
// rect depending on the arrow direction and state.
TEST_P(NativeThemeFluentTest, VerifyArrowRectWithArrowIcons) {
SetArrowIconsAvailable(true);
VerifyArrowRect();
}
// Verify that the thumb paint function draws a round rectangle. Generally,
// `NativeThemeFluent::Paint*()` functions are covered by Blink's web tests; but
// in web tests we render the thumbs as squares instead of pill-shaped. This
// test ensures we don't lose coverage on the PaintOp called to draw the thumb.
TEST_F(NativeThemeFluentTest, PaintThumbRoundedCorners) {
cc::RecordPaintCanvas canvas;
PaintScrollbarThumb(&canvas);
EXPECT_EQ(canvas.TotalOpCount(), 1u);
EXPECT_EQ(canvas.ReleaseAsRecord().GetFirstOp().GetType(),
cc::PaintOpType::kDrawRRect);
}
// Verify that GetThumbColor returns the correct color given the scrollbar state
// and extra params.
TEST_F(NativeThemeFluentTest, GetThumbColor) {
const std::unique_ptr<ColorProvider> color_provider =
CreateDefaultColorProviderForBlink(/*dark_mode=*/false);
// When there are no extra params set, the colors should be the ones that
// correspond to the ColorId.
EXPECT_EQ(color_provider->GetColor(kColorWebNativeControlScrollbarThumb),
theme().GetScrollbarThumbColor(color_provider.get(),
NativeTheme::kNormal, {}));
const auto hovered_thumb_color =
color_provider->GetColor(kColorWebNativeControlScrollbarThumbHovered);
EXPECT_EQ(hovered_thumb_color,
theme().GetScrollbarThumbColor(color_provider.get(),
NativeTheme::kHovered, {}));
const auto pressed_thumb_color =
color_provider->GetColor(kColorWebNativeControlScrollbarThumbPressed);
EXPECT_EQ(pressed_thumb_color,
theme().GetScrollbarThumbColor(color_provider.get(),
NativeTheme::kPressed, {}));
// When the thumb is being painted in minimal mode, the normal state should
// return the minimal mode's transparent color while the other states remain
// unaffected.
static constexpr NativeTheme::ScrollbarThumbExtraParams kMinimalParams = {
.is_thumb_minimal_mode = true};
EXPECT_EQ(color_provider->GetColor(
kColorWebNativeControlScrollbarThumbOverlayMinimalMode),
theme().GetScrollbarThumbColor(
color_provider.get(), NativeTheme::kNormal, kMinimalParams));
EXPECT_EQ(hovered_thumb_color,
theme().GetScrollbarThumbColor(
color_provider.get(), NativeTheme::kHovered, kMinimalParams));
EXPECT_EQ(pressed_thumb_color,
theme().GetScrollbarThumbColor(
color_provider.get(), NativeTheme::kPressed, kMinimalParams));
// When there is a css color set in the extra params, we modify the color
// when it is hovered or pressed to signal the change in state.
static constexpr auto kCssColor = SK_ColorGREEN;
static constexpr NativeTheme::ScrollbarThumbExtraParams kColorParams = {
.thumb_color = kCssColor};
EXPECT_EQ(kCssColor,
theme().GetScrollbarThumbColor(color_provider.get(),
NativeTheme::kNormal, kColorParams));
EXPECT_NE(kCssColor,
theme().GetScrollbarThumbColor(
color_provider.get(), NativeTheme::kHovered, kColorParams));
EXPECT_NE(kCssColor,
theme().GetScrollbarThumbColor(
color_provider.get(), NativeTheme::kPressed, kColorParams));
}
INSTANTIATE_TEST_SUITE_P(All,
NativeThemeFluentTest,
::testing::Values(1.0f, 1.25f, 1.5f, 1.75f, 2.0f));
} // namespace ui