/* -*- 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; using ScrollCornerRects = Array; 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 = [&]() -> CSSIntSize { switch (aAppearance) { case StyleAppearance::ScrollbarthumbHorizontal: return {26, 0}; case StyleAppearance::ScrollbarthumbVertical: return {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 = GetCSSScrollbarSize( scrollbarWidth, Overlay(aPresContext->UseOverlayScrollbars())); return {size, size}; } case StyleAppearance::ScrollbarbuttonUp: case StyleAppearance::ScrollbarbuttonDown: return {15, 16}; case StyleAppearance::ScrollbarbuttonLeft: case StyleAppearance::ScrollbarbuttonRight: return {16, 15}; default: return {}; } }(); auto dpi = GetDPIRatioForScrollbarPart(aPresContext); return LayoutDeviceIntSize::Round(CSSSize(minSize) * dpi); } static ThumbRect GetThumbRect(const LayoutDeviceRect& aRect, const ScrollbarParams& aParams, float aScale) { // 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; } 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; } // 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 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 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 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() { // FIXME(emilio): This doesn't respect the // StaticPrefs::widget_non_native_theme_scrollbar_size_override() pref; ConfigureScrollbarSize(15); // Just in case, for future-proofing ConfigureScrollbarSize(StyleScrollbarWidth::Auto, Overlay::No, 15); ConfigureScrollbarSize(StyleScrollbarWidth::Thin, Overlay::No, 11); ConfigureScrollbarSize(StyleScrollbarWidth::Auto, Overlay::Yes, 16); ConfigureScrollbarSize(StyleScrollbarWidth::Thin, Overlay::Yes, 14); } } // namespace mozilla::widget