gecko-dev/widget/Theme.cpp
Emilio Cobos Álvarez 6fa764c7d3 Bug 1796849 - Teach the non-native theme to draw tooltips. r=stransky
We haven't seen this with Adwaita because it uses dark tooltips in both
light and dark mode, so stuff kinda worked out in the end.

Differential Revision: https://phabricator.services.mozilla.com/D159966
2022-10-24 09:05:31 +00:00

1653 lines
62 KiB
C++

/* -*- Mode: C++; tab-width: 40; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "Theme.h"
#include "ThemeCocoa.h"
#include "mozilla/MathAlgorithms.h"
#include "mozilla/ClearOnShutdown.h"
#include "mozilla/dom/Document.h"
#include "mozilla/dom/HTMLMeterElement.h"
#include "mozilla/dom/HTMLProgressElement.h"
#include "mozilla/gfx/Rect.h"
#include "mozilla/gfx/Types.h"
#include "mozilla/gfx/Filters.h"
#include "mozilla/RelativeLuminanceUtils.h"
#include "mozilla/StaticPrefs_widget.h"
#include "mozilla/webrender/WebRenderAPI.h"
#include "nsCSSColorUtils.h"
#include "nsCSSRendering.h"
#include "nsScrollbarFrame.h"
#include "nsIScrollbarMediator.h"
#include "nsDeviceContext.h"
#include "nsLayoutUtils.h"
#include "nsRangeFrame.h"
#include "PathHelpers.h"
#include "ScrollbarDrawingAndroid.h"
#include "ScrollbarDrawingCocoa.h"
#include "ScrollbarDrawingGTK.h"
#include "ScrollbarDrawingWin.h"
#include "ScrollbarDrawingWin11.h"
#ifdef XP_WIN
# include "mozilla/WindowsVersion.h"
#endif
using namespace mozilla;
using namespace mozilla::dom;
using namespace mozilla::gfx;
using namespace mozilla::widget;
namespace {
static constexpr gfx::sRGBColor sColorGrey10(
gfx::sRGBColor::UnusualFromARGB(0xffe9e9ed));
static constexpr gfx::sRGBColor sColorGrey10Alpha50(
gfx::sRGBColor::UnusualFromARGB(0x7fe9e9ed));
static constexpr gfx::sRGBColor sColorGrey20(
gfx::sRGBColor::UnusualFromARGB(0xffd0d0d7));
static constexpr gfx::sRGBColor sColorGrey30(
gfx::sRGBColor::UnusualFromARGB(0xffb1b1b9));
static constexpr gfx::sRGBColor sColorGrey40(
gfx::sRGBColor::UnusualFromARGB(0xff8f8f9d));
static constexpr gfx::sRGBColor sColorGrey40Alpha50(
gfx::sRGBColor::UnusualFromARGB(0x7f8f8f9d));
static constexpr gfx::sRGBColor sColorGrey50(
gfx::sRGBColor::UnusualFromARGB(0xff676774));
static constexpr gfx::sRGBColor sColorGrey60(
gfx::sRGBColor::UnusualFromARGB(0xff484851));
static constexpr gfx::sRGBColor sColorMeterGreen10(
gfx::sRGBColor::UnusualFromARGB(0xff00ab60));
static constexpr gfx::sRGBColor sColorMeterGreen20(
gfx::sRGBColor::UnusualFromARGB(0xff056139));
static constexpr gfx::sRGBColor sColorMeterYellow10(
gfx::sRGBColor::UnusualFromARGB(0xffffbd4f));
static constexpr gfx::sRGBColor sColorMeterYellow20(
gfx::sRGBColor::UnusualFromARGB(0xffd2811e));
static constexpr gfx::sRGBColor sColorMeterRed10(
gfx::sRGBColor::UnusualFromARGB(0xffe22850));
static constexpr gfx::sRGBColor sColorMeterRed20(
gfx::sRGBColor::UnusualFromARGB(0xff810220));
static const CSSCoord kMinimumRangeThumbSize = 20.0f;
static const CSSCoord kMinimumDropdownArrowButtonWidth = 18.0f;
static const CSSCoord kMinimumSpinnerButtonWidth = 18.0f;
static const CSSCoord kMinimumSpinnerButtonHeight = 9.0f;
static const CSSCoord kButtonBorderWidth = 1.0f;
static const CSSCoord kMenulistBorderWidth = 1.0f;
static const CSSCoord kTextFieldBorderWidth = 1.0f;
static const CSSCoord kRangeHeight = 6.0f;
static const CSSCoord kProgressbarHeight = 6.0f;
static const CSSCoord kMeterHeight = 12.0f;
// nsCheckboxRadioFrame takes the bottom of the content box as the baseline.
// This border-width makes its baseline 2px under the bottom, which is nice.
static constexpr CSSCoord kCheckboxRadioBorderWidth = 2.0f;
static constexpr sRGBColor sTransparent = sRGBColor::White(0.0);
// This pushes and pops a clip rect to the draw target.
//
// This is done to reduce fuzz in places where we may have antialiasing,
// because skia is not clip-invariant: given different clips, it does not
// guarantee the same result, even if the painted content doesn't intersect
// the clips.
//
// This is a bit sad, overall, but...
struct MOZ_RAII AutoClipRect {
AutoClipRect(DrawTarget& aDt, const LayoutDeviceRect& aRect) : mDt(aDt) {
mDt.PushClipRect(aRect.ToUnknownRect());
}
~AutoClipRect() { mDt.PopClip(); }
private:
DrawTarget& mDt;
};
static StaticRefPtr<Theme> gNativeInstance;
static StaticRefPtr<Theme> gNonNativeInstance;
static StaticRefPtr<Theme> gRDMInstance;
} // namespace
#ifdef ANDROID
already_AddRefed<Theme> do_CreateNativeThemeDoNotUseDirectly() {
// Android doesn't have a native theme.
return do_AddRef(new Theme(Theme::ScrollbarStyle()));
}
#endif
already_AddRefed<nsITheme> do_GetBasicNativeThemeDoNotUseDirectly() {
if (MOZ_UNLIKELY(!gNonNativeInstance)) {
UniquePtr<ScrollbarDrawing> scrollbarDrawing = Theme::ScrollbarStyle();
#ifdef MOZ_WIDGET_COCOA
gNonNativeInstance = new ThemeCocoa(std::move(scrollbarDrawing));
#else
gNonNativeInstance = new Theme(std::move(scrollbarDrawing));
#endif
ClearOnShutdown(&gNonNativeInstance);
}
return do_AddRef(gNonNativeInstance);
}
already_AddRefed<nsITheme> do_GetNativeThemeDoNotUseDirectly() {
if (MOZ_UNLIKELY(!gNativeInstance)) {
gNativeInstance = do_CreateNativeThemeDoNotUseDirectly();
ClearOnShutdown(&gNativeInstance);
}
return do_AddRef(gNativeInstance);
}
already_AddRefed<nsITheme> do_GetRDMThemeDoNotUseDirectly() {
if (MOZ_UNLIKELY(!gRDMInstance)) {
gRDMInstance = new Theme(MakeUnique<ScrollbarDrawingAndroid>());
ClearOnShutdown(&gRDMInstance);
}
return do_AddRef(gRDMInstance);
}
namespace mozilla::widget {
NS_IMPL_ISUPPORTS_INHERITED(Theme, nsNativeTheme, nsITheme)
static constexpr nsLiteralCString kPrefs[] = {
"widget.non-native-theme.use-theme-accent"_ns,
"widget.non-native-theme.win.scrollbar.use-system-size"_ns,
"widget.non-native-theme.scrollbar.size.override"_ns,
"widget.non-native-theme.scrollbar.style"_ns,
};
void Theme::Init() {
for (const auto& pref : kPrefs) {
Preferences::RegisterCallback(PrefChangedCallback, pref);
}
LookAndFeelChanged();
}
void Theme::Shutdown() {
for (const auto& pref : kPrefs) {
Preferences::UnregisterCallback(PrefChangedCallback, pref);
}
}
/* static */
void Theme::LookAndFeelChanged() {
ThemeColors::RecomputeAccentColors();
if (gNonNativeInstance) {
gNonNativeInstance->SetScrollbarDrawing(ScrollbarStyle());
}
if (gNativeInstance) {
gNativeInstance->SetScrollbarDrawing(ScrollbarStyle());
}
}
/* static */
auto Theme::GetDPIRatio(nsPresContext* aPc, StyleAppearance aAppearance)
-> DPIRatio {
// Widgets react to zoom, except scrollbars.
if (IsWidgetScrollbarPart(aAppearance)) {
return ScrollbarDrawing::GetDPIRatioForScrollbarPart(aPc);
}
return DPIRatio(float(AppUnitsPerCSSPixel()) / aPc->AppUnitsPerDevPixel());
}
/* static */
auto Theme::GetDPIRatio(nsIFrame* aFrame, StyleAppearance aAppearance)
-> DPIRatio {
return GetDPIRatio(aFrame->PresContext(), aAppearance);
}
// Checkbox and radio need to preserve aspect-ratio for compat. We also snap the
// size to exact device pixels to avoid snapping disorting the circles.
static LayoutDeviceRect CheckBoxRadioRect(const LayoutDeviceRect& aRect) {
// Place a square rect in the center of aRect.
auto size = std::trunc(std::min(aRect.width, aRect.height));
auto position = aRect.Center() - LayoutDevicePoint(size * 0.5, size * 0.5);
return LayoutDeviceRect(position, LayoutDeviceSize(size, size));
}
std::tuple<sRGBColor, sRGBColor, sRGBColor> Theme::ComputeCheckboxColors(
const ElementState& aState, StyleAppearance aAppearance,
const Colors& aColors) {
MOZ_ASSERT(aAppearance == StyleAppearance::Checkbox ||
aAppearance == StyleAppearance::Radio);
bool isDisabled = aState.HasState(ElementState::DISABLED);
bool isChecked = aState.HasState(ElementState::CHECKED);
bool isIndeterminate = aAppearance == StyleAppearance::Checkbox &&
aState.HasState(ElementState::INDETERMINATE);
if (isChecked || isIndeterminate) {
if (isDisabled) {
auto bg = ComputeBorderColor(aState, aColors, OutlineCoversBorder::No);
auto fg = aColors.HighContrast()
? aColors.System(StyleSystemColor::Graytext)
: sRGBColor::White(.8f);
return std::make_tuple(bg, bg, fg);
}
if (aColors.HighContrast()) {
auto bg = aColors.System(StyleSystemColor::Selecteditem);
auto fg = aColors.System(StyleSystemColor::Selecteditemtext);
return std::make_tuple(bg, bg, fg);
}
bool isActive =
aState.HasAllStates(ElementState::HOVER | ElementState::ACTIVE);
bool isHovered = aState.HasState(ElementState::HOVER);
const auto& bg = isActive ? aColors.Accent().GetDarker()
: isHovered ? aColors.Accent().GetDark()
: aColors.Accent().Get();
const auto& fg = aColors.Accent().GetForeground();
return std::make_tuple(bg, bg, fg);
}
auto [bg, border] =
ComputeTextfieldColors(aState, aColors, OutlineCoversBorder::No);
// We don't paint a checkmark in this case so any color would do.
return std::make_tuple(bg, border, sTransparent);
}
sRGBColor Theme::ComputeBorderColor(const ElementState& aState,
const Colors& aColors,
OutlineCoversBorder aOutlineCoversBorder) {
bool isDisabled = aState.HasState(ElementState::DISABLED);
if (aColors.HighContrast()) {
return aColors.System(isDisabled ? StyleSystemColor::Graytext
: StyleSystemColor::Buttontext);
}
bool isActive =
aState.HasAllStates(ElementState::HOVER | ElementState::ACTIVE);
bool isHovered = aState.HasState(ElementState::HOVER);
bool isFocused = aState.HasState(ElementState::FOCUSRING);
if (isDisabled) {
return sColorGrey40Alpha50;
}
if (isFocused && aOutlineCoversBorder == OutlineCoversBorder::Yes) {
// If we draw the outline over the border, prevent issues where the border
// shows underneath if it snaps in the wrong direction by using a
// transparent border. An alternative to this is ensuring that we snap the
// offset in PaintRoundedFocusRect the same was a we snap border widths, so
// that negative offsets are guaranteed to cover the border.
// But this looks harder to mess up.
return sTransparent;
}
bool dark = aColors.IsDark();
if (isActive) {
return dark ? sColorGrey20 : sColorGrey60;
}
if (isHovered) {
return dark ? sColorGrey30 : sColorGrey50;
}
return sColorGrey40;
}
std::pair<sRGBColor, sRGBColor> Theme::ComputeButtonColors(
const ElementState& aState, const Colors& aColors, nsIFrame* aFrame) {
bool isActive =
aState.HasAllStates(ElementState::HOVER | ElementState::ACTIVE);
bool isDisabled = aState.HasState(ElementState::DISABLED);
bool isHovered = aState.HasState(ElementState::HOVER);
nscolor backgroundColor = [&] {
if (isDisabled) {
return aColors.SystemNs(StyleSystemColor::MozButtondisabledface);
}
if (isActive) {
return aColors.SystemNs(StyleSystemColor::MozButtonactiveface);
}
if (isHovered) {
return aColors.SystemNs(StyleSystemColor::MozButtonhoverface);
}
return aColors.SystemNs(StyleSystemColor::Buttonface);
}();
if (aState.HasState(ElementState::AUTOFILL)) {
backgroundColor = NS_ComposeColors(
backgroundColor,
aColors.SystemNs(StyleSystemColor::MozAutofillBackground));
}
const sRGBColor borderColor =
ComputeBorderColor(aState, aColors, OutlineCoversBorder::Yes);
return std::make_pair(sRGBColor::FromABGR(backgroundColor), borderColor);
}
std::pair<sRGBColor, sRGBColor> Theme::ComputeTextfieldColors(
const ElementState& aState, const Colors& aColors,
OutlineCoversBorder aOutlineCoversBorder) {
nscolor backgroundColor = [&] {
if (aState.HasState(ElementState::DISABLED)) {
return aColors.SystemNs(StyleSystemColor::MozDisabledfield);
}
return aColors.SystemNs(StyleSystemColor::Field);
}();
if (aState.HasState(ElementState::AUTOFILL)) {
backgroundColor = NS_ComposeColors(
backgroundColor,
aColors.SystemNs(StyleSystemColor::MozAutofillBackground));
}
const sRGBColor borderColor =
ComputeBorderColor(aState, aColors, aOutlineCoversBorder);
return std::make_pair(sRGBColor::FromABGR(backgroundColor), borderColor);
}
std::pair<sRGBColor, sRGBColor> Theme::ComputeRangeProgressColors(
const ElementState& aState, const Colors& aColors) {
if (aColors.HighContrast()) {
return aColors.SystemPair(StyleSystemColor::Selecteditem,
StyleSystemColor::Buttontext);
}
bool isActive =
aState.HasAllStates(ElementState::HOVER | ElementState::ACTIVE);
bool isDisabled = aState.HasState(ElementState::DISABLED);
bool isHovered = aState.HasState(ElementState::HOVER);
if (isDisabled) {
return std::make_pair(sColorGrey40Alpha50, sColorGrey40Alpha50);
}
if (isActive || isHovered) {
return std::make_pair(aColors.Accent().GetDark(),
aColors.Accent().GetDarker());
}
return std::make_pair(aColors.Accent().Get(), aColors.Accent().GetDark());
}
std::pair<sRGBColor, sRGBColor> Theme::ComputeRangeTrackColors(
const ElementState& aState, const Colors& aColors) {
if (aColors.HighContrast()) {
return aColors.SystemPair(StyleSystemColor::Window,
StyleSystemColor::Buttontext);
}
bool isActive =
aState.HasAllStates(ElementState::HOVER | ElementState::ACTIVE);
bool isDisabled = aState.HasState(ElementState::DISABLED);
bool isHovered = aState.HasState(ElementState::HOVER);
if (isDisabled) {
return std::make_pair(sColorGrey10Alpha50, sColorGrey40Alpha50);
}
if (isActive || isHovered) {
return std::make_pair(sColorGrey20, sColorGrey50);
}
return std::make_pair(sColorGrey10, sColorGrey40);
}
std::pair<sRGBColor, sRGBColor> Theme::ComputeRangeThumbColors(
const ElementState& aState, const Colors& aColors) {
if (aColors.HighContrast()) {
return aColors.SystemPair(StyleSystemColor::Selecteditemtext,
StyleSystemColor::Selecteditem);
}
bool isActive =
aState.HasAllStates(ElementState::HOVER | ElementState::ACTIVE);
bool isDisabled = aState.HasState(ElementState::DISABLED);
bool isHovered = aState.HasState(ElementState::HOVER);
const sRGBColor& backgroundColor = [&] {
if (isDisabled) {
return sColorGrey40;
}
if (isActive) {
return aColors.Accent().Get();
}
if (isHovered) {
return sColorGrey60;
}
return sColorGrey50;
}();
const sRGBColor borderColor = sRGBColor::OpaqueWhite();
return std::make_pair(backgroundColor, borderColor);
}
std::pair<sRGBColor, sRGBColor> Theme::ComputeProgressColors(
const Colors& aColors) {
if (aColors.HighContrast()) {
return aColors.SystemPair(StyleSystemColor::Selecteditem,
StyleSystemColor::Buttontext);
}
return std::make_pair(aColors.Accent().Get(), aColors.Accent().GetDark());
}
std::pair<sRGBColor, sRGBColor> Theme::ComputeProgressTrackColors(
const Colors& aColors) {
if (aColors.HighContrast()) {
return aColors.SystemPair(StyleSystemColor::Buttonface,
StyleSystemColor::Buttontext);
}
return std::make_pair(sColorGrey10, sColorGrey40);
}
std::pair<sRGBColor, sRGBColor> Theme::ComputeMeterchunkColors(
const ElementState& aMeterState, const Colors& aColors) {
if (aColors.HighContrast()) {
return ComputeProgressColors(aColors);
}
sRGBColor borderColor = sColorMeterGreen20;
sRGBColor chunkColor = sColorMeterGreen10;
if (aMeterState.HasState(ElementState::SUB_OPTIMUM)) {
borderColor = sColorMeterYellow20;
chunkColor = sColorMeterYellow10;
} else if (aMeterState.HasState(ElementState::SUB_SUB_OPTIMUM)) {
borderColor = sColorMeterRed20;
chunkColor = sColorMeterRed10;
}
return std::make_pair(chunkColor, borderColor);
}
std::array<sRGBColor, 3> Theme::ComputeFocusRectColors(const Colors& aColors) {
if (aColors.HighContrast()) {
return {aColors.System(StyleSystemColor::Selecteditem),
aColors.System(StyleSystemColor::Buttontext),
aColors.System(StyleSystemColor::Window)};
}
const auto& accent = aColors.Accent();
const sRGBColor middle =
aColors.IsDark() ? sRGBColor::Black(.3f) : sRGBColor::White(.3f);
return {accent.Get(), middle, accent.GetLight()};
}
template <typename PaintBackendData>
void Theme::PaintRoundedFocusRect(PaintBackendData& aBackendData,
const LayoutDeviceRect& aRect,
const Colors& aColors, DPIRatio aDpiRatio,
CSSCoord aRadius, CSSCoord aOffset) {
// NOTE(emilio): If the widths or offsets here change, make sure to tweak
// the GetWidgetOverflow path for FocusOutline.
auto [innerColor, middleColor, outerColor] = ComputeFocusRectColors(aColors);
LayoutDeviceRect focusRect(aRect);
// The focus rect is painted outside of the border area (aRect), see:
//
// data:text/html,<div style="border: 1px solid; outline: 2px solid
// red">Foobar</div>
//
// But some controls might provide a negative offset to cover the border, if
// necessary.
CSSCoord strokeWidth = 2.0f;
auto strokeWidthDevPx =
LayoutDeviceCoord(ThemeDrawing::SnapBorderWidth(strokeWidth, aDpiRatio));
CSSCoord strokeRadius = aRadius;
focusRect.Inflate(aOffset * aDpiRatio + strokeWidthDevPx);
ThemeDrawing::PaintRoundedRectWithRadius(
aBackendData, focusRect, sTransparent, innerColor, strokeWidth,
strokeRadius, aDpiRatio);
strokeWidth = CSSCoord(1.0f);
strokeWidthDevPx =
LayoutDeviceCoord(ThemeDrawing::SnapBorderWidth(strokeWidth, aDpiRatio));
strokeRadius += strokeWidth;
focusRect.Inflate(strokeWidthDevPx);
ThemeDrawing::PaintRoundedRectWithRadius(
aBackendData, focusRect, sTransparent, middleColor, strokeWidth,
strokeRadius, aDpiRatio);
strokeWidth = CSSCoord(2.0f);
strokeWidthDevPx =
LayoutDeviceCoord(ThemeDrawing::SnapBorderWidth(strokeWidth, aDpiRatio));
strokeRadius += strokeWidth;
focusRect.Inflate(strokeWidthDevPx);
ThemeDrawing::PaintRoundedRectWithRadius(
aBackendData, focusRect, sTransparent, outerColor, strokeWidth,
strokeRadius, aDpiRatio);
}
void Theme::PaintCheckboxControl(DrawTarget& aDrawTarget,
const LayoutDeviceRect& aRect,
const ElementState& aState,
const Colors& aColors, DPIRatio aDpiRatio) {
auto [backgroundColor, borderColor, checkColor] =
ComputeCheckboxColors(aState, StyleAppearance::Checkbox, aColors);
{
const CSSCoord radius = 2.0f;
CSSCoord borderWidth = kCheckboxRadioBorderWidth;
if (backgroundColor == borderColor) {
borderWidth = 0.0f;
}
ThemeDrawing::PaintRoundedRectWithRadius(aDrawTarget, aRect,
backgroundColor, borderColor,
borderWidth, radius, aDpiRatio);
}
if (aState.HasState(ElementState::INDETERMINATE)) {
PaintIndeterminateMark(aDrawTarget, aRect, checkColor);
} else if (aState.HasState(ElementState::CHECKED)) {
PaintCheckMark(aDrawTarget, aRect, checkColor);
}
if (aState.HasState(ElementState::FOCUSRING)) {
PaintRoundedFocusRect(aDrawTarget, aRect, aColors, aDpiRatio, 5.0f, 1.0f);
}
}
constexpr CSSCoord kCheckboxRadioContentBoxSize = 10.0f;
constexpr CSSCoord kCheckboxRadioBorderBoxSize =
kCheckboxRadioContentBoxSize + kCheckboxRadioBorderWidth * 2.0f;
void Theme::PaintCheckMark(DrawTarget& aDrawTarget,
const LayoutDeviceRect& aRect,
const sRGBColor& aColor) {
// Points come from the coordinates on a 14X14 (kCheckboxRadioBorderBoxSize)
// unit box centered at 0,0
const float checkPolygonX[] = {-4.5f, -1.5f, -0.5f, 5.0f, 4.75f,
3.5f, -0.5f, -1.5f, -3.5f};
const float checkPolygonY[] = {0.5f, 4.0f, 4.0f, -2.5f, -4.0f,
-4.0f, 1.0f, 1.25f, -1.0f};
const int32_t checkNumPoints = sizeof(checkPolygonX) / sizeof(float);
const float scale =
ThemeDrawing::ScaleToFillRect(aRect, kCheckboxRadioBorderBoxSize);
auto center = aRect.Center().ToUnknownPoint();
RefPtr<PathBuilder> builder = aDrawTarget.CreatePathBuilder();
Point p = center + Point(checkPolygonX[0] * scale, checkPolygonY[0] * scale);
builder->MoveTo(p);
for (int32_t i = 1; i < checkNumPoints; i++) {
p = center + Point(checkPolygonX[i] * scale, checkPolygonY[i] * scale);
builder->LineTo(p);
}
RefPtr<Path> path = builder->Finish();
aDrawTarget.Fill(path, ColorPattern(ToDeviceColor(aColor)));
}
void Theme::PaintIndeterminateMark(DrawTarget& aDrawTarget,
const LayoutDeviceRect& aRect,
const sRGBColor& aColor) {
const CSSCoord borderWidth = 2.0f;
const float scale =
ThemeDrawing::ScaleToFillRect(aRect, kCheckboxRadioBorderBoxSize);
Rect rect = aRect.ToUnknownRect();
rect.y += (rect.height / 2) - (borderWidth * scale / 2);
rect.height = borderWidth * scale;
rect.x += (borderWidth * scale) + (borderWidth * scale / 8);
rect.width -= ((borderWidth * scale) + (borderWidth * scale / 8)) * 2;
aDrawTarget.FillRect(rect, ColorPattern(ToDeviceColor(aColor)));
}
template <typename PaintBackendData>
void Theme::PaintStrokedCircle(PaintBackendData& aPaintData,
const LayoutDeviceRect& aRect,
const sRGBColor& aBackgroundColor,
const sRGBColor& aBorderColor,
const CSSCoord aBorderWidth,
DPIRatio aDpiRatio) {
auto radius = LayoutDeviceCoord(aRect.Size().width) / aDpiRatio;
ThemeDrawing::PaintRoundedRectWithRadius(aPaintData, aRect, aBackgroundColor,
aBorderColor, aBorderWidth, radius,
aDpiRatio);
}
void Theme::PaintCircleShadow(WebRenderBackendData& aWrData,
const LayoutDeviceRect& aBoxRect,
const LayoutDeviceRect& aClipRect,
float aShadowAlpha, const CSSPoint& aShadowOffset,
CSSCoord aShadowBlurStdDev, DPIRatio aDpiRatio) {
const bool kBackfaceIsVisible = true;
const LayoutDeviceCoord stdDev = aShadowBlurStdDev * aDpiRatio;
const LayoutDevicePoint shadowOffset = aShadowOffset * aDpiRatio;
const IntSize inflation =
gfxAlphaBoxBlur::CalculateBlurRadius(gfxPoint(stdDev, stdDev));
LayoutDeviceRect shadowRect = aBoxRect;
shadowRect.MoveBy(shadowOffset);
shadowRect.Inflate(inflation.width, inflation.height);
const auto boxRect = wr::ToLayoutRect(aBoxRect);
aWrData.mBuilder.PushBoxShadow(
wr::ToLayoutRect(shadowRect), wr::ToLayoutRect(aClipRect),
kBackfaceIsVisible, boxRect,
wr::ToLayoutVector2D(aShadowOffset * aDpiRatio),
wr::ToColorF(DeviceColor(0.0f, 0.0f, 0.0f, aShadowAlpha)), stdDev,
/* aSpread = */ 0.0f,
wr::ToBorderRadius(gfx::RectCornerRadii(aBoxRect.Size().width)),
wr::BoxShadowClipMode::Outset);
}
void Theme::PaintCircleShadow(DrawTarget& aDrawTarget,
const LayoutDeviceRect& aBoxRect,
const LayoutDeviceRect& aClipRect,
float aShadowAlpha, const CSSPoint& aShadowOffset,
CSSCoord aShadowBlurStdDev, DPIRatio aDpiRatio) {
Float stdDev = aShadowBlurStdDev * aDpiRatio;
Point offset = (aShadowOffset * aDpiRatio).ToUnknownPoint();
RefPtr<FilterNode> blurFilter =
aDrawTarget.CreateFilter(FilterType::GAUSSIAN_BLUR);
if (!blurFilter) {
return;
}
blurFilter->SetAttribute(ATT_GAUSSIAN_BLUR_STD_DEVIATION, stdDev);
IntSize inflation =
gfxAlphaBoxBlur::CalculateBlurRadius(gfxPoint(stdDev, stdDev));
Rect inflatedRect = aBoxRect.ToUnknownRect();
inflatedRect.Inflate(inflation.width, inflation.height);
Rect sourceRectInFilterSpace =
inflatedRect - aBoxRect.TopLeft().ToUnknownPoint();
Point destinationPointOfSourceRect = inflatedRect.TopLeft() + offset;
IntSize dtSize = RoundedToInt(aBoxRect.Size().ToUnknownSize());
RefPtr<DrawTarget> ellipseDT = aDrawTarget.CreateSimilarDrawTargetForFilter(
dtSize, SurfaceFormat::A8, blurFilter, blurFilter,
sourceRectInFilterSpace, destinationPointOfSourceRect);
if (!ellipseDT) {
return;
}
AutoClipRect clipRect(aDrawTarget, aClipRect);
RefPtr<Path> ellipse = MakePathForEllipse(
*ellipseDT, (aBoxRect - aBoxRect.TopLeft()).Center().ToUnknownPoint(),
aBoxRect.Size().ToUnknownSize());
ellipseDT->Fill(ellipse,
ColorPattern(DeviceColor(0.0f, 0.0f, 0.0f, aShadowAlpha)));
RefPtr<SourceSurface> ellipseSurface = ellipseDT->Snapshot();
blurFilter->SetInput(IN_GAUSSIAN_BLUR_IN, ellipseSurface);
aDrawTarget.DrawFilter(blurFilter, sourceRectInFilterSpace,
destinationPointOfSourceRect);
}
template <typename PaintBackendData>
void Theme::PaintRadioControl(PaintBackendData& aPaintData,
const LayoutDeviceRect& aRect,
const ElementState& aState, const Colors& aColors,
DPIRatio aDpiRatio) {
auto [backgroundColor, borderColor, checkColor] =
ComputeCheckboxColors(aState, StyleAppearance::Radio, aColors);
{
CSSCoord borderWidth = kCheckboxRadioBorderWidth;
if (backgroundColor == borderColor) {
borderWidth = 0.0f;
}
PaintStrokedCircle(aPaintData, aRect, backgroundColor, borderColor,
borderWidth, aDpiRatio);
}
if (aState.HasState(ElementState::CHECKED)) {
LayoutDeviceRect rect(aRect);
auto width = LayoutDeviceCoord(
ThemeDrawing::SnapBorderWidth(kCheckboxRadioBorderWidth, aDpiRatio));
rect.Deflate(width);
PaintStrokedCircle(aPaintData, rect, backgroundColor, checkColor,
kCheckboxRadioBorderWidth, aDpiRatio);
}
if (aState.HasState(ElementState::FOCUSRING)) {
PaintRoundedFocusRect(aPaintData, aRect, aColors, aDpiRatio, 5.0f, 1.0f);
}
}
template <typename PaintBackendData>
void Theme::PaintTextField(PaintBackendData& aPaintData,
const LayoutDeviceRect& aRect,
const ElementState& aState, const Colors& aColors,
DPIRatio aDpiRatio) {
auto [backgroundColor, borderColor] =
ComputeTextfieldColors(aState, aColors, OutlineCoversBorder::Yes);
const CSSCoord radius = 2.0f;
ThemeDrawing::PaintRoundedRectWithRadius(aPaintData, aRect, backgroundColor,
borderColor, kTextFieldBorderWidth,
radius, aDpiRatio);
if (aState.HasState(ElementState::FOCUSRING)) {
PaintRoundedFocusRect(aPaintData, aRect, aColors, aDpiRatio,
radius + kTextFieldBorderWidth,
-kTextFieldBorderWidth);
}
}
template <typename PaintBackendData>
void Theme::PaintListbox(PaintBackendData& aPaintData,
const LayoutDeviceRect& aRect,
const ElementState& aState, const Colors& aColors,
DPIRatio aDpiRatio) {
const CSSCoord radius = 2.0f;
auto [backgroundColor, borderColor] =
ComputeTextfieldColors(aState, aColors, OutlineCoversBorder::Yes);
ThemeDrawing::PaintRoundedRectWithRadius(aPaintData, aRect, backgroundColor,
borderColor, kMenulistBorderWidth,
radius, aDpiRatio);
if (aState.HasState(ElementState::FOCUSRING)) {
PaintRoundedFocusRect(aPaintData, aRect, aColors, aDpiRatio,
radius + kMenulistBorderWidth, -kMenulistBorderWidth);
}
}
template <typename PaintBackendData>
void Theme::PaintMenulist(PaintBackendData& aDrawTarget,
const LayoutDeviceRect& aRect,
const ElementState& aState, const Colors& aColors,
DPIRatio aDpiRatio) {
const CSSCoord radius = 4.0f;
auto [backgroundColor, borderColor] = ComputeButtonColors(aState, aColors);
ThemeDrawing::PaintRoundedRectWithRadius(aDrawTarget, aRect, backgroundColor,
borderColor, kMenulistBorderWidth,
radius, aDpiRatio);
if (aState.HasState(ElementState::FOCUSRING)) {
PaintRoundedFocusRect(aDrawTarget, aRect, aColors, aDpiRatio,
radius + kMenulistBorderWidth, -kMenulistBorderWidth);
}
}
void Theme::PaintMenulistArrowButton(nsIFrame* aFrame, DrawTarget& aDrawTarget,
const LayoutDeviceRect& aRect,
const ElementState& aState) {
// not const: these may be negated in-place below
float polygonX[] = {-4.0f, -0.5f, 0.5f, 4.0f, 4.0f,
3.0f, 0.0f, 0.0f, -3.0f, -4.0f};
float polygonY[] = {-1, 3.0f, 3.0f, -1.0f, -2.0f,
-2.0f, 1.5f, 1.5f, -2.0f, -2.0f};
const float kPolygonSize = kMinimumDropdownArrowButtonWidth;
auto const [xs, ys] = [&] {
using Pair = std::pair<const float*, const float*>;
switch (aFrame->GetWritingMode().GetBlockDir()) {
case WritingMode::BlockDir::eBlockRL:
// rotate 90°: [[0,1],[-1,0]]
for (float& f : polygonY) {
f = -f;
}
return Pair(polygonY, polygonX);
case WritingMode::BlockDir::eBlockLR:
// rotate 270°: [[0,-1],[1,0]]
for (float& f : polygonX) {
f = -f;
}
return Pair(polygonY, polygonX);
case WritingMode::BlockDir::eBlockTB:
// rotate 0°: [[1,0],[0,1]]
return Pair(polygonX, polygonY);
}
MOZ_ASSERT_UNREACHABLE("unhandled BlockDir");
return Pair(polygonX, polygonY);
}();
const auto arrowColor = sRGBColor::FromABGR(
nsLayoutUtils::GetColor(aFrame, &nsStyleText::mWebkitTextFillColor));
ThemeDrawing::PaintArrow(aDrawTarget, aRect, xs, ys, kPolygonSize,
ArrayLength(polygonX), arrowColor);
}
void Theme::PaintSpinnerButton(nsIFrame* aFrame, DrawTarget& aDrawTarget,
const LayoutDeviceRect& aRect,
const ElementState& aState,
StyleAppearance aAppearance,
const Colors& aColors, DPIRatio aDpiRatio) {
auto [backgroundColor, borderColor] = ComputeButtonColors(aState, aColors);
aDrawTarget.FillRect(aRect.ToUnknownRect(),
ColorPattern(ToDeviceColor(backgroundColor)));
const float kPolygonX[] = {-3.5f, -0.5f, 0.5f, 3.5f, 3.5f,
2.5f, 0.0f, 0.0f, -2.5f, -3.5f};
float polygonY[] = {-1.5f, 1.5f, 1.5f, -1.5f, -2.5f,
-2.5f, 0.0f, 0.0f, -2.5f, -2.5f};
const float kPolygonSize = kMinimumSpinnerButtonHeight;
if (aAppearance == StyleAppearance::SpinnerUpbutton) {
for (auto& coord : polygonY) {
coord = -coord;
}
}
ThemeDrawing::PaintArrow(aDrawTarget, aRect, kPolygonX, polygonY,
kPolygonSize, ArrayLength(kPolygonX), borderColor);
}
template <typename PaintBackendData>
void Theme::PaintRange(nsIFrame* aFrame, PaintBackendData& aPaintData,
const LayoutDeviceRect& aRect,
const ElementState& aState, const Colors& aColors,
DPIRatio aDpiRatio, bool aHorizontal) {
nsRangeFrame* rangeFrame = do_QueryFrame(aFrame);
if (!rangeFrame) {
return;
}
double progress = rangeFrame->GetValueAsFractionOfRange();
auto rect = aRect;
LayoutDeviceRect thumbRect(0, 0, kMinimumRangeThumbSize * aDpiRatio,
kMinimumRangeThumbSize * aDpiRatio);
LayoutDeviceRect progressClipRect(aRect);
LayoutDeviceRect trackClipRect(aRect);
const LayoutDeviceCoord verticalSize = kRangeHeight * aDpiRatio;
if (aHorizontal) {
rect.height = verticalSize;
rect.y = aRect.y + (aRect.height - rect.height) / 2;
thumbRect.y = aRect.y + (aRect.height - thumbRect.height) / 2;
if (IsFrameRTL(aFrame)) {
thumbRect.x =
aRect.x + (aRect.width - thumbRect.width) * (1.0 - progress);
float midPoint = thumbRect.Center().X();
trackClipRect.SetBoxX(aRect.X(), midPoint);
progressClipRect.SetBoxX(midPoint, aRect.XMost());
} else {
thumbRect.x = aRect.x + (aRect.width - thumbRect.width) * progress;
float midPoint = thumbRect.Center().X();
progressClipRect.SetBoxX(aRect.X(), midPoint);
trackClipRect.SetBoxX(midPoint, aRect.XMost());
}
} else {
rect.width = verticalSize;
rect.x = aRect.x + (aRect.width - rect.width) / 2;
thumbRect.x = aRect.x + (aRect.width - thumbRect.width) / 2;
thumbRect.y =
aRect.y + (aRect.height - thumbRect.height) * (1.0 - progress);
float midPoint = thumbRect.Center().Y();
trackClipRect.SetBoxY(aRect.Y(), midPoint);
progressClipRect.SetBoxY(midPoint, aRect.YMost());
}
const CSSCoord borderWidth = 1.0f;
const CSSCoord radius = 3.0f;
auto [progressColor, progressBorderColor] =
ComputeRangeProgressColors(aState, aColors);
auto [trackColor, trackBorderColor] =
ComputeRangeTrackColors(aState, aColors);
ThemeDrawing::PaintRoundedRectWithRadius(aPaintData, rect, progressClipRect,
progressColor, progressBorderColor,
borderWidth, radius, aDpiRatio);
ThemeDrawing::PaintRoundedRectWithRadius(aPaintData, rect, trackClipRect,
trackColor, trackBorderColor,
borderWidth, radius, aDpiRatio);
if (!aState.HasState(ElementState::DISABLED)) {
// Ensure the shadow doesn't expand outside of our overflow rect declared in
// GetWidgetOverflow().
auto overflowRect = aRect;
overflowRect.Inflate(CSSCoord(6.0f) * aDpiRatio);
// Thumb shadow
PaintCircleShadow(aPaintData, thumbRect, overflowRect, 0.3f,
CSSPoint(0.0f, 2.0f), 2.0f, aDpiRatio);
}
// Draw the thumb on top.
const CSSCoord thumbBorderWidth = 2.0f;
auto [thumbColor, thumbBorderColor] =
ComputeRangeThumbColors(aState, aColors);
PaintStrokedCircle(aPaintData, thumbRect, thumbColor, thumbBorderColor,
thumbBorderWidth, aDpiRatio);
if (aState.HasState(ElementState::FOCUSRING)) {
PaintRoundedFocusRect(aPaintData, aRect, aColors, aDpiRatio, radius, 1.0f);
}
}
template <typename PaintBackendData>
void Theme::PaintProgress(nsIFrame* aFrame, PaintBackendData& aPaintData,
const LayoutDeviceRect& aRect,
const ElementState& aState, const Colors& aColors,
DPIRatio aDpiRatio, bool aIsMeter) {
const CSSCoord borderWidth = 1.0f;
const CSSCoord radius = aIsMeter ? 6.0f : 3.0f;
LayoutDeviceRect rect(aRect);
const LayoutDeviceCoord thickness =
(aIsMeter ? kMeterHeight : kProgressbarHeight) * aDpiRatio;
const bool isHorizontal = !nsNativeTheme::IsVerticalProgress(aFrame);
if (isHorizontal) {
// Center it vertically.
rect.y += (rect.height - thickness) / 2;
rect.height = thickness;
} else {
// Center it horizontally.
rect.x += (rect.width - thickness) / 2;
rect.width = thickness;
}
{
// Paint the track, unclipped.
auto [backgroundColor, borderColor] = ComputeProgressTrackColors(aColors);
ThemeDrawing::PaintRoundedRectWithRadius(aPaintData, rect, rect,
backgroundColor, borderColor,
borderWidth, radius, aDpiRatio);
}
// Now paint the chunk, clipped as needed.
LayoutDeviceRect clipRect = rect;
if (aState.HasState(ElementState::INDETERMINATE)) {
// For indeterminate progress, we paint an animated chunk of 1/3 of the
// progress size.
//
// Animation speed and math borrowed from GTK.
const LayoutDeviceCoord size = isHorizontal ? rect.width : rect.height;
const LayoutDeviceCoord barSize = size * 0.3333f;
const LayoutDeviceCoord travel = 2.0f * (size - barSize);
// Period equals to travel / pixelsPerMillisecond where pixelsPerMillisecond
// equals progressSize / 1000.0. This is equivalent to 1600.
const unsigned kPeriod = 1600;
const int t = PR_IntervalToMilliseconds(PR_IntervalNow()) % kPeriod;
const LayoutDeviceCoord dx = travel * float(t) / float(kPeriod);
if (isHorizontal) {
rect.width = barSize;
rect.x += (dx < travel * .5f) ? dx : travel - dx;
} else {
rect.height = barSize;
rect.y += (dx < travel * .5f) ? dx : travel - dx;
}
clipRect = rect;
// Queue the next frame if needed.
if (!QueueAnimatedContentForRefresh(aFrame->GetContent(), 60)) {
NS_WARNING("Couldn't refresh indeterminate <progress>");
}
} else {
// This is the progress chunk, clip it to the right amount.
double position = [&] {
if (aIsMeter) {
auto* meter = dom::HTMLMeterElement::FromNode(aFrame->GetContent());
if (!meter) {
return 0.0;
}
return meter->Position();
}
auto* progress = dom::HTMLProgressElement::FromNode(aFrame->GetContent());
if (!progress) {
return 0.0;
}
return progress->Position();
}();
if (isHorizontal) {
double clipWidth = rect.width * position;
clipRect.width = clipWidth;
if (IsFrameRTL(aFrame)) {
clipRect.x += rect.width - clipWidth;
}
} else {
double clipHeight = rect.height * position;
clipRect.height = clipHeight;
clipRect.y += rect.height - clipHeight;
}
}
auto [backgroundColor, borderColor] =
aIsMeter ? ComputeMeterchunkColors(aState, aColors)
: ComputeProgressColors(aColors);
ThemeDrawing::PaintRoundedRectWithRadius(aPaintData, rect, clipRect,
backgroundColor, borderColor,
borderWidth, radius, aDpiRatio);
}
template <typename PaintBackendData>
void Theme::PaintButton(nsIFrame* aFrame, PaintBackendData& aPaintData,
const LayoutDeviceRect& aRect,
const ElementState& aState, const Colors& aColors,
DPIRatio aDpiRatio) {
const CSSCoord radius = 4.0f;
auto [backgroundColor, borderColor] =
ComputeButtonColors(aState, aColors, aFrame);
ThemeDrawing::PaintRoundedRectWithRadius(aPaintData, aRect, backgroundColor,
borderColor, kButtonBorderWidth,
radius, aDpiRatio);
if (aState.HasState(ElementState::FOCUSRING)) {
PaintRoundedFocusRect(aPaintData, aRect, aColors, aDpiRatio,
radius + kButtonBorderWidth, -kButtonBorderWidth);
}
}
NS_IMETHODIMP
Theme::DrawWidgetBackground(gfxContext* aContext, nsIFrame* aFrame,
StyleAppearance aAppearance, const nsRect& aRect,
const nsRect& /* aDirtyRect */,
DrawOverflow aDrawOverflow) {
if (!DoDrawWidgetBackground(*aContext->GetDrawTarget(), aFrame, aAppearance,
aRect, aDrawOverflow)) {
return NS_ERROR_NOT_IMPLEMENTED;
}
return NS_OK;
}
bool Theme::CreateWebRenderCommandsForWidget(
mozilla::wr::DisplayListBuilder& aBuilder,
mozilla::wr::IpcResourceUpdateQueue& aResources,
const mozilla::layers::StackingContextHelper& aSc,
mozilla::layers::RenderRootStateManager* aManager, nsIFrame* aFrame,
StyleAppearance aAppearance, const nsRect& aRect) {
if (!StaticPrefs::widget_non_native_theme_webrender()) {
return false;
}
WebRenderBackendData data{aBuilder, aResources, aSc, aManager};
return DoDrawWidgetBackground(data, aFrame, aAppearance, aRect,
DrawOverflow::Yes);
}
static LayoutDeviceRect ToSnappedRect(const nsRect& aRect,
nscoord aTwipsPerPixel, DrawTarget& aDt) {
return LayoutDeviceRect::FromUnknownRect(
NSRectToSnappedRect(aRect, aTwipsPerPixel, aDt));
}
static LayoutDeviceRect ToSnappedRect(const nsRect& aRect,
nscoord aTwipsPerPixel,
WebRenderBackendData& aDt) {
// TODO: Do we need to do any more snapping here?
return LayoutDeviceRect::FromAppUnits(aRect, aTwipsPerPixel);
}
static ScrollbarDrawing::ScrollbarKind ComputeScrollbarKind(
nsIFrame* aFrame, bool aIsHorizontal) {
if (aIsHorizontal) {
return ScrollbarDrawing::ScrollbarKind::Horizontal;
}
nsIFrame* scrollbar = ScrollbarDrawing::GetParentScrollbarFrame(aFrame);
if (NS_WARN_IF(!scrollbar)) {
return ScrollbarDrawing::ScrollbarKind::VerticalRight;
}
MOZ_ASSERT(scrollbar->IsScrollbarFrame());
nsIScrollbarMediator* sm =
static_cast<nsScrollbarFrame*>(scrollbar)->GetScrollbarMediator();
if (NS_WARN_IF(!sm)) {
return ScrollbarDrawing::ScrollbarKind::VerticalRight;
}
return sm->IsScrollbarOnRight()
? ScrollbarDrawing::ScrollbarKind::VerticalRight
: ScrollbarDrawing::ScrollbarKind::VerticalLeft;
}
template <typename PaintBackendData>
bool Theme::DoDrawWidgetBackground(PaintBackendData& aPaintData,
nsIFrame* aFrame,
StyleAppearance aAppearance,
const nsRect& aRect,
DrawOverflow aDrawOverflow) {
static_assert(std::is_same_v<PaintBackendData, DrawTarget> ||
std::is_same_v<PaintBackendData, WebRenderBackendData>);
const nsPresContext* pc = aFrame->PresContext();
const nscoord twipsPerPixel = pc->AppUnitsPerDevPixel();
const auto devPxRect = ToSnappedRect(aRect, twipsPerPixel, aPaintData);
const DocumentState docState = pc->Document()->GetDocumentState();
ElementState elementState = GetContentState(aFrame, aAppearance);
if (aAppearance == StyleAppearance::MozMenulistArrowButton) {
bool isHTML = IsHTMLContent(aFrame);
nsIFrame* parentFrame = aFrame->GetParent();
bool isMenulist = !isHTML && parentFrame->IsMenuFrame();
// HTML select and XUL menulist dropdown buttons get state from the
// parent.
if (isHTML || isMenulist) {
aFrame = parentFrame;
elementState = GetContentState(parentFrame, aAppearance);
}
}
// Paint the outline iff we're asked to draw overflow and we have
// outline-style: auto.
if (aDrawOverflow == DrawOverflow::Yes &&
aFrame->StyleOutline()->mOutlineStyle.IsAuto()) {
elementState |= ElementState::FOCUSRING;
} else {
elementState &= ~ElementState::FOCUSRING;
}
// Hack to avoid skia fuzziness: Add a dummy clip if the widget doesn't
// overflow devPxRect.
Maybe<AutoClipRect> maybeClipRect;
if constexpr (std::is_same_v<PaintBackendData, DrawTarget>) {
if (aAppearance != StyleAppearance::FocusOutline &&
aAppearance != StyleAppearance::Range &&
!elementState.HasState(ElementState::FOCUSRING)) {
maybeClipRect.emplace(aPaintData, devPxRect);
}
}
const Colors colors(aFrame, aAppearance);
DPIRatio dpiRatio = GetDPIRatio(aFrame, aAppearance);
switch (aAppearance) {
case StyleAppearance::Radio: {
auto rect = CheckBoxRadioRect(devPxRect);
PaintRadioControl(aPaintData, rect, elementState, colors, dpiRatio);
break;
}
case StyleAppearance::Checkbox: {
if constexpr (std::is_same_v<PaintBackendData, WebRenderBackendData>) {
// TODO: Need to figure out how to best draw this using WR.
return false;
} else {
auto rect = CheckBoxRadioRect(devPxRect);
PaintCheckboxControl(aPaintData, rect, elementState, colors, dpiRatio);
}
break;
}
case StyleAppearance::Textarea:
case StyleAppearance::Textfield:
case StyleAppearance::NumberInput:
PaintTextField(aPaintData, devPxRect, elementState, colors, dpiRatio);
break;
case StyleAppearance::Listbox:
PaintListbox(aPaintData, devPxRect, elementState, colors, dpiRatio);
break;
case StyleAppearance::MenulistButton:
case StyleAppearance::Menulist:
PaintMenulist(aPaintData, devPxRect, elementState, colors, dpiRatio);
break;
case StyleAppearance::MozMenulistArrowButton:
if constexpr (std::is_same_v<PaintBackendData, WebRenderBackendData>) {
// TODO: Need to figure out how to best draw this using WR.
return false;
} else {
PaintMenulistArrowButton(aFrame, aPaintData, devPxRect, elementState);
}
break;
case StyleAppearance::Tooltip: {
const CSSCoord strokeWidth(1.0f);
const CSSCoord strokeRadius(2.0f);
ThemeDrawing::PaintRoundedRectWithRadius(
aPaintData, devPxRect,
colors.System(StyleSystemColor::Infobackground),
colors.System(StyleSystemColor::Infotext), strokeWidth, strokeRadius,
dpiRatio);
break;
}
case StyleAppearance::Menuitem: {
ThemeDrawing::FillRect(aPaintData, devPxRect, [&] {
if (CheckBooleanAttr(aFrame, nsGkAtoms::menuactive)) {
if (elementState.HasState(ElementState::DISABLED)) {
return colors.System(StyleSystemColor::MozMenuhoverdisabled);
}
return colors.System(StyleSystemColor::MozMenuhover);
}
return sTransparent;
}());
break;
}
case StyleAppearance::SpinnerUpbutton:
case StyleAppearance::SpinnerDownbutton:
if constexpr (std::is_same_v<PaintBackendData, WebRenderBackendData>) {
// TODO: Need to figure out how to best draw this using WR.
return false;
} else {
PaintSpinnerButton(aFrame, aPaintData, devPxRect, elementState,
aAppearance, colors, dpiRatio);
}
break;
case StyleAppearance::Range:
PaintRange(aFrame, aPaintData, devPxRect, elementState, colors, dpiRatio,
IsRangeHorizontal(aFrame));
break;
case StyleAppearance::RangeThumb:
// Painted as part of StyleAppearance::Range.
break;
case StyleAppearance::ProgressBar:
PaintProgress(aFrame, aPaintData, devPxRect, elementState, colors,
dpiRatio,
/* aIsMeter = */ false);
break;
case StyleAppearance::Progresschunk:
/* Painted as part of the progress bar */
break;
case StyleAppearance::Meter:
PaintProgress(aFrame, aPaintData, devPxRect, elementState, colors,
dpiRatio,
/* aIsMeter = */ true);
break;
case StyleAppearance::Meterchunk:
/* Painted as part of the meter bar */
break;
case StyleAppearance::ScrollbarthumbHorizontal:
case StyleAppearance::ScrollbarthumbVertical: {
bool isHorizontal =
aAppearance == StyleAppearance::ScrollbarthumbHorizontal;
auto kind = ComputeScrollbarKind(aFrame, isHorizontal);
return GetScrollbarDrawing().PaintScrollbarThumb(
aPaintData, devPxRect, kind, aFrame,
*nsLayoutUtils::StyleForScrollbar(aFrame), elementState, docState,
colors, dpiRatio);
}
case StyleAppearance::ScrollbartrackHorizontal:
case StyleAppearance::ScrollbartrackVertical: {
bool isHorizontal =
aAppearance == StyleAppearance::ScrollbartrackHorizontal;
auto kind = ComputeScrollbarKind(aFrame, isHorizontal);
return GetScrollbarDrawing().PaintScrollbarTrack(
aPaintData, devPxRect, kind, aFrame,
*nsLayoutUtils::StyleForScrollbar(aFrame), docState, colors,
dpiRatio);
}
case StyleAppearance::ScrollbarHorizontal:
case StyleAppearance::ScrollbarVertical: {
bool isHorizontal = aAppearance == StyleAppearance::ScrollbarHorizontal;
auto kind = ComputeScrollbarKind(aFrame, isHorizontal);
return GetScrollbarDrawing().PaintScrollbar(
aPaintData, devPxRect, kind, aFrame,
*nsLayoutUtils::StyleForScrollbar(aFrame), elementState, docState,
colors, dpiRatio);
}
case StyleAppearance::Scrollcorner: {
auto kind = ComputeScrollbarKind(aFrame, false);
return GetScrollbarDrawing().PaintScrollCorner(
aPaintData, devPxRect, kind, aFrame,
*nsLayoutUtils::StyleForScrollbar(aFrame), docState, colors,
dpiRatio);
}
case StyleAppearance::ScrollbarbuttonUp:
case StyleAppearance::ScrollbarbuttonDown:
case StyleAppearance::ScrollbarbuttonLeft:
case StyleAppearance::ScrollbarbuttonRight: {
// For scrollbar-width:thin, we don't display the buttons.
if (!ScrollbarDrawing::IsScrollbarWidthThin(aFrame)) {
if constexpr (std::is_same_v<PaintBackendData, WebRenderBackendData>) {
// TODO: Need to figure out how to best draw this using WR.
return false;
} else {
bool isHorizontal =
aAppearance == StyleAppearance::ScrollbarbuttonLeft ||
aAppearance == StyleAppearance::ScrollbarbuttonRight;
auto kind = ComputeScrollbarKind(aFrame, isHorizontal);
GetScrollbarDrawing().PaintScrollbarButton(
aPaintData, aAppearance, devPxRect, kind, aFrame,
*nsLayoutUtils::StyleForScrollbar(aFrame), elementState, docState,
colors, dpiRatio);
}
}
break;
}
case StyleAppearance::Button:
PaintButton(aFrame, aPaintData, devPxRect, elementState, colors,
dpiRatio);
break;
case StyleAppearance::FocusOutline:
PaintAutoStyleOutline(aFrame, aPaintData, devPxRect, colors, dpiRatio);
break;
default:
// Various appearance values are used for XUL elements. Normally these
// will not be available in content documents (and thus in the content
// processes where the native basic theme can be used), but tests are
// run with the remote XUL pref enabled and so we can get in here. So
// we just return an error rather than assert.
return false;
}
return true;
}
template <typename PaintBackendData>
void Theme::PaintAutoStyleOutline(nsIFrame* aFrame,
PaintBackendData& aPaintData,
const LayoutDeviceRect& aRect,
const Colors& aColors, DPIRatio aDpiRatio) {
const auto& accentColor = aColors.Accent();
const bool solid = StaticPrefs::widget_non_native_theme_solid_outline_style();
LayoutDeviceCoord strokeWidth(ThemeDrawing::SnapBorderWidth(2.0f, aDpiRatio));
LayoutDeviceRect rect(aRect);
rect.Inflate(strokeWidth);
const nscoord a2d = aFrame->PresContext()->AppUnitsPerDevPixel();
nscoord cssOffset = aFrame->StyleOutline()->mOutlineOffset.ToAppUnits();
nscoord cssRadii[8] = {0};
if (!aFrame->GetBorderRadii(cssRadii)) {
const auto twoPixels = 2 * AppUnitsPerCSSPixel();
const nscoord radius =
cssOffset >= 0 ? twoPixels : std::max(twoPixels + cssOffset, 0);
cssOffset = -twoPixels;
for (auto& r : cssRadii) {
r = radius;
}
}
auto offset = LayoutDeviceCoord::FromAppUnits(cssOffset, a2d);
RectCornerRadii innerRadii;
nsCSSRendering::ComputePixelRadii(cssRadii, a2d, &innerRadii);
// NOTE(emilio): This doesn't use PaintRoundedRectWithRadius because we need
// to support arbitrary radii.
auto DrawRect = [&](const sRGBColor& aColor) {
RectCornerRadii outerRadii;
if constexpr (std::is_same_v<PaintBackendData, WebRenderBackendData>) {
const Float widths[4] = {strokeWidth + offset, strokeWidth + offset,
strokeWidth + offset, strokeWidth + offset};
nsCSSBorderRenderer::ComputeOuterRadii(innerRadii, widths, &outerRadii);
const auto dest = wr::ToLayoutRect(rect);
const auto side =
wr::ToBorderSide(ToDeviceColor(aColor), StyleBorderStyle::Solid);
const wr::BorderSide sides[4] = {side, side, side, side};
const bool kBackfaceIsVisible = true;
const auto wrWidths = wr::ToBorderWidths(strokeWidth, strokeWidth,
strokeWidth, strokeWidth);
const auto wrRadius = wr::ToBorderRadius(outerRadii);
aPaintData.mBuilder.PushBorder(dest, dest, kBackfaceIsVisible, wrWidths,
{sides, 4}, wrRadius);
} else {
const LayoutDeviceCoord halfWidth = strokeWidth * 0.5f;
const Float widths[4] = {halfWidth + offset, halfWidth + offset,
halfWidth + offset, halfWidth + offset};
nsCSSBorderRenderer::ComputeOuterRadii(innerRadii, widths, &outerRadii);
LayoutDeviceRect dest(rect);
dest.Deflate(halfWidth);
RefPtr<Path> path =
MakePathForRoundedRect(aPaintData, dest.ToUnknownRect(), outerRadii);
aPaintData.Stroke(path, ColorPattern(ToDeviceColor(aColor)),
StrokeOptions(strokeWidth));
}
};
DrawRect(accentColor.Get());
if (solid) {
return;
}
offset += strokeWidth;
strokeWidth =
LayoutDeviceCoord(ThemeDrawing::SnapBorderWidth(1.0f, aDpiRatio));
rect.Inflate(strokeWidth);
DrawRect(accentColor.GetForeground());
}
LayoutDeviceIntMargin Theme::GetWidgetBorder(nsDeviceContext* aContext,
nsIFrame* aFrame,
StyleAppearance aAppearance) {
switch (aAppearance) {
case StyleAppearance::Textfield:
case StyleAppearance::Textarea:
case StyleAppearance::NumberInput:
case StyleAppearance::Listbox:
case StyleAppearance::Menulist:
case StyleAppearance::MenulistButton:
case StyleAppearance::Button:
// Return the border size from the UA sheet, even though what we paint
// doesn't actually match that. We know this is the UA sheet border
// because we disable native theming when different border widths are
// specified by authors, see Theme::IsWidgetStyled.
//
// The Rounded() bit is technically redundant, but needed to appease the
// type system, we should always end up with full device pixels due to
// round_border_to_device_pixels at style time.
return LayoutDeviceIntMargin::FromAppUnits(
aFrame->StyleBorder()->GetComputedBorder(),
aFrame->PresContext()->AppUnitsPerDevPixel())
.Rounded();
case StyleAppearance::Checkbox:
case StyleAppearance::Radio: {
DPIRatio dpiRatio = GetDPIRatio(aFrame, aAppearance);
LayoutDeviceIntCoord w =
ThemeDrawing::SnapBorderWidth(kCheckboxRadioBorderWidth, dpiRatio);
return LayoutDeviceIntMargin(w, w, w, w);
}
default:
return LayoutDeviceIntMargin();
}
}
bool Theme::GetWidgetPadding(nsDeviceContext* aContext, nsIFrame* aFrame,
StyleAppearance aAppearance,
LayoutDeviceIntMargin* aResult) {
switch (aAppearance) {
// Radios and checkboxes return a fixed size in GetMinimumWidgetSize
// and have a meaningful baseline, so they can't have
// author-specified padding.
case StyleAppearance::Radio:
case StyleAppearance::Checkbox:
aResult->SizeTo(0, 0, 0, 0);
return true;
default:
break;
}
return false;
}
bool Theme::GetWidgetOverflow(nsDeviceContext* aContext, nsIFrame* aFrame,
StyleAppearance aAppearance,
nsRect* aOverflowRect) {
CSSIntMargin overflow;
switch (aAppearance) {
case StyleAppearance::FocusOutline: {
// 2px * one segment, or 2px + 1px
const auto width =
StaticPrefs::widget_non_native_theme_solid_outline_style() ? 2 : 3;
overflow.SizeTo(width, width, width, width);
break;
}
case StyleAppearance::Radio:
case StyleAppearance::Checkbox:
case StyleAppearance::Range:
// 2px for each outline segment, plus 1px separation, plus we paint with a
// 1px extra offset, so 6px.
overflow.SizeTo(6, 6, 6, 6);
break;
case StyleAppearance::Textarea:
case StyleAppearance::Textfield:
case StyleAppearance::NumberInput:
case StyleAppearance::Listbox:
case StyleAppearance::MenulistButton:
case StyleAppearance::Menulist:
case StyleAppearance::Button:
// 2px for each segment, plus 1px separation, but we paint 1px inside
// the border area so 4px overflow.
overflow.SizeTo(4, 4, 4, 4);
break;
default:
return false;
}
// TODO: This should convert from device pixels to app units, not from CSS
// pixels. And it should take the dpi ratio into account.
// Using CSS pixels can cause the overflow to be too small if the page is
// zoomed out.
aOverflowRect->Inflate(CSSPixel::ToAppUnits(overflow));
return true;
}
auto Theme::GetScrollbarSizes(nsPresContext* aPresContext,
StyleScrollbarWidth aWidth, Overlay aOverlay)
-> ScrollbarSizes {
return GetScrollbarDrawing().GetScrollbarSizes(aPresContext, aWidth,
aOverlay);
}
nscoord Theme::GetCheckboxRadioPrefSize() {
return CSSPixel::ToAppUnits(kCheckboxRadioContentBoxSize);
}
/* static */
UniquePtr<ScrollbarDrawing> Theme::ScrollbarStyle() {
switch (StaticPrefs::widget_non_native_theme_scrollbar_style()) {
case 1:
return MakeUnique<ScrollbarDrawingCocoa>();
case 2:
return MakeUnique<ScrollbarDrawingGTK>();
case 3:
return MakeUnique<ScrollbarDrawingAndroid>();
case 4:
return MakeUnique<ScrollbarDrawingWin>();
case 5:
return MakeUnique<ScrollbarDrawingWin11>();
default:
break;
}
// Default to native scrollbar style for each platform.
#ifdef XP_WIN
if (IsWin11OrLater()) {
return MakeUnique<ScrollbarDrawingWin11>();
}
return MakeUnique<ScrollbarDrawingWin>();
#elif MOZ_WIDGET_COCOA
return MakeUnique<ScrollbarDrawingCocoa>();
#elif MOZ_WIDGET_GTK
return MakeUnique<ScrollbarDrawingGTK>();
#elif ANDROID
return MakeUnique<ScrollbarDrawingAndroid>();
#else
# error "Unknown platform, need scrollbar implementation."
#endif
}
LayoutDeviceIntSize Theme::GetMinimumWidgetSize(nsPresContext* aPresContext,
nsIFrame* aFrame,
StyleAppearance aAppearance) {
DPIRatio dpiRatio = GetDPIRatio(aFrame, aAppearance);
if (IsWidgetScrollbarPart(aAppearance)) {
return GetScrollbarDrawing().GetMinimumWidgetSize(aPresContext, aAppearance,
aFrame);
}
LayoutDeviceIntSize result;
switch (aAppearance) {
case StyleAppearance::RangeThumb:
result.SizeTo((kMinimumRangeThumbSize * dpiRatio).Rounded(),
(kMinimumRangeThumbSize * dpiRatio).Rounded());
break;
case StyleAppearance::MozMenulistArrowButton:
result.width = (kMinimumDropdownArrowButtonWidth * dpiRatio).Rounded();
break;
case StyleAppearance::SpinnerUpbutton:
case StyleAppearance::SpinnerDownbutton:
result.width = (kMinimumSpinnerButtonWidth * dpiRatio).Rounded();
result.height = (kMinimumSpinnerButtonHeight * dpiRatio).Rounded();
break;
default:
break;
}
return result;
}
nsITheme::Transparency Theme::GetWidgetTransparency(
nsIFrame* aFrame, StyleAppearance aAppearance) {
return GetScrollbarDrawing()
.GetScrollbarPartTransparency(aFrame, aAppearance)
.valueOr(eUnknownTransparency);
}
NS_IMETHODIMP
Theme::WidgetStateChanged(nsIFrame* aFrame, StyleAppearance aAppearance,
nsAtom* aAttribute, bool* aShouldRepaint,
const nsAttrValue* aOldValue) {
if (!aAttribute) {
// Hover/focus/active changed. Always repaint.
*aShouldRepaint = true;
} else {
// Check the attribute to see if it's relevant.
// disabled, checked, dlgtype, default, etc.
*aShouldRepaint = false;
if (aAttribute == nsGkAtoms::disabled || aAttribute == nsGkAtoms::checked ||
aAttribute == nsGkAtoms::selected ||
aAttribute == nsGkAtoms::visuallyselected ||
aAttribute == nsGkAtoms::menuactive ||
aAttribute == nsGkAtoms::sortDirection ||
aAttribute == nsGkAtoms::focused || aAttribute == nsGkAtoms::_default ||
aAttribute == nsGkAtoms::open || aAttribute == nsGkAtoms::hover) {
*aShouldRepaint = true;
}
}
return NS_OK;
}
NS_IMETHODIMP
Theme::ThemeChanged() { return NS_OK; }
bool Theme::WidgetAppearanceDependsOnWindowFocus(StyleAppearance aAppearance) {
return IsWidgetScrollbarPart(aAppearance);
}
nsITheme::ThemeGeometryType Theme::ThemeGeometryTypeForWidget(
nsIFrame* aFrame, StyleAppearance aAppearance) {
return eThemeGeometryTypeUnknown;
}
bool Theme::ThemeSupportsWidget(nsPresContext* aPresContext, nsIFrame* aFrame,
StyleAppearance aAppearance) {
switch (aAppearance) {
case StyleAppearance::Radio:
case StyleAppearance::Checkbox:
case StyleAppearance::FocusOutline:
case StyleAppearance::Textarea:
case StyleAppearance::Textfield:
case StyleAppearance::Range:
case StyleAppearance::RangeThumb:
case StyleAppearance::ProgressBar:
case StyleAppearance::Progresschunk:
case StyleAppearance::Meter:
case StyleAppearance::Meterchunk:
case StyleAppearance::ScrollbarbuttonUp:
case StyleAppearance::ScrollbarbuttonDown:
case StyleAppearance::ScrollbarbuttonLeft:
case StyleAppearance::ScrollbarbuttonRight:
case StyleAppearance::ScrollbarthumbHorizontal:
case StyleAppearance::ScrollbarthumbVertical:
case StyleAppearance::ScrollbartrackHorizontal:
case StyleAppearance::ScrollbartrackVertical:
case StyleAppearance::ScrollbarHorizontal:
case StyleAppearance::ScrollbarVertical:
case StyleAppearance::Scrollcorner:
case StyleAppearance::Button:
case StyleAppearance::Listbox:
case StyleAppearance::Menulist:
case StyleAppearance::MenulistButton:
case StyleAppearance::NumberInput:
case StyleAppearance::MozMenulistArrowButton:
case StyleAppearance::SpinnerUpbutton:
case StyleAppearance::SpinnerDownbutton:
case StyleAppearance::Menuitem:
case StyleAppearance::Tooltip:
return !IsWidgetStyled(aPresContext, aFrame, aAppearance);
default:
return false;
}
}
bool Theme::WidgetIsContainer(StyleAppearance aAppearance) {
switch (aAppearance) {
case StyleAppearance::MozMenulistArrowButton:
case StyleAppearance::Radio:
case StyleAppearance::Checkbox:
return false;
default:
return true;
}
}
bool Theme::ThemeDrawsFocusForWidget(nsIFrame*, StyleAppearance) {
return true;
}
bool Theme::ThemeNeedsComboboxDropmarker() { return true; }
bool Theme::ThemeSupportsScrollbarButtons() {
return GetScrollbarDrawing().ShouldDrawScrollbarButtons();
}
} // namespace mozilla::widget