Bug 1852149 Part 1 - Add margin-rule pref r=firefox-style-system-reviewers,emilio
[gecko.git] / widget / ScrollbarDrawingWin11.cpp
blob8517b60baa81a1a32a25c6bff1e1cd1d35fbfca9
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"
12 #include "Theme.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.
27 enum class Style {
28 Overlay,
29 ThinThumb,
30 ThickThumb,
33 static Style ScrollbarStyle(nsPresContext* aPresContext) {
34 if (aPresContext->UseOverlayScrollbars()) {
35 return Style::Overlay;
37 if (StaticPrefs::
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 =
46 CSSIntCoord(10);
48 LayoutDeviceIntSize ScrollbarDrawingWin11::GetMinimumWidgetSize(
49 nsPresContext* aPresContext, StyleAppearance aAppearance,
50 nsIFrame* aFrame) {
51 MOZ_ASSERT(nsNativeTheme::IsWidgetScrollbarPart(aAppearance));
52 if (ScrollbarStyle(aPresContext) != Style::ThinThumb) {
53 return ScrollbarDrawingWin::GetMinimumWidgetSize(aPresContext, aAppearance,
54 aFrame);
56 constexpr float kArrowRatio = 14.0f / kDefaultWinScrollbarSize;
57 switch (aAppearance) {
58 case StyleAppearance::ScrollbarbuttonUp:
59 case StyleAppearance::ScrollbarbuttonDown:
60 case StyleAppearance::ScrollbarbuttonLeft:
61 case StyleAppearance::ScrollbarbuttonRight: {
62 if (IsScrollbarWidthThin(aFrame)) {
63 return {};
65 const LayoutDeviceIntCoord size =
66 ScrollbarDrawing::GetScrollbarSize(aPresContext, aFrame);
67 return LayoutDeviceIntSize{
68 size, (kArrowRatio * LayoutDeviceCoord(size)).Rounded()};
70 default:
71 return ScrollbarDrawingWin::GetMinimumWidgetSize(aPresContext,
72 aAppearance, aFrame);
76 sRGBColor ScrollbarDrawingWin11::ComputeScrollbarTrackColor(
77 nsIFrame* aFrame, const ComputedStyle& aStyle,
78 const DocumentState& aDocumentState, const Colors& aColors) {
79 if (aColors.HighContrast()) {
80 return ScrollbarDrawingWin::ComputeScrollbarTrackColor(
81 aFrame, aStyle, aDocumentState, aColors);
83 const nsStyleUI* ui = aStyle.StyleUI();
84 if (ui->mScrollbarColor.IsColors()) {
85 return sRGBColor::FromABGR(
86 ui->mScrollbarColor.AsColors().track.CalcColor(aStyle));
88 return aColors.IsDark() ? sRGBColor::FromU8(23, 23, 23, 255)
89 : sRGBColor::FromU8(240, 240, 240, 255);
92 sRGBColor ScrollbarDrawingWin11::ComputeScrollbarThumbColor(
93 nsIFrame* aFrame, const ComputedStyle& aStyle,
94 const ElementState& aElementState, const DocumentState& aDocumentState,
95 const Colors& aColors) {
96 if (aColors.HighContrast()) {
97 return ScrollbarDrawingWin::ComputeScrollbarThumbColor(
98 aFrame, aStyle, aElementState, aDocumentState, aColors);
100 const nscolor baseColor = [&] {
101 const nsStyleUI* ui = aStyle.StyleUI();
102 if (ui->mScrollbarColor.IsColors()) {
103 return ui->mScrollbarColor.AsColors().thumb.CalcColor(aStyle);
105 return aColors.IsDark() ? NS_RGBA(149, 149, 149, 255)
106 : NS_RGBA(133, 133, 133, 255);
107 }();
108 ElementState state = aElementState;
109 if (!IsScrollbarWidthThin(aStyle)) {
110 // non-thin scrollbars get hover feedback by changing thumb shape, so we
111 // only provide active feedback (and we use the hover state for that as it's
112 // more subtle).
113 state &= ~ElementState::HOVER;
114 if (state.HasState(ElementState::ACTIVE)) {
115 state &= ~ElementState::ACTIVE;
116 state |= ElementState::HOVER;
119 return sRGBColor::FromABGR(
120 ThemeColors::AdjustUnthemedScrollbarThumbColor(baseColor, state));
123 std::pair<sRGBColor, sRGBColor>
124 ScrollbarDrawingWin11::ComputeScrollbarButtonColors(
125 nsIFrame* aFrame, StyleAppearance aAppearance, const ComputedStyle& aStyle,
126 const ElementState& aElementState, const DocumentState& aDocumentState,
127 const Colors& aColors) {
128 if (aColors.HighContrast()) {
129 return ScrollbarDrawingWin::ComputeScrollbarButtonColors(
130 aFrame, aAppearance, aStyle, aElementState, aDocumentState, aColors);
132 // The button always looks transparent (the track behind it is visible), so we
133 // can hardcode it.
134 sRGBColor arrowColor = ComputeScrollbarThumbColor(
135 aFrame, aStyle, aElementState, aDocumentState, aColors);
136 return {sRGBColor::White(0.0f), arrowColor};
139 bool ScrollbarDrawingWin11::PaintScrollbarButton(
140 DrawTarget& aDrawTarget, StyleAppearance aAppearance,
141 const LayoutDeviceRect& aRect, ScrollbarKind aScrollbarKind,
142 nsIFrame* aFrame, const ComputedStyle& aStyle,
143 const ElementState& aElementState, const DocumentState& aDocumentState,
144 const Colors& aColors, const DPIRatio& aDpiRatio) {
145 if (!ScrollbarDrawing::IsParentScrollbarHoveredOrActive(aFrame)) {
146 return true;
149 const auto style = ScrollbarStyle(aFrame->PresContext());
150 auto [buttonColor, arrowColor] = ComputeScrollbarButtonColors(
151 aFrame, aAppearance, aStyle, aElementState, aDocumentState, aColors);
152 if (style != Style::Overlay) {
153 aDrawTarget.FillRect(aRect.ToUnknownRect(),
154 gfx::ColorPattern(ToDeviceColor(buttonColor)));
157 // Start with Up arrow.
158 float arrowPolygonX[] = {-4.5f, 4.5f, 4.5f, 0.5f, -0.5f, -4.5f, -4.5f};
159 float arrowPolygonXActive[] = {-4.0f, 4.0f, 4.0f, -0.25f,
160 -0.25f, -4.0f, -4.0f};
161 float arrowPolygonXHover[] = {-5.0f, 5.0f, 5.0f, 0.75f, -0.75f, -5.0f, -5.0f};
162 float arrowPolygonY[] = {2.5f, 2.5f, 1.0f, -4.0f, -4.0f, 1.0f, 2.5f};
163 float arrowPolygonYActive[] = {2.0f, 2.0f, 0.5f, -3.5f, -3.5f, 0.5f, 2.0f};
164 float arrowPolygonYHover[] = {3.0f, 3.0f, 1.5f, -4.5f, -4.5f, 1.5f, 3.0f};
165 float* arrowX = arrowPolygonX;
166 float* arrowY = arrowPolygonY;
167 const bool horizontal = aScrollbarKind == ScrollbarKind::Horizontal;
169 const float verticalOffset = [&] {
170 if (style != Style::Overlay) {
171 return 0.0f;
173 // To compensate for the scrollbar track radius we shift stuff vertically a
174 // bit. This 1px is arbitrary, but enough for the triangle not to overflow.
175 return 1.0f;
176 }();
177 const float horizontalOffset = [&] {
178 if (style != Style::ThinThumb) {
179 return 0.0f; // Always center it in the rect.
181 // Compensate for the displacement we do of the thumb position by displacing
182 // the arrow as well, see comment in DoPaintScrollbarThumb.
183 if (horizontal) {
184 return -0.5f;
186 return aScrollbarKind == ScrollbarKind::VerticalRight ? 0.5f : -0.5f;
187 }();
188 const float polygonSize = style == Style::Overlay
189 ? float(kDefaultWinOverlayScrollbarSize)
190 : float(kDefaultWinScrollbarSize);
191 const int32_t arrowNumPoints = ArrayLength(arrowPolygonX);
193 if (aElementState.HasState(ElementState::ACTIVE)) {
194 arrowX = arrowPolygonXActive;
195 arrowY = arrowPolygonYActive;
196 } else if (aElementState.HasState(ElementState::HOVER)) {
197 arrowX = arrowPolygonXHover;
198 arrowY = arrowPolygonYHover;
201 switch (aAppearance) {
202 case StyleAppearance::ScrollbarbuttonDown:
203 case StyleAppearance::ScrollbarbuttonRight:
204 for (int32_t i = 0; i < arrowNumPoints; i++) {
205 arrowY[i] += verticalOffset;
206 arrowY[i] *= -1;
208 [[fallthrough]];
209 case StyleAppearance::ScrollbarbuttonUp:
210 case StyleAppearance::ScrollbarbuttonLeft:
211 if (horizontalOffset != 0.0f) {
212 for (int32_t i = 0; i < arrowNumPoints; i++) {
213 arrowX[i] += horizontalOffset;
216 break;
217 default:
218 return false;
221 if (horizontal) {
222 std::swap(arrowX, arrowY);
225 LayoutDeviceRect arrowRect(aRect);
226 if (style != Style::ThinThumb) {
227 auto margin = CSSCoord(style == Style::Overlay ? 1 : 2) * aDpiRatio;
228 arrowRect.Deflate(margin, margin);
231 ThemeDrawing::PaintArrow(aDrawTarget, arrowRect, arrowX, arrowY, polygonSize,
232 arrowNumPoints, arrowColor);
233 return true;
236 template <typename PaintBackendData>
237 bool ScrollbarDrawingWin11::DoPaintScrollbarThumb(
238 PaintBackendData& aPaintData, const LayoutDeviceRect& aRect,
239 ScrollbarKind aScrollbarKind, nsIFrame* aFrame, const ComputedStyle& aStyle,
240 const ElementState& aElementState, const DocumentState& aDocumentState,
241 const Colors& aColors, const DPIRatio& aDpiRatio) {
242 sRGBColor thumbColor = ComputeScrollbarThumbColor(
243 aFrame, aStyle, aElementState, aDocumentState, aColors);
245 LayoutDeviceRect thumbRect(aRect);
247 const auto style = ScrollbarStyle(aFrame->PresContext());
248 const bool hovered =
249 ScrollbarDrawing::IsParentScrollbarHoveredOrActive(aFrame) ||
250 (style != Style::Overlay && IsScrollbarWidthThin(aStyle));
251 const bool horizontal = aScrollbarKind == ScrollbarKind::Horizontal;
252 if (style == Style::ThickThumb) {
253 constexpr float kHoveredThumbRatio =
254 (1.0f - (11.0f / kDefaultWinScrollbarSize)) / 2.0f;
255 constexpr float kUnhoveredThumbRatio =
256 (1.0f - (9.0f / kDefaultWinScrollbarSize)) / 2.0f;
257 const float ratio = hovered ? kHoveredThumbRatio : kUnhoveredThumbRatio;
258 if (horizontal) {
259 thumbRect.Deflate(0, thumbRect.height * ratio);
260 } else {
261 thumbRect.Deflate(thumbRect.width * ratio, 0);
264 auto radius = CSSCoord(hovered ? 2 : 0);
265 ThemeDrawing::PaintRoundedRectWithRadius(aPaintData, thumbRect, thumbColor,
266 sRGBColor(), 0, radius, aDpiRatio);
267 return true;
270 const float defaultTrackSize = style == Style::Overlay
271 ? float(kDefaultWinOverlayScrollbarSize)
272 : float(kDefaultWinScrollbarSize);
273 const float trackSize = horizontal ? thumbRect.height : thumbRect.width;
274 const float thumbSizeInPixels = hovered ? 6.0f : 2.0f;
276 // The thumb might be a bit off-center, depending on our scrollbar styles.
278 // Hovered shifts, if any, need to be accounted for in PaintScrollbarButton.
279 // For example, for the hovered horizontal thin scrollbar shift:
281 // Scrollbar is 17px high by default. We make the thumb 6px tall and move
282 // it 5px towards the bottom, so the center (8.5 initially) is displaced
283 // by:
284 // (5px + 6px / 2) - 8.5px = -0.5px
286 // Same calculations apply to other shifts.
287 const float shiftInPixels = [&] {
288 if (style == Style::Overlay) {
289 if (hovered) {
290 // Keep the center intact.
291 return (defaultTrackSize - thumbSizeInPixels) / 2.0f;
293 // We want logical pixels from the thumb to the edge. For LTR and
294 // horizontal scrollbars that means shifting down the scrollbar size minus
295 // the thumb.
296 constexpr float kSpaceToEdge = 3.0f;
297 if (horizontal || aScrollbarKind == ScrollbarKind::VerticalRight) {
298 return defaultTrackSize - thumbSizeInPixels - kSpaceToEdge;
300 // For rtl is simpler.
301 return kSpaceToEdge;
303 if (horizontal) {
304 return hovered ? 5.0f : 7.0f;
306 const bool ltr = aScrollbarKind == ScrollbarKind::VerticalRight;
307 return ltr ? (hovered ? 6.0f : 8.0f) : (hovered ? 5.0f : 7.0f);
308 }();
310 if (horizontal) {
311 thumbRect.y += shiftInPixels * trackSize / defaultTrackSize;
312 thumbRect.height *= thumbSizeInPixels / defaultTrackSize;
313 } else {
314 thumbRect.x += shiftInPixels * trackSize / defaultTrackSize;
315 thumbRect.width *= thumbSizeInPixels / defaultTrackSize;
318 if (style == Style::Overlay || hovered) {
319 LayoutDeviceCoord radius =
320 (horizontal ? thumbRect.height : thumbRect.width) / 2.0f;
322 MOZ_ASSERT(aRect.Contains(thumbRect));
323 ThemeDrawing::PaintRoundedRectWithRadius(aPaintData, thumbRect, thumbColor,
324 sRGBColor(), 0, radius / aDpiRatio,
325 aDpiRatio);
326 return true;
329 ThemeDrawing::FillRect(aPaintData, thumbRect, thumbColor);
330 return true;
333 bool ScrollbarDrawingWin11::PaintScrollbarThumb(
334 DrawTarget& aDrawTarget, const LayoutDeviceRect& aRect,
335 ScrollbarKind aScrollbarKind, nsIFrame* aFrame, const ComputedStyle& aStyle,
336 const ElementState& aElementState, const DocumentState& aDocumentState,
337 const Colors& aColors, const DPIRatio& aDpiRatio) {
338 return DoPaintScrollbarThumb(aDrawTarget, aRect, aScrollbarKind, aFrame,
339 aStyle, aElementState, aDocumentState, aColors,
340 aDpiRatio);
343 bool ScrollbarDrawingWin11::PaintScrollbarThumb(
344 WebRenderBackendData& aWrData, const LayoutDeviceRect& aRect,
345 ScrollbarKind aScrollbarKind, nsIFrame* aFrame, const ComputedStyle& aStyle,
346 const ElementState& aElementState, const DocumentState& aDocumentState,
347 const Colors& aColors, const DPIRatio& aDpiRatio) {
348 return DoPaintScrollbarThumb(aWrData, aRect, aScrollbarKind, aFrame, aStyle,
349 aElementState, aDocumentState, aColors,
350 aDpiRatio);
353 void ScrollbarDrawingWin11::RecomputeScrollbarParams() {
354 ScrollbarDrawingWin::RecomputeScrollbarParams();
355 // TODO(emilio): Maybe make this configurable? Though this doesn't respect
356 // classic Windows registry settings, and cocoa overlay scrollbars also don't
357 // respect the override it seems, so this should be fine.
358 ConfigureScrollbarSize(StyleScrollbarWidth::Thin, Overlay::Yes,
359 kDefaultWinOverlayThinScrollbarSize);
360 ConfigureScrollbarSize(StyleScrollbarWidth::Auto, Overlay::Yes,
361 kDefaultWinOverlayScrollbarSize);
364 } // namespace mozilla::widget