Bug 1769628 [wpt PR 34081] - Update wpt metadata, a=testonly
[gecko.git] / widget / ScrollbarDrawingCocoa.cpp
blob3454044bef84f27ece61c0e829228e4d0e5e4567
1 /* -*- Mode: C++; c-basic-offset: 2; indent-tabs-mode: nil; tab-width: 2; -*- */
2 /* vim: set sw=2 ts=8 et tw=80 : */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
7 #include "ScrollbarDrawingCocoa.h"
9 #include "mozilla/gfx/Helpers.h"
10 #include "mozilla/RelativeLuminanceUtils.h"
11 #include "mozilla/StaticPrefs_widget.h"
12 #include "nsContainerFrame.h"
13 #include "nsAlgorithm.h"
14 #include "nsIFrame.h"
15 #include "nsLayoutUtils.h"
16 #include "nsLookAndFeel.h"
17 #include "nsNativeTheme.h"
19 using namespace mozilla::gfx;
20 namespace mozilla::widget {
22 using ScrollbarKind = ScrollbarDrawing::ScrollbarKind;
24 struct ColoredRect {
25 LayoutDeviceRect mRect;
26 nscolor mColor = 0;
29 // The caller can draw this rectangle with rounded corners as appropriate.
30 struct ThumbRect {
31 LayoutDeviceRect mRect;
32 nscolor mFillColor = 0;
33 nscolor mStrokeColor = 0;
34 float mStrokeWidth = 0.0f;
35 float mStrokeOutset = 0.0f;
38 using ScrollbarTrackRects = Array<ColoredRect, 4>;
39 using ScrollCornerRects = Array<ColoredRect, 7>;
41 struct ScrollbarParams {
42 bool isOverlay = false;
43 bool isRolledOver = false;
44 bool isSmall = false;
45 bool isHorizontal = false;
46 bool isRtl = false;
47 bool isOnDarkBackground = false;
48 bool isCustom = false;
49 // Two colors only used when custom is true.
50 nscolor trackColor = NS_RGBA(0, 0, 0, 0);
51 nscolor faceColor = NS_RGBA(0, 0, 0, 0);
54 static ScrollbarParams ComputeScrollbarParams(nsIFrame* aFrame,
55 const ComputedStyle& aStyle,
56 ScrollbarKind aScrollbarKind) {
57 ScrollbarParams params;
58 params.isOverlay =
59 nsLookAndFeel::GetInt(LookAndFeel::IntID::UseOverlayScrollbars) != 0;
60 params.isRolledOver = ScrollbarDrawing::IsParentScrollbarRolledOver(aFrame);
61 params.isSmall =
62 aStyle.StyleUIReset()->ScrollbarWidth() == StyleScrollbarWidth::Thin;
63 params.isRtl = aScrollbarKind == ScrollbarKind::VerticalLeft;
64 params.isHorizontal = aScrollbarKind == ScrollbarKind::Horizontal;
65 params.isOnDarkBackground = !StaticPrefs::widget_disable_dark_scrollbar() &&
66 nsNativeTheme::IsDarkBackground(aFrame);
68 const nsStyleUI* ui = aStyle.StyleUI();
69 if (ui->HasCustomScrollbars()) {
70 const auto& colors = ui->mScrollbarColor.AsColors();
71 params.isCustom = true;
72 params.trackColor = colors.track.CalcColor(aStyle);
73 params.faceColor = colors.thumb.CalcColor(aStyle);
76 return params;
79 LayoutDeviceIntSize ScrollbarDrawingCocoa::GetMinimumWidgetSize(
80 nsPresContext* aPresContext, StyleAppearance aAppearance,
81 nsIFrame* aFrame) {
82 MOZ_ASSERT(nsNativeTheme::IsWidgetScrollbarPart(aAppearance));
84 auto minSize = [&] {
85 switch (aAppearance) {
86 case StyleAppearance::ScrollbarthumbHorizontal:
87 return IntSize{26, 0};
88 case StyleAppearance::ScrollbarthumbVertical:
89 return IntSize{0, 26};
90 case StyleAppearance::ScrollbarVertical:
91 case StyleAppearance::ScrollbarHorizontal:
92 case StyleAppearance::ScrollbartrackVertical:
93 case StyleAppearance::ScrollbartrackHorizontal: {
94 ComputedStyle* style = nsLayoutUtils::StyleForScrollbar(aFrame);
95 auto scrollbarWidth = style->StyleUIReset()->ScrollbarWidth();
96 auto size = GetScrollbarSize(
97 scrollbarWidth,
98 LookAndFeel::GetInt(LookAndFeel::IntID::UseOverlayScrollbars));
99 return IntSize{size, size};
101 case StyleAppearance::ScrollbarbuttonUp:
102 case StyleAppearance::ScrollbarbuttonDown:
103 return IntSize{15, 16};
104 case StyleAppearance::ScrollbarbuttonLeft:
105 case StyleAppearance::ScrollbarbuttonRight:
106 return IntSize{16, 15};
107 default:
108 return IntSize{};
110 }();
112 auto dpi = GetDPIRatioForScrollbarPart(aPresContext).scale;
113 if (dpi >= 2.0f) {
114 return LayoutDeviceIntSize{minSize.width * 2, minSize.height * 2};
116 return LayoutDeviceIntSize{minSize.width, minSize.height};
119 /*static*/
120 CSSIntCoord ScrollbarDrawingCocoa::GetScrollbarSize(StyleScrollbarWidth aWidth,
121 bool aOverlay) {
122 bool isSmall = aWidth == StyleScrollbarWidth::Thin;
123 if (aOverlay) {
124 return isSmall ? 14 : 16;
126 return isSmall ? 11 : 15;
129 /*static*/
130 LayoutDeviceIntCoord ScrollbarDrawingCocoa::GetScrollbarSize(
131 StyleScrollbarWidth aWidth, bool aOverlay, DPIRatio aDpiRatio) {
132 CSSIntCoord size = GetScrollbarSize(aWidth, aOverlay);
133 if (aDpiRatio.scale >= 2.0f) {
134 return int32_t(size) * 2;
136 return int32_t(size);
139 auto ScrollbarDrawingCocoa::GetScrollbarSizes(nsPresContext* aPresContext,
140 StyleScrollbarWidth aWidth,
141 Overlay aOverlay)
142 -> ScrollbarSizes {
143 auto size = GetScrollbarSize(aWidth, aOverlay == Overlay::Yes,
144 GetDPIRatioForScrollbarPart(aPresContext));
145 return {size, size};
148 static ThumbRect GetThumbRect(const LayoutDeviceRect& aRect,
149 const ScrollbarParams& aParams, float aScale) {
150 // This matches the sizing checks in GetMinimumWidgetSize etc.
151 aScale = aScale >= 2.0f ? 2.0f : 1.0f;
153 // Compute the thumb thickness. This varies based on aParams.small,
154 // aParams.overlay and aParams.rolledOver. non-overlay: 6 / 8, overlay
155 // non-hovered: 5 / 7, overlay hovered: 9 / 11
156 float thickness = aParams.isSmall ? 6.0f : 8.0f;
157 if (aParams.isOverlay) {
158 thickness -= 1.0f;
159 if (aParams.isRolledOver) {
160 thickness += 4.0f;
163 thickness *= aScale;
165 // Compute the thumb rect.
166 const float outerSpacing =
167 ((aParams.isOverlay || aParams.isSmall) ? 1.0f : 2.0f) * aScale;
168 LayoutDeviceRect thumbRect = aRect;
169 thumbRect.Deflate(1.0f * aScale);
170 if (aParams.isHorizontal) {
171 float bottomEdge = thumbRect.YMost() - outerSpacing;
172 thumbRect.SetBoxY(bottomEdge - thickness, bottomEdge);
173 } else {
174 if (aParams.isRtl) {
175 float leftEdge = thumbRect.X() + outerSpacing;
176 thumbRect.SetBoxX(leftEdge, leftEdge + thickness);
177 } else {
178 float rightEdge = thumbRect.XMost() - outerSpacing;
179 thumbRect.SetBoxX(rightEdge - thickness, rightEdge);
183 // Compute the thumb fill color.
184 nscolor faceColor;
185 if (aParams.isCustom) {
186 faceColor = aParams.faceColor;
187 } else {
188 if (aParams.isOverlay) {
189 faceColor = aParams.isOnDarkBackground ? NS_RGBA(255, 255, 255, 128)
190 : NS_RGBA(0, 0, 0, 128);
191 } else if (aParams.isOnDarkBackground) {
192 faceColor = aParams.isRolledOver ? NS_RGBA(158, 158, 158, 255)
193 : NS_RGBA(117, 117, 117, 255);
194 } else {
195 faceColor = aParams.isRolledOver ? NS_RGBA(125, 125, 125, 255)
196 : NS_RGBA(194, 194, 194, 255);
200 nscolor strokeColor = 0;
201 float strokeOutset = 0.0f;
202 float strokeWidth = 0.0f;
204 // Overlay scrollbars have an additional stroke around the fill.
205 if (aParams.isOverlay) {
206 // For the default alpha of 128 we want to end up with 48 in the outline.
207 constexpr float kAlphaScaling = 48.0f / 128.0f;
208 const uint8_t strokeAlpha =
209 uint8_t(clamped(NS_GET_A(faceColor) * kAlphaScaling, 0.0f, 48.0f));
210 if (strokeAlpha) {
211 strokeOutset = (aParams.isOnDarkBackground ? 0.3f : 0.5f) * aScale;
212 strokeWidth = (aParams.isOnDarkBackground ? 0.6f : 0.8f) * aScale;
214 strokeColor = aParams.isOnDarkBackground
215 ? NS_RGBA(0, 0, 0, strokeAlpha)
216 : NS_RGBA(255, 255, 255, strokeAlpha);
220 return {thumbRect, faceColor, strokeColor, strokeWidth, strokeOutset};
223 struct ScrollbarTrackDecorationColors {
224 nscolor mInnerColor = 0;
225 nscolor mShadowColor = 0;
226 nscolor mOuterColor = 0;
229 static ScrollbarTrackDecorationColors ComputeScrollbarTrackDecorationColors(
230 nscolor aTrackColor) {
231 ScrollbarTrackDecorationColors result;
232 float luminance = RelativeLuminanceUtils::Compute(aTrackColor);
233 if (luminance >= 0.5f) {
234 result.mInnerColor =
235 RelativeLuminanceUtils::Adjust(aTrackColor, luminance * 0.836f);
236 result.mShadowColor =
237 RelativeLuminanceUtils::Adjust(aTrackColor, luminance * 0.982f);
238 result.mOuterColor =
239 RelativeLuminanceUtils::Adjust(aTrackColor, luminance * 0.886f);
240 } else {
241 result.mInnerColor =
242 RelativeLuminanceUtils::Adjust(aTrackColor, luminance * 1.196f);
243 result.mShadowColor =
244 RelativeLuminanceUtils::Adjust(aTrackColor, luminance * 1.018f);
245 result.mOuterColor =
246 RelativeLuminanceUtils::Adjust(aTrackColor, luminance * 1.129f);
248 return result;
251 static bool GetScrollbarTrackRects(const LayoutDeviceRect& aRect,
252 const ScrollbarParams& aParams, float aScale,
253 ScrollbarTrackRects& aRects) {
254 if (aParams.isOverlay && !aParams.isRolledOver) {
255 // Non-hovered overlay scrollbars don't have a track. Draw nothing.
256 return false;
259 // This matches the sizing checks in GetMinimumWidgetSize etc.
260 aScale = aScale >= 2.0f ? 2.0f : 1.0f;
262 nscolor trackColor;
263 if (aParams.isCustom) {
264 trackColor = aParams.trackColor;
265 } else {
266 if (aParams.isOverlay) {
267 trackColor = aParams.isOnDarkBackground ? NS_RGBA(201, 201, 201, 38)
268 : NS_RGBA(250, 250, 250, 191);
269 } else {
270 trackColor = aParams.isOnDarkBackground ? NS_RGBA(46, 46, 46, 255)
271 : NS_RGBA(250, 250, 250, 255);
275 float thickness = aParams.isHorizontal ? aRect.height : aRect.width;
277 // The scrollbar track is drawn as multiple non-overlapping segments, which
278 // make up lines of different widths and with slightly different shading.
279 ScrollbarTrackDecorationColors colors =
280 ComputeScrollbarTrackDecorationColors(trackColor);
281 struct {
282 nscolor color;
283 float thickness;
284 } segments[] = {
285 {colors.mInnerColor, 1.0f * aScale},
286 {colors.mShadowColor, 1.0f * aScale},
287 {trackColor, thickness - 3.0f * aScale},
288 {colors.mOuterColor, 1.0f * aScale},
291 // Iterate over the segments "from inside to outside" and fill each segment.
292 // For horizontal scrollbars, iterate top to bottom.
293 // For vertical scrollbars, iterate left to right or right to left based on
294 // aParams.isRtl.
295 auto current = aRects.begin();
296 float accumulatedThickness = 0.0f;
297 for (const auto& segment : segments) {
298 LayoutDeviceRect segmentRect = aRect;
299 float startThickness = accumulatedThickness;
300 float endThickness = startThickness + segment.thickness;
301 if (aParams.isHorizontal) {
302 segmentRect.SetBoxY(aRect.Y() + startThickness, aRect.Y() + endThickness);
303 } else {
304 if (aParams.isRtl) {
305 segmentRect.SetBoxX(aRect.XMost() - endThickness,
306 aRect.XMost() - startThickness);
307 } else {
308 segmentRect.SetBoxX(aRect.X() + startThickness,
309 aRect.X() + endThickness);
312 accumulatedThickness = endThickness;
313 *current++ = {segmentRect, segment.color};
316 return true;
319 static bool GetScrollCornerRects(const LayoutDeviceRect& aRect,
320 const ScrollbarParams& aParams, float aScale,
321 ScrollCornerRects& aRects) {
322 if (aParams.isOverlay && !aParams.isRolledOver) {
323 // Non-hovered overlay scrollbars don't have a corner. Draw nothing.
324 return false;
327 // This matches the sizing checks in GetMinimumWidgetSize etc.
328 aScale = aScale >= 2.0f ? 2.0f : 1.0f;
330 // Draw the following scroll corner.
332 // Output: Rectangles:
333 // +---+---+----------+---+ +---+---+----------+---+
334 // | I | S | T ... T | O | | I | S | T ... T | O |
335 // +---+ | | | +---+---+ | |
336 // | S S | T ... T | | | S S | T ... T | . |
337 // +-------+ | . | +-------+----------+ . |
338 // | T ... T | . | | T ... T | . |
339 // | . . | . | | . . | |
340 // | T ... T | | | T ... T | O |
341 // +------------------+ | +------------------+---+
342 // | O ... O | | O ... O |
343 // +----------------------+ +----------------------+
345 float width = aRect.width;
346 float height = aRect.height;
347 nscolor trackColor;
348 if (aParams.isCustom) {
349 trackColor = aParams.trackColor;
350 } else {
351 trackColor = aParams.isOnDarkBackground ? NS_RGBA(46, 46, 46, 255)
352 : NS_RGBA(250, 250, 250, 255);
354 ScrollbarTrackDecorationColors colors =
355 ComputeScrollbarTrackDecorationColors(trackColor);
356 struct {
357 nscolor color;
358 LayoutDeviceRect relativeRect;
359 } pieces[] = {
360 {colors.mInnerColor, {0.0f, 0.0f, 1.0f * aScale, 1.0f * aScale}},
361 {colors.mShadowColor,
362 {1.0f * aScale, 0.0f, 1.0f * aScale, 1.0f * aScale}},
363 {colors.mShadowColor,
364 {0.0f, 1.0f * aScale, 2.0f * aScale, 1.0f * aScale}},
365 {trackColor, {2.0f * aScale, 0.0f, width - 3.0f * aScale, 2.0f * aScale}},
366 {trackColor,
367 {0.0f, 2.0f * aScale, width - 1.0f * aScale, height - 3.0f * aScale}},
368 {colors.mOuterColor,
369 {width - 1.0f * aScale, 0.0f, 1.0f * aScale, height - 1.0f * aScale}},
370 {colors.mOuterColor,
371 {0.0f, height - 1.0f * aScale, width, 1.0f * aScale}},
374 auto current = aRects.begin();
375 for (const auto& piece : pieces) {
376 LayoutDeviceRect pieceRect = piece.relativeRect + aRect.TopLeft();
377 if (aParams.isRtl) {
378 pieceRect.x = aRect.XMost() - piece.relativeRect.XMost();
380 *current++ = {pieceRect, piece.color};
382 return true;
385 template <typename PaintBackendData>
386 void ScrollbarDrawingCocoa::DoPaintScrollbarThumb(
387 PaintBackendData& aPaintData, const LayoutDeviceRect& aRect,
388 ScrollbarKind aScrollbarKind, nsIFrame* aFrame, const ComputedStyle& aStyle,
389 const EventStates& aElementState, const EventStates& aDocumentState,
390 const DPIRatio& aDpiRatio) {
391 ScrollbarParams params =
392 ComputeScrollbarParams(aFrame, aStyle, aScrollbarKind);
393 auto thumb = GetThumbRect(aRect, params, aDpiRatio.scale);
394 LayoutDeviceCoord radius =
395 (params.isHorizontal ? thumb.mRect.Height() : thumb.mRect.Width()) / 2.0f;
396 ThemeDrawing::PaintRoundedRectWithRadius(
397 aPaintData, thumb.mRect, thumb.mRect,
398 sRGBColor::FromABGR(thumb.mFillColor), sRGBColor::White(0.0f), 0.0f,
399 radius / aDpiRatio, aDpiRatio);
400 if (!thumb.mStrokeColor) {
401 return;
404 // Paint the stroke if needed.
405 auto strokeRect = thumb.mRect;
406 strokeRect.Inflate(thumb.mStrokeOutset + thumb.mStrokeWidth);
407 radius =
408 (params.isHorizontal ? strokeRect.Height() : strokeRect.Width()) / 2.0f;
409 ThemeDrawing::PaintRoundedRectWithRadius(
410 aPaintData, strokeRect, sRGBColor::White(0.0f),
411 sRGBColor::FromABGR(thumb.mStrokeColor), thumb.mStrokeWidth,
412 radius / aDpiRatio, aDpiRatio);
415 bool ScrollbarDrawingCocoa::PaintScrollbarThumb(
416 DrawTarget& aDt, const LayoutDeviceRect& aRect,
417 ScrollbarKind aScrollbarKind, nsIFrame* aFrame, const ComputedStyle& aStyle,
418 const EventStates& aElementState, const EventStates& aDocumentState,
419 const Colors&, const DPIRatio& aDpiRatio) {
420 // TODO: Maybe respect the UseSystemColors setting?
421 DoPaintScrollbarThumb(aDt, aRect, aScrollbarKind, aFrame, aStyle,
422 aElementState, aDocumentState, aDpiRatio);
423 return true;
426 bool ScrollbarDrawingCocoa::PaintScrollbarThumb(
427 WebRenderBackendData& aWrData, const LayoutDeviceRect& aRect,
428 ScrollbarKind aScrollbarKind, nsIFrame* aFrame, const ComputedStyle& aStyle,
429 const EventStates& aElementState, const EventStates& aDocumentState,
430 const Colors&, const DPIRatio& aDpiRatio) {
431 // TODO: Maybe respect the UseSystemColors setting?
432 DoPaintScrollbarThumb(aWrData, aRect, aScrollbarKind, aFrame, aStyle,
433 aElementState, aDocumentState, aDpiRatio);
434 return true;
437 template <typename PaintBackendData>
438 void ScrollbarDrawingCocoa::DoPaintScrollbarTrack(
439 PaintBackendData& aPaintData, const LayoutDeviceRect& aRect,
440 ScrollbarKind aScrollbarKind, nsIFrame* aFrame, const ComputedStyle& aStyle,
441 const EventStates& aDocumentState, const DPIRatio& aDpiRatio) {
442 ScrollbarParams params =
443 ComputeScrollbarParams(aFrame, aStyle, aScrollbarKind);
444 ScrollbarTrackRects rects;
445 if (GetScrollbarTrackRects(aRect, params, aDpiRatio.scale, rects)) {
446 for (const auto& rect : rects) {
447 ThemeDrawing::FillRect(aPaintData, rect.mRect,
448 sRGBColor::FromABGR(rect.mColor));
453 bool ScrollbarDrawingCocoa::PaintScrollbarTrack(
454 DrawTarget& aDt, const LayoutDeviceRect& aRect,
455 ScrollbarKind aScrollbarKind, nsIFrame* aFrame, const ComputedStyle& aStyle,
456 const EventStates& aDocumentState, const Colors&,
457 const DPIRatio& aDpiRatio) {
458 // TODO: Maybe respect the UseSystemColors setting?
459 DoPaintScrollbarTrack(aDt, aRect, aScrollbarKind, aFrame, aStyle,
460 aDocumentState, aDpiRatio);
461 return true;
464 bool ScrollbarDrawingCocoa::PaintScrollbarTrack(
465 WebRenderBackendData& aWrData, const LayoutDeviceRect& aRect,
466 ScrollbarKind aScrollbarKind, nsIFrame* aFrame, const ComputedStyle& aStyle,
467 const EventStates& aDocumentState, const Colors&,
468 const DPIRatio& aDpiRatio) {
469 // TODO: Maybe respect the UseSystemColors setting?
470 DoPaintScrollbarTrack(aWrData, aRect, aScrollbarKind, aFrame, aStyle,
471 aDocumentState, aDpiRatio);
472 return true;
475 template <typename PaintBackendData>
476 void ScrollbarDrawingCocoa::DoPaintScrollCorner(
477 PaintBackendData& aPaintData, const LayoutDeviceRect& aRect,
478 ScrollbarKind aScrollbarKind, nsIFrame* aFrame, const ComputedStyle& aStyle,
479 const EventStates& aDocumentState, const DPIRatio& aDpiRatio) {
480 ScrollbarParams params =
481 ComputeScrollbarParams(aFrame, aStyle, aScrollbarKind);
482 ScrollCornerRects rects;
483 if (GetScrollCornerRects(aRect, params, aDpiRatio.scale, rects)) {
484 for (const auto& rect : rects) {
485 ThemeDrawing::FillRect(aPaintData, rect.mRect,
486 sRGBColor::FromABGR(rect.mColor));
491 bool ScrollbarDrawingCocoa::PaintScrollCorner(
492 DrawTarget& aDt, const LayoutDeviceRect& aRect,
493 ScrollbarKind aScrollbarKind, nsIFrame* aFrame, const ComputedStyle& aStyle,
494 const EventStates& aDocumentState, const Colors&,
495 const DPIRatio& aDpiRatio) {
496 // TODO: Maybe respect the UseSystemColors setting?
497 DoPaintScrollCorner(aDt, aRect, aScrollbarKind, aFrame, aStyle,
498 aDocumentState, aDpiRatio);
499 return true;
502 bool ScrollbarDrawingCocoa::PaintScrollCorner(
503 WebRenderBackendData& aWrData, const LayoutDeviceRect& aRect,
504 ScrollbarKind aScrollbarKind, nsIFrame* aFrame, const ComputedStyle& aStyle,
505 const EventStates& aDocumentState, const Colors&,
506 const DPIRatio& aDpiRatio) {
507 // TODO: Maybe respect the UseSystemColors setting?
508 DoPaintScrollCorner(aWrData, aRect, aScrollbarKind, aFrame, aStyle,
509 aDocumentState, aDpiRatio);
510 return true;
513 void ScrollbarDrawingCocoa::RecomputeScrollbarParams() {
514 uint32_t defaultSize = 17;
515 uint32_t overrideSize =
516 StaticPrefs::widget_non_native_theme_scrollbar_size_override();
517 if (overrideSize > 0) {
518 defaultSize = overrideSize;
520 mHorizontalScrollbarHeight = mVerticalScrollbarWidth = defaultSize;
523 } // namespace mozilla::widget