1 /* -*- Mode: C++; tab-width: 40; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* This Source Code Form is subject to the terms of the Mozilla Public
3 * License, v. 2.0. If a copy of the MPL was not distributed with this
4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6 #include "ScrollbarDrawingWin11.h"
8 #include "mozilla/gfx/Helpers.h"
9 #include "mozilla/Maybe.h"
10 #include "mozilla/StaticPrefs_widget.h"
11 #include "nsLayoutUtils.h"
13 #include "nsNativeTheme.h"
15 using mozilla::gfx::sRGBColor
;
17 namespace mozilla::widget
{
19 // There are effectively three kinds of scrollbars in Windows 11:
21 // * Overlay scrollbars (the ones where the scrollbar disappears automatically
22 // and doesn't take space)
23 // * Non-overlay scrollbar with thin (overlay-like) thumb.
24 // * Non-overlay scrollbar with thick thumb.
26 // See bug 1755193 for some discussion on non-overlay scrollbar styles.
33 static Style
ScrollbarStyle(nsPresContext
* aPresContext
) {
34 if (aPresContext
->UseOverlayScrollbars()) {
35 return Style::Overlay
;
38 widget_non_native_theme_win11_scrollbar_force_overlay_style()) {
39 return Style::ThinThumb
;
41 return Style::ThickThumb
;
44 static constexpr CSSIntCoord kDefaultWinOverlayScrollbarSize
= CSSIntCoord(12);
45 static constexpr CSSIntCoord kDefaultWinOverlayThinScrollbarSize
=
48 auto ScrollbarDrawingWin11::GetScrollbarSizes(nsPresContext
* aPresContext
,
49 StyleScrollbarWidth aWidth
,
52 if (aOverlay
== Overlay::Yes
) {
53 // TODO(emilio): Maybe make this configurable? Though this doesn't respect
54 // classic Windows registry settings, and cocoa overlay scrollbars also
55 // don't respect the override it seems, so this should be fine.
56 CSSCoord
cssSize(aWidth
== StyleScrollbarWidth::Thin
57 ? kDefaultWinOverlayThinScrollbarSize
58 : kDefaultWinOverlayScrollbarSize
);
59 auto size
= (cssSize
* GetDPIRatioForScrollbarPart(aPresContext
)).Rounded();
62 return ScrollbarDrawingWin::GetScrollbarSizes(aPresContext
, aWidth
, aOverlay
);
65 LayoutDeviceIntSize
ScrollbarDrawingWin11::GetMinimumWidgetSize(
66 nsPresContext
* aPresContext
, StyleAppearance aAppearance
,
68 MOZ_ASSERT(nsNativeTheme::IsWidgetScrollbarPart(aAppearance
));
69 if (ScrollbarStyle(aPresContext
) != Style::ThinThumb
) {
70 return ScrollbarDrawingWin::GetMinimumWidgetSize(aPresContext
, aAppearance
,
73 constexpr float kArrowRatio
= 14.0f
/ kDefaultWinScrollbarSize
;
74 switch (aAppearance
) {
75 case StyleAppearance::ScrollbarbuttonUp
:
76 case StyleAppearance::ScrollbarbuttonDown
: {
77 if (IsScrollbarWidthThin(aFrame
)) {
80 const LayoutDeviceIntCoord size
=
81 ScrollbarDrawing::GetScrollbarSizes(aPresContext
, aFrame
).mVertical
;
82 return LayoutDeviceIntSize
{
83 size
, (kArrowRatio
* LayoutDeviceCoord(size
)).Rounded()};
85 case StyleAppearance::ScrollbarbuttonLeft
:
86 case StyleAppearance::ScrollbarbuttonRight
: {
87 if (IsScrollbarWidthThin(aFrame
)) {
90 const LayoutDeviceIntCoord size
=
91 ScrollbarDrawing::GetScrollbarSizes(aPresContext
, aFrame
).mHorizontal
;
92 return LayoutDeviceIntSize
{
93 (kArrowRatio
* LayoutDeviceCoord(size
)).Rounded(), size
};
96 return ScrollbarDrawingWin::GetMinimumWidgetSize(aPresContext
,
101 sRGBColor
ScrollbarDrawingWin11::ComputeScrollbarTrackColor(
102 nsIFrame
* aFrame
, const ComputedStyle
& aStyle
,
103 const EventStates
& aDocumentState
, const Colors
& aColors
) {
104 if (aColors
.HighContrast()) {
105 return ScrollbarDrawingWin::ComputeScrollbarTrackColor(
106 aFrame
, aStyle
, aDocumentState
, aColors
);
108 const nsStyleUI
* ui
= aStyle
.StyleUI();
109 if (ui
->mScrollbarColor
.IsColors()) {
110 return sRGBColor::FromABGR(
111 ui
->mScrollbarColor
.AsColors().track
.CalcColor(aStyle
));
113 return aColors
.IsDark() ? sRGBColor::FromU8(23, 23, 23, 255)
114 : sRGBColor::FromU8(240, 240, 240, 255);
117 sRGBColor
ScrollbarDrawingWin11::ComputeScrollbarThumbColor(
118 nsIFrame
* aFrame
, const ComputedStyle
& aStyle
,
119 const EventStates
& aElementState
, const EventStates
& aDocumentState
,
120 const Colors
& aColors
) {
121 if (aColors
.HighContrast()) {
122 return ScrollbarDrawingWin::ComputeScrollbarThumbColor(
123 aFrame
, aStyle
, aElementState
, aDocumentState
, aColors
);
125 const nscolor baseColor
= [&] {
126 const nsStyleUI
* ui
= aStyle
.StyleUI();
127 if (ui
->mScrollbarColor
.IsColors()) {
128 return ui
->mScrollbarColor
.AsColors().thumb
.CalcColor(aStyle
);
130 return aColors
.IsDark() ? NS_RGBA(149, 149, 149, 255)
131 : NS_RGBA(133, 133, 133, 255);
133 EventStates state
= aElementState
;
134 if (!IsScrollbarWidthThin(aStyle
)) {
135 // non-thin scrollbars get hover feedback by changing thumb shape, so we
136 // only provide active feedback (and we use the hover state for that as it's
138 state
&= ~NS_EVENT_STATE_HOVER
;
139 if (state
.HasState(NS_EVENT_STATE_ACTIVE
)) {
140 state
&= ~NS_EVENT_STATE_ACTIVE
;
141 state
|= NS_EVENT_STATE_HOVER
;
144 return sRGBColor::FromABGR(
145 ThemeColors::AdjustUnthemedScrollbarThumbColor(baseColor
, state
));
148 std::pair
<sRGBColor
, sRGBColor
>
149 ScrollbarDrawingWin11::ComputeScrollbarButtonColors(
150 nsIFrame
* aFrame
, StyleAppearance aAppearance
, const ComputedStyle
& aStyle
,
151 const EventStates
& aElementState
, const EventStates
& aDocumentState
,
152 const Colors
& aColors
) {
153 if (aColors
.HighContrast()) {
154 return ScrollbarDrawingWin::ComputeScrollbarButtonColors(
155 aFrame
, aAppearance
, aStyle
, aElementState
, aDocumentState
, aColors
);
157 // The button always looks transparent (the track behind it is visible), so we
159 sRGBColor arrowColor
= ComputeScrollbarThumbColor(
160 aFrame
, aStyle
, aElementState
, aDocumentState
, aColors
);
161 return {sRGBColor::White(0.0f
), arrowColor
};
164 bool ScrollbarDrawingWin11::PaintScrollbarButton(
165 DrawTarget
& aDrawTarget
, StyleAppearance aAppearance
,
166 const LayoutDeviceRect
& aRect
, ScrollbarKind aScrollbarKind
,
167 nsIFrame
* aFrame
, const ComputedStyle
& aStyle
,
168 const EventStates
& aElementState
, const EventStates
& aDocumentState
,
169 const Colors
& aColors
, const DPIRatio
& aDpiRatio
) {
170 if (!ScrollbarDrawing::IsParentScrollbarHoveredOrActive(aFrame
)) {
174 const auto style
= ScrollbarStyle(aFrame
->PresContext());
175 auto [buttonColor
, arrowColor
] = ComputeScrollbarButtonColors(
176 aFrame
, aAppearance
, aStyle
, aElementState
, aDocumentState
, aColors
);
177 if (style
!= Style::Overlay
) {
178 aDrawTarget
.FillRect(aRect
.ToUnknownRect(),
179 gfx::ColorPattern(ToDeviceColor(buttonColor
)));
182 // Start with Up arrow.
183 float arrowPolygonX
[] = {-4.5f
, 4.5f
, 4.5f
, 0.5f
, -0.5f
, -4.5f
, -4.5f
};
184 float arrowPolygonXActive
[] = {-4.0f
, 4.0f
, 4.0f
, -0.25f
,
185 -0.25f
, -4.0f
, -4.0f
};
186 float arrowPolygonXHover
[] = {-5.0f
, 5.0f
, 5.0f
, 0.75f
, -0.75f
, -5.0f
, -5.0f
};
187 float arrowPolygonY
[] = {2.5f
, 2.5f
, 1.0f
, -4.0f
, -4.0f
, 1.0f
, 2.5f
};
188 float arrowPolygonYActive
[] = {2.0f
, 2.0f
, 0.5f
, -3.5f
, -3.5f
, 0.5f
, 2.0f
};
189 float arrowPolygonYHover
[] = {3.0f
, 3.0f
, 1.5f
, -4.5f
, -4.5f
, 1.5f
, 3.0f
};
190 float* arrowX
= arrowPolygonX
;
191 float* arrowY
= arrowPolygonY
;
192 const bool horizontal
= aScrollbarKind
== ScrollbarKind::Horizontal
;
194 const float verticalOffset
= [&] {
195 if (style
!= Style::Overlay
) {
198 // To compensate for the scrollbar track radius we shift stuff vertically a
199 // bit. This 1px is arbitrary, but enough for the triangle not to overflow.
202 const float horizontalOffset
= [&] {
203 if (style
!= Style::ThinThumb
) {
204 return 0.0f
; // Always center it in the rect.
206 // Compensate for the displacement we do of the thumb position by displacing
207 // the arrow as well, see comment in DoPaintScrollbarThumb.
211 return aScrollbarKind
== ScrollbarKind::VerticalRight
? 0.5f
: -0.5f
;
213 const float polygonSize
= style
== Style::Overlay
214 ? float(kDefaultWinOverlayScrollbarSize
)
215 : float(kDefaultWinScrollbarSize
);
216 const int32_t arrowNumPoints
= ArrayLength(arrowPolygonX
);
218 if (aElementState
.HasState(NS_EVENT_STATE_ACTIVE
)) {
219 arrowX
= arrowPolygonXActive
;
220 arrowY
= arrowPolygonYActive
;
221 } else if (aElementState
.HasState(NS_EVENT_STATE_HOVER
)) {
222 arrowX
= arrowPolygonXHover
;
223 arrowY
= arrowPolygonYHover
;
226 switch (aAppearance
) {
227 case StyleAppearance::ScrollbarbuttonDown
:
228 case StyleAppearance::ScrollbarbuttonRight
:
229 for (int32_t i
= 0; i
< arrowNumPoints
; i
++) {
230 arrowY
[i
] += verticalOffset
;
234 case StyleAppearance::ScrollbarbuttonUp
:
235 case StyleAppearance::ScrollbarbuttonLeft
:
236 if (horizontalOffset
!= 0.0f
) {
237 for (int32_t i
= 0; i
< arrowNumPoints
; i
++) {
238 arrowX
[i
] += horizontalOffset
;
247 std::swap(arrowX
, arrowY
);
250 LayoutDeviceRect
arrowRect(aRect
);
251 if (style
!= Style::ThinThumb
) {
252 auto margin
= CSSCoord(style
== Style::Overlay
? 1 : 2) * aDpiRatio
;
253 arrowRect
.Deflate(margin
, margin
);
256 ThemeDrawing::PaintArrow(aDrawTarget
, arrowRect
, arrowX
, arrowY
, polygonSize
,
257 arrowNumPoints
, arrowColor
);
261 template <typename PaintBackendData
>
262 bool ScrollbarDrawingWin11::DoPaintScrollbarThumb(
263 PaintBackendData
& aPaintData
, const LayoutDeviceRect
& aRect
,
264 ScrollbarKind aScrollbarKind
, nsIFrame
* aFrame
, const ComputedStyle
& aStyle
,
265 const EventStates
& aElementState
, const EventStates
& aDocumentState
,
266 const Colors
& aColors
, const DPIRatio
& aDpiRatio
) {
267 sRGBColor thumbColor
= ComputeScrollbarThumbColor(
268 aFrame
, aStyle
, aElementState
, aDocumentState
, aColors
);
270 LayoutDeviceRect
thumbRect(aRect
);
272 const auto style
= ScrollbarStyle(aFrame
->PresContext());
274 ScrollbarDrawing::IsParentScrollbarHoveredOrActive(aFrame
) ||
275 (style
!= Style::Overlay
&& IsScrollbarWidthThin(aStyle
));
276 const bool horizontal
= aScrollbarKind
== ScrollbarKind::Horizontal
;
277 if (style
== Style::ThickThumb
) {
278 constexpr float kHoveredThumbRatio
=
279 (1.0f
- (11.0f
/ kDefaultWinScrollbarSize
)) / 2.0f
;
280 constexpr float kUnhoveredThumbRatio
=
281 (1.0f
- (9.0f
/ kDefaultWinScrollbarSize
)) / 2.0f
;
282 const float ratio
= hovered
? kHoveredThumbRatio
: kUnhoveredThumbRatio
;
284 thumbRect
.Deflate(0, thumbRect
.height
* ratio
);
286 thumbRect
.Deflate(thumbRect
.width
* ratio
, 0);
289 auto radius
= CSSCoord(hovered
? 2 : 0);
290 ThemeDrawing::PaintRoundedRectWithRadius(aPaintData
, thumbRect
, thumbColor
,
291 sRGBColor(), 0, radius
, aDpiRatio
);
295 const float defaultTrackSize
= style
== Style::Overlay
296 ? float(kDefaultWinOverlayScrollbarSize
)
297 : float(kDefaultWinScrollbarSize
);
298 const float trackSize
= horizontal
? thumbRect
.height
: thumbRect
.width
;
299 const float thumbSizeInPixels
= hovered
? 6.0f
: 2.0f
;
301 // The thumb might be a bit off-center, depending on our scrollbar styles.
303 // Hovered shifts, if any, need to be accounted for in PaintScrollbarButton.
304 // For example, for the hovered horizontal thin scrollbar shift:
306 // Scrollbar is 17px high by default. We make the thumb 6px tall and move
307 // it 5px towards the bottom, so the center (8.5 initially) is displaced
309 // (5px + 6px / 2) - 8.5px = -0.5px
311 // Same calculations apply to other shifts.
312 const float shiftInPixels
= [&] {
313 if (style
== Style::Overlay
) {
315 // Keep the center intact.
316 return (defaultTrackSize
- thumbSizeInPixels
) / 2.0f
;
318 // We want logical pixels from the thumb to the edge. For LTR and
319 // horizontal scrollbars that means shifting down the scrollbar size minus
321 constexpr float kSpaceToEdge
= 3.0f
;
322 if (horizontal
|| aScrollbarKind
== ScrollbarKind::VerticalRight
) {
323 return defaultTrackSize
- thumbSizeInPixels
- kSpaceToEdge
;
325 // For rtl is simpler.
329 return hovered
? 5.0f
: 7.0f
;
331 const bool ltr
= aScrollbarKind
== ScrollbarKind::VerticalRight
;
332 return ltr
? (hovered
? 6.0f
: 8.0f
) : (hovered
? 5.0f
: 7.0f
);
336 thumbRect
.y
+= shiftInPixels
* trackSize
/ defaultTrackSize
;
337 thumbRect
.height
*= thumbSizeInPixels
/ defaultTrackSize
;
339 thumbRect
.x
+= shiftInPixels
* trackSize
/ defaultTrackSize
;
340 thumbRect
.width
*= thumbSizeInPixels
/ defaultTrackSize
;
343 if (style
== Style::Overlay
|| hovered
) {
344 LayoutDeviceCoord radius
=
345 (horizontal
? thumbRect
.height
: thumbRect
.width
) / 2.0f
;
347 MOZ_ASSERT(aRect
.Contains(thumbRect
));
348 ThemeDrawing::PaintRoundedRectWithRadius(aPaintData
, thumbRect
, thumbColor
,
349 sRGBColor(), 0, radius
/ aDpiRatio
,
354 ThemeDrawing::FillRect(aPaintData
, thumbRect
, thumbColor
);
358 bool ScrollbarDrawingWin11::PaintScrollbarThumb(
359 DrawTarget
& aDrawTarget
, const LayoutDeviceRect
& aRect
,
360 ScrollbarKind aScrollbarKind
, nsIFrame
* aFrame
, const ComputedStyle
& aStyle
,
361 const EventStates
& aElementState
, const EventStates
& aDocumentState
,
362 const Colors
& aColors
, const DPIRatio
& aDpiRatio
) {
363 return DoPaintScrollbarThumb(aDrawTarget
, aRect
, aScrollbarKind
, aFrame
,
364 aStyle
, aElementState
, aDocumentState
, aColors
,
368 bool ScrollbarDrawingWin11::PaintScrollbarThumb(
369 WebRenderBackendData
& aWrData
, const LayoutDeviceRect
& aRect
,
370 ScrollbarKind aScrollbarKind
, nsIFrame
* aFrame
, const ComputedStyle
& aStyle
,
371 const EventStates
& aElementState
, const EventStates
& aDocumentState
,
372 const Colors
& aColors
, const DPIRatio
& aDpiRatio
) {
373 return DoPaintScrollbarThumb(aWrData
, aRect
, aScrollbarKind
, aFrame
, aStyle
,
374 aElementState
, aDocumentState
, aColors
,
378 } // namespace mozilla::widget