diff options
Diffstat (limited to 'widget/Theme.cpp')
-rw-r--r-- | widget/Theme.cpp | 1686 |
1 files changed, 1686 insertions, 0 deletions
diff --git a/widget/Theme.cpp b/widget/Theme.cpp new file mode 100644 index 0000000000..5915f2fcaf --- /dev/null +++ b/widget/Theme.cpp @@ -0,0 +1,1686 @@ +/* -*- 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 <utility> +#include "ThemeCocoa.h" + +#include "ThemeDrawing.h" +#include "Units.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()); + } +} + +auto Theme::GetDPIRatio(nsPresContext* aPc, StyleAppearance aAppearance) + -> DPIRatio { + // Widgets react to zoom, except scrollbars. + if (IsWidgetScrollbarPart(aAppearance)) { + return GetScrollbarDrawing().GetDPIRatioForScrollbarPart(aPc); + } + return DPIRatio(float(AppUnitsPerCSSPixel()) / aPc->AppUnitsPerDevPixel()); +} + +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; + } + + auto tickMarks = rangeFrame->TickMarks(); + 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; + const LayoutDeviceCoord tickMarkWidth( + ThemeDrawing::SnapBorderWidth(1.0f, aDpiRatio)); + const LayoutDeviceCoord tickMarkHeight( + ThemeDrawing::SnapBorderWidth(5.0f, aDpiRatio)); + LayoutDevicePoint tickMarkOrigin, tickMarkDirection; + LayoutDeviceSize tickMarkSize; + if (aHorizontal) { + rect.height = verticalSize; + rect.y = aRect.y + (aRect.height - rect.height) / 2; + tickMarkSize = LayoutDeviceSize(tickMarkWidth, tickMarkHeight); + thumbRect.y = aRect.y + (aRect.height - thumbRect.height) / 2; + + if (IsFrameRTL(aFrame)) { + tickMarkOrigin = + LayoutDevicePoint(aRect.XMost() - thumbRect.width / 2, aRect.YMost()); + tickMarkDirection = LayoutDevicePoint(-1.0f, 0.0f); + 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 { + tickMarkOrigin = + LayoutDevicePoint(aRect.x + thumbRect.width / 2, aRect.YMost()); + tickMarkDirection = LayoutDevicePoint(1.0, 0.0f); + 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; + tickMarkOrigin = LayoutDevicePoint(aRect.XMost() - tickMarkHeight / 4, + aRect.YMost() - thumbRect.width / 2); + tickMarkDirection = LayoutDevicePoint(0.0f, -1.0f); + tickMarkSize = LayoutDeviceSize(tickMarkHeight, tickMarkWidth); + 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); + auto tickMarkColor = trackBorderColor; + + 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); + } + + tickMarkDirection.x *= aRect.width - thumbRect.width; + tickMarkDirection.y *= aRect.height - thumbRect.height; + tickMarkOrigin -= + LayoutDevicePoint(tickMarkSize.width, tickMarkSize.height) / 2; + auto tickMarkRect = LayoutDeviceRect(tickMarkOrigin, tickMarkSize); + for (auto tickMark : tickMarks) { + auto tickMarkOffset = + tickMarkDirection * + float(rangeFrame->GetDoubleAsFractionOfRange(tickMark)); + ThemeDrawing::FillRect(aPaintData, tickMarkRect + tickMarkOffset, + tickMarkColor); + } + + // 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) { + // HTML select and XUL menulist dropdown buttons get state from the + // parent. + nsIFrame* parentFrame = aFrame->GetParent(); + 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 = LayoutDevicePixel::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; +} + +LayoutDeviceIntCoord Theme::GetScrollbarSize(const nsPresContext* aPresContext, + StyleScrollbarWidth aWidth, + Overlay aOverlay) { + return GetScrollbarDrawing().GetScrollbarSize(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) { + if (auto scrollbar = GetScrollbarDrawing().GetScrollbarPartTransparency( + aFrame, aAppearance)) { + return *scrollbar; + } + if (aAppearance == StyleAppearance::Tooltip) { + // We draw a rounded rect, so we need transparency. + return eTransparent; + } + return 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 |