diff options
Diffstat (limited to 'widget/nsNativeBasicTheme.cpp')
-rw-r--r-- | widget/nsNativeBasicTheme.cpp | 1599 |
1 files changed, 1599 insertions, 0 deletions
diff --git a/widget/nsNativeBasicTheme.cpp b/widget/nsNativeBasicTheme.cpp new file mode 100644 index 0000000000..60083ae5f7 --- /dev/null +++ b/widget/nsNativeBasicTheme.cpp @@ -0,0 +1,1599 @@ +/* -*- 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 "nsNativeBasicTheme.h" + +#include "gfxBlur.h" +#include "mozilla/dom/Document.h" +#include "mozilla/gfx/Rect.h" +#include "mozilla/gfx/Types.h" +#include "mozilla/gfx/Filters.h" +#include "nsCSSColorUtils.h" +#include "nsCSSRendering.h" +#include "nsLayoutUtils.h" +#include "PathHelpers.h" + +using namespace mozilla; +using namespace mozilla::widget; +using namespace mozilla::gfx; + +NS_IMPL_ISUPPORTS_INHERITED(nsNativeBasicTheme, nsNativeTheme, nsITheme) + +namespace { + +// 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; +}; + +} // namespace + +static bool IsScrollbarWidthThin(nsIFrame* aFrame) { + ComputedStyle* style = nsLayoutUtils::StyleForScrollbar(aFrame); + auto scrollbarWidth = style->StyleUIReset()->mScrollbarWidth; + return scrollbarWidth == StyleScrollbarWidth::Thin; +} + +/* static */ +auto nsNativeBasicTheme::GetDPIRatio(nsIFrame* aFrame) -> DPIRatio { + return DPIRatio(float(AppUnitsPerCSSPixel()) / + aFrame->PresContext() + ->DeviceContext() + ->AppUnitsPerDevPixelAtUnitFullZoom()); +} + +/* static */ +bool nsNativeBasicTheme::IsDateTimeResetButton(nsIFrame* aFrame) { + if (!aFrame) { + return false; + } + + nsIFrame* parent = aFrame->GetParent(); + if (parent && (parent = parent->GetParent()) && + (parent = parent->GetParent())) { + nsDateTimeControlFrame* dateTimeFrame = do_QueryFrame(parent); + if (dateTimeFrame) { + return true; + } + } + return false; +} + +/* static */ +bool nsNativeBasicTheme::IsDateTimeTextField(nsIFrame* aFrame) { + nsDateTimeControlFrame* dateTimeFrame = do_QueryFrame(aFrame); + return dateTimeFrame; +} + +/* static */ +bool nsNativeBasicTheme::IsColorPickerButton(nsIFrame* aFrame) { + nsColorControlFrame* colorPickerButton = do_QueryFrame(aFrame); + return colorPickerButton; +} + +/* static */ +LayoutDeviceRect nsNativeBasicTheme::FixAspectRatio( + const LayoutDeviceRect& aRect) { + // Checkbox and radio need to preserve aspect-ratio for compat. + LayoutDeviceRect rect(aRect); + if (rect.width == rect.height) { + return rect; + } + + if (rect.width > rect.height) { + auto diff = rect.width - rect.height; + rect.width = rect.height; + rect.x += diff / 2; + } else { + auto diff = rect.height - rect.width; + rect.height = rect.width; + rect.y += diff / 2; + } + + return rect; +} + +std::pair<sRGBColor, sRGBColor> nsNativeBasicTheme::ComputeCheckboxColors( + const EventStates& aState, StyleAppearance aAppearance) { + MOZ_ASSERT(aAppearance == StyleAppearance::Checkbox || + aAppearance == StyleAppearance::Radio); + + bool isDisabled = aState.HasState(NS_EVENT_STATE_DISABLED); + bool isPressed = !isDisabled && aState.HasAllStates(NS_EVENT_STATE_HOVER | + NS_EVENT_STATE_ACTIVE); + bool isHovered = !isDisabled && aState.HasState(NS_EVENT_STATE_HOVER); + bool isChecked = aState.HasState(NS_EVENT_STATE_CHECKED); + bool isIndeterminate = aAppearance == StyleAppearance::Checkbox && + aState.HasState(NS_EVENT_STATE_INDETERMINATE); + + sRGBColor backgroundColor = sColorWhite; + sRGBColor borderColor = sColorGrey40; + if (isDisabled) { + if (isChecked || isIndeterminate) { + backgroundColor = borderColor = sColorGrey40Alpha50; + } else { + backgroundColor = sColorWhiteAlpha50; + borderColor = sColorGrey40Alpha50; + } + } else { + if (isChecked || isIndeterminate) { + if (isPressed) { + backgroundColor = borderColor = sColorAccentDarker; + } else if (isHovered) { + backgroundColor = borderColor = sColorAccentDark; + } else { + backgroundColor = borderColor = sColorAccent; + } + } else if (isPressed) { + backgroundColor = sColorGrey20; + borderColor = sColorGrey60; + } else if (isHovered) { + backgroundColor = sColorWhite; + borderColor = sColorGrey50; + } else { + backgroundColor = sColorWhite; + borderColor = sColorGrey40; + } + } + + return std::make_pair(backgroundColor, borderColor); +} + +sRGBColor nsNativeBasicTheme::ComputeCheckmarkColor(const EventStates& aState) { + bool isDisabled = aState.HasState(NS_EVENT_STATE_DISABLED); + return isDisabled ? sColorWhiteAlpha50 : sColorWhite; +} + +std::pair<sRGBColor, sRGBColor> nsNativeBasicTheme::ComputeRadioCheckmarkColors( + const EventStates& aState) { + auto [unusedColor, checkColor] = + ComputeCheckboxColors(aState, StyleAppearance::Radio); + Unused << unusedColor; + + return std::make_pair(sColorWhite, checkColor); +} + +sRGBColor nsNativeBasicTheme::ComputeBorderColor(const EventStates& aState) { + bool isDisabled = aState.HasState(NS_EVENT_STATE_DISABLED); + bool isActive = + aState.HasAllStates(NS_EVENT_STATE_HOVER | NS_EVENT_STATE_ACTIVE); + bool isHovered = !isDisabled && aState.HasState(NS_EVENT_STATE_HOVER); + bool isFocused = aState.HasState(NS_EVENT_STATE_FOCUSRING); + if (isDisabled) { + return sColorGrey40Alpha50; + } + if (isFocused) { + return sColorAccent; + } + if (isActive) { + return sColorGrey60; + } + if (isHovered) { + return sColorGrey50; + } + return sColorGrey40; +} + +std::pair<sRGBColor, sRGBColor> nsNativeBasicTheme::ComputeButtonColors( + const EventStates& aState, nsIFrame* aFrame) { + bool isActive = + aState.HasAllStates(NS_EVENT_STATE_HOVER | NS_EVENT_STATE_ACTIVE); + bool isDisabled = aState.HasState(NS_EVENT_STATE_DISABLED); + bool isHovered = !isDisabled && aState.HasState(NS_EVENT_STATE_HOVER); + + const sRGBColor& backgroundColor = [&] { + if (isDisabled) { + return sColorGrey10Alpha50; + } + if (IsDateTimeResetButton(aFrame)) { + return sColorWhite; + } + if (isActive) { + return sColorGrey30; + } + if (isHovered) { + return sColorGrey20; + } + return sColorGrey10; + }(); + + const sRGBColor borderColor = ComputeBorderColor(aState); + return std::make_pair(backgroundColor, borderColor); +} + +std::pair<sRGBColor, sRGBColor> nsNativeBasicTheme::ComputeTextfieldColors( + const EventStates& aState) { + bool isDisabled = aState.HasState(NS_EVENT_STATE_DISABLED); + const sRGBColor& backgroundColor = + isDisabled ? sColorWhiteAlpha50 : sColorWhite; + const sRGBColor borderColor = ComputeBorderColor(aState); + + return std::make_pair(backgroundColor, borderColor); +} + +std::pair<sRGBColor, sRGBColor> nsNativeBasicTheme::ComputeRangeProgressColors( + const EventStates& aState) { + bool isActive = + aState.HasAllStates(NS_EVENT_STATE_HOVER | NS_EVENT_STATE_ACTIVE); + bool isDisabled = aState.HasState(NS_EVENT_STATE_DISABLED); + bool isHovered = !isDisabled && aState.HasState(NS_EVENT_STATE_HOVER); + + if (isDisabled) { + return std::make_pair(sColorGrey40Alpha50, sColorGrey40Alpha50); + } + if (isActive || isHovered) { + return std::make_pair(sColorAccentDark, sColorAccentDarker); + } + return std::make_pair(sColorAccent, sColorAccentDark); +} + +std::pair<sRGBColor, sRGBColor> nsNativeBasicTheme::ComputeRangeTrackColors( + const EventStates& aState) { + bool isActive = + aState.HasAllStates(NS_EVENT_STATE_HOVER | NS_EVENT_STATE_ACTIVE); + bool isDisabled = aState.HasState(NS_EVENT_STATE_DISABLED); + bool isHovered = !isDisabled && aState.HasState(NS_EVENT_STATE_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> nsNativeBasicTheme::ComputeRangeThumbColors( + const EventStates& aState) { + bool isActive = + aState.HasAllStates(NS_EVENT_STATE_HOVER | NS_EVENT_STATE_ACTIVE); + bool isDisabled = aState.HasState(NS_EVENT_STATE_DISABLED); + bool isHovered = !isDisabled && aState.HasState(NS_EVENT_STATE_HOVER); + + const sRGBColor& backgroundColor = [&] { + if (isDisabled) { + return sColorGrey50Alpha50; + } + if (isActive) { + return sColorAccent; + } + if (isHovered) { + return sColorGrey60; + } + return sColorGrey50; + }(); + + const sRGBColor borderColor = sColorWhite; + + return std::make_pair(backgroundColor, borderColor); +} + +std::pair<sRGBColor, sRGBColor> nsNativeBasicTheme::ComputeProgressColors() { + return std::make_pair(sColorAccent, sColorAccentDark); +} + +std::pair<sRGBColor, sRGBColor> +nsNativeBasicTheme::ComputeProgressTrackColors() { + return std::make_pair(sColorGrey10, sColorGrey40); +} + +std::pair<sRGBColor, sRGBColor> nsNativeBasicTheme::ComputeMeterchunkColors( + const EventStates& aMeterState) { + sRGBColor borderColor = sColorMeterGreen20; + sRGBColor chunkColor = sColorMeterGreen10; + + if (aMeterState.HasState(NS_EVENT_STATE_SUB_OPTIMUM)) { + borderColor = sColorMeterYellow20; + chunkColor = sColorMeterYellow10; + } else if (aMeterState.HasState(NS_EVENT_STATE_SUB_SUB_OPTIMUM)) { + borderColor = sColorMeterRed20; + chunkColor = sColorMeterRed10; + } + + return std::make_pair(chunkColor, borderColor); +} + +std::pair<sRGBColor, sRGBColor> nsNativeBasicTheme::ComputeMeterTrackColors() { + return std::make_pair(sColorGrey10, sColorGrey40); +} + +sRGBColor nsNativeBasicTheme::ComputeMenulistArrowButtonColor( + const EventStates& aState) { + bool isDisabled = aState.HasState(NS_EVENT_STATE_DISABLED); + return isDisabled ? sColorGrey60Alpha50 : sColorGrey60; +} + +std::array<sRGBColor, 3> nsNativeBasicTheme::ComputeFocusRectColors() { + return {sColorAccent, sColorWhiteAlpha80, sColorAccentLight}; +} + +std::pair<sRGBColor, sRGBColor> nsNativeBasicTheme::ComputeScrollbarColors( + nsIFrame* aFrame, const ComputedStyle& aStyle, + const EventStates& aDocumentState, bool aIsRoot) { + const nsStyleUI* ui = aStyle.StyleUI(); + nscolor color; + if (ui->mScrollbarColor.IsColors()) { + color = ui->mScrollbarColor.AsColors().track.CalcColor(aStyle); + } else if (aDocumentState.HasAllStates(NS_DOCUMENT_STATE_WINDOW_INACTIVE)) { + color = LookAndFeel::GetColor(LookAndFeel::ColorID::ThemedScrollbarInactive, + sScrollbarColor.ToABGR()); + } else { + color = LookAndFeel::GetColor(LookAndFeel::ColorID::ThemedScrollbar, + sScrollbarColor.ToABGR()); + } + if (aIsRoot) { + // Root scrollbars must be opaque. + nscolor bg = LookAndFeel::GetColor(LookAndFeel::ColorID::WindowBackground, + NS_RGB(0xff, 0xff, 0xff)); + color = NS_ComposeColors(bg, color); + } + return std::make_pair(gfx::sRGBColor::FromABGR(color), sScrollbarBorderColor); +} + +sRGBColor nsNativeBasicTheme::ComputeScrollbarThumbColor( + nsIFrame* aFrame, const ComputedStyle& aStyle, + const EventStates& aElementState, const EventStates& aDocumentState) { + const nsStyleUI* ui = aStyle.StyleUI(); + nscolor color; + if (ui->mScrollbarColor.IsColors()) { + color = ui->mScrollbarColor.AsColors().thumb.CalcColor(aStyle); + } else if (aDocumentState.HasAllStates(NS_DOCUMENT_STATE_WINDOW_INACTIVE)) { + color = LookAndFeel::GetColor( + LookAndFeel::ColorID::ThemedScrollbarThumbInactive, + sScrollbarThumbColor.ToABGR()); + } else if (aElementState.HasAllStates(NS_EVENT_STATE_ACTIVE)) { + color = + LookAndFeel::GetColor(LookAndFeel::ColorID::ThemedScrollbarThumbActive, + sScrollbarThumbColorActive.ToABGR()); + } else if (aElementState.HasAllStates(NS_EVENT_STATE_HOVER)) { + color = + LookAndFeel::GetColor(LookAndFeel::ColorID::ThemedScrollbarThumbHover, + sScrollbarThumbColorHover.ToABGR()); + } else { + color = LookAndFeel::GetColor(LookAndFeel::ColorID::ThemedScrollbarThumb, + sScrollbarThumbColor.ToABGR()); + } + return gfx::sRGBColor::FromABGR(color); +} + +std::array<sRGBColor, 3> nsNativeBasicTheme::ComputeScrollbarButtonColors( + nsIFrame* aFrame, StyleAppearance aAppearance, const ComputedStyle& aStyle, + const EventStates& aElementState, const EventStates& aDocumentState) { + bool isActive = aElementState.HasState(NS_EVENT_STATE_ACTIVE); + bool isHovered = aElementState.HasState(NS_EVENT_STATE_HOVER); + + bool hasCustomColor = aStyle.StyleUI()->mScrollbarColor.IsColors(); + sRGBColor buttonColor; + if (hasCustomColor) { + // When scrollbar-color is in use, use the thumb color for the button. + buttonColor = ComputeScrollbarThumbColor(aFrame, aStyle, aElementState, + aDocumentState); + } else if (isActive) { + buttonColor = sScrollbarButtonActiveColor; + } else if (!hasCustomColor && isHovered) { + buttonColor = sScrollbarButtonHoverColor; + } else { + buttonColor = sScrollbarColor; + } + + sRGBColor arrowColor; + if (hasCustomColor) { + // When scrollbar-color is in use, derive the arrow color from the button + // color. + nscolor bg = buttonColor.ToABGR(); + bool darken = NS_GetLuminosity(bg) >= NS_MAX_LUMINOSITY / 2; + if (isActive) { + float c = darken ? 0.0f : 1.0f; + arrowColor = sRGBColor(c, c, c); + } else { + uint8_t c = darken ? 0 : 255; + arrowColor = + sRGBColor::FromABGR(NS_ComposeColors(bg, NS_RGBA(c, c, c, 160))); + } + } else if (isActive) { + arrowColor = sScrollbarArrowColorActive; + } else if (isHovered) { + arrowColor = sScrollbarArrowColorHover; + } else { + arrowColor = sScrollbarArrowColor; + } + + return {buttonColor, arrowColor, sScrollbarBorderColor}; +} + +static already_AddRefed<Path> GetFocusStrokePath( + DrawTarget* aDrawTarget, LayoutDeviceRect& aFocusRect, + LayoutDeviceCoord aOffset, const LayoutDeviceCoord aRadius, + LayoutDeviceCoord aFocusWidth) { + RectCornerRadii radii(aRadius, aRadius, aRadius, aRadius); + aFocusRect.Inflate(aOffset); + + LayoutDeviceRect focusRect(aFocusRect); + // Deflate the rect by half the border width, so that the middle of the stroke + // fills exactly the area we want to fill and not more. + focusRect.Deflate(aFocusWidth * 0.5f); + + return MakePathForRoundedRect(*aDrawTarget, focusRect.ToUnknownRect(), radii); +} + +void nsNativeBasicTheme::PaintRoundedFocusRect(DrawTarget* aDrawTarget, + const LayoutDeviceRect& aRect, + 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(); + + 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. + LayoutDeviceCoord offset = aOffset * aDpiRatio; + LayoutDeviceCoord strokeWidth = CSSCoord(2.0f) * aDpiRatio; + focusRect.Inflate(strokeWidth); + + LayoutDeviceCoord strokeRadius = aRadius * aDpiRatio; + RefPtr<Path> roundedRect = GetFocusStrokePath(aDrawTarget, focusRect, offset, + strokeRadius, strokeWidth); + aDrawTarget->Stroke(roundedRect, ColorPattern(ToDeviceColor(innerColor)), + StrokeOptions(strokeWidth)); + + offset = CSSCoord(1.0f) * aDpiRatio; + strokeRadius += offset; + strokeWidth = CSSCoord(1.0f) * aDpiRatio; + roundedRect = GetFocusStrokePath(aDrawTarget, focusRect, offset, strokeRadius, + strokeWidth); + aDrawTarget->Stroke(roundedRect, ColorPattern(ToDeviceColor(middleColor)), + StrokeOptions(strokeWidth)); + + offset = CSSCoord(2.0f) * aDpiRatio; + strokeRadius += offset; + strokeWidth = CSSCoord(2.0f) * aDpiRatio; + roundedRect = GetFocusStrokePath(aDrawTarget, focusRect, offset, strokeRadius, + strokeWidth); + aDrawTarget->Stroke(roundedRect, ColorPattern(ToDeviceColor(outerColor)), + StrokeOptions(strokeWidth)); +} + +void nsNativeBasicTheme::PaintRoundedRect(DrawTarget* aDrawTarget, + const LayoutDeviceRect& aRect, + const sRGBColor& aBackgroundColor, + const sRGBColor& aBorderColor, + CSSCoord aBorderWidth, + RectCornerRadii aDpiAdjustedRadii, + DPIRatio aDpiRatio) { + const LayoutDeviceCoord borderWidth(aBorderWidth * aDpiRatio); + + LayoutDeviceRect rect(aRect); + // Deflate the rect by half the border width, so that the middle of the stroke + // fills exactly the area we want to fill and not more. + rect.Deflate(borderWidth * 0.5f); + + RefPtr<Path> roundedRect = MakePathForRoundedRect( + *aDrawTarget, rect.ToUnknownRect(), aDpiAdjustedRadii); + + aDrawTarget->Fill(roundedRect, ColorPattern(ToDeviceColor(aBackgroundColor))); + aDrawTarget->Stroke(roundedRect, ColorPattern(ToDeviceColor(aBorderColor)), + StrokeOptions(borderWidth)); +} + +void nsNativeBasicTheme::PaintRoundedRectWithRadius( + DrawTarget* aDrawTarget, const LayoutDeviceRect& aRect, + const sRGBColor& aBackgroundColor, const sRGBColor& aBorderColor, + CSSCoord aBorderWidth, CSSCoord aRadius, DPIRatio aDpiRatio) { + const LayoutDeviceCoord radius(aRadius * aDpiRatio); + RectCornerRadii radii(radius, radius, radius, radius); + PaintRoundedRect(aDrawTarget, aRect, aBackgroundColor, aBorderColor, + aBorderWidth, radii, aDpiRatio); +} + +void nsNativeBasicTheme::PaintCheckboxControl(DrawTarget* aDrawTarget, + const LayoutDeviceRect& aRect, + const EventStates& aState, + DPIRatio aDpiRatio) { + const CSSCoord borderWidth = 2.0f; + const CSSCoord radius = 2.0f; + auto [backgroundColor, borderColor] = + ComputeCheckboxColors(aState, StyleAppearance::Checkbox); + PaintRoundedRectWithRadius(aDrawTarget, aRect, backgroundColor, borderColor, + borderWidth, radius, aDpiRatio); + + if (aState.HasState(NS_EVENT_STATE_FOCUSRING)) { + PaintRoundedFocusRect(aDrawTarget, aRect, aDpiRatio, 5.0f, 1.0f); + } +} + +// Returns the right scale to cover aRect in the smaller dimension. +static float ScaleToWidgetRect(const LayoutDeviceRect& aRect) { + return std::min(aRect.width, aRect.height) / kMinimumWidgetSize; +} + +void nsNativeBasicTheme::PaintCheckMark(DrawTarget* aDrawTarget, + const LayoutDeviceRect& aRect, + const EventStates& aState) { + // Points come from the coordinates on a 14X14 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 = ScaleToWidgetRect(aRect); + 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(); + + sRGBColor fillColor = ComputeCheckmarkColor(aState); + aDrawTarget->Fill(path, ColorPattern(ToDeviceColor(fillColor))); +} + +void nsNativeBasicTheme::PaintIndeterminateMark(DrawTarget* aDrawTarget, + const LayoutDeviceRect& aRect, + const EventStates& aState) { + const CSSCoord borderWidth = 2.0f; + const float scale = ScaleToWidgetRect(aRect); + + 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; + + sRGBColor fillColor = ComputeCheckmarkColor(aState); + aDrawTarget->FillRect(rect, ColorPattern(ToDeviceColor(fillColor))); +} + +void nsNativeBasicTheme::PaintStrokedEllipse(DrawTarget* aDrawTarget, + const LayoutDeviceRect& aRect, + const sRGBColor& aBackgroundColor, + const sRGBColor& aBorderColor, + const CSSCoord aBorderWidth, + DPIRatio aDpiRatio) { + const LayoutDeviceCoord borderWidth(aBorderWidth * aDpiRatio); + RefPtr<PathBuilder> builder = aDrawTarget->CreatePathBuilder(); + + // Deflate for the same reason as PaintRoundedRectWithRadius. Note that the + // size is the diameter, so we just shrink by the border width once. + auto size = aRect.Size() - LayoutDeviceSize(borderWidth, borderWidth); + AppendEllipseToPath(builder, aRect.Center().ToUnknownPoint(), + size.ToUnknownSize()); + RefPtr<Path> ellipse = builder->Finish(); + + aDrawTarget->Fill(ellipse, ColorPattern(ToDeviceColor(aBackgroundColor))); + aDrawTarget->Stroke(ellipse, ColorPattern(ToDeviceColor(aBorderColor)), + StrokeOptions(borderWidth)); +} + +void nsNativeBasicTheme::PaintEllipseShadow(DrawTarget* aDrawTarget, + const LayoutDeviceRect& aRect, + 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 = aRect.ToUnknownRect(); + inflatedRect.Inflate(inflation.width, inflation.height); + Rect sourceRectInFilterSpace = + inflatedRect - aRect.TopLeft().ToUnknownPoint(); + Point destinationPointOfSourceRect = inflatedRect.TopLeft() + offset; + + IntSize dtSize = RoundedToInt(aRect.Size().ToUnknownSize()); + RefPtr<DrawTarget> ellipseDT = aDrawTarget->CreateSimilarDrawTargetForFilter( + dtSize, SurfaceFormat::A8, blurFilter, blurFilter, + sourceRectInFilterSpace, destinationPointOfSourceRect); + if (!ellipseDT) { + return; + } + + RefPtr<Path> ellipse = MakePathForEllipse( + *ellipseDT, (aRect - aRect.TopLeft()).Center().ToUnknownPoint(), + aRect.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); +} + +void nsNativeBasicTheme::PaintRadioControl(DrawTarget* aDrawTarget, + const LayoutDeviceRect& aRect, + const EventStates& aState, + DPIRatio aDpiRatio) { + const CSSCoord borderWidth = 2.0f; + auto [backgroundColor, borderColor] = + ComputeCheckboxColors(aState, StyleAppearance::Radio); + + PaintStrokedEllipse(aDrawTarget, aRect, backgroundColor, borderColor, + borderWidth, aDpiRatio); + + if (aState.HasState(NS_EVENT_STATE_FOCUSRING)) { + PaintRoundedFocusRect(aDrawTarget, aRect, aDpiRatio, 5.0f, 1.0f); + } +} + +void nsNativeBasicTheme::PaintRadioCheckmark(DrawTarget* aDrawTarget, + const LayoutDeviceRect& aRect, + const EventStates& aState, + DPIRatio aDpiRatio) { + const CSSCoord borderWidth = 2.0f; + const float scale = ScaleToWidgetRect(aRect); + auto [backgroundColor, checkColor] = ComputeRadioCheckmarkColors(aState); + + LayoutDeviceRect rect(aRect); + rect.Deflate(borderWidth * scale); + + PaintStrokedEllipse(aDrawTarget, rect, checkColor, backgroundColor, + borderWidth, aDpiRatio); +} + +void nsNativeBasicTheme::PaintTextField(DrawTarget* aDrawTarget, + const LayoutDeviceRect& aRect, + const EventStates& aState, + DPIRatio aDpiRatio) { + auto [backgroundColor, borderColor] = ComputeTextfieldColors(aState); + + const CSSCoord radius = 2.0f; + + PaintRoundedRectWithRadius(aDrawTarget, aRect, backgroundColor, borderColor, + kTextFieldBorderWidth, radius, aDpiRatio); + + if (aState.HasState(NS_EVENT_STATE_FOCUSRING)) { + PaintRoundedFocusRect(aDrawTarget, aRect, aDpiRatio, radius, + -kTextFieldBorderWidth); + } +} + +void nsNativeBasicTheme::PaintListbox(DrawTarget* aDrawTarget, + const LayoutDeviceRect& aRect, + const EventStates& aState, + DPIRatio aDpiRatio) { + const CSSCoord radius = 2.0f; + auto [backgroundColor, borderColor] = ComputeTextfieldColors(aState); + + PaintRoundedRectWithRadius(aDrawTarget, aRect, backgroundColor, borderColor, + kMenulistBorderWidth, radius, aDpiRatio); + + if (aState.HasState(NS_EVENT_STATE_FOCUSRING)) { + PaintRoundedFocusRect(aDrawTarget, aRect, aDpiRatio, radius, + -kMenulistBorderWidth); + } +} + +void nsNativeBasicTheme::PaintMenulist(DrawTarget* aDrawTarget, + const LayoutDeviceRect& aRect, + const EventStates& aState, + DPIRatio aDpiRatio) { + const CSSCoord radius = 4.0f; + auto [backgroundColor, borderColor] = ComputeButtonColors(aState); + + PaintRoundedRectWithRadius(aDrawTarget, aRect, backgroundColor, borderColor, + kMenulistBorderWidth, radius, aDpiRatio); + + if (aState.HasState(NS_EVENT_STATE_FOCUSRING)) { + PaintRoundedFocusRect(aDrawTarget, aRect, aDpiRatio, radius, + -kMenulistBorderWidth); + } +} + +void nsNativeBasicTheme::PaintArrow(DrawTarget* aDrawTarget, + const LayoutDeviceRect& aRect, + const float aArrowPolygonX[], + const float aArrowPolygonY[], + const int32_t aArrowNumPoints, + const sRGBColor aFillColor) { + const float scale = ScaleToWidgetRect(aRect); + + auto center = aRect.Center().ToUnknownPoint(); + + RefPtr<PathBuilder> builder = aDrawTarget->CreatePathBuilder(); + Point p = + center + Point(aArrowPolygonX[0] * scale, aArrowPolygonY[0] * scale); + builder->MoveTo(p); + for (int32_t i = 1; i < aArrowNumPoints; i++) { + p = center + Point(aArrowPolygonX[i] * scale, aArrowPolygonY[i] * scale); + builder->LineTo(p); + } + RefPtr<Path> path = builder->Finish(); + + aDrawTarget->Fill(path, ColorPattern(ToDeviceColor(aFillColor))); +} + +void nsNativeBasicTheme::PaintMenulistArrowButton(nsIFrame* aFrame, + DrawTarget* aDrawTarget, + const LayoutDeviceRect& aRect, + const EventStates& aState) { + const float arrowPolygonX[] = {-3.5f, -0.5f, 0.5f, 3.5f, 3.5f, + 3.0f, 0.5f, -0.5f, -3.0f, -3.5f}; + const float arrowPolygonY[] = {-0.5f, 2.5f, 2.5f, -0.5f, -2.0f, + -2.0f, 1.0f, 1.0f, -2.0f, -2.0f}; + const int32_t arrowNumPoints = ArrayLength(arrowPolygonX); + sRGBColor arrowColor = ComputeMenulistArrowButtonColor(aState); + PaintArrow(aDrawTarget, aRect, arrowPolygonX, arrowPolygonY, arrowNumPoints, + arrowColor); +} + +void nsNativeBasicTheme::PaintSpinnerButton(nsIFrame* aFrame, + DrawTarget* aDrawTarget, + const LayoutDeviceRect& aRect, + const EventStates& aState, + StyleAppearance aAppearance, + DPIRatio aDpiRatio) { + auto [backgroundColor, borderColor] = ComputeButtonColors(aState); + + RefPtr<Path> pathRect = MakePathForRect(*aDrawTarget, aRect.ToUnknownRect()); + + aDrawTarget->Fill(pathRect, ColorPattern(ToDeviceColor(backgroundColor))); + + RefPtr<PathBuilder> builder = aDrawTarget->CreatePathBuilder(); + Point p; + if (IsFrameRTL(aFrame)) { + p = Point(aRect.x + aRect.width - 0.5f, aRect.y); + } else { + p = Point(aRect.x - 0.5f, aRect.y); + } + builder->MoveTo(p); + p = Point(p.x, p.y + aRect.height); + builder->LineTo(p); + RefPtr<Path> path = builder->Finish(); + + aDrawTarget->Stroke(path, ColorPattern(ToDeviceColor(borderColor)), + StrokeOptions(kSpinnerBorderWidth * aDpiRatio)); + const float arrowPolygonX[] = {-5.25f, -0.75f, 0.75f, 5.25f, 5.25f, + 4.5f, 0.75f, -0.75f, -4.5f, -5.25f}; + const float arrowPolygonY[] = {-1.875f, 2.625f, 2.625f, -1.875f, -4.125f, + -4.125f, 0.375f, 0.375f, -4.125f, -4.125f}; + const int32_t arrowNumPoints = ArrayLength(arrowPolygonX); + const float scaleX = ScaleToWidgetRect(aRect); + const float scaleY = + aAppearance == StyleAppearance::SpinnerDownbutton ? scaleX : -scaleX; + + builder = aDrawTarget->CreatePathBuilder(); + auto center = aRect.Center().ToUnknownPoint(); + p = center + Point(arrowPolygonX[0] * scaleX, arrowPolygonY[0] * scaleY); + builder->MoveTo(p); + for (int32_t i = 1; i < arrowNumPoints; i++) { + p = center + Point(arrowPolygonX[i] * scaleX, arrowPolygonY[i] * scaleY); + builder->LineTo(p); + } + path = builder->Finish(); + aDrawTarget->Fill(path, ColorPattern(ToDeviceColor(borderColor))); +} + +void nsNativeBasicTheme::PaintRange(nsIFrame* aFrame, DrawTarget* aDrawTarget, + const LayoutDeviceRect& aRect, + const EventStates& aState, + 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); + Rect overflowRect = aRect.ToUnknownRect(); + overflowRect.Inflate(CSSCoord(6.0f) * aDpiRatio); // See GetWidgetOverflow + Rect progressClipRect(overflowRect); + Rect trackClipRect(overflowRect); + 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(overflowRect.X(), midPoint); + progressClipRect.SetBoxX(midPoint, overflowRect.XMost()); + } else { + thumbRect.x = aRect.x + (aRect.width - thumbRect.width) * progress; + float midPoint = thumbRect.Center().X(); + progressClipRect.SetBoxX(overflowRect.X(), midPoint); + trackClipRect.SetBoxX(midPoint, overflowRect.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) * progress; + float midPoint = thumbRect.Center().Y(); + trackClipRect.SetBoxY(overflowRect.Y(), midPoint); + progressClipRect.SetBoxY(midPoint, overflowRect.YMost()); + } + + const CSSCoord borderWidth = 1.0f; + const CSSCoord radius = 2.0f; + + auto [progressColor, progressBorderColor] = + ComputeRangeProgressColors(aState); + auto [trackColor, trackBorderColor] = ComputeRangeTrackColors(aState); + + // Make a path that clips out the range thumb. + RefPtr<PathBuilder> builder = + aDrawTarget->CreatePathBuilder(FillRule::FILL_EVEN_ODD); + AppendRectToPath(builder, overflowRect); + AppendEllipseToPath(builder, thumbRect.Center().ToUnknownPoint(), + thumbRect.Size().ToUnknownSize()); + RefPtr<Path> path = builder->Finish(); + + // Draw the progress and track pieces with the thumb clipped out, so that + // they're not visible behind the thumb even if the thumb is partially + // transparent (which is the case in the disabled state). + aDrawTarget->PushClip(path); + { + aDrawTarget->PushClipRect(progressClipRect); + PaintRoundedRectWithRadius(aDrawTarget, rect, progressColor, + progressBorderColor, borderWidth, radius, + aDpiRatio); + aDrawTarget->PopClip(); + + aDrawTarget->PushClipRect(trackClipRect); + PaintRoundedRectWithRadius(aDrawTarget, rect, trackColor, trackBorderColor, + borderWidth, radius, aDpiRatio); + aDrawTarget->PopClip(); + + if (!aState.HasState(NS_EVENT_STATE_DISABLED)) { + // Thumb shadow + PaintEllipseShadow(aDrawTarget, thumbRect, 0.3f, CSSPoint(0.0f, 2.0f), + 2.0f, aDpiRatio); + } + } + aDrawTarget->PopClip(); + + // Draw the thumb on top. + const CSSCoord thumbBorderWidth = 2.0f; + auto [thumbColor, thumbBorderColor] = ComputeRangeThumbColors(aState); + + PaintStrokedEllipse(aDrawTarget, thumbRect, thumbColor, thumbBorderColor, + thumbBorderWidth, aDpiRatio); + + if (aState.HasState(NS_EVENT_STATE_FOCUSRING)) { + PaintRoundedFocusRect(aDrawTarget, aRect, aDpiRatio, radius, 1.0f); + } +} + +void nsNativeBasicTheme::PaintProgressBar(DrawTarget* aDrawTarget, + const LayoutDeviceRect& aRect, + const EventStates& aState, + DPIRatio aDpiRatio) { + const CSSCoord borderWidth = 1.0f; + const CSSCoord radius = 2.0f; + + LayoutDeviceRect rect(aRect); + const LayoutDeviceCoord height = kProgressbarHeight * aDpiRatio; + rect.y += (rect.height - height) / 2; + rect.height = height; + + auto [trackColor, trackBorderColor] = ComputeProgressTrackColors(); + + PaintRoundedRectWithRadius(aDrawTarget, rect, trackColor, trackBorderColor, + borderWidth, radius, aDpiRatio); +} + +void nsNativeBasicTheme::PaintProgresschunk(nsIFrame* aFrame, + DrawTarget* aDrawTarget, + const LayoutDeviceRect& aRect, + const EventStates& aState, + DPIRatio aDpiRatio) { + // TODO: vertical? + // TODO: Address artifacts when position is between 0 and radius + border. + // TODO: Handle indeterminate case. + nsProgressFrame* progressFrame = do_QueryFrame(aFrame->GetParent()); + if (!progressFrame) { + return; + } + + const CSSCoord borderWidth = 1.0f; + const LayoutDeviceCoord radius = CSSCoord(2.0f) * aDpiRatio; + LayoutDeviceCoord progressEndRadius = 0.0f; + + LayoutDeviceRect rect(aRect); + const LayoutDeviceCoord height = kProgressbarHeight * aDpiRatio; + rect.y += (rect.height - height) / 2; + rect.height = height; + + double position = GetProgressValue(aFrame) / GetProgressMaxValue(aFrame); + if (rect.width - (rect.width * position) < + (borderWidth * aDpiRatio + radius)) { + // Round corners when the progress chunk approaches the maximum value to + // avoid artifacts. + progressEndRadius = radius; + } + RectCornerRadii radii; + if (IsFrameRTL(aFrame)) { + radii = + RectCornerRadii(progressEndRadius, radius, radius, progressEndRadius); + } else { + radii = + RectCornerRadii(radius, progressEndRadius, progressEndRadius, radius); + } + + auto [progressColor, progressBorderColor] = ComputeProgressColors(); + + PaintRoundedRect(aDrawTarget, rect, progressColor, progressBorderColor, + borderWidth, radii, aDpiRatio); +} + +void nsNativeBasicTheme::PaintMeter(DrawTarget* aDrawTarget, + const LayoutDeviceRect& aRect, + const EventStates& aState, + DPIRatio aDpiRatio) { + const CSSCoord borderWidth = 1.0f; + const CSSCoord radius = 5.0f; + + LayoutDeviceRect rect(aRect); + const LayoutDeviceCoord height = kMeterHeight * aDpiRatio; + rect.y += (rect.height - height) / 2; + rect.height = height; + + auto [backgroundColor, borderColor] = ComputeMeterTrackColors(); + + PaintRoundedRectWithRadius(aDrawTarget, rect, backgroundColor, borderColor, + borderWidth, radius, aDpiRatio); +} + +void nsNativeBasicTheme::PaintMeterchunk(nsIFrame* aFrame, + DrawTarget* aDrawTarget, + const LayoutDeviceRect& aRect, + DPIRatio aDpiRatio) { + // TODO: Address artifacts when position is between 0 and (radius + border). + nsMeterFrame* meterFrame = do_QueryFrame(aFrame->GetParent()); + if (!meterFrame) { + return; + } + + const CSSCoord borderWidth = 1.0f; + const LayoutDeviceCoord radius = CSSCoord(5.0f) * aDpiRatio; + LayoutDeviceCoord progressEndRadius = 0.0f; + + LayoutDeviceRect rect(aRect); + const LayoutDeviceCoord height = kMeterHeight * aDpiRatio; + rect.y += (rect.height - height) / 2; + rect.height = height; + + auto* meter = + static_cast<mozilla::dom::HTMLMeterElement*>(meterFrame->GetContent()); + double value = meter->Value(); + double max = meter->Max(); + double position = value / max; + if (rect.width - (rect.width * position) < + (borderWidth * aDpiRatio + radius)) { + // Round corners when the progress chunk approaches the maximum value to + // avoid artifacts. + progressEndRadius = radius; + } + RectCornerRadii radii; + if (IsFrameRTL(aFrame)) { + radii = + RectCornerRadii(progressEndRadius, radius, radius, progressEndRadius); + } else { + radii = + RectCornerRadii(radius, progressEndRadius, progressEndRadius, radius); + } + + auto [chunkColor, borderColor] = ComputeMeterchunkColors(meter->State()); + + PaintRoundedRect(aDrawTarget, rect, chunkColor, borderColor, borderWidth, + radii, aDpiRatio); +} + +void nsNativeBasicTheme::PaintButton(nsIFrame* aFrame, DrawTarget* aDrawTarget, + const LayoutDeviceRect& aRect, + const EventStates& aState, + DPIRatio aDpiRatio) { + const CSSCoord radius = 4.0f; + auto [backgroundColor, borderColor] = ComputeButtonColors(aState, aFrame); + + PaintRoundedRectWithRadius(aDrawTarget, aRect, backgroundColor, borderColor, + kButtonBorderWidth, radius, aDpiRatio); + + if (aState.HasState(NS_EVENT_STATE_FOCUSRING)) { + PaintRoundedFocusRect(aDrawTarget, aRect, aDpiRatio, radius, + -kButtonBorderWidth); + } +} + +void nsNativeBasicTheme::PaintScrollbarThumb(DrawTarget* aDrawTarget, + const LayoutDeviceRect& aRect, + bool aHorizontal, nsIFrame* aFrame, + const ComputedStyle& aStyle, + const EventStates& aElementState, + const EventStates& aDocumentState, + DPIRatio aDpiRatio) { + sRGBColor thumbColor = + ComputeScrollbarThumbColor(aFrame, aStyle, aElementState, aDocumentState); + + aDrawTarget->FillRect(aRect.ToUnknownRect(), + ColorPattern(ToDeviceColor(thumbColor))); +} + +void nsNativeBasicTheme::PaintScrollbarTrack(DrawTarget* aDrawTarget, + const LayoutDeviceRect& aRect, + bool aHorizontal, nsIFrame* aFrame, + const ComputedStyle& aStyle, + const EventStates& aDocumentState, + DPIRatio aDpiRatio, bool aIsRoot) { + // Draw nothing by default. Subclasses can override this. +} + +void nsNativeBasicTheme::PaintScrollbar(DrawTarget* aDrawTarget, + const LayoutDeviceRect& aRect, + bool aHorizontal, nsIFrame* aFrame, + const ComputedStyle& aStyle, + const EventStates& aDocumentState, + DPIRatio aDpiRatio, bool aIsRoot) { + auto [scrollbarColor, borderColor] = + ComputeScrollbarColors(aFrame, aStyle, aDocumentState, aIsRoot); + aDrawTarget->FillRect(aRect.ToUnknownRect(), + ColorPattern(ToDeviceColor(scrollbarColor))); + // FIXME(heycam): We should probably derive the border color when custom + // scrollbar colors are in use too. But for now, just skip painting it, + // to avoid ugliness. + if (aStyle.StyleUI()->mScrollbarColor.IsAuto()) { + RefPtr<PathBuilder> builder = aDrawTarget->CreatePathBuilder(); + LayoutDeviceRect strokeRect(aRect); + strokeRect.Deflate(CSSCoord(0.5f) * aDpiRatio); + builder->MoveTo(strokeRect.TopLeft().ToUnknownPoint()); + builder->LineTo( + (aHorizontal ? strokeRect.TopRight() : strokeRect.BottomLeft()) + .ToUnknownPoint()); + RefPtr<Path> path = builder->Finish(); + aDrawTarget->Stroke(path, ColorPattern(ToDeviceColor(borderColor)), + StrokeOptions(CSSCoord(1.0f) * aDpiRatio)); + } +} + +void nsNativeBasicTheme::PaintScrollCorner(DrawTarget* aDrawTarget, + const LayoutDeviceRect& aRect, + nsIFrame* aFrame, + const ComputedStyle& aStyle, + const EventStates& aDocumentState, + DPIRatio aDpiRatio, bool aIsRoot) { + auto [scrollbarColor, borderColor] = + ComputeScrollbarColors(aFrame, aStyle, aDocumentState, aIsRoot); + Unused << borderColor; + aDrawTarget->FillRect(aRect.ToUnknownRect(), + ColorPattern(ToDeviceColor(scrollbarColor))); +} + +void nsNativeBasicTheme::PaintScrollbarButton( + DrawTarget* aDrawTarget, StyleAppearance aAppearance, + const LayoutDeviceRect& aRect, nsIFrame* aFrame, + const ComputedStyle& aStyle, const EventStates& aElementState, + const EventStates& aDocumentState, DPIRatio aDpiRatio) { + bool hasCustomColor = aStyle.StyleUI()->mScrollbarColor.IsColors(); + auto [buttonColor, arrowColor, borderColor] = ComputeScrollbarButtonColors( + aFrame, aAppearance, aStyle, aElementState, aDocumentState); + aDrawTarget->FillRect(aRect.ToUnknownRect(), + ColorPattern(ToDeviceColor(buttonColor))); + + // Start with Up arrow. + float arrowPolygonX[] = {-3.0f, 0.0f, 3.0f, 3.0f, 0.0f, -3.0f}; + float arrowPolygonY[] = {0.4f, -3.1f, 0.4f, 2.7f, -0.8f, 2.7f}; + + const int32_t arrowNumPoints = ArrayLength(arrowPolygonX); + switch (aAppearance) { + case StyleAppearance::ScrollbarbuttonUp: + break; + case StyleAppearance::ScrollbarbuttonDown: + for (int32_t i = 0; i < arrowNumPoints; i++) { + arrowPolygonY[i] *= -1; + } + break; + case StyleAppearance::ScrollbarbuttonLeft: + for (int32_t i = 0; i < arrowNumPoints; i++) { + int32_t temp = arrowPolygonX[i]; + arrowPolygonX[i] = arrowPolygonY[i]; + arrowPolygonY[i] = temp; + } + break; + case StyleAppearance::ScrollbarbuttonRight: + for (int32_t i = 0; i < arrowNumPoints; i++) { + int32_t temp = arrowPolygonX[i]; + arrowPolygonX[i] = arrowPolygonY[i] * -1; + arrowPolygonY[i] = temp; + } + break; + default: + return; + } + PaintArrow(aDrawTarget, aRect, arrowPolygonX, arrowPolygonY, arrowNumPoints, + arrowColor); + + // FIXME(heycam): We should probably derive the border color when custom + // scrollbar colors are in use too. But for now, just skip painting it, + // to avoid ugliness. + if (!hasCustomColor) { + RefPtr<PathBuilder> builder = aDrawTarget->CreatePathBuilder(); + builder->MoveTo(Point(aRect.x, aRect.y)); + if (aAppearance == StyleAppearance::ScrollbarbuttonUp || + aAppearance == StyleAppearance::ScrollbarbuttonDown) { + builder->LineTo(Point(aRect.x, aRect.y + aRect.height)); + } else { + builder->LineTo(Point(aRect.x + aRect.width, aRect.y)); + } + + RefPtr<Path> path = builder->Finish(); + aDrawTarget->Stroke(path, ColorPattern(ToDeviceColor(borderColor)), + StrokeOptions(CSSCoord(1.0f) * aDpiRatio)); + } +} + +// Checks whether the frame is for a root <scrollbar> or <scrollcorner>, which +// influences some platforms' scrollbar rendering. +bool nsNativeBasicTheme::IsRootScrollbar(nsIFrame* aFrame) { + return CheckBooleanAttr(aFrame, nsGkAtoms::root_) && + aFrame->PresContext()->IsRootContentDocument() && + aFrame->GetContent() && + aFrame->GetContent()->IsInNamespace(kNameSpaceID_XUL); +} + +NS_IMETHODIMP +nsNativeBasicTheme::DrawWidgetBackground(gfxContext* aContext, nsIFrame* aFrame, + StyleAppearance aAppearance, + const nsRect& aRect, + const nsRect& /* aDirtyRect */) { + DrawTarget* dt = aContext->GetDrawTarget(); + // FIXME(emilio): Why does this use AppUnitsPerDevPixel() but GetDPIRatio() + // uses AppUnitsPerDevPixelAtUnitFullZoom()? + const nscoord twipsPerPixel = aFrame->PresContext()->AppUnitsPerDevPixel(); + EventStates eventState = GetContentState(aFrame, aAppearance); + EventStates docState = aFrame->GetContent()->OwnerDoc()->GetDocumentState(); + auto devPxRect = LayoutDeviceRect::FromUnknownRect( + NSRectToSnappedRect(aRect, twipsPerPixel, *dt)); + + 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; + eventState = GetContentState(parentFrame, aAppearance); + } + } + + // Hack to avoid skia fuzziness: Add a dummy clip if the widget doesn't + // overflow devPxRect. + Maybe<AutoClipRect> maybeClipRect; + if (aAppearance != StyleAppearance::FocusOutline && + aAppearance != StyleAppearance::Range && + !eventState.HasState(NS_EVENT_STATE_FOCUSRING)) { + maybeClipRect.emplace(*dt, devPxRect); + } + + DPIRatio dpiRatio = GetDPIRatio(aFrame); + + switch (aAppearance) { + case StyleAppearance::Radio: { + auto rect = FixAspectRatio(devPxRect); + PaintRadioControl(dt, rect, eventState, dpiRatio); + if (IsSelected(aFrame)) { + PaintRadioCheckmark(dt, rect, eventState, dpiRatio); + } + break; + } + case StyleAppearance::Checkbox: { + auto rect = FixAspectRatio(devPxRect); + PaintCheckboxControl(dt, rect, eventState, dpiRatio); + if (GetIndeterminate(aFrame)) { + PaintIndeterminateMark(dt, rect, eventState); + } else if (IsChecked(aFrame)) { + PaintCheckMark(dt, rect, eventState); + } + break; + } + case StyleAppearance::Textarea: + case StyleAppearance::Textfield: + case StyleAppearance::NumberInput: + PaintTextField(dt, devPxRect, eventState, dpiRatio); + break; + case StyleAppearance::Listbox: + PaintListbox(dt, devPxRect, eventState, dpiRatio); + break; + case StyleAppearance::MenulistButton: + case StyleAppearance::Menulist: + PaintMenulist(dt, devPxRect, eventState, dpiRatio); + break; + case StyleAppearance::MozMenulistArrowButton: + PaintMenulistArrowButton(aFrame, dt, devPxRect, eventState); + break; + case StyleAppearance::SpinnerUpbutton: + case StyleAppearance::SpinnerDownbutton: + PaintSpinnerButton(aFrame, dt, devPxRect, eventState, aAppearance, + dpiRatio); + break; + case StyleAppearance::Range: + PaintRange(aFrame, dt, devPxRect, eventState, dpiRatio, + IsRangeHorizontal(aFrame)); + break; + case StyleAppearance::RangeThumb: + // Painted as part of StyleAppearance::Range. + break; + case StyleAppearance::ProgressBar: + PaintProgressBar(dt, devPxRect, eventState, dpiRatio); + break; + case StyleAppearance::Progresschunk: + PaintProgresschunk(aFrame, dt, devPxRect, eventState, dpiRatio); + break; + case StyleAppearance::Meter: + PaintMeter(dt, devPxRect, eventState, dpiRatio); + break; + case StyleAppearance::Meterchunk: + PaintMeterchunk(aFrame, dt, devPxRect, dpiRatio); + break; + case StyleAppearance::ScrollbarthumbHorizontal: + case StyleAppearance::ScrollbarthumbVertical: { + bool isHorizontal = + aAppearance == StyleAppearance::ScrollbarthumbHorizontal; + PaintScrollbarThumb(dt, devPxRect, isHorizontal, aFrame, + *nsLayoutUtils::StyleForScrollbar(aFrame), eventState, + docState, dpiRatio); + break; + } + case StyleAppearance::ScrollbartrackHorizontal: + case StyleAppearance::ScrollbartrackVertical: { + bool isHorizontal = + aAppearance == StyleAppearance::ScrollbartrackHorizontal; + PaintScrollbarTrack(dt, devPxRect, isHorizontal, aFrame, + *nsLayoutUtils::StyleForScrollbar(aFrame), docState, + dpiRatio, IsRootScrollbar(aFrame)); + break; + } + case StyleAppearance::ScrollbarHorizontal: + case StyleAppearance::ScrollbarVertical: { + bool isHorizontal = aAppearance == StyleAppearance::ScrollbarHorizontal; + PaintScrollbar(dt, devPxRect, isHorizontal, aFrame, + *nsLayoutUtils::StyleForScrollbar(aFrame), docState, + dpiRatio, IsRootScrollbar(aFrame)); + break; + } + case StyleAppearance::Scrollcorner: + PaintScrollCorner(dt, devPxRect, aFrame, + *nsLayoutUtils::StyleForScrollbar(aFrame), docState, + dpiRatio, IsRootScrollbar(aFrame)); + break; + case StyleAppearance::ScrollbarbuttonUp: + case StyleAppearance::ScrollbarbuttonDown: + case StyleAppearance::ScrollbarbuttonLeft: + case StyleAppearance::ScrollbarbuttonRight: + // For scrollbar-width:thin, we don't display the buttons. + if (!IsScrollbarWidthThin(aFrame)) { + PaintScrollbarButton(dt, aAppearance, devPxRect, aFrame, + *nsLayoutUtils::StyleForScrollbar(aFrame), + eventState, docState, dpiRatio); + } + break; + case StyleAppearance::Button: + PaintButton(aFrame, dt, devPxRect, eventState, dpiRatio); + break; + case StyleAppearance::FocusOutline: + // TODO(emilio): Consider supporting outline-radius / outline-offset? + PaintRoundedFocusRect(dt, devPxRect, dpiRatio, 0.0f, 0.0f); + 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 NS_ERROR_NOT_IMPLEMENTED; + } + + return NS_OK; +} + +/*bool +nsNativeBasicTheme::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) { +}*/ + +LayoutDeviceIntMargin nsNativeBasicTheme::GetWidgetBorder( + nsDeviceContext* aContext, nsIFrame* aFrame, StyleAppearance aAppearance) { + DPIRatio dpiRatio = GetDPIRatio(aFrame); + switch (aAppearance) { + case StyleAppearance::Textfield: + case StyleAppearance::Textarea: + case StyleAppearance::NumberInput: { + // FIXME: Do we want this margin not to be int-based? The native windows + // theme rounds (see ScaleForDPI)... + LayoutDeviceIntCoord w = (kTextFieldBorderWidth * dpiRatio).Rounded(); + return LayoutDeviceIntMargin(w, w, w, w); + } + case StyleAppearance::Listbox: + case StyleAppearance::Menulist: + case StyleAppearance::MenulistButton: { + LayoutDeviceIntCoord w = (kMenulistBorderWidth * dpiRatio).Rounded(); + return LayoutDeviceIntMargin(w, w, w, w); + } + case StyleAppearance::Button: { + LayoutDeviceIntCoord w = (kButtonBorderWidth * dpiRatio).Rounded(); + return LayoutDeviceIntMargin(w, w, w, w); + } + case StyleAppearance::Checkbox: + case StyleAppearance::Radio: { + LayoutDeviceIntCoord w = (kCheckboxRadioBorderWidth * dpiRatio).Rounded(); + return LayoutDeviceIntMargin(w, w, w, w); + } + default: + return LayoutDeviceIntMargin(); + } +} + +bool nsNativeBasicTheme::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 nsNativeBasicTheme::GetWidgetOverflow(nsDeviceContext* aContext, + nsIFrame* aFrame, + StyleAppearance aAppearance, + nsRect* aOverflowRect) { + nsIntMargin overflow; + switch (aAppearance) { + case StyleAppearance::FocusOutline: + // 2px * each of the segments + 1 px for the separation between them. + overflow.SizeTo(5, 5, 5, 5); + break; + case StyleAppearance::Radio: + case StyleAppearance::Checkbox: + case StyleAppearance::Range: + 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(nsMargin(CSSPixel::ToAppUnits(overflow.top), + CSSPixel::ToAppUnits(overflow.right), + CSSPixel::ToAppUnits(overflow.bottom), + CSSPixel::ToAppUnits(overflow.left))); + + return true; +} + +NS_IMETHODIMP +nsNativeBasicTheme::GetMinimumWidgetSize(nsPresContext* aPresContext, + nsIFrame* aFrame, + StyleAppearance aAppearance, + LayoutDeviceIntSize* aResult, + bool* aIsOverridable) { + DPIRatio dpiRatio = GetDPIRatio(aFrame); + + aResult->width = aResult->height = (kMinimumWidgetSize * dpiRatio).Rounded(); + + switch (aAppearance) { + case StyleAppearance::Button: + if (IsColorPickerButton(aFrame)) { + aResult->height = (kMinimumColorPickerHeight * dpiRatio).Rounded(); + } + break; + case StyleAppearance::RangeThumb: + aResult->SizeTo((kMinimumRangeThumbSize * dpiRatio).Rounded(), + (kMinimumRangeThumbSize * dpiRatio).Rounded()); + break; + case StyleAppearance::MozMenulistArrowButton: + aResult->width = (kMinimumDropdownArrowButtonWidth * dpiRatio).Rounded(); + break; + case StyleAppearance::SpinnerUpbutton: + case StyleAppearance::SpinnerDownbutton: + aResult->width = (kMinimumSpinnerButtonWidth * dpiRatio).Rounded(); + aResult->height = (kMinimumSpinnerButtonHeight * dpiRatio).Rounded(); + break; + case StyleAppearance::ScrollbarbuttonUp: + case StyleAppearance::ScrollbarbuttonDown: + case StyleAppearance::ScrollbarbuttonLeft: + case StyleAppearance::ScrollbarbuttonRight: + // For scrollbar-width:thin, we don't display the buttons. + if (IsScrollbarWidthThin(aFrame)) { + aResult->SizeTo(0, 0); + break; + } + [[fallthrough]]; + case StyleAppearance::ScrollbarthumbVertical: + case StyleAppearance::ScrollbarthumbHorizontal: + case StyleAppearance::ScrollbarVertical: + case StyleAppearance::ScrollbarHorizontal: + case StyleAppearance::ScrollbartrackHorizontal: + case StyleAppearance::ScrollbartrackVertical: + case StyleAppearance::Scrollcorner: { + if (IsScrollbarWidthThin(aFrame)) { + aResult->SizeTo((kMinimumThinScrollbarSize * dpiRatio).Rounded(), + (kMinimumThinScrollbarSize * dpiRatio).Rounded()); + } else { + aResult->SizeTo((kMinimumScrollbarSize * dpiRatio).Rounded(), + (kMinimumScrollbarSize * dpiRatio).Rounded()); + } + break; + } + default: + break; + } + + *aIsOverridable = true; + return NS_OK; +} + +nsITheme::Transparency nsNativeBasicTheme::GetWidgetTransparency( + nsIFrame* aFrame, StyleAppearance aAppearance) { + return eUnknownTransparency; +} + +NS_IMETHODIMP +nsNativeBasicTheme::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 +nsNativeBasicTheme::ThemeChanged() { return NS_OK; } + +bool nsNativeBasicTheme::WidgetAppearanceDependsOnWindowFocus( + StyleAppearance aAppearance) { + return IsWidgetScrollbarPart(aAppearance); +} + +nsITheme::ThemeGeometryType nsNativeBasicTheme::ThemeGeometryTypeForWidget( + nsIFrame* aFrame, StyleAppearance aAppearance) { + return eThemeGeometryTypeUnknown; +} + +bool nsNativeBasicTheme::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::Menuitem: + case StyleAppearance::Menuitemtext: + case StyleAppearance::MenulistText: + case StyleAppearance::MenulistButton: + case StyleAppearance::NumberInput: + case StyleAppearance::MozMenulistArrowButton: + case StyleAppearance::SpinnerUpbutton: + case StyleAppearance::SpinnerDownbutton: + return !IsWidgetStyled(aPresContext, aFrame, aAppearance); + default: + return false; + } +} + +bool nsNativeBasicTheme::WidgetIsContainer(StyleAppearance aAppearance) { + switch (aAppearance) { + case StyleAppearance::MozMenulistArrowButton: + case StyleAppearance::Radio: + case StyleAppearance::Checkbox: + return false; + default: + return true; + } +} + +bool nsNativeBasicTheme::ThemeDrawsFocusForWidget(StyleAppearance aAppearance) { + return true; +} + +bool nsNativeBasicTheme::ThemeNeedsComboboxDropmarker() { return true; } |