summaryrefslogtreecommitdiffstats
path: root/widget/ScrollbarDrawingCocoa.cpp
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--widget/ScrollbarDrawingCocoa.cpp518
1 files changed, 518 insertions, 0 deletions
diff --git a/widget/ScrollbarDrawingCocoa.cpp b/widget/ScrollbarDrawingCocoa.cpp
new file mode 100644
index 0000000000..c58b7509e5
--- /dev/null
+++ b/widget/ScrollbarDrawingCocoa.cpp
@@ -0,0 +1,518 @@
+/* -*- Mode: C++; c-basic-offset: 2; indent-tabs-mode: nil; tab-width: 2; -*- */
+/* vim: set sw=2 ts=8 et tw=80 : */
+/* 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 "ScrollbarDrawingCocoa.h"
+
+#include "mozilla/gfx/Helpers.h"
+#include "mozilla/RelativeLuminanceUtils.h"
+#include "mozilla/StaticPrefs_widget.h"
+#include "nsContainerFrame.h"
+#include "nsAlgorithm.h"
+#include "nsIFrame.h"
+#include "nsLayoutUtils.h"
+#include "nsLookAndFeel.h"
+#include "nsNativeTheme.h"
+
+using namespace mozilla::gfx;
+namespace mozilla::widget {
+
+using ScrollbarKind = ScrollbarDrawing::ScrollbarKind;
+
+struct ColoredRect {
+ LayoutDeviceRect mRect;
+ nscolor mColor = 0;
+};
+
+// The caller can draw this rectangle with rounded corners as appropriate.
+struct ThumbRect {
+ LayoutDeviceRect mRect;
+ nscolor mFillColor = 0;
+ nscolor mStrokeColor = 0;
+ float mStrokeWidth = 0.0f;
+ float mStrokeOutset = 0.0f;
+};
+
+using ScrollbarTrackRects = Array<ColoredRect, 4>;
+using ScrollCornerRects = Array<ColoredRect, 7>;
+
+struct ScrollbarParams {
+ bool isOverlay = false;
+ bool isRolledOver = false;
+ bool isSmall = false;
+ bool isHorizontal = false;
+ bool isRtl = false;
+ bool isDark = false;
+ bool isCustom = false;
+ // Two colors only used when custom is true.
+ nscolor trackColor = NS_RGBA(0, 0, 0, 0);
+ nscolor faceColor = NS_RGBA(0, 0, 0, 0);
+};
+
+static ScrollbarParams ComputeScrollbarParams(nsIFrame* aFrame,
+ const ComputedStyle& aStyle,
+ const ThemeColors& aColors,
+ ScrollbarKind aScrollbarKind) {
+ ScrollbarParams params;
+ params.isOverlay =
+ nsLookAndFeel::GetInt(LookAndFeel::IntID::UseOverlayScrollbars) != 0;
+ params.isRolledOver = ScrollbarDrawing::IsParentScrollbarRolledOver(aFrame);
+ params.isSmall =
+ aStyle.StyleUIReset()->ScrollbarWidth() == StyleScrollbarWidth::Thin;
+ params.isRtl = aScrollbarKind == ScrollbarKind::VerticalLeft;
+ params.isHorizontal = aScrollbarKind == ScrollbarKind::Horizontal;
+ params.isDark = aColors.IsDark();
+
+ const nsStyleUI* ui = aStyle.StyleUI();
+ if (ui->HasCustomScrollbars()) {
+ const auto& colors = ui->mScrollbarColor.AsColors();
+ params.isCustom = true;
+ params.trackColor = colors.track.CalcColor(aStyle);
+ params.faceColor = colors.thumb.CalcColor(aStyle);
+ }
+
+ return params;
+}
+
+LayoutDeviceIntSize ScrollbarDrawingCocoa::GetMinimumWidgetSize(
+ nsPresContext* aPresContext, StyleAppearance aAppearance,
+ nsIFrame* aFrame) {
+ MOZ_ASSERT(nsNativeTheme::IsWidgetScrollbarPart(aAppearance));
+
+ auto minSize = [&] {
+ switch (aAppearance) {
+ case StyleAppearance::ScrollbarthumbHorizontal:
+ return IntSize{26, 0};
+ case StyleAppearance::ScrollbarthumbVertical:
+ return IntSize{0, 26};
+ case StyleAppearance::ScrollbarVertical:
+ case StyleAppearance::ScrollbarHorizontal:
+ case StyleAppearance::ScrollbartrackVertical:
+ case StyleAppearance::ScrollbartrackHorizontal: {
+ ComputedStyle* style = nsLayoutUtils::StyleForScrollbar(aFrame);
+ auto scrollbarWidth = style->StyleUIReset()->ScrollbarWidth();
+ auto size = GetScrollbarSize(
+ scrollbarWidth,
+ LookAndFeel::GetInt(LookAndFeel::IntID::UseOverlayScrollbars));
+ return IntSize{size, size};
+ }
+ case StyleAppearance::ScrollbarbuttonUp:
+ case StyleAppearance::ScrollbarbuttonDown:
+ return IntSize{15, 16};
+ case StyleAppearance::ScrollbarbuttonLeft:
+ case StyleAppearance::ScrollbarbuttonRight:
+ return IntSize{16, 15};
+ default:
+ return IntSize{};
+ }
+ }();
+
+ auto dpi = GetDPIRatioForScrollbarPart(aPresContext).scale;
+ if (dpi >= 2.0f) {
+ return LayoutDeviceIntSize{minSize.width * 2, minSize.height * 2};
+ }
+ return LayoutDeviceIntSize{minSize.width, minSize.height};
+}
+
+/*static*/
+CSSIntCoord ScrollbarDrawingCocoa::GetScrollbarSize(StyleScrollbarWidth aWidth,
+ bool aOverlay) {
+ bool isSmall = aWidth == StyleScrollbarWidth::Thin;
+ if (aOverlay) {
+ return isSmall ? 14 : 16;
+ }
+ return isSmall ? 11 : 15;
+}
+
+/*static*/
+LayoutDeviceIntCoord ScrollbarDrawingCocoa::GetScrollbarSize(
+ StyleScrollbarWidth aWidth, bool aOverlay, DPIRatio aDpiRatio) {
+ CSSIntCoord size = GetScrollbarSize(aWidth, aOverlay);
+ if (aDpiRatio.scale >= 2.0f) {
+ return int32_t(size) * 2;
+ }
+ return int32_t(size);
+}
+
+auto ScrollbarDrawingCocoa::GetScrollbarSizes(nsPresContext* aPresContext,
+ StyleScrollbarWidth aWidth,
+ Overlay aOverlay)
+ -> ScrollbarSizes {
+ auto size = GetScrollbarSize(aWidth, aOverlay == Overlay::Yes,
+ GetDPIRatioForScrollbarPart(aPresContext));
+ return {size, size};
+}
+
+static ThumbRect GetThumbRect(const LayoutDeviceRect& aRect,
+ const ScrollbarParams& aParams, float aScale) {
+ // This matches the sizing checks in GetMinimumWidgetSize etc.
+ aScale = aScale >= 2.0f ? 2.0f : 1.0f;
+
+ // Compute the thumb thickness. This varies based on aParams.small,
+ // aParams.overlay and aParams.rolledOver. non-overlay: 6 / 8, overlay
+ // non-hovered: 5 / 7, overlay hovered: 9 / 11
+ float thickness = aParams.isSmall ? 6.0f : 8.0f;
+ if (aParams.isOverlay) {
+ thickness -= 1.0f;
+ if (aParams.isRolledOver) {
+ thickness += 4.0f;
+ }
+ }
+ thickness *= aScale;
+
+ // Compute the thumb rect.
+ const float outerSpacing =
+ ((aParams.isOverlay || aParams.isSmall) ? 1.0f : 2.0f) * aScale;
+ LayoutDeviceRect thumbRect = aRect;
+ thumbRect.Deflate(1.0f * aScale);
+ if (aParams.isHorizontal) {
+ float bottomEdge = thumbRect.YMost() - outerSpacing;
+ thumbRect.SetBoxY(bottomEdge - thickness, bottomEdge);
+ } else {
+ if (aParams.isRtl) {
+ float leftEdge = thumbRect.X() + outerSpacing;
+ thumbRect.SetBoxX(leftEdge, leftEdge + thickness);
+ } else {
+ float rightEdge = thumbRect.XMost() - outerSpacing;
+ thumbRect.SetBoxX(rightEdge - thickness, rightEdge);
+ }
+ }
+
+ // Compute the thumb fill color.
+ nscolor faceColor;
+ if (aParams.isCustom) {
+ faceColor = aParams.faceColor;
+ } else {
+ if (aParams.isOverlay) {
+ faceColor =
+ aParams.isDark ? NS_RGBA(255, 255, 255, 128) : NS_RGBA(0, 0, 0, 128);
+ } else if (aParams.isDark) {
+ faceColor = aParams.isRolledOver ? NS_RGBA(158, 158, 158, 255)
+ : NS_RGBA(117, 117, 117, 255);
+ } else {
+ faceColor = aParams.isRolledOver ? NS_RGBA(125, 125, 125, 255)
+ : NS_RGBA(194, 194, 194, 255);
+ }
+ }
+
+ nscolor strokeColor = 0;
+ float strokeOutset = 0.0f;
+ float strokeWidth = 0.0f;
+
+ // Overlay scrollbars have an additional stroke around the fill.
+ if (aParams.isOverlay) {
+ // For the default alpha of 128 we want to end up with 48 in the outline.
+ constexpr float kAlphaScaling = 48.0f / 128.0f;
+ const uint8_t strokeAlpha =
+ uint8_t(clamped(NS_GET_A(faceColor) * kAlphaScaling, 0.0f, 48.0f));
+ if (strokeAlpha) {
+ strokeOutset = (aParams.isDark ? 0.3f : 0.5f) * aScale;
+ strokeWidth = (aParams.isDark ? 0.6f : 0.8f) * aScale;
+
+ strokeColor = aParams.isDark ? NS_RGBA(0, 0, 0, strokeAlpha)
+ : NS_RGBA(255, 255, 255, strokeAlpha);
+ }
+ }
+
+ return {thumbRect, faceColor, strokeColor, strokeWidth, strokeOutset};
+}
+
+struct ScrollbarTrackDecorationColors {
+ nscolor mInnerColor = 0;
+ nscolor mShadowColor = 0;
+ nscolor mOuterColor = 0;
+};
+
+static ScrollbarTrackDecorationColors ComputeScrollbarTrackDecorationColors(
+ nscolor aTrackColor) {
+ ScrollbarTrackDecorationColors result;
+ float luminance = RelativeLuminanceUtils::Compute(aTrackColor);
+ if (luminance >= 0.5f) {
+ result.mInnerColor =
+ RelativeLuminanceUtils::Adjust(aTrackColor, luminance * 0.836f);
+ result.mShadowColor =
+ RelativeLuminanceUtils::Adjust(aTrackColor, luminance * 0.982f);
+ result.mOuterColor =
+ RelativeLuminanceUtils::Adjust(aTrackColor, luminance * 0.886f);
+ } else {
+ result.mInnerColor =
+ RelativeLuminanceUtils::Adjust(aTrackColor, luminance * 1.196f);
+ result.mShadowColor =
+ RelativeLuminanceUtils::Adjust(aTrackColor, luminance * 1.018f);
+ result.mOuterColor =
+ RelativeLuminanceUtils::Adjust(aTrackColor, luminance * 1.129f);
+ }
+ return result;
+}
+
+static bool GetScrollbarTrackRects(const LayoutDeviceRect& aRect,
+ const ScrollbarParams& aParams, float aScale,
+ ScrollbarTrackRects& aRects) {
+ if (aParams.isOverlay && !aParams.isRolledOver) {
+ // Non-hovered overlay scrollbars don't have a track. Draw nothing.
+ return false;
+ }
+
+ // This matches the sizing checks in GetMinimumWidgetSize etc.
+ aScale = aScale >= 2.0f ? 2.0f : 1.0f;
+
+ nscolor trackColor;
+ if (aParams.isCustom) {
+ trackColor = aParams.trackColor;
+ } else {
+ if (aParams.isOverlay) {
+ trackColor = aParams.isDark ? NS_RGBA(201, 201, 201, 38)
+ : NS_RGBA(250, 250, 250, 191);
+ } else {
+ trackColor = aParams.isDark ? NS_RGBA(46, 46, 46, 255)
+ : NS_RGBA(250, 250, 250, 255);
+ }
+ }
+
+ float thickness = aParams.isHorizontal ? aRect.height : aRect.width;
+
+ // The scrollbar track is drawn as multiple non-overlapping segments, which
+ // make up lines of different widths and with slightly different shading.
+ ScrollbarTrackDecorationColors colors =
+ ComputeScrollbarTrackDecorationColors(trackColor);
+ struct {
+ nscolor color;
+ float thickness;
+ } segments[] = {
+ {colors.mInnerColor, 1.0f * aScale},
+ {colors.mShadowColor, 1.0f * aScale},
+ {trackColor, thickness - 3.0f * aScale},
+ {colors.mOuterColor, 1.0f * aScale},
+ };
+
+ // Iterate over the segments "from inside to outside" and fill each segment.
+ // For horizontal scrollbars, iterate top to bottom.
+ // For vertical scrollbars, iterate left to right or right to left based on
+ // aParams.isRtl.
+ auto current = aRects.begin();
+ float accumulatedThickness = 0.0f;
+ for (const auto& segment : segments) {
+ LayoutDeviceRect segmentRect = aRect;
+ float startThickness = accumulatedThickness;
+ float endThickness = startThickness + segment.thickness;
+ if (aParams.isHorizontal) {
+ segmentRect.SetBoxY(aRect.Y() + startThickness, aRect.Y() + endThickness);
+ } else {
+ if (aParams.isRtl) {
+ segmentRect.SetBoxX(aRect.XMost() - endThickness,
+ aRect.XMost() - startThickness);
+ } else {
+ segmentRect.SetBoxX(aRect.X() + startThickness,
+ aRect.X() + endThickness);
+ }
+ }
+ accumulatedThickness = endThickness;
+ *current++ = {segmentRect, segment.color};
+ }
+
+ return true;
+}
+
+static bool GetScrollCornerRects(const LayoutDeviceRect& aRect,
+ const ScrollbarParams& aParams, float aScale,
+ ScrollCornerRects& aRects) {
+ if (aParams.isOverlay && !aParams.isRolledOver) {
+ // Non-hovered overlay scrollbars don't have a corner. Draw nothing.
+ return false;
+ }
+
+ // This matches the sizing checks in GetMinimumWidgetSize etc.
+ aScale = aScale >= 2.0f ? 2.0f : 1.0f;
+
+ // Draw the following scroll corner.
+ //
+ // Output: Rectangles:
+ // +---+---+----------+---+ +---+---+----------+---+
+ // | I | S | T ... T | O | | I | S | T ... T | O |
+ // +---+ | | | +---+---+ | |
+ // | S S | T ... T | | | S S | T ... T | . |
+ // +-------+ | . | +-------+----------+ . |
+ // | T ... T | . | | T ... T | . |
+ // | . . | . | | . . | |
+ // | T ... T | | | T ... T | O |
+ // +------------------+ | +------------------+---+
+ // | O ... O | | O ... O |
+ // +----------------------+ +----------------------+
+
+ float width = aRect.width;
+ float height = aRect.height;
+ nscolor trackColor;
+ if (aParams.isCustom) {
+ trackColor = aParams.trackColor;
+ } else {
+ trackColor =
+ aParams.isDark ? NS_RGBA(46, 46, 46, 255) : NS_RGBA(250, 250, 250, 255);
+ }
+ ScrollbarTrackDecorationColors colors =
+ ComputeScrollbarTrackDecorationColors(trackColor);
+ struct {
+ nscolor color;
+ LayoutDeviceRect relativeRect;
+ } pieces[] = {
+ {colors.mInnerColor, {0.0f, 0.0f, 1.0f * aScale, 1.0f * aScale}},
+ {colors.mShadowColor,
+ {1.0f * aScale, 0.0f, 1.0f * aScale, 1.0f * aScale}},
+ {colors.mShadowColor,
+ {0.0f, 1.0f * aScale, 2.0f * aScale, 1.0f * aScale}},
+ {trackColor, {2.0f * aScale, 0.0f, width - 3.0f * aScale, 2.0f * aScale}},
+ {trackColor,
+ {0.0f, 2.0f * aScale, width - 1.0f * aScale, height - 3.0f * aScale}},
+ {colors.mOuterColor,
+ {width - 1.0f * aScale, 0.0f, 1.0f * aScale, height - 1.0f * aScale}},
+ {colors.mOuterColor,
+ {0.0f, height - 1.0f * aScale, width, 1.0f * aScale}},
+ };
+
+ auto current = aRects.begin();
+ for (const auto& piece : pieces) {
+ LayoutDeviceRect pieceRect = piece.relativeRect + aRect.TopLeft();
+ if (aParams.isRtl) {
+ pieceRect.x = aRect.XMost() - piece.relativeRect.XMost();
+ }
+ *current++ = {pieceRect, piece.color};
+ }
+ return true;
+}
+
+template <typename PaintBackendData>
+void ScrollbarDrawingCocoa::DoPaintScrollbarThumb(
+ PaintBackendData& aPaintData, const LayoutDeviceRect& aRect,
+ ScrollbarKind aScrollbarKind, nsIFrame* aFrame, const ComputedStyle& aStyle,
+ const ElementState& aElementState, const DocumentState& aDocumentState,
+ const Colors& aColors, const DPIRatio& aDpiRatio) {
+ ScrollbarParams params =
+ ComputeScrollbarParams(aFrame, aStyle, aColors, aScrollbarKind);
+ auto thumb = GetThumbRect(aRect, params, aDpiRatio.scale);
+ LayoutDeviceCoord radius =
+ (params.isHorizontal ? thumb.mRect.Height() : thumb.mRect.Width()) / 2.0f;
+ ThemeDrawing::PaintRoundedRectWithRadius(
+ aPaintData, thumb.mRect, thumb.mRect,
+ sRGBColor::FromABGR(thumb.mFillColor), sRGBColor::White(0.0f), 0.0f,
+ radius / aDpiRatio, aDpiRatio);
+ if (!thumb.mStrokeColor) {
+ return;
+ }
+
+ // Paint the stroke if needed.
+ auto strokeRect = thumb.mRect;
+ strokeRect.Inflate(thumb.mStrokeOutset + thumb.mStrokeWidth);
+ radius =
+ (params.isHorizontal ? strokeRect.Height() : strokeRect.Width()) / 2.0f;
+ ThemeDrawing::PaintRoundedRectWithRadius(
+ aPaintData, strokeRect, sRGBColor::White(0.0f),
+ sRGBColor::FromABGR(thumb.mStrokeColor), thumb.mStrokeWidth,
+ radius / aDpiRatio, aDpiRatio);
+}
+
+bool ScrollbarDrawingCocoa::PaintScrollbarThumb(
+ DrawTarget& aDt, const LayoutDeviceRect& aRect,
+ ScrollbarKind aScrollbarKind, nsIFrame* aFrame, const ComputedStyle& aStyle,
+ const ElementState& aElementState, const DocumentState& aDocumentState,
+ const Colors& aColors, const DPIRatio& aDpiRatio) {
+ DoPaintScrollbarThumb(aDt, aRect, aScrollbarKind, aFrame, aStyle,
+ aElementState, aDocumentState, aColors, aDpiRatio);
+ return true;
+}
+
+bool ScrollbarDrawingCocoa::PaintScrollbarThumb(
+ WebRenderBackendData& aWrData, const LayoutDeviceRect& aRect,
+ ScrollbarKind aScrollbarKind, nsIFrame* aFrame, const ComputedStyle& aStyle,
+ const ElementState& aElementState, const DocumentState& aDocumentState,
+ const Colors& aColors, const DPIRatio& aDpiRatio) {
+ DoPaintScrollbarThumb(aWrData, aRect, aScrollbarKind, aFrame, aStyle,
+ aElementState, aDocumentState, aColors, aDpiRatio);
+ return true;
+}
+
+template <typename PaintBackendData>
+void ScrollbarDrawingCocoa::DoPaintScrollbarTrack(
+ PaintBackendData& aPaintData, const LayoutDeviceRect& aRect,
+ ScrollbarKind aScrollbarKind, nsIFrame* aFrame, const ComputedStyle& aStyle,
+ const DocumentState& aDocumentState, const Colors& aColors,
+ const DPIRatio& aDpiRatio) {
+ ScrollbarParams params =
+ ComputeScrollbarParams(aFrame, aStyle, aColors, aScrollbarKind);
+ ScrollbarTrackRects rects;
+ if (GetScrollbarTrackRects(aRect, params, aDpiRatio.scale, rects)) {
+ for (const auto& rect : rects) {
+ ThemeDrawing::FillRect(aPaintData, rect.mRect,
+ sRGBColor::FromABGR(rect.mColor));
+ }
+ }
+}
+
+bool ScrollbarDrawingCocoa::PaintScrollbarTrack(
+ DrawTarget& aDt, const LayoutDeviceRect& aRect,
+ ScrollbarKind aScrollbarKind, nsIFrame* aFrame, const ComputedStyle& aStyle,
+ const DocumentState& aDocumentState, const Colors& aColors,
+ const DPIRatio& aDpiRatio) {
+ DoPaintScrollbarTrack(aDt, aRect, aScrollbarKind, aFrame, aStyle,
+ aDocumentState, aColors, aDpiRatio);
+ return true;
+}
+
+bool ScrollbarDrawingCocoa::PaintScrollbarTrack(
+ WebRenderBackendData& aWrData, const LayoutDeviceRect& aRect,
+ ScrollbarKind aScrollbarKind, nsIFrame* aFrame, const ComputedStyle& aStyle,
+ const DocumentState& aDocumentState, const Colors& aColors,
+ const DPIRatio& aDpiRatio) {
+ DoPaintScrollbarTrack(aWrData, aRect, aScrollbarKind, aFrame, aStyle,
+ aDocumentState, aColors, aDpiRatio);
+ return true;
+}
+
+template <typename PaintBackendData>
+void ScrollbarDrawingCocoa::DoPaintScrollCorner(
+ PaintBackendData& aPaintData, const LayoutDeviceRect& aRect,
+ ScrollbarKind aScrollbarKind, nsIFrame* aFrame, const ComputedStyle& aStyle,
+ const DocumentState& aDocumentState, const Colors& aColors,
+ const DPIRatio& aDpiRatio) {
+ ScrollbarParams params =
+ ComputeScrollbarParams(aFrame, aStyle, aColors, aScrollbarKind);
+ ScrollCornerRects rects;
+ if (GetScrollCornerRects(aRect, params, aDpiRatio.scale, rects)) {
+ for (const auto& rect : rects) {
+ ThemeDrawing::FillRect(aPaintData, rect.mRect,
+ sRGBColor::FromABGR(rect.mColor));
+ }
+ }
+}
+
+bool ScrollbarDrawingCocoa::PaintScrollCorner(
+ DrawTarget& aDt, const LayoutDeviceRect& aRect,
+ ScrollbarKind aScrollbarKind, nsIFrame* aFrame, const ComputedStyle& aStyle,
+ const DocumentState& aDocumentState, const Colors& aColors,
+ const DPIRatio& aDpiRatio) {
+ DoPaintScrollCorner(aDt, aRect, aScrollbarKind, aFrame, aStyle,
+ aDocumentState, aColors, aDpiRatio);
+ return true;
+}
+
+bool ScrollbarDrawingCocoa::PaintScrollCorner(
+ WebRenderBackendData& aWrData, const LayoutDeviceRect& aRect,
+ ScrollbarKind aScrollbarKind, nsIFrame* aFrame, const ComputedStyle& aStyle,
+ const DocumentState& aDocumentState, const Colors& aColors,
+ const DPIRatio& aDpiRatio) {
+ DoPaintScrollCorner(aWrData, aRect, aScrollbarKind, aFrame, aStyle,
+ aDocumentState, aColors, aDpiRatio);
+ return true;
+}
+
+void ScrollbarDrawingCocoa::RecomputeScrollbarParams() {
+ uint32_t defaultSize = 17;
+ uint32_t overrideSize =
+ StaticPrefs::widget_non_native_theme_scrollbar_size_override();
+ if (overrideSize > 0) {
+ defaultSize = overrideSize;
+ }
+ mHorizontalScrollbarHeight = mVerticalScrollbarWidth = defaultSize;
+}
+
+} // namespace mozilla::widget