Bug 1831502 [wpt PR 39860] - HTML: sync html5lib-tests, a=testonly
[gecko.git] / widget / ScrollbarDrawingCocoa.cpp
blobcf9f45d892ec48a40d5068402ecf1ffb876d6c29
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 isDark = 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 const ThemeColors& aColors,
57 ScrollbarKind aScrollbarKind) {
58 ScrollbarParams params;
59 params.isOverlay =
60 nsLookAndFeel::GetInt(LookAndFeel::IntID::UseOverlayScrollbars) != 0;
61 params.isRolledOver = ScrollbarDrawing::IsParentScrollbarRolledOver(aFrame);
62 params.isSmall =
63 aStyle.StyleUIReset()->ScrollbarWidth() == StyleScrollbarWidth::Thin;
64 params.isRtl = aScrollbarKind == ScrollbarKind::VerticalLeft;
65 params.isHorizontal = aScrollbarKind == ScrollbarKind::Horizontal;
66 params.isDark = aColors.IsDark();
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 = [&]() -> CSSIntSize {
85 switch (aAppearance) {
86 case StyleAppearance::ScrollbarthumbHorizontal:
87 return {26, 0};
88 case StyleAppearance::ScrollbarthumbVertical:
89 return {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 = GetCSSScrollbarSize(
97 scrollbarWidth, Overlay(aPresContext->UseOverlayScrollbars()));
98 return {size, size};
100 case StyleAppearance::ScrollbarbuttonUp:
101 case StyleAppearance::ScrollbarbuttonDown:
102 return {15, 16};
103 case StyleAppearance::ScrollbarbuttonLeft:
104 case StyleAppearance::ScrollbarbuttonRight:
105 return {16, 15};
106 default:
107 return {};
109 }();
111 auto dpi = GetDPIRatioForScrollbarPart(aPresContext);
112 return LayoutDeviceIntSize::Round(CSSSize(minSize) * dpi);
115 static ThumbRect GetThumbRect(const LayoutDeviceRect& aRect,
116 const ScrollbarParams& aParams, float aScale) {
117 // Compute the thumb thickness. This varies based on aParams.small,
118 // aParams.overlay and aParams.rolledOver. non-overlay: 6 / 8, overlay
119 // non-hovered: 5 / 7, overlay hovered: 9 / 11
120 float thickness = aParams.isSmall ? 6.0f : 8.0f;
121 if (aParams.isOverlay) {
122 thickness -= 1.0f;
123 if (aParams.isRolledOver) {
124 thickness += 4.0f;
127 thickness *= aScale;
129 // Compute the thumb rect.
130 const float outerSpacing =
131 ((aParams.isOverlay || aParams.isSmall) ? 1.0f : 2.0f) * aScale;
132 LayoutDeviceRect thumbRect = aRect;
133 thumbRect.Deflate(1.0f * aScale);
134 if (aParams.isHorizontal) {
135 float bottomEdge = thumbRect.YMost() - outerSpacing;
136 thumbRect.SetBoxY(bottomEdge - thickness, bottomEdge);
137 } else {
138 if (aParams.isRtl) {
139 float leftEdge = thumbRect.X() + outerSpacing;
140 thumbRect.SetBoxX(leftEdge, leftEdge + thickness);
141 } else {
142 float rightEdge = thumbRect.XMost() - outerSpacing;
143 thumbRect.SetBoxX(rightEdge - thickness, rightEdge);
147 // Compute the thumb fill color.
148 nscolor faceColor;
149 if (aParams.isCustom) {
150 faceColor = aParams.faceColor;
151 } else {
152 if (aParams.isOverlay) {
153 faceColor =
154 aParams.isDark ? NS_RGBA(255, 255, 255, 128) : NS_RGBA(0, 0, 0, 128);
155 } else if (aParams.isDark) {
156 faceColor = aParams.isRolledOver ? NS_RGBA(158, 158, 158, 255)
157 : NS_RGBA(117, 117, 117, 255);
158 } else {
159 faceColor = aParams.isRolledOver ? NS_RGBA(125, 125, 125, 255)
160 : NS_RGBA(194, 194, 194, 255);
164 nscolor strokeColor = 0;
165 float strokeOutset = 0.0f;
166 float strokeWidth = 0.0f;
168 // Overlay scrollbars have an additional stroke around the fill.
169 if (aParams.isOverlay) {
170 // For the default alpha of 128 we want to end up with 48 in the outline.
171 constexpr float kAlphaScaling = 48.0f / 128.0f;
172 const uint8_t strokeAlpha =
173 uint8_t(clamped(NS_GET_A(faceColor) * kAlphaScaling, 0.0f, 48.0f));
174 if (strokeAlpha) {
175 strokeOutset = (aParams.isDark ? 0.3f : 0.5f) * aScale;
176 strokeWidth = (aParams.isDark ? 0.6f : 0.8f) * aScale;
178 strokeColor = aParams.isDark ? NS_RGBA(0, 0, 0, strokeAlpha)
179 : NS_RGBA(255, 255, 255, strokeAlpha);
183 return {thumbRect, faceColor, strokeColor, strokeWidth, strokeOutset};
186 struct ScrollbarTrackDecorationColors {
187 nscolor mInnerColor = 0;
188 nscolor mShadowColor = 0;
189 nscolor mOuterColor = 0;
192 static ScrollbarTrackDecorationColors ComputeScrollbarTrackDecorationColors(
193 nscolor aTrackColor) {
194 ScrollbarTrackDecorationColors result;
195 float luminance = RelativeLuminanceUtils::Compute(aTrackColor);
196 if (luminance >= 0.5f) {
197 result.mInnerColor =
198 RelativeLuminanceUtils::Adjust(aTrackColor, luminance * 0.836f);
199 result.mShadowColor =
200 RelativeLuminanceUtils::Adjust(aTrackColor, luminance * 0.982f);
201 result.mOuterColor =
202 RelativeLuminanceUtils::Adjust(aTrackColor, luminance * 0.886f);
203 } else {
204 result.mInnerColor =
205 RelativeLuminanceUtils::Adjust(aTrackColor, luminance * 1.196f);
206 result.mShadowColor =
207 RelativeLuminanceUtils::Adjust(aTrackColor, luminance * 1.018f);
208 result.mOuterColor =
209 RelativeLuminanceUtils::Adjust(aTrackColor, luminance * 1.129f);
211 return result;
214 static bool GetScrollbarTrackRects(const LayoutDeviceRect& aRect,
215 const ScrollbarParams& aParams, float aScale,
216 ScrollbarTrackRects& aRects) {
217 if (aParams.isOverlay && !aParams.isRolledOver) {
218 // Non-hovered overlay scrollbars don't have a track. Draw nothing.
219 return false;
222 nscolor trackColor;
223 if (aParams.isCustom) {
224 trackColor = aParams.trackColor;
225 } else {
226 if (aParams.isOverlay) {
227 trackColor = aParams.isDark ? NS_RGBA(201, 201, 201, 38)
228 : NS_RGBA(250, 250, 250, 191);
229 } else {
230 trackColor = aParams.isDark ? NS_RGBA(46, 46, 46, 255)
231 : NS_RGBA(250, 250, 250, 255);
235 float thickness = aParams.isHorizontal ? aRect.height : aRect.width;
237 // The scrollbar track is drawn as multiple non-overlapping segments, which
238 // make up lines of different widths and with slightly different shading.
239 ScrollbarTrackDecorationColors colors =
240 ComputeScrollbarTrackDecorationColors(trackColor);
241 struct {
242 nscolor color;
243 float thickness;
244 } segments[] = {
245 {colors.mInnerColor, 1.0f * aScale},
246 {colors.mShadowColor, 1.0f * aScale},
247 {trackColor, thickness - 3.0f * aScale},
248 {colors.mOuterColor, 1.0f * aScale},
251 // Iterate over the segments "from inside to outside" and fill each segment.
252 // For horizontal scrollbars, iterate top to bottom.
253 // For vertical scrollbars, iterate left to right or right to left based on
254 // aParams.isRtl.
255 auto current = aRects.begin();
256 float accumulatedThickness = 0.0f;
257 for (const auto& segment : segments) {
258 LayoutDeviceRect segmentRect = aRect;
259 float startThickness = accumulatedThickness;
260 float endThickness = startThickness + segment.thickness;
261 if (aParams.isHorizontal) {
262 segmentRect.SetBoxY(aRect.Y() + startThickness, aRect.Y() + endThickness);
263 } else {
264 if (aParams.isRtl) {
265 segmentRect.SetBoxX(aRect.XMost() - endThickness,
266 aRect.XMost() - startThickness);
267 } else {
268 segmentRect.SetBoxX(aRect.X() + startThickness,
269 aRect.X() + endThickness);
272 accumulatedThickness = endThickness;
273 *current++ = {segmentRect, segment.color};
276 return true;
279 static bool GetScrollCornerRects(const LayoutDeviceRect& aRect,
280 const ScrollbarParams& aParams, float aScale,
281 ScrollCornerRects& aRects) {
282 if (aParams.isOverlay && !aParams.isRolledOver) {
283 // Non-hovered overlay scrollbars don't have a corner. Draw nothing.
284 return false;
287 // Draw the following scroll corner.
289 // Output: Rectangles:
290 // +---+---+----------+---+ +---+---+----------+---+
291 // | I | S | T ... T | O | | I | S | T ... T | O |
292 // +---+ | | | +---+---+ | |
293 // | S S | T ... T | | | S S | T ... T | . |
294 // +-------+ | . | +-------+----------+ . |
295 // | T ... T | . | | T ... T | . |
296 // | . . | . | | . . | |
297 // | T ... T | | | T ... T | O |
298 // +------------------+ | +------------------+---+
299 // | O ... O | | O ... O |
300 // +----------------------+ +----------------------+
302 float width = aRect.width;
303 float height = aRect.height;
304 nscolor trackColor;
305 if (aParams.isCustom) {
306 trackColor = aParams.trackColor;
307 } else {
308 trackColor =
309 aParams.isDark ? NS_RGBA(46, 46, 46, 255) : NS_RGBA(250, 250, 250, 255);
311 ScrollbarTrackDecorationColors colors =
312 ComputeScrollbarTrackDecorationColors(trackColor);
313 struct {
314 nscolor color;
315 LayoutDeviceRect relativeRect;
316 } pieces[] = {
317 {colors.mInnerColor, {0.0f, 0.0f, 1.0f * aScale, 1.0f * aScale}},
318 {colors.mShadowColor,
319 {1.0f * aScale, 0.0f, 1.0f * aScale, 1.0f * aScale}},
320 {colors.mShadowColor,
321 {0.0f, 1.0f * aScale, 2.0f * aScale, 1.0f * aScale}},
322 {trackColor, {2.0f * aScale, 0.0f, width - 3.0f * aScale, 2.0f * aScale}},
323 {trackColor,
324 {0.0f, 2.0f * aScale, width - 1.0f * aScale, height - 3.0f * aScale}},
325 {colors.mOuterColor,
326 {width - 1.0f * aScale, 0.0f, 1.0f * aScale, height - 1.0f * aScale}},
327 {colors.mOuterColor,
328 {0.0f, height - 1.0f * aScale, width, 1.0f * aScale}},
331 auto current = aRects.begin();
332 for (const auto& piece : pieces) {
333 LayoutDeviceRect pieceRect = piece.relativeRect + aRect.TopLeft();
334 if (aParams.isRtl) {
335 pieceRect.x = aRect.XMost() - piece.relativeRect.XMost();
337 *current++ = {pieceRect, piece.color};
339 return true;
342 template <typename PaintBackendData>
343 void ScrollbarDrawingCocoa::DoPaintScrollbarThumb(
344 PaintBackendData& aPaintData, 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 ScrollbarParams params =
349 ComputeScrollbarParams(aFrame, aStyle, aColors, aScrollbarKind);
350 auto thumb = GetThumbRect(aRect, params, aDpiRatio.scale);
351 LayoutDeviceCoord radius =
352 (params.isHorizontal ? thumb.mRect.Height() : thumb.mRect.Width()) / 2.0f;
353 ThemeDrawing::PaintRoundedRectWithRadius(
354 aPaintData, thumb.mRect, thumb.mRect,
355 sRGBColor::FromABGR(thumb.mFillColor), sRGBColor::White(0.0f), 0.0f,
356 radius / aDpiRatio, aDpiRatio);
357 if (!thumb.mStrokeColor) {
358 return;
361 // Paint the stroke if needed.
362 auto strokeRect = thumb.mRect;
363 strokeRect.Inflate(thumb.mStrokeOutset + thumb.mStrokeWidth);
364 radius =
365 (params.isHorizontal ? strokeRect.Height() : strokeRect.Width()) / 2.0f;
366 ThemeDrawing::PaintRoundedRectWithRadius(
367 aPaintData, strokeRect, sRGBColor::White(0.0f),
368 sRGBColor::FromABGR(thumb.mStrokeColor), thumb.mStrokeWidth,
369 radius / aDpiRatio, aDpiRatio);
372 bool ScrollbarDrawingCocoa::PaintScrollbarThumb(
373 DrawTarget& aDt, const LayoutDeviceRect& aRect,
374 ScrollbarKind aScrollbarKind, nsIFrame* aFrame, const ComputedStyle& aStyle,
375 const ElementState& aElementState, const DocumentState& aDocumentState,
376 const Colors& aColors, const DPIRatio& aDpiRatio) {
377 DoPaintScrollbarThumb(aDt, aRect, aScrollbarKind, aFrame, aStyle,
378 aElementState, aDocumentState, aColors, aDpiRatio);
379 return true;
382 bool ScrollbarDrawingCocoa::PaintScrollbarThumb(
383 WebRenderBackendData& aWrData, const LayoutDeviceRect& aRect,
384 ScrollbarKind aScrollbarKind, nsIFrame* aFrame, const ComputedStyle& aStyle,
385 const ElementState& aElementState, const DocumentState& aDocumentState,
386 const Colors& aColors, const DPIRatio& aDpiRatio) {
387 DoPaintScrollbarThumb(aWrData, aRect, aScrollbarKind, aFrame, aStyle,
388 aElementState, aDocumentState, aColors, aDpiRatio);
389 return true;
392 template <typename PaintBackendData>
393 void ScrollbarDrawingCocoa::DoPaintScrollbarTrack(
394 PaintBackendData& aPaintData, const LayoutDeviceRect& aRect,
395 ScrollbarKind aScrollbarKind, nsIFrame* aFrame, const ComputedStyle& aStyle,
396 const DocumentState& aDocumentState, const Colors& aColors,
397 const DPIRatio& aDpiRatio) {
398 ScrollbarParams params =
399 ComputeScrollbarParams(aFrame, aStyle, aColors, aScrollbarKind);
400 ScrollbarTrackRects rects;
401 if (GetScrollbarTrackRects(aRect, params, aDpiRatio.scale, rects)) {
402 for (const auto& rect : rects) {
403 ThemeDrawing::FillRect(aPaintData, rect.mRect,
404 sRGBColor::FromABGR(rect.mColor));
409 bool ScrollbarDrawingCocoa::PaintScrollbarTrack(
410 DrawTarget& aDt, const LayoutDeviceRect& aRect,
411 ScrollbarKind aScrollbarKind, nsIFrame* aFrame, const ComputedStyle& aStyle,
412 const DocumentState& aDocumentState, const Colors& aColors,
413 const DPIRatio& aDpiRatio) {
414 DoPaintScrollbarTrack(aDt, aRect, aScrollbarKind, aFrame, aStyle,
415 aDocumentState, aColors, aDpiRatio);
416 return true;
419 bool ScrollbarDrawingCocoa::PaintScrollbarTrack(
420 WebRenderBackendData& aWrData, const LayoutDeviceRect& aRect,
421 ScrollbarKind aScrollbarKind, nsIFrame* aFrame, const ComputedStyle& aStyle,
422 const DocumentState& aDocumentState, const Colors& aColors,
423 const DPIRatio& aDpiRatio) {
424 DoPaintScrollbarTrack(aWrData, aRect, aScrollbarKind, aFrame, aStyle,
425 aDocumentState, aColors, aDpiRatio);
426 return true;
429 template <typename PaintBackendData>
430 void ScrollbarDrawingCocoa::DoPaintScrollCorner(
431 PaintBackendData& aPaintData, const LayoutDeviceRect& aRect,
432 ScrollbarKind aScrollbarKind, nsIFrame* aFrame, const ComputedStyle& aStyle,
433 const DocumentState& aDocumentState, const Colors& aColors,
434 const DPIRatio& aDpiRatio) {
435 ScrollbarParams params =
436 ComputeScrollbarParams(aFrame, aStyle, aColors, aScrollbarKind);
437 ScrollCornerRects rects;
438 if (GetScrollCornerRects(aRect, params, aDpiRatio.scale, rects)) {
439 for (const auto& rect : rects) {
440 ThemeDrawing::FillRect(aPaintData, rect.mRect,
441 sRGBColor::FromABGR(rect.mColor));
446 bool ScrollbarDrawingCocoa::PaintScrollCorner(
447 DrawTarget& aDt, const LayoutDeviceRect& aRect,
448 ScrollbarKind aScrollbarKind, nsIFrame* aFrame, const ComputedStyle& aStyle,
449 const DocumentState& aDocumentState, const Colors& aColors,
450 const DPIRatio& aDpiRatio) {
451 DoPaintScrollCorner(aDt, aRect, aScrollbarKind, aFrame, aStyle,
452 aDocumentState, aColors, aDpiRatio);
453 return true;
456 bool ScrollbarDrawingCocoa::PaintScrollCorner(
457 WebRenderBackendData& aWrData, const LayoutDeviceRect& aRect,
458 ScrollbarKind aScrollbarKind, nsIFrame* aFrame, const ComputedStyle& aStyle,
459 const DocumentState& aDocumentState, const Colors& aColors,
460 const DPIRatio& aDpiRatio) {
461 DoPaintScrollCorner(aWrData, aRect, aScrollbarKind, aFrame, aStyle,
462 aDocumentState, aColors, aDpiRatio);
463 return true;
466 void ScrollbarDrawingCocoa::RecomputeScrollbarParams() {
467 // FIXME(emilio): This doesn't respect the
468 // StaticPrefs::widget_non_native_theme_scrollbar_size_override() pref;
469 ConfigureScrollbarSize(15); // Just in case, for future-proofing
470 ConfigureScrollbarSize(StyleScrollbarWidth::Auto, Overlay::No, 15);
471 ConfigureScrollbarSize(StyleScrollbarWidth::Thin, Overlay::No, 11);
472 ConfigureScrollbarSize(StyleScrollbarWidth::Auto, Overlay::Yes, 16);
473 ConfigureScrollbarSize(StyleScrollbarWidth::Thin, Overlay::Yes, 14);
476 } // namespace mozilla::widget