Bug 1460456 part 4 - Implement custom scrollbar color on Windows. r?jimm draft
authorXidorn Quan <me@upsuper.org>
Mon, 14 May 2018 11:05:28 +1000
changeset 795173 d6ad6f16b116a731fb97e8a6969db4411dfdc9bd
parent 795172 d6c176f410fd9ab262855845404938ba5fda7ff1
push id109884
push userxquan@mozilla.com
push dateTue, 15 May 2018 05:52:23 +0000
reviewersjimm
bugs1460456
milestone62.0a1
Bug 1460456 part 4 - Implement custom scrollbar color on Windows. r?jimm MozReview-Commit-ID: FJoU72Kvazd
widget/windows/nsNativeThemeWin.cpp
widget/windows/nsNativeThemeWin.h
--- a/widget/windows/nsNativeThemeWin.cpp
+++ b/widget/windows/nsNativeThemeWin.cpp
@@ -3,16 +3,17 @@
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include "nsNativeThemeWin.h"
 
 #include "mozilla/EventStates.h"
 #include "mozilla/Logging.h"
 #include "mozilla/WindowsVersion.h"
+#include "nsColor.h"
 #include "nsDeviceContext.h"
 #include "nsRect.h"
 #include "nsSize.h"
 #include "nsTransform2D.h"
 #include "nsThemeConstants.h"
 #include "nsIPresShell.h"
 #include "nsPresContext.h"
 #include "nsIContent.h"
@@ -1515,23 +1516,49 @@ GetThemeDpiScaleFactor(nsIFrame* aFrame)
     if (rootWidget) {
       double systemScale = WinUtils::SystemScaleFactor();
       return rootWidget->GetDefaultScale().scale / systemScale;
     }
   }
   return 1.0;
 }
 
+static bool
+IsWidgetScrollbarPart(uint8_t aWidgetType)
+{
+  switch (aWidgetType) {
+    case NS_THEME_SCROLLBAR:
+    case NS_THEME_SCROLLBAR_SMALL:
+    case NS_THEME_SCROLLBAR_VERTICAL:
+    case NS_THEME_SCROLLBAR_HORIZONTAL:
+    case NS_THEME_SCROLLBARBUTTON_UP:
+    case NS_THEME_SCROLLBARBUTTON_DOWN:
+    case NS_THEME_SCROLLBARBUTTON_LEFT:
+    case NS_THEME_SCROLLBARBUTTON_RIGHT:
+    case NS_THEME_SCROLLBARTHUMB_VERTICAL:
+    case NS_THEME_SCROLLBARTHUMB_HORIZONTAL:
+      return true;
+    default:
+      return false;
+  }
+}
+
 NS_IMETHODIMP
 nsNativeThemeWin::DrawWidgetBackground(gfxContext* aContext,
                                        nsIFrame* aFrame,
                                        uint8_t aWidgetType,
                                        const nsRect& aRect,
                                        const nsRect& aDirtyRect)
 {
+  if (aFrame->StyleUserInterface()->HasCustomScrollbars() &&
+      IsWidgetScrollbarPart(aWidgetType)) {
+    return DrawCustomScrollbarPart(aContext, aFrame, aWidgetType,
+                                   aRect, aDirtyRect);
+  }
+
   HANDLE theme = GetTheme(aWidgetType);
   if (!theme)
     return ClassicDrawWidgetBackground(aContext, aFrame, aWidgetType, aRect, aDirtyRect); 
 
   // ^^ without the right sdk, assume xp theming and fall through.
   if (nsUXThemeData::CheckForCompositor()) {
     switch (aWidgetType) {
       case NS_THEME_WINDOW_TITLEBAR:
@@ -1919,16 +1946,57 @@ RENDER_AGAIN:
   if (nativeDrawing.ShouldRenderAgain())
     goto RENDER_AGAIN;
 
   nativeDrawing.PaintToContext();
 
   return NS_OK;
 }
 
+static nscolor
+GetScrollbarFaceColorForAuto()
+{
+  // Do we want to derive from scrollbar-track-color when possible?
+  DWORD sysColor = ::GetSysColor(COLOR_SCROLLBAR);
+  return NS_RGB(GetRValue(sysColor),
+                GetGValue(sysColor),
+                GetBValue(sysColor));
+}
+
+static nscolor
+GetScrollbarTrackColorForAuto(ComputedStyle* aStyle)
+{
+  // Fallback to background color for now. Do we want to derive from
+  // scrollbar-face-color somehow?
+  return aStyle->StyleBackground()->BackgroundColor(aStyle);
+}
+
+nscolor
+nsNativeThemeWin::GetWidgetAutoColor(ComputedStyle* aStyle, uint8_t aWidgetType)
+{
+  switch (aWidgetType) {
+    case NS_THEME_SCROLLBAR:
+    case NS_THEME_SCROLLBAR_SMALL:
+    case NS_THEME_SCROLLBAR_VERTICAL:
+    case NS_THEME_SCROLLBAR_HORIZONTAL:
+    case NS_THEME_SCROLLBARBUTTON_UP:
+    case NS_THEME_SCROLLBARBUTTON_DOWN:
+    case NS_THEME_SCROLLBARBUTTON_LEFT:
+    case NS_THEME_SCROLLBARBUTTON_RIGHT:
+      return GetScrollbarTrackColorForAuto(aStyle);
+
+    case NS_THEME_SCROLLBARTHUMB_VERTICAL:
+    case NS_THEME_SCROLLBARTHUMB_HORIZONTAL:
+      return GetScrollbarFaceColorForAuto();
+
+    default:
+      return nsITheme::GetWidgetAutoColor(aStyle, aWidgetType);
+  }
+}
+
 static void
 ScaleForFrameDPI(nsIntMargin* aMargin, nsIFrame* aFrame)
 {
   double themeScale = GetThemeDpiScaleFactor(aFrame);
   if (themeScale != 1.0) {
     aMargin->top = NSToIntRound(aMargin->top * themeScale);
     aMargin->left = NSToIntRound(aMargin->left * themeScale);
     aMargin->bottom = NSToIntRound(aMargin->bottom * themeScale);
@@ -4031,16 +4099,271 @@ nsNativeThemeWin::GetWidgetNativeDrawing
   }
 
   return
     gfxWindowsNativeDrawing::CANNOT_DRAW_TO_COLOR_ALPHA |
     gfxWindowsNativeDrawing::CANNOT_AXIS_ALIGNED_SCALE |
     gfxWindowsNativeDrawing::CANNOT_COMPLEX_TRANSFORM;
 }
 
+static COLORREF
+ToColorRef(nscolor aColor)
+{
+  return RGB(NS_GET_R(aColor), NS_GET_G(aColor), NS_GET_B(aColor));
+}
+
+static nscolor
+GetOpaqueBackgroundColor(ComputedStyle* aStyle)
+{
+  nscolor color = aStyle->StyleBackground()->BackgroundColor(aStyle);
+  if (NS_GET_A(color) == 255) {
+    return color;
+  }
+  // Compose white background with the background color.
+  return NS_ComposeColors(NS_RGB(255, 255, 255), color);
+}
+
+static nscolor
+GetScrollbarFaceColor(ComputedStyle* aStyle)
+{
+  StyleComplexColor complexColor =
+    aStyle->StyleUserInterface()->mScrollbarFaceColor;
+  if (complexColor.mIsAuto) {
+    return GetScrollbarFaceColorForAuto();
+  }
+  nscolor color = complexColor.CalcColor(aStyle);
+  if (NS_GET_A(color) == 255) {
+    return color;
+  }
+  nscolor bgColor = GetOpaqueBackgroundColor(aStyle);
+  return NS_ComposeColors(bgColor, color);
+}
+
+static nscolor
+GetScrollbarTrackColor(ComputedStyle* aStyle)
+{
+  StyleComplexColor complexColor =
+    aStyle->StyleUserInterface()->mScrollbarTrackColor;
+  nscolor color;
+  if (complexColor.mIsAuto) {
+    color = GetScrollbarTrackColorForAuto(aStyle);
+  } else {
+    color = complexColor.CalcColor(aStyle);
+  }
+  if (NS_GET_A(color) == 255) {
+    return color;
+  }
+  nscolor bgColor = GetOpaqueBackgroundColor(aStyle);
+  return NS_ComposeColors(bgColor, color);
+}
+
+static float
+ComputeColorComponentLuminance(uint8_t aComponent)
+{
+  float v = float(aComponent) / 255.0f;
+  if (v <= 0.03928f) {
+    return v / 12.92f;
+  }
+  return std::pow((v + 0.055f) / 1.055f, 2.4f);
+}
+
+static constexpr float
+ComputeRelativeLuminanceFromComponents(float aR, float aG, float aB)
+{
+  return 0.2126f * aR + 0.7152f * aG + 0.0722f * aB;
+}
+
+// This function is written according to the algorithm defined in
+// https://www.w3.org/TR/WCAG20/#relativeluminancedef
+static float
+ComputeRelativeLuminance(nscolor aColor)
+{
+  float r = ComputeColorComponentLuminance(NS_GET_R(aColor));
+  float g = ComputeColorComponentLuminance(NS_GET_G(aColor));
+  float b = ComputeColorComponentLuminance(NS_GET_B(aColor));
+  return ComputeRelativeLuminanceFromComponents(r, g, b);
+}
+
+// Inverse function of ComputeColorComponentLuminance.
+static uint8_t
+DecomputeColorComponentLuminance(float aComponent)
+{
+  if (aComponent <= 0.03928f / 12.92f) {
+    aComponent *= 12.92f;
+  } else {
+    aComponent = std::pow(aComponent, 1.0f / 2.4f) * 1.055f - 0.055f;
+  }
+  return ClampColor(aComponent * 255.0f);
+}
+
+// Adjust the luminance of the color to the given value.
+static nscolor
+AdjustColorLuminance(nscolor aColor, float aLuminance)
+{
+  float r = ComputeColorComponentLuminance(NS_GET_R(aColor));
+  float g = ComputeColorComponentLuminance(NS_GET_G(aColor));
+  float b = ComputeColorComponentLuminance(NS_GET_B(aColor));
+  float luminance = ComputeRelativeLuminanceFromComponents(r, g, b);
+  float factor = aLuminance / luminance;
+  uint8_t r1 = DecomputeColorComponentLuminance(r * factor);
+  uint8_t g1 = DecomputeColorComponentLuminance(g * factor);
+  uint8_t b1 = DecomputeColorComponentLuminance(b * factor);
+  return NS_RGB(r1, g1, b1);
+}
+
+static nscolor
+GetScrollbarArrowColor(nscolor aTrackColor)
+{
+  // In Windows 10 scrollbar, there are several gray colors used:
+  //
+  // State  | Background (lum) | Arrow   | Contrast
+  // -------+------------------+---------+---------
+  // Normal | Gray 240 (87.1%) | Gray 96 |     5.5
+  // Hover  | Gray 218 (70.1%) | Black   |    15.0
+  // Active | Gray 96  (11.7%) | White   |     6.3
+  //
+  // Contrast value is computed based on the definition in
+  // https://www.w3.org/TR/WCAG20/#contrast-ratiodef
+  //
+  // This function is written based on these values.
+
+  float luminance = ComputeRelativeLuminance(aTrackColor);
+  // Color with luminance larger than 0.72 has contrast ratio over 4.6
+  // to color with luminance of gray 96, so this value is chosen for
+  // this range. It is the luminance of gray 221.
+  if (luminance >= 0.72) {
+    // ComputeRelativeLuminanceFromComponents(96). That function cannot
+    // be constexpr because of std::pow.
+    const float GRAY96_LUMINANCE = 0.117f;
+    return AdjustColorLuminance(aTrackColor, GRAY96_LUMINANCE);
+  }
+  // The contrast ratio of a color to black equals that to white when its
+  // luminance is around 0.18, with a contrast ratio ~4.6 to both sides,
+  // thus the value below. It's the lumanince of gray 118.
+  if (luminance >= 0.18) {
+    return NS_RGB(0, 0, 0);
+  }
+  return NS_RGB(255, 255, 255);
+}
+
+// This tries to draw a Windows 10 style scrollbar with given colors.
+nsresult
+nsNativeThemeWin::DrawCustomScrollbarPart(gfxContext* aContext,
+                                          nsIFrame* aFrame,
+                                          uint8_t aWidgetType,
+                                          const nsRect& aRect,
+                                          const nsRect& aClipRect)
+{
+  ComputedStyle* style = aFrame->Style();
+  MOZ_ASSERT(!style->StyleUserInterface()->mScrollbarFaceColor.mIsAuto ||
+             !style->StyleUserInterface()->mScrollbarTrackColor.mIsAuto);
+
+  gfxRect tr(aRect.X(), aRect.Y(), aRect.Width(), aRect.Height()),
+          dr(aClipRect.X(), aClipRect.Y(),
+             aClipRect.Width(), aClipRect.Height());
+
+  nscolor trackColor = GetScrollbarTrackColor(style);
+  HBRUSH dcBrush = (HBRUSH) GetStockObject(DC_BRUSH);
+
+  gfxFloat p2a = gfxFloat(aFrame->PresContext()->AppUnitsPerDevPixel());
+  tr.Scale(1.0 / p2a);
+  dr.Scale(1.0 / p2a);
+
+  RefPtr<gfxContext> ctx = aContext;
+
+  uint32_t flags = GetWidgetNativeDrawingFlags(aWidgetType);
+  gfxWindowsNativeDrawing nativeDrawing(ctx, dr, flags);
+
+  do {
+    HDC hdc = nativeDrawing.BeginNativeDrawing();
+    if (!hdc) {
+      return NS_ERROR_FAILURE;
+    }
+
+    RECT widgetRect;
+    nativeDrawing.TransformToNativeRect(tr, widgetRect);
+    ::SetDCBrushColor(hdc, ToColorRef(trackColor));
+    ::SelectObject(hdc, dcBrush);
+    ::FillRect(hdc, &widgetRect, dcBrush);
+
+    switch (aWidgetType) {
+      case NS_THEME_SCROLLBARTHUMB_VERTICAL:
+      case NS_THEME_SCROLLBARTHUMB_HORIZONTAL: {
+        // Scrollbar thumb is two CSS pixels thinner than the track.
+        gfxRect tr2 = tr;
+        gfxFloat dev2css = round(AppUnitsPerCSSPixel() / p2a);
+        if (aWidgetType == NS_THEME_SCROLLBARTHUMB_VERTICAL) {
+          tr2.Deflate(dev2css, 0);
+        } else {
+          tr2.Deflate(0, dev2css);
+        }
+        nativeDrawing.TransformToNativeRect(tr2, widgetRect);
+        nscolor faceColor = GetScrollbarFaceColor(style);
+        ::SetDCBrushColor(hdc, ToColorRef(faceColor));
+        ::FillRect(hdc, &widgetRect, dcBrush);
+        break;
+      }
+      case NS_THEME_SCROLLBARBUTTON_UP:
+      case NS_THEME_SCROLLBARBUTTON_DOWN:
+      case NS_THEME_SCROLLBARBUTTON_LEFT:
+      case NS_THEME_SCROLLBARBUTTON_RIGHT: {
+        // kPath is the path of scrollbar up arrow on Windows 10
+        // in a 17x17 area.
+        const LONG kSize = 17;
+        const POINT kPath[] = {
+          { 5, 9 }, { 8, 6 }, { 11, 9 }, { 11, 11 }, { 8, 8 }, { 5, 11 },
+        };
+        const size_t kCount = ArrayLength(kPath);
+        // Calculate necessary parameters for positioning the arrow.
+        LONG width = widgetRect.right - widgetRect.left;
+        LONG height = widgetRect.bottom - widgetRect.top;
+        LONG size = std::min(width, height);
+        LONG left = (width - size) / 2 + widgetRect.left;
+        LONG top = (height - size) / 2 + widgetRect.top;
+        float unit = float(size) / kSize;
+        POINT path[kCount];
+        // Flip the path for different direction, then resize and align
+        // it to the middle of the area.
+        for (size_t i = 0; i < kCount; i++) {
+          if (aWidgetType == NS_THEME_SCROLLBARBUTTON_UP) {
+            path[i] = kPath[i];
+          } else if (aWidgetType == NS_THEME_SCROLLBARBUTTON_DOWN) {
+            path[i].x = kPath[i].x;
+            path[i].y = kSize - kPath[i].y;
+          } else if (aWidgetType == NS_THEME_SCROLLBARBUTTON_LEFT) {
+            path[i].x = kPath[i].y;
+            path[i].y = kPath[i].x;
+          } else {
+            path[i].x = kSize - kPath[i].y;
+            path[i].y = kPath[i].x;
+          }
+          path[i].x = left + (LONG) round(unit * path[i].x);
+          path[i].y = top + (LONG) round(unit * path[i].y);
+        }
+        // Paint the arrow.
+        COLORREF arrowColor = ToColorRef(GetScrollbarArrowColor(trackColor));
+        // XXX Somehow we need to paint with both pen and brush to get
+        //     the desired shape. Can we do so only with brush?
+        ::SetDCPenColor(hdc, arrowColor);
+        ::SetDCBrushColor(hdc, arrowColor);
+        ::SelectObject(hdc, GetStockObject(DC_PEN));
+        ::Polygon(hdc, path, kCount);
+        break;
+      }
+      default:
+        break;
+    }
+
+    nativeDrawing.EndNativeDrawing();
+  } while (nativeDrawing.ShouldRenderAgain());
+
+  nativeDrawing.PaintToContext();
+  return NS_OK;
+}
+
 ///////////////////////////////////////////
 // Creation Routine
 ///////////////////////////////////////////
 
 // from nsWindow.cpp
 extern bool gDisableNativeTheme;
 
 nsresult NS_NewNativeTheme(nsISupports *aOuter, REFNSIID aIID, void **aResult)
--- a/widget/windows/nsNativeThemeWin.h
+++ b/widget/windows/nsNativeThemeWin.h
@@ -32,16 +32,19 @@ public:
 
   // The nsITheme interface.
   NS_IMETHOD DrawWidgetBackground(gfxContext* aContext,
                                   nsIFrame* aFrame,
                                   uint8_t aWidgetType,
                                   const nsRect& aRect,
                                   const nsRect& aDirtyRect) override;
 
+  nscolor GetWidgetAutoColor(mozilla::ComputedStyle* aStyle,
+                             uint8_t aWidgetType) override;
+
   NS_IMETHOD GetWidgetBorder(nsDeviceContext* aContext, 
                              nsIFrame* aFrame,
                              uint8_t aWidgetType,
                              nsIntMargin* aResult) override;
 
   virtual bool GetWidgetPadding(nsDeviceContext* aContext,
                                   nsIFrame* aFrame,
                                   uint8_t aWidgetType,
@@ -106,16 +109,21 @@ protected:
                                uint8_t aWidgetType,
                                nsIntMargin* aResult);
   nsresult ClassicGetMinimumWidgetSize(nsIFrame* aFrame, uint8_t aWidgetType,
                                        mozilla::LayoutDeviceIntSize* aResult,
                                        bool* aIsOverridable);
   bool ClassicThemeSupportsWidget(nsIFrame* aFrame, uint8_t aWidgetType);
   void DrawCheckedRect(HDC hdc, const RECT& rc, int32_t fore, int32_t back,
                        HBRUSH defaultBack);
+  nsresult DrawCustomScrollbarPart(gfxContext* aContext,
+                                   nsIFrame* aFrame,
+                                   uint8_t aWidgetType,
+                                   const nsRect& aRect,
+                                   const nsRect& aClipRect);
   uint32_t GetWidgetNativeDrawingFlags(uint8_t aWidgetType);
   int32_t StandardGetState(nsIFrame* aFrame, uint8_t aWidgetType, bool wantFocused);
   bool IsMenuActive(nsIFrame* aFrame, uint8_t aWidgetType);
   RECT CalculateProgressOverlayRect(nsIFrame* aFrame, RECT* aWidgetRect,
                                     bool aIsVertical, bool aIsIndeterminate,
                                     bool aIsClassic);
   void DrawThemedProgressMeter(nsIFrame* aFrame, int aWidgetType,
                                HANDLE aTheme, HDC aHdc,