summaryrefslogtreecommitdiffstats
path: root/widget/ScrollbarDrawingWin11.cpp
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--widget/ScrollbarDrawingWin11.cpp378
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