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/. */
6 
7 #include "ScrollbarDrawingMac.h"
8 #include "mozilla/gfx/Helpers.h"
9 #include "mozilla/RelativeLuminanceUtils.h"
10 #include "nsLayoutUtils.h"
11 #include "nsIFrame.h"
12 #include "nsLookAndFeel.h"
13 #include "nsContainerFrame.h"
14 #include "nsNativeTheme.h"
15 
16 namespace mozilla {
17 
18 using namespace gfx;
19 
20 namespace widget {
21 
GetParentScrollbarFrame(nsIFrame * aFrame)22 static nsIFrame* GetParentScrollbarFrame(nsIFrame* aFrame) {
23   // Walk our parents to find a scrollbar frame
24   nsIFrame* scrollbarFrame = aFrame;
25   do {
26     if (scrollbarFrame->IsScrollbarFrame()) {
27       break;
28     }
29   } while ((scrollbarFrame = scrollbarFrame->GetParent()));
30 
31   // We return null if we can't find a parent scrollbar frame
32   return scrollbarFrame;
33 }
34 
IsParentScrollbarRolledOver(nsIFrame * aFrame)35 static bool IsParentScrollbarRolledOver(nsIFrame* aFrame) {
36   nsIFrame* scrollbarFrame = GetParentScrollbarFrame(aFrame);
37   return nsLookAndFeel::GetInt(LookAndFeel::IntID::UseOverlayScrollbars) != 0
38              ? nsNativeTheme::CheckBooleanAttr(scrollbarFrame, nsGkAtoms::hover)
39              : nsNativeTheme::GetContentState(scrollbarFrame,
40                                               StyleAppearance::None)
41                    .HasState(NS_EVENT_STATE_HOVER);
42 }
43 
GetScrollbarSize(StyleScrollbarWidth aWidth,bool aOverlay)44 CSSIntCoord ScrollbarDrawingMac::GetScrollbarSize(StyleScrollbarWidth aWidth,
45                                                   bool aOverlay) {
46   bool isSmall = aWidth == StyleScrollbarWidth::Thin;
47   if (aOverlay) {
48     return isSmall ? 14 : 16;
49   }
50   return isSmall ? 11 : 15;
51 }
52 
GetScrollbarSize(StyleScrollbarWidth aWidth,bool aOverlay,float aDpiRatio)53 LayoutDeviceIntCoord ScrollbarDrawingMac::GetScrollbarSize(
54     StyleScrollbarWidth aWidth, bool aOverlay, float aDpiRatio) {
55   CSSIntCoord size = GetScrollbarSize(aWidth, aOverlay);
56   if (aDpiRatio >= 2.0f) {
57     return int32_t(size) * 2;
58   }
59   return int32_t(size);
60 }
61 
GetMinimumWidgetSize(StyleAppearance aAppearance,nsIFrame * aFrame,float aDpiRatio)62 LayoutDeviceIntSize ScrollbarDrawingMac::GetMinimumWidgetSize(
63     StyleAppearance aAppearance, nsIFrame* aFrame, float aDpiRatio) {
64   auto minSize = [&] {
65     switch (aAppearance) {
66       case StyleAppearance::ScrollbarthumbHorizontal:
67         return IntSize{26, 0};
68       case StyleAppearance::ScrollbarthumbVertical:
69         return IntSize{0, 26};
70       case StyleAppearance::ScrollbarVertical:
71       case StyleAppearance::ScrollbarHorizontal:
72       case StyleAppearance::ScrollbartrackVertical:
73       case StyleAppearance::ScrollbartrackHorizontal: {
74         ComputedStyle* style = nsLayoutUtils::StyleForScrollbar(aFrame);
75         auto scrollbarWidth = style->StyleUIReset()->mScrollbarWidth;
76         auto size = GetScrollbarSize(
77             scrollbarWidth,
78             LookAndFeel::GetInt(LookAndFeel::IntID::UseOverlayScrollbars));
79         return IntSize{size, size};
80       }
81       case StyleAppearance::MozMenulistArrowButton: {
82         auto size =
83             GetScrollbarSize(StyleScrollbarWidth::Auto, /* aOverlay = */ false);
84         return IntSize{size, size};
85       }
86       case StyleAppearance::ScrollbarbuttonUp:
87       case StyleAppearance::ScrollbarbuttonDown:
88         return IntSize{15, 16};
89       case StyleAppearance::ScrollbarbuttonLeft:
90       case StyleAppearance::ScrollbarbuttonRight:
91         return IntSize{16, 15};
92       default:
93         return IntSize{};
94     }
95   }();
96 
97   if (aDpiRatio >= 2.0f) {
98     return LayoutDeviceIntSize{minSize.width * 2, minSize.height * 2};
99   }
100   return LayoutDeviceIntSize{minSize.width, minSize.height};
101 }
102 
ComputeScrollbarParams(nsIFrame * aFrame,const ComputedStyle & aStyle,bool aIsHorizontal)103 ScrollbarParams ScrollbarDrawingMac::ComputeScrollbarParams(
104     nsIFrame* aFrame, const ComputedStyle& aStyle, bool aIsHorizontal) {
105   ScrollbarParams params;
106   params.overlay =
107       nsLookAndFeel::GetInt(LookAndFeel::IntID::UseOverlayScrollbars) != 0;
108   params.rolledOver = IsParentScrollbarRolledOver(aFrame);
109   params.small =
110       aStyle.StyleUIReset()->mScrollbarWidth == StyleScrollbarWidth::Thin;
111   params.rtl = nsNativeTheme::IsFrameRTL(aFrame);
112   params.horizontal = aIsHorizontal;
113   params.onDarkBackground = !StaticPrefs::widget_disable_dark_scrollbar() &&
114                             nsNativeTheme::IsDarkBackground(aFrame);
115   // Don't use custom scrollbars for overlay scrollbars since they are
116   // generally good enough for use cases of custom scrollbars.
117   if (!params.overlay) {
118     const nsStyleUI* ui = aStyle.StyleUI();
119     if (ui->HasCustomScrollbars()) {
120       const auto& colors = ui->mScrollbarColor.AsColors();
121       params.custom = true;
122       params.trackColor = colors.track.CalcColor(aStyle);
123       params.faceColor = colors.thumb.CalcColor(aStyle);
124     }
125   }
126 
127   return params;
128 }
129 
GetThumbRect(const Rect & aRect,const ScrollbarParams & aParams,float aScale)130 auto ScrollbarDrawingMac::GetThumbRect(const Rect& aRect,
131                                        const ScrollbarParams& aParams,
132                                        float aScale) -> ThumbRect {
133   // This matches the sizing checks in GetMinimumWidgetSize etc.
134   aScale = aScale >= 2.0f ? 2.0f : 1.0f;
135 
136   // Compute the thumb thickness. This varies based on aParams.small,
137   // aParams.overlay and aParams.rolledOver. non-overlay: 6 / 8, overlay
138   // non-hovered: 5 / 7, overlay hovered: 9 / 11
139   float thickness = aParams.small ? 6.0f : 8.0f;
140   if (aParams.overlay) {
141     thickness -= 1.0f;
142     if (aParams.rolledOver) {
143       thickness += 4.0f;
144     }
145   }
146   thickness *= aScale;
147 
148   // Compute the thumb rect.
149   const float outerSpacing =
150       ((aParams.overlay || aParams.small) ? 1.0f : 2.0f) * aScale;
151   Rect thumbRect = aRect;
152   thumbRect.Deflate(1.0f * aScale);
153   if (aParams.horizontal) {
154     float bottomEdge = thumbRect.YMost() - outerSpacing;
155     thumbRect.SetBoxY(bottomEdge - thickness, bottomEdge);
156   } else {
157     if (aParams.rtl) {
158       float leftEdge = thumbRect.X() + outerSpacing;
159       thumbRect.SetBoxX(leftEdge, leftEdge + thickness);
160     } else {
161       float rightEdge = thumbRect.XMost() - outerSpacing;
162       thumbRect.SetBoxX(rightEdge - thickness, rightEdge);
163     }
164   }
165 
166   // Compute the thumb fill color.
167   nscolor faceColor;
168   if (aParams.custom) {
169     faceColor = aParams.faceColor;
170   } else {
171     if (aParams.overlay) {
172       faceColor = aParams.onDarkBackground ? NS_RGBA(255, 255, 255, 128)
173                                            : NS_RGBA(0, 0, 0, 128);
174     } else if (aParams.onDarkBackground) {
175       faceColor = aParams.rolledOver ? NS_RGBA(158, 158, 158, 255)
176                                      : NS_RGBA(117, 117, 117, 255);
177     } else {
178       faceColor = aParams.rolledOver ? NS_RGBA(125, 125, 125, 255)
179                                      : NS_RGBA(194, 194, 194, 255);
180     }
181   }
182 
183   nscolor strokeColor = 0;
184   float strokeOutset = 0.0f;
185   float strokeWidth = 0.0f;
186 
187   // Overlay scrollbars have an additional stroke around the fill.
188   if (aParams.overlay) {
189     strokeOutset = (aParams.onDarkBackground ? 0.3f : 0.5f) * aScale;
190     strokeWidth = (aParams.onDarkBackground ? 0.6f : 0.8f) * aScale;
191 
192     strokeColor = aParams.onDarkBackground ? NS_RGBA(0, 0, 0, 48)
193                                            : NS_RGBA(255, 255, 255, 48);
194   }
195 
196   return {thumbRect, faceColor, strokeColor, strokeWidth, strokeOutset};
197 }
198 
199 struct ScrollbarTrackDecorationColors {
200   nscolor mInnerColor = 0;
201   nscolor mShadowColor = 0;
202   nscolor mOuterColor = 0;
203 };
204 
ComputeScrollbarTrackDecorationColors(nscolor aTrackColor)205 static ScrollbarTrackDecorationColors ComputeScrollbarTrackDecorationColors(
206     nscolor aTrackColor) {
207   ScrollbarTrackDecorationColors result;
208   float luminance = RelativeLuminanceUtils::Compute(aTrackColor);
209   if (luminance >= 0.5f) {
210     result.mInnerColor =
211         RelativeLuminanceUtils::Adjust(aTrackColor, luminance * 0.836f);
212     result.mShadowColor =
213         RelativeLuminanceUtils::Adjust(aTrackColor, luminance * 0.982f);
214     result.mOuterColor =
215         RelativeLuminanceUtils::Adjust(aTrackColor, luminance * 0.886f);
216   } else {
217     result.mInnerColor =
218         RelativeLuminanceUtils::Adjust(aTrackColor, luminance * 1.196f);
219     result.mShadowColor =
220         RelativeLuminanceUtils::Adjust(aTrackColor, luminance * 1.018f);
221     result.mOuterColor =
222         RelativeLuminanceUtils::Adjust(aTrackColor, luminance * 1.129f);
223   }
224   return result;
225 }
226 
GetScrollbarTrackRects(const Rect & aRect,const ScrollbarParams & aParams,float aScale,ScrollbarTrackRects & aRects)227 bool ScrollbarDrawingMac::GetScrollbarTrackRects(const Rect& aRect,
228                                                  const ScrollbarParams& aParams,
229                                                  float aScale,
230                                                  ScrollbarTrackRects& aRects) {
231   if (aParams.overlay && !aParams.rolledOver) {
232     // Non-hovered overlay scrollbars don't have a track. Draw nothing.
233     return false;
234   }
235 
236   // This matches the sizing checks in GetMinimumWidgetSize etc.
237   aScale = aScale >= 2.0f ? 2.0f : 1.0f;
238 
239   nscolor trackColor;
240   if (aParams.custom) {
241     trackColor = aParams.trackColor;
242   } else {
243     if (aParams.overlay) {
244       trackColor = aParams.onDarkBackground ? NS_RGBA(201, 201, 201, 38)
245                                             : NS_RGBA(250, 250, 250, 191);
246     } else {
247       trackColor = aParams.onDarkBackground ? NS_RGBA(46, 46, 46, 255)
248                                             : NS_RGBA(250, 250, 250, 255);
249     }
250   }
251 
252   float thickness = aParams.horizontal ? aRect.height : aRect.width;
253 
254   // The scrollbar track is drawn as multiple non-overlapping segments, which
255   // make up lines of different widths and with slightly different shading.
256   ScrollbarTrackDecorationColors colors =
257       ComputeScrollbarTrackDecorationColors(trackColor);
258   struct {
259     nscolor color;
260     float thickness;
261   } segments[] = {
262       {colors.mInnerColor, 1.0f * aScale},
263       {colors.mShadowColor, 1.0f * aScale},
264       {trackColor, thickness - 3.0f * aScale},
265       {colors.mOuterColor, 1.0f * aScale},
266   };
267 
268   // Iterate over the segments "from inside to outside" and fill each segment.
269   // For horizontal scrollbars, iterate top to bottom.
270   // For vertical scrollbars, iterate left to right or right to left based on
271   // aParams.rtl.
272   auto current = aRects.begin();
273   float accumulatedThickness = 0.0f;
274   for (const auto& segment : segments) {
275     Rect segmentRect = aRect;
276     float startThickness = accumulatedThickness;
277     float endThickness = startThickness + segment.thickness;
278     if (aParams.horizontal) {
279       segmentRect.SetBoxY(aRect.Y() + startThickness, aRect.Y() + endThickness);
280     } else {
281       if (aParams.rtl) {
282         segmentRect.SetBoxX(aRect.XMost() - endThickness,
283                             aRect.XMost() - startThickness);
284       } else {
285         segmentRect.SetBoxX(aRect.X() + startThickness,
286                             aRect.X() + endThickness);
287       }
288     }
289     accumulatedThickness = endThickness;
290     *current++ = {segmentRect, segment.color};
291   }
292 
293   return true;
294 }
295 
GetScrollCornerRects(const Rect & aRect,const ScrollbarParams & aParams,float aScale,ScrollCornerRects & aRects)296 bool ScrollbarDrawingMac::GetScrollCornerRects(const Rect& aRect,
297                                                const ScrollbarParams& aParams,
298                                                float aScale,
299                                                ScrollCornerRects& aRects) {
300   if (aParams.overlay && !aParams.rolledOver) {
301     // Non-hovered overlay scrollbars don't have a corner. Draw nothing.
302     return false;
303   }
304 
305   // This matches the sizing checks in GetMinimumWidgetSize etc.
306   aScale = aScale >= 2.0f ? 2.0f : 1.0f;
307 
308   // Draw the following scroll corner.
309   //
310   //        Output:                      Rectangles:
311   // +---+---+----------+---+     +---+---+----------+---+
312   // | I | S | T ...  T | O |     | I | S | T ...  T | O |
313   // +---+   |          |   |     +---+---+          |   |
314   // | S   S | T ...  T |   |     | S   S | T ...  T | . |
315   // +-------+          | . |     +-------+----------+ . |
316   // | T      ...     T | . |     | T      ...     T | . |
317   // | .              . | . |     | .              . |   |
318   // | T      ...     T |   |     | T      ...     T | O |
319   // +------------------+   |     +------------------+---+
320   // | O       ...        O |     | O       ...        O |
321   // +----------------------+     +----------------------+
322 
323   float width = aRect.width;
324   float height = aRect.height;
325   nscolor trackColor;
326   if (aParams.custom) {
327     trackColor = aParams.trackColor;
328   } else {
329     trackColor = aParams.onDarkBackground ? NS_RGBA(46, 46, 46, 255)
330                                           : NS_RGBA(250, 250, 250, 255);
331   }
332   ScrollbarTrackDecorationColors colors =
333       ComputeScrollbarTrackDecorationColors(trackColor);
334   struct {
335     nscolor color;
336     Rect relativeRect;
337   } pieces[] = {
338       {colors.mInnerColor, {0.0f, 0.0f, 1.0f * aScale, 1.0f * aScale}},
339       {colors.mShadowColor,
340        {1.0f * aScale, 0.0f, 1.0f * aScale, 1.0f * aScale}},
341       {colors.mShadowColor,
342        {0.0f, 1.0f * aScale, 2.0f * aScale, 1.0f * aScale}},
343       {trackColor, {2.0f * aScale, 0.0f, width - 3.0f * aScale, 2.0f * aScale}},
344       {trackColor,
345        {0.0f, 2.0f * aScale, width - 1.0f * aScale, height - 3.0f * aScale}},
346       {colors.mOuterColor,
347        {width - 1.0f * aScale, 0.0f, 1.0f * aScale, height - 1.0f * aScale}},
348       {colors.mOuterColor,
349        {0.0f, height - 1.0f * aScale, width, 1.0f * aScale}},
350   };
351 
352   auto current = aRects.begin();
353   for (const auto& piece : pieces) {
354     Rect pieceRect = piece.relativeRect + aRect.TopLeft();
355     if (aParams.rtl) {
356       pieceRect.x = aRect.XMost() - piece.relativeRect.XMost();
357     }
358     *current++ = {pieceRect, piece.color};
359   }
360   return true;
361 }
362 
363 }  // namespace widget
364 }  // namespace mozilla
365