blob: 405a660571933e4da517299c4a49c4719868db2d [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 <cmath>
#include <optional>
#include "base/check_op.h"
#include "base/containers/fixed_flat_map.h"
#include "base/numerics/safe_conversions.h"
#include "base/strings/cstring_view.h"
#include "cc/paint/paint_canvas.h"
#include "cc/paint/paint_flags.h"
#include "skia/ext/font_utils.h"
#include "third_party/skia/include/core/SkColor.h"
#include "third_party/skia/include/core/SkFont.h"
#include "third_party/skia/include/core/SkFontMgr.h"
#include "third_party/skia/include/core/SkFontStyle.h"
#include "third_party/skia/include/core/SkRRect.h"
#include "third_party/skia/include/core/SkRect.h"
#include "third_party/skia/include/core/SkRefCnt.h"
#include "third_party/skia/include/core/SkTextBlob.h"
#include "third_party/skia/include/core/SkTypeface.h"
#include "ui/color/color_id.h"
#include "ui/color/color_provider.h"
#include "ui/gfx/geometry/insets.h"
#include "ui/gfx/geometry/insets_f.h"
#include "ui/gfx/geometry/outsets.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/gfx/geometry/rect_f.h"
#include "ui/gfx/geometry/rrect_f.h"
#include "ui/gfx/geometry/size.h"
#include "ui/gfx/geometry/size_f.h"
#include "ui/gfx/geometry/skia_conversions.h"
#include "ui/gfx/geometry/vector2d_f.h"
#include "ui/native_theme/features/native_theme_features.h"
namespace ui {
namespace {
// A sufficiently large value ensures the most round curve for the corners of
// the scrollbar thumb and overlay buttons.
constexpr int kScrollbarPartsRadius = 999;
// The outline width used to paint track and buttons in High Contrast mode.
constexpr float kScrollbarTrackOutlineWidth = 1.0f;
} // namespace
int NativeThemeFluent::GetPaintedScrollbarTrackInset() const {
return 1;
}
bool NativeThemeFluent::GetArrowIconsAvailable() const {
return !!GetArrowIconTypeface();
}
void NativeThemeFluent::SetArrowIconsAvailableForTesting(bool available) {
typeface_ = available ? skia::DefaultTypeface() : nullptr;
}
NativeThemeFluent::NativeThemeFluent() {
set_use_overlay_scrollbar(IsFluentOverlayScrollbarEnabled());
}
NativeThemeFluent::~NativeThemeFluent() = default;
gfx::Size NativeThemeFluent::GetVerticalScrollbarButtonSize() const {
return gfx::Size(kScrollbarThickness, kScrollbarButtonSideLength);
}
gfx::Size NativeThemeFluent::GetVerticalScrollbarThumbSize() const {
return gfx::Size(9, 17);
}
gfx::RectF NativeThemeFluent::GetArrowRect(const gfx::Rect& rect,
Part part,
State state) const {
const bool arrow_icons_available = GetArrowIconsAvailable();
int unscaled_arrow_side = 9;
if (state == kPressed) {
unscaled_arrow_side = arrow_icons_available ? 8 : 7;
}
// Note: Using initializer_list form forces returning by copy, not ref.
const auto [min_rect_side, max_rect_side] =
std::minmax({rect.width(), rect.height()});
const float scale_factor =
max_rect_side / static_cast<float>(kScrollbarButtonSideLength);
int arrow_side = base::ClampCeil(unscaled_arrow_side * scale_factor);
gfx::RectF arrow_rect(rect);
if (arrow_icons_available) {
arrow_rect.ClampToCenteredSize(
static_cast<gfx::SizeF>(gfx::Size(arrow_side, arrow_side)));
} 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 anti-aliasing.
arrow_side += (min_rect_side - arrow_side) % 2;
arrow_rect.ClampToCenteredSize(
static_cast<gfx::SizeF>(gfx::Size(arrow_side, arrow_side)));
arrow_rect.set_origin(
{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.
const int unscaled_offset =
(part == kScrollbarUpArrow || part == kScrollbarLeftArrow) ? -1 : 1;
gfx::Vector2dF offset(std::round(unscaled_offset * scale_factor), 0);
if (part == kScrollbarUpArrow || part == kScrollbarDownArrow) {
offset.Transpose();
}
return arrow_rect + offset;
}
std::optional<ColorId> NativeThemeFluent::GetScrollbarThumbColorId(
State state,
const ScrollbarThumbExtraParams& extra_params) const {
return (extra_params.is_thumb_minimal_mode && state != kHovered &&
state != kPressed)
? std::make_optional(
kColorWebNativeControlScrollbarThumbOverlayMinimalMode)
: std::nullopt;
}
float NativeThemeFluent::GetScrollbarPartContrastRatioForState(
State state) const {
return 1.8f;
}
void NativeThemeFluent::PaintArrowButton(
cc::PaintCanvas* canvas,
const ColorProvider* color_provider,
const gfx::Rect& rect,
Part part,
State state,
bool forced_colors,
bool dark_mode,
PreferredContrast contrast,
const ScrollbarArrowExtraParams& extra_params) const {
const auto paint_button = [this, canvas, part](const gfx::RectF& paint_rect,
cc::PaintFlags paint_flags) {
if (!use_overlay_scrollbar()) {
canvas->drawRect(gfx::RectFToSkRect(paint_rect), paint_flags);
return;
}
SkScalar ul = 0, ll = 0, ur = 0, lr = 0;
if (part == kScrollbarUpArrow) {
ul = kScrollbarPartsRadius;
ur = kScrollbarPartsRadius;
} else if (part == kScrollbarDownArrow) {
ll = kScrollbarPartsRadius;
lr = kScrollbarPartsRadius;
} else if (part == kScrollbarLeftArrow) {
ul = kScrollbarPartsRadius;
ll = kScrollbarPartsRadius;
} else if (part == kScrollbarRightArrow) {
ur = kScrollbarPartsRadius;
lr = kScrollbarPartsRadius;
}
const gfx::RRectF rrect(paint_rect, ul, ul, ur, ur, lr, lr, ll, ll);
paint_flags.setAntiAlias(true);
canvas->drawRRect(static_cast<SkRRect>(rrect), paint_flags);
};
gfx::RectF button_fill_rect(rect);
// Windows native Fluent scrollbars draw a border around the button in forced
// colors mode regardless of the contrast of the colors; and the border seems
// clearly beneficial in high contrast, especially on platforms that don't
// natively do forced colors.
if (forced_colors || contrast == PreferredContrast::kMore) {
gfx::OutsetsF edge_outsets;
if (part == kScrollbarUpArrow) {
edge_outsets.set_bottom(kScrollbarTrackOutlineWidth);
} else if (part == kScrollbarDownArrow) {
edge_outsets.set_top(kScrollbarTrackOutlineWidth);
} else if (part == kScrollbarLeftArrow) {
edge_outsets.set_right(kScrollbarTrackOutlineWidth);
} else if (part == kScrollbarRightArrow) {
edge_outsets.set_left(kScrollbarTrackOutlineWidth);
}
button_fill_rect.Outset(edge_outsets);
gfx::RectF outline_rect = button_fill_rect;
outline_rect.Inset(kScrollbarTrackOutlineWidth / 2.0f);
cc::PaintFlags outline_flags;
outline_flags.setColor(
GetScrollbarThumbColor(color_provider, state,
{.is_hovering = extra_params.is_hovering,
.thumb_color = extra_params.thumb_color,
.track_color = extra_params.track_color}));
outline_flags.setStyle(cc::PaintFlags::kStroke_Style);
outline_flags.setStrokeWidth(kScrollbarTrackOutlineWidth);
paint_button(outline_rect, outline_flags);
// Adjust the fill rect to not overlap with the outline stroke rect.
button_fill_rect.Inset(kScrollbarTrackOutlineWidth);
}
// Paint button background.
const SkColor bg_color = GetScrollbarArrowBackgroundColor(
extra_params, state, dark_mode, contrast, color_provider);
cc::PaintFlags bg_flags;
bg_flags.setColor(bg_color);
paint_button(button_fill_rect, bg_flags);
// Paint arrow.
const SkColor fg_color = GetScrollbarArrowForegroundColor(
bg_color, extra_params, state, dark_mode, contrast, color_provider);
if (const auto typeface = GetArrowIconTypeface()) {
static constexpr auto kCodePointMap =
base::MakeFixedFlatMap<Part, base::cstring_view>(
{{kScrollbarDownArrow, "\uEDDC"},
{kScrollbarLeftArrow, "\uEDD9"},
{kScrollbarRightArrow, "\uEDDA"},
{kScrollbarUpArrow, "\uEDDB"}});
const gfx::RectF bounding_rect = GetArrowRect(rect, part, state);
// The bounding rect for an arrow is square, so we can use the width
// regardless of the arrow direction.
SkFont font(typeface, bounding_rect.width());
font.setEdging(SkFont::Edging::kAntiAlias);
font.setSubpixel(true);
cc::PaintFlags fg_flags;
fg_flags.setAntiAlias(true);
fg_flags.setColor(fg_color);
canvas->drawTextBlob(
SkTextBlob::MakeFromString(kCodePointMap.at(part).c_str(), font),
bounding_rect.x(), bounding_rect.bottom(), fg_flags);
} else {
// Paint regular triangular arrows if arrow icons are not available.
PaintArrow(canvas, rect, part, state, fg_color);
}
}
void NativeThemeFluent::PaintScrollbarThumb(
cc::PaintCanvas* canvas,
const ColorProvider* color_provider,
Part part,
State state,
const gfx::Rect& rect,
const ScrollbarThumbExtraParams& extra_params) const {
cc::PaintFlags flags;
flags.setAntiAlias(true);
flags.setColor(GetScrollbarThumbColor(color_provider, state, extra_params));
const SkRect sk_rect = gfx::RectToSkRect(rect);
if (extra_params.is_web_test) {
// Web tests draw the thumb as a square to avoid trivial discrepancies due
// to rounding/AA.
canvas->drawRect(sk_rect, flags);
} else {
canvas->drawRRect(SkRRect::MakeRectXY(sk_rect, kScrollbarPartsRadius,
kScrollbarPartsRadius),
flags);
}
}
void NativeThemeFluent::PaintScrollbarTrack(
cc::PaintCanvas* canvas,
const ColorProvider* color_provider,
Part part,
State state,
const ScrollbarTrackExtraParams& extra_params,
const gfx::Rect& rect,
bool forced_colors,
PreferredContrast contrast) const {
gfx::Rect track_fill_rect = rect;
// See comments in `PaintArrowButton()` re: the condition here.
if (forced_colors || contrast == PreferredContrast::kMore) {
gfx::Insets edge_insets;
if (part == kScrollbarHorizontalTrack) {
edge_insets.set_left_right(-kScrollbarTrackOutlineWidth,
-kScrollbarTrackOutlineWidth);
} else {
edge_insets.set_top_bottom(-kScrollbarTrackOutlineWidth,
-kScrollbarTrackOutlineWidth);
}
const gfx::InsetsF outline_insets(kScrollbarTrackOutlineWidth / 2.0f);
gfx::RectF outline_rect(rect);
outline_rect.Inset(outline_insets + gfx::InsetsF(edge_insets));
cc::PaintFlags outline_flags;
outline_flags.setColor(
color_provider->GetColor(kColorWebNativeControlScrollbarThumb));
outline_flags.setStyle(cc::PaintFlags::kStroke_Style);
outline_flags.setStrokeWidth(kScrollbarTrackOutlineWidth);
canvas->drawRect(gfx::RectFToSkRect(outline_rect), outline_flags);
// Adjust fill rect to not overlap with the outline stroke rect.
track_fill_rect.Inset(gfx::Insets(kScrollbarTrackOutlineWidth) +
edge_insets);
}
cc::PaintFlags flags;
flags.setColor(extra_params.track_color.value_or(
color_provider->GetColor(kColorWebNativeControlScrollbarTrack)));
canvas->drawIRect(gfx::RectToSkIRect(track_fill_rect), flags);
}
void NativeThemeFluent::PaintScrollbarCorner(
cc::PaintCanvas* canvas,
const ColorProvider* color_provider,
State state,
const gfx::Rect& rect,
const ScrollbarTrackExtraParams& extra_params) const {
cc::PaintFlags flags;
flags.setColor(extra_params.track_color.value_or(
color_provider->GetColor(kColorWebNativeControlScrollbarCorner)));
canvas->drawIRect(RectToSkIRect(rect), flags);
}
sk_sp<SkTypeface> NativeThemeFluent::GetArrowIconTypeface() const {
if (!typeface_.has_value()) {
const sk_sp<SkFontMgr> font_manager(skia::DefaultFontMgr());
typeface_ = font_manager->matchFamilyStyle("Segoe Fluent Icons", {});
}
return typeface_.value();
}
} // namespace ui