diff options
Diffstat (limited to '')
-rw-r--r-- | widget/ScrollbarDrawingWin11.cpp | 378 |
1 files changed, 378 insertions, 0 deletions
diff --git a/widget/ScrollbarDrawingWin11.cpp b/widget/ScrollbarDrawingWin11.cpp new file mode 100644 index 0000000000..a1b9be3519 --- /dev/null +++ b/widget/ScrollbarDrawingWin11.cpp @@ -0,0 +1,378 @@ +/* -*- 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 "ScrollbarDrawingWin11.h" + +#include "mozilla/gfx/Helpers.h" +#include "mozilla/Maybe.h" +#include "mozilla/StaticPrefs_widget.h" +#include "nsLayoutUtils.h" +#include "Theme.h" +#include "nsNativeTheme.h" + +using mozilla::gfx::sRGBColor; + +namespace mozilla::widget { + +// There are effectively three kinds of scrollbars in Windows 11: +// +// * Overlay scrollbars (the ones where the scrollbar disappears automatically +// and doesn't take space) +// * Non-overlay scrollbar with thin (overlay-like) thumb. +// * Non-overlay scrollbar with thick thumb. +// +// See bug 1755193 for some discussion on non-overlay scrollbar styles. +enum class Style { + Overlay, + ThinThumb, + ThickThumb, +}; + +static Style ScrollbarStyle(nsPresContext* aPresContext) { + if (aPresContext->UseOverlayScrollbars()) { + return Style::Overlay; + } + if (StaticPrefs:: + widget_non_native_theme_win11_scrollbar_force_overlay_style()) { + return Style::ThinThumb; + } + return Style::ThickThumb; +} + +static constexpr CSSIntCoord kDefaultWinOverlayScrollbarSize = CSSIntCoord(12); +static constexpr CSSIntCoord kDefaultWinOverlayThinScrollbarSize = + CSSIntCoord(10); + +auto ScrollbarDrawingWin11::GetScrollbarSizes(nsPresContext* aPresContext, + StyleScrollbarWidth aWidth, + Overlay aOverlay) + -> ScrollbarSizes { + if (aOverlay == Overlay::Yes) { + // TODO(emilio): Maybe make this configurable? Though this doesn't respect + // classic Windows registry settings, and cocoa overlay scrollbars also + // don't respect the override it seems, so this should be fine. + CSSCoord cssSize(aWidth == StyleScrollbarWidth::Thin + ? kDefaultWinOverlayThinScrollbarSize + : kDefaultWinOverlayScrollbarSize); + auto size = (cssSize * GetDPIRatioForScrollbarPart(aPresContext)).Rounded(); + return {size, size}; + } + return ScrollbarDrawingWin::GetScrollbarSizes(aPresContext, aWidth, aOverlay); +} + +LayoutDeviceIntSize ScrollbarDrawingWin11::GetMinimumWidgetSize( + nsPresContext* aPresContext, StyleAppearance aAppearance, + nsIFrame* aFrame) { + MOZ_ASSERT(nsNativeTheme::IsWidgetScrollbarPart(aAppearance)); + if (ScrollbarStyle(aPresContext) != Style::ThinThumb) { + return ScrollbarDrawingWin::GetMinimumWidgetSize(aPresContext, aAppearance, + aFrame); + } + constexpr float kArrowRatio = 14.0f / kDefaultWinScrollbarSize; + switch (aAppearance) { + case StyleAppearance::ScrollbarbuttonUp: + case StyleAppearance::ScrollbarbuttonDown: { + if (IsScrollbarWidthThin(aFrame)) { + return {}; + } + const LayoutDeviceIntCoord size = + ScrollbarDrawing::GetScrollbarSizes(aPresContext, aFrame).mVertical; + return LayoutDeviceIntSize{ + size, (kArrowRatio * LayoutDeviceCoord(size)).Rounded()}; + } + case StyleAppearance::ScrollbarbuttonLeft: + case StyleAppearance::ScrollbarbuttonRight: { + if (IsScrollbarWidthThin(aFrame)) { + return {}; + } + const LayoutDeviceIntCoord size = + ScrollbarDrawing::GetScrollbarSizes(aPresContext, aFrame).mHorizontal; + return LayoutDeviceIntSize{ + (kArrowRatio * LayoutDeviceCoord(size)).Rounded(), size}; + } + default: + return ScrollbarDrawingWin::GetMinimumWidgetSize(aPresContext, + aAppearance, aFrame); + } +} + +sRGBColor ScrollbarDrawingWin11::ComputeScrollbarTrackColor( + nsIFrame* aFrame, const ComputedStyle& aStyle, + const DocumentState& aDocumentState, const Colors& aColors) { + if (aColors.HighContrast()) { + return ScrollbarDrawingWin::ComputeScrollbarTrackColor( + aFrame, aStyle, aDocumentState, aColors); + } + const nsStyleUI* ui = aStyle.StyleUI(); + if (ui->mScrollbarColor.IsColors()) { + return sRGBColor::FromABGR( + ui->mScrollbarColor.AsColors().track.CalcColor(aStyle)); + } + return aColors.IsDark() ? sRGBColor::FromU8(23, 23, 23, 255) + : sRGBColor::FromU8(240, 240, 240, 255); +} + +sRGBColor ScrollbarDrawingWin11::ComputeScrollbarThumbColor( + nsIFrame* aFrame, const ComputedStyle& aStyle, + const ElementState& aElementState, const DocumentState& aDocumentState, + const Colors& aColors) { + if (aColors.HighContrast()) { + return ScrollbarDrawingWin::ComputeScrollbarThumbColor( + aFrame, aStyle, aElementState, aDocumentState, aColors); + } + const nscolor baseColor = [&] { + const nsStyleUI* ui = aStyle.StyleUI(); + if (ui->mScrollbarColor.IsColors()) { + return ui->mScrollbarColor.AsColors().thumb.CalcColor(aStyle); + } + return aColors.IsDark() ? NS_RGBA(149, 149, 149, 255) + : NS_RGBA(133, 133, 133, 255); + }(); + ElementState state = aElementState; + if (!IsScrollbarWidthThin(aStyle)) { + // non-thin scrollbars get hover feedback by changing thumb shape, so we + // only provide active feedback (and we use the hover state for that as it's + // more subtle). + state &= ~ElementState::HOVER; + if (state.HasState(ElementState::ACTIVE)) { + state &= ~ElementState::ACTIVE; + state |= ElementState::HOVER; + } + } + return sRGBColor::FromABGR( + ThemeColors::AdjustUnthemedScrollbarThumbColor(baseColor, state)); +} + +std::pair<sRGBColor, sRGBColor> +ScrollbarDrawingWin11::ComputeScrollbarButtonColors( + nsIFrame* aFrame, StyleAppearance aAppearance, const ComputedStyle& aStyle, + const ElementState& aElementState, const DocumentState& aDocumentState, + const Colors& aColors) { + if (aColors.HighContrast()) { + return ScrollbarDrawingWin::ComputeScrollbarButtonColors( + aFrame, aAppearance, aStyle, aElementState, aDocumentState, aColors); + } + // The button always looks transparent (the track behind it is visible), so we + // can hardcode it. + sRGBColor arrowColor = ComputeScrollbarThumbColor( + aFrame, aStyle, aElementState, aDocumentState, aColors); + return {sRGBColor::White(0.0f), arrowColor}; +} + +bool ScrollbarDrawingWin11::PaintScrollbarButton( + DrawTarget& aDrawTarget, StyleAppearance aAppearance, + const LayoutDeviceRect& aRect, ScrollbarKind aScrollbarKind, + nsIFrame* aFrame, const ComputedStyle& aStyle, + const ElementState& aElementState, const DocumentState& aDocumentState, + const Colors& aColors, const DPIRatio& aDpiRatio) { + if (!ScrollbarDrawing::IsParentScrollbarHoveredOrActive(aFrame)) { + return true; + } + + const auto style = ScrollbarStyle(aFrame->PresContext()); + auto [buttonColor, arrowColor] = ComputeScrollbarButtonColors( + aFrame, aAppearance, aStyle, aElementState, aDocumentState, aColors); + if (style != Style::Overlay) { + aDrawTarget.FillRect(aRect.ToUnknownRect(), + gfx::ColorPattern(ToDeviceColor(buttonColor))); + } + + // Start with Up arrow. + float arrowPolygonX[] = {-4.5f, 4.5f, 4.5f, 0.5f, -0.5f, -4.5f, -4.5f}; + float arrowPolygonXActive[] = {-4.0f, 4.0f, 4.0f, -0.25f, + -0.25f, -4.0f, -4.0f}; + float arrowPolygonXHover[] = {-5.0f, 5.0f, 5.0f, 0.75f, -0.75f, -5.0f, -5.0f}; + float arrowPolygonY[] = {2.5f, 2.5f, 1.0f, -4.0f, -4.0f, 1.0f, 2.5f}; + float arrowPolygonYActive[] = {2.0f, 2.0f, 0.5f, -3.5f, -3.5f, 0.5f, 2.0f}; + float arrowPolygonYHover[] = {3.0f, 3.0f, 1.5f, -4.5f, -4.5f, 1.5f, 3.0f}; + float* arrowX = arrowPolygonX; + float* arrowY = arrowPolygonY; + const bool horizontal = aScrollbarKind == ScrollbarKind::Horizontal; + + const float verticalOffset = [&] { + if (style != Style::Overlay) { + return 0.0f; + } + // To compensate for the scrollbar track radius we shift stuff vertically a + // bit. This 1px is arbitrary, but enough for the triangle not to overflow. + return 1.0f; + }(); + const float horizontalOffset = [&] { + if (style != Style::ThinThumb) { + return 0.0f; // Always center it in the rect. + } + // Compensate for the displacement we do of the thumb position by displacing + // the arrow as well, see comment in DoPaintScrollbarThumb. + if (horizontal) { + return -0.5f; + } + return aScrollbarKind == ScrollbarKind::VerticalRight ? 0.5f : -0.5f; + }(); + const float polygonSize = style == Style::Overlay + ? float(kDefaultWinOverlayScrollbarSize) + : float(kDefaultWinScrollbarSize); + const int32_t arrowNumPoints = ArrayLength(arrowPolygonX); + + if (aElementState.HasState(ElementState::ACTIVE)) { + arrowX = arrowPolygonXActive; + arrowY = arrowPolygonYActive; + } else if (aElementState.HasState(ElementState::HOVER)) { + arrowX = arrowPolygonXHover; + arrowY = arrowPolygonYHover; + } + + switch (aAppearance) { + case StyleAppearance::ScrollbarbuttonDown: + case StyleAppearance::ScrollbarbuttonRight: + for (int32_t i = 0; i < arrowNumPoints; i++) { + arrowY[i] += verticalOffset; + arrowY[i] *= -1; + } + [[fallthrough]]; + case StyleAppearance::ScrollbarbuttonUp: + case StyleAppearance::ScrollbarbuttonLeft: + if (horizontalOffset != 0.0f) { + for (int32_t i = 0; i < arrowNumPoints; i++) { + arrowX[i] += horizontalOffset; + } + } + break; + default: + return false; + } + + if (horizontal) { + std::swap(arrowX, arrowY); + } + + LayoutDeviceRect arrowRect(aRect); + if (style != Style::ThinThumb) { + auto margin = CSSCoord(style == Style::Overlay ? 1 : 2) * aDpiRatio; + arrowRect.Deflate(margin, margin); + } + + ThemeDrawing::PaintArrow(aDrawTarget, arrowRect, arrowX, arrowY, polygonSize, + arrowNumPoints, arrowColor); + return true; +} + +template <typename PaintBackendData> +bool ScrollbarDrawingWin11::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) { + sRGBColor thumbColor = ComputeScrollbarThumbColor( + aFrame, aStyle, aElementState, aDocumentState, aColors); + + LayoutDeviceRect thumbRect(aRect); + + const auto style = ScrollbarStyle(aFrame->PresContext()); + const bool hovered = + ScrollbarDrawing::IsParentScrollbarHoveredOrActive(aFrame) || + (style != Style::Overlay && IsScrollbarWidthThin(aStyle)); + const bool horizontal = aScrollbarKind == ScrollbarKind::Horizontal; + if (style == Style::ThickThumb) { + constexpr float kHoveredThumbRatio = + (1.0f - (11.0f / kDefaultWinScrollbarSize)) / 2.0f; + constexpr float kUnhoveredThumbRatio = + (1.0f - (9.0f / kDefaultWinScrollbarSize)) / 2.0f; + const float ratio = hovered ? kHoveredThumbRatio : kUnhoveredThumbRatio; + if (horizontal) { + thumbRect.Deflate(0, thumbRect.height * ratio); + } else { + thumbRect.Deflate(thumbRect.width * ratio, 0); + } + + auto radius = CSSCoord(hovered ? 2 : 0); + ThemeDrawing::PaintRoundedRectWithRadius(aPaintData, thumbRect, thumbColor, + sRGBColor(), 0, radius, aDpiRatio); + return true; + } + + const float defaultTrackSize = style == Style::Overlay + ? float(kDefaultWinOverlayScrollbarSize) + : float(kDefaultWinScrollbarSize); + const float trackSize = horizontal ? thumbRect.height : thumbRect.width; + const float thumbSizeInPixels = hovered ? 6.0f : 2.0f; + + // The thumb might be a bit off-center, depending on our scrollbar styles. + // + // Hovered shifts, if any, need to be accounted for in PaintScrollbarButton. + // For example, for the hovered horizontal thin scrollbar shift: + // + // Scrollbar is 17px high by default. We make the thumb 6px tall and move + // it 5px towards the bottom, so the center (8.5 initially) is displaced + // by: + // (5px + 6px / 2) - 8.5px = -0.5px + // + // Same calculations apply to other shifts. + const float shiftInPixels = [&] { + if (style == Style::Overlay) { + if (hovered) { + // Keep the center intact. + return (defaultTrackSize - thumbSizeInPixels) / 2.0f; + } + // We want logical pixels from the thumb to the edge. For LTR and + // horizontal scrollbars that means shifting down the scrollbar size minus + // the thumb. + constexpr float kSpaceToEdge = 3.0f; + if (horizontal || aScrollbarKind == ScrollbarKind::VerticalRight) { + return defaultTrackSize - thumbSizeInPixels - kSpaceToEdge; + } + // For rtl is simpler. + return kSpaceToEdge; + } + if (horizontal) { + return hovered ? 5.0f : 7.0f; + } + const bool ltr = aScrollbarKind == ScrollbarKind::VerticalRight; + return ltr ? (hovered ? 6.0f : 8.0f) : (hovered ? 5.0f : 7.0f); + }(); + + if (horizontal) { + thumbRect.y += shiftInPixels * trackSize / defaultTrackSize; + thumbRect.height *= thumbSizeInPixels / defaultTrackSize; + } else { + thumbRect.x += shiftInPixels * trackSize / defaultTrackSize; + thumbRect.width *= thumbSizeInPixels / defaultTrackSize; + } + + if (style == Style::Overlay || hovered) { + LayoutDeviceCoord radius = + (horizontal ? thumbRect.height : thumbRect.width) / 2.0f; + + MOZ_ASSERT(aRect.Contains(thumbRect)); + ThemeDrawing::PaintRoundedRectWithRadius(aPaintData, thumbRect, thumbColor, + sRGBColor(), 0, radius / aDpiRatio, + aDpiRatio); + return true; + } + + ThemeDrawing::FillRect(aPaintData, thumbRect, thumbColor); + return true; +} + +bool ScrollbarDrawingWin11::PaintScrollbarThumb( + DrawTarget& aDrawTarget, const LayoutDeviceRect& aRect, + ScrollbarKind aScrollbarKind, nsIFrame* aFrame, const ComputedStyle& aStyle, + const ElementState& aElementState, const DocumentState& aDocumentState, + const Colors& aColors, const DPIRatio& aDpiRatio) { + return DoPaintScrollbarThumb(aDrawTarget, aRect, aScrollbarKind, aFrame, + aStyle, aElementState, aDocumentState, aColors, + aDpiRatio); +} + +bool ScrollbarDrawingWin11::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) { + return DoPaintScrollbarThumb(aWrData, aRect, aScrollbarKind, aFrame, aStyle, + aElementState, aDocumentState, aColors, + aDpiRatio); +} + +} // namespace mozilla::widget |