1// Copyright (c) 2013 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5#include "ui/native_theme/native_theme_mac.h"
6
7#import <Cocoa/Cocoa.h>
8
9#include "base/basictypes.h"
10#include "base/mac/mac_util.h"
11#include "base/mac/scoped_cftyperef.h"
12#include "base/mac/sdk_forward_declarations.h"
13#include "ui/native_theme/common_theme.h"
14#import "skia/ext/skia_utils_mac.h"
15#include "third_party/skia/include/effects/SkGradientShader.h"
16#include "ui/gfx/geometry/rect.h"
17#include "ui/gfx/skia_util.h"
18
19namespace {
20
21const SkColor kScrollerTrackGradientColors[] = {
22    SkColorSetRGB(0xEF, 0xEF, 0xEF),
23    SkColorSetRGB(0xF9, 0xF9, 0xF9),
24    SkColorSetRGB(0xFD, 0xFD, 0xFD),
25    SkColorSetRGB(0xF6, 0xF6, 0xF6) };
26const SkColor kScrollerTrackInnerBorderColor = SkColorSetRGB(0xE4, 0xE4, 0xE4);
27const SkColor kScrollerTrackOuterBorderColor = SkColorSetRGB(0xEF, 0xEF, 0xEF);
28const SkColor kScrollerThumbColor = SkColorSetARGB(0x38, 0, 0, 0);
29const SkColor kScrollerThumbHoverColor = SkColorSetARGB(0x80, 0, 0, 0);
30const int kScrollerTrackBorderWidth = 1;
31
32// The amount the thumb is inset from both the ends and the sides of the track.
33const int kScrollerThumbInset = 3;
34
35// Values calculated by reading pixels and solving simultaneous equations
36// derived from "A over B" alpha compositing. Steps: Sample the semi-transparent
37// pixel over two backgrounds; P1, P2 over backgrounds B1, B2. Use the color
38// value between 0.0 and 1.0 (i.e. divide by 255.0). Then,
39// alpha = (P2 - P1 + B1 - B2) / (B1 - B2)
40// color = (P1 - B1 + alpha * B1) / alpha.
41const SkColor kMenuPopupBackgroundColor = SkColorSetARGB(251, 255, 255, 255);
42const SkColor kMenuSeparatorColor = SkColorSetARGB(243, 228, 228, 228);
43const SkColor kMenuBorderColor = SkColorSetARGB(60, 0, 0, 0);
44
45// Hardcoded color used for some existing dialogs in Chrome's Cocoa UI.
46const SkColor kDialogBackgroundColor = SkColorSetRGB(251, 251, 251);
47
48// On 10.6 and 10.7 there is no way to get components from system colors. Here,
49// system colors are just opaque objects that can paint themselves and otherwise
50// tell you nothing. In 10.8, some of the system color classes have incomplete
51// implementations and throw exceptions even attempting to convert using
52// -[NSColor colorUsingColorSpace:], so don't bother there either.
53// This function paints a single pixel to a 1x1 swatch and reads it back.
54SkColor GetSystemColorUsingSwatch(NSColor* color) {
55  SkColor swatch;
56  base::ScopedCFTypeRef<CGColorSpaceRef> color_space(
57      CGColorSpaceCreateWithName(kCGColorSpaceGenericRGB));
58  const size_t bytes_per_row = 4;
59  COMPILE_ASSERT(sizeof(swatch) == bytes_per_row, skcolor_not_4_bytes);
60  CGBitmapInfo bitmap_info =
61      kCGImageAlphaPremultipliedFirst | kCGBitmapByteOrder32Host;
62  base::ScopedCFTypeRef<CGContextRef> context(CGBitmapContextCreate(
63      &swatch, 1, 1, 8, bytes_per_row, color_space, bitmap_info));
64
65  NSGraphicsContext* drawing_context =
66      [NSGraphicsContext graphicsContextWithGraphicsPort:context flipped:NO];
67  [NSGraphicsContext saveGraphicsState];
68  [NSGraphicsContext setCurrentContext:drawing_context];
69  [color drawSwatchInRect:NSMakeRect(0, 0, 1, 1)];
70  [NSGraphicsContext restoreGraphicsState];
71  return swatch;
72}
73
74// NSColor has a number of methods that return system colors (i.e. controlled by
75// user preferences). This function converts the color given by an NSColor class
76// method to an SkColor. Official documentation suggests developers only rely on
77// +[NSColor selectedTextBackgroundColor] and +[NSColor selectedControlColor],
78// but other colors give a good baseline. For many, a gradient is involved; the
79// palette chosen based on the enum value given by +[NSColor currentColorTint].
80// Apple's documentation also suggests to use NSColorList, but the system color
81// list is just populated with class methods on NSColor.
82SkColor NSSystemColorToSkColor(NSColor* color) {
83  if (base::mac::IsOSMountainLionOrEarlier())
84    return GetSystemColorUsingSwatch(color);
85
86  // System colors use the an NSNamedColorSpace called "System", so first step
87  // is to convert the color into something that can be worked with.
88  NSColor* device_color =
89      [color colorUsingColorSpace:[NSColorSpace deviceRGBColorSpace]];
90  if (device_color)
91    return gfx::NSDeviceColorToSkColor(device_color);
92
93  // Sometimes the conversion is not possible, but we can get an approximation
94  // by going through a CGColorRef. Note that simply using NSColor methods for
95  // accessing components for system colors results in exceptions like
96  // "-numberOfComponents not valid for the NSColor NSNamedColorSpace System
97  // windowBackgroundColor; need to first convert colorspace." Hence the
98  // conversion first to CGColor.
99  CGColorRef cg_color = [color CGColor];
100  if (CGColorGetNumberOfComponents(cg_color) == 4)
101    return gfx::CGColorRefToSkColor(cg_color);
102
103  CHECK_EQ(2u, CGColorGetNumberOfComponents(cg_color));
104  // Two components means a grayscale channel and an alpha channel, which
105  // CGColorRefToSkColor will not like. But RGB is additive, so the conversion
106  // is easy (RGB to grayscale is less easy).
107  const CGFloat* components = CGColorGetComponents(cg_color);
108  return SkColorSetARGB(SkScalarRoundToInt(255.0 * components[1]),
109                        SkScalarRoundToInt(255.0 * components[0]),
110                        SkScalarRoundToInt(255.0 * components[0]),
111                        SkScalarRoundToInt(255.0 * components[0]));
112}
113
114}  // namespace
115
116namespace ui {
117
118// static
119NativeTheme* NativeTheme::instance() {
120  return NativeThemeMac::instance();
121}
122
123// static
124NativeThemeMac* NativeThemeMac::instance() {
125  CR_DEFINE_STATIC_LOCAL(NativeThemeMac, s_native_theme, ());
126  return &s_native_theme;
127}
128
129SkColor NativeThemeMac::GetSystemColor(ColorId color_id) const {
130  // TODO(tapted): Add caching for these, and listen for
131  // NSSystemColorsDidChangeNotification.
132  switch (color_id) {
133    case kColorId_WindowBackground:
134      return NSSystemColorToSkColor([NSColor windowBackgroundColor]);
135    case kColorId_DialogBackground:
136      return kDialogBackgroundColor;
137
138    case kColorId_FocusedBorderColor:
139    case kColorId_FocusedMenuButtonBorderColor:
140      return NSSystemColorToSkColor([NSColor keyboardFocusIndicatorColor]);
141    case kColorId_UnfocusedBorderColor:
142      return NSSystemColorToSkColor([NSColor controlColor]);
143
144    // Buttons and labels.
145    case kColorId_ButtonBackgroundColor:
146    case kColorId_ButtonHoverBackgroundColor:
147    case kColorId_HoverMenuButtonBorderColor:
148    case kColorId_LabelBackgroundColor:
149      return NSSystemColorToSkColor([NSColor controlBackgroundColor]);
150    case kColorId_ButtonEnabledColor:
151    case kColorId_EnabledMenuButtonBorderColor:
152    case kColorId_LabelEnabledColor:
153      return NSSystemColorToSkColor([NSColor controlTextColor]);
154    case kColorId_ButtonDisabledColor:
155    case kColorId_LabelDisabledColor:
156      return NSSystemColorToSkColor([NSColor disabledControlTextColor]);
157    case kColorId_ButtonHighlightColor:
158    case kColorId_ButtonHoverColor:
159      return NSSystemColorToSkColor([NSColor selectedControlTextColor]);
160
161    // Menus.
162    case kColorId_EnabledMenuItemForegroundColor:
163      return NSSystemColorToSkColor([NSColor controlTextColor]);
164    case kColorId_DisabledMenuItemForegroundColor:
165    case kColorId_DisabledEmphasizedMenuItemForegroundColor:
166      return NSSystemColorToSkColor([NSColor disabledControlTextColor]);
167    case kColorId_SelectedMenuItemForegroundColor:
168      return NSSystemColorToSkColor([NSColor selectedMenuItemTextColor]);
169    case kColorId_FocusedMenuItemBackgroundColor:
170    case kColorId_HoverMenuItemBackgroundColor:
171      return NSSystemColorToSkColor([NSColor selectedMenuItemColor]);
172    case kColorId_MenuBackgroundColor:
173      return kMenuPopupBackgroundColor;
174    case kColorId_MenuSeparatorColor:
175      return kMenuSeparatorColor;
176    case kColorId_MenuBorderColor:
177      return kMenuBorderColor;
178
179    // Text fields.
180    case kColorId_TextfieldDefaultColor:
181    case kColorId_TextfieldReadOnlyColor:
182      return NSSystemColorToSkColor([NSColor textColor]);
183    case kColorId_TextfieldDefaultBackground:
184    case kColorId_TextfieldReadOnlyBackground:
185      return NSSystemColorToSkColor([NSColor textBackgroundColor]);
186    case kColorId_TextfieldSelectionColor:
187      return NSSystemColorToSkColor([NSColor selectedTextColor]);
188    case kColorId_TextfieldSelectionBackgroundFocused:
189      return NSSystemColorToSkColor([NSColor selectedTextBackgroundColor]);
190
191    default:
192      break;  // TODO(tapted): Handle all values and remove the default case.
193  }
194
195  SkColor color;
196  if (CommonThemeGetSystemColor(color_id, &color))
197    return color;
198
199  NOTIMPLEMENTED() << " Invalid color_id: " << color_id;
200  return FallbackTheme::GetSystemColor(color_id);
201}
202
203void NativeThemeMac::PaintScrollbarTrack(
204    SkCanvas* canvas,
205    Part part,
206    State state,
207    const ScrollbarTrackExtraParams& extra_params,
208    const gfx::Rect& rect) const {
209  // Emulate the non-overlay scroller style from OSX 10.7 and later.
210  SkPoint gradient_bounds[2];
211  if (part == kScrollbarVerticalTrack) {
212    gradient_bounds[0].set(rect.x(), rect.y());
213    gradient_bounds[1].set(rect.right(), rect.y());
214  } else {
215    DCHECK_EQ(part, kScrollbarHorizontalTrack);
216    gradient_bounds[0].set(rect.x(), rect.y());
217    gradient_bounds[1].set(rect.x(), rect.bottom());
218  }
219  skia::RefPtr<SkShader> shader = skia::AdoptRef(
220      SkGradientShader::CreateLinear(gradient_bounds,
221                                     kScrollerTrackGradientColors,
222                                     NULL,
223                                     arraysize(kScrollerTrackGradientColors),
224                                     SkShader::kClamp_TileMode));
225  SkPaint gradient;
226  gradient.setShader(shader.get());
227
228  SkIRect track_rect = gfx::RectToSkIRect(rect);
229  canvas->drawIRect(track_rect, gradient);
230
231  // Draw inner and outer line borders.
232  if (part == kScrollbarVerticalTrack) {
233    SkPaint paint;
234    paint.setColor(kScrollerTrackInnerBorderColor);
235    canvas->drawRectCoords(track_rect.left(),
236                           track_rect.top(),
237                           track_rect.left() + kScrollerTrackBorderWidth,
238                           track_rect.bottom(),
239                           paint);
240    paint.setColor(kScrollerTrackOuterBorderColor);
241    canvas->drawRectCoords(track_rect.right() - kScrollerTrackBorderWidth,
242                           track_rect.top(),
243                           track_rect.right(),
244                           track_rect.bottom(),
245                           paint);
246  } else {
247    SkPaint paint;
248    paint.setColor(kScrollerTrackInnerBorderColor);
249    canvas->drawRectCoords(track_rect.left(),
250                           track_rect.top(),
251                           track_rect.right(),
252                           track_rect.top() + kScrollerTrackBorderWidth,
253                           paint);
254    paint.setColor(kScrollerTrackOuterBorderColor);
255    canvas->drawRectCoords(track_rect.left(),
256                           track_rect.bottom() - kScrollerTrackBorderWidth,
257                           track_rect.right(),
258                           track_rect.bottom(),
259                           paint);
260  }
261}
262
263void NativeThemeMac::PaintScrollbarThumb(SkCanvas* canvas,
264                                         Part part,
265                                         State state,
266                                         const gfx::Rect& rect) const {
267  gfx::Rect thumb_rect(rect);
268  switch (part) {
269    case kScrollbarHorizontalThumb:
270      thumb_rect.Inset(0, kScrollerTrackBorderWidth, 0, 0);
271      break;
272    case kScrollbarVerticalThumb:
273      thumb_rect.Inset(kScrollerTrackBorderWidth, 0, 0, 0);
274      break;
275    default:
276      NOTREACHED();
277      break;
278  }
279
280  thumb_rect.Inset(kScrollerThumbInset, kScrollerThumbInset);
281
282  SkPaint paint;
283  paint.setAntiAlias(true);
284  paint.setColor(state == kHovered ? thumb_active_color_
285                                   : thumb_inactive_color_);
286  const SkScalar radius = std::min(rect.width(), rect.height());
287  canvas->drawRoundRect(gfx::RectToSkRect(thumb_rect), radius, radius, paint);
288}
289
290void NativeThemeMac::PaintScrollbarCorner(SkCanvas* canvas,
291                                          State state,
292                                          const gfx::Rect& rect) const {
293  DCHECK_GT(rect.width(), 0);
294  DCHECK_GT(rect.height(), 0);
295
296  // Draw radial gradient from top-left corner.
297  skia::RefPtr<SkShader> shader = skia::AdoptRef(
298      SkGradientShader::CreateRadial(SkPoint::Make(rect.x(), rect.y()),
299                                     rect.width(),
300                                     kScrollerTrackGradientColors,
301                                     NULL,
302                                     arraysize(kScrollerTrackGradientColors),
303                                     SkShader::kClamp_TileMode));
304  SkPaint gradient;
305  gradient.setStyle(SkPaint::kFill_Style);
306  gradient.setAntiAlias(true);
307  gradient.setShader(shader.get());
308  canvas->drawRect(gfx::RectToSkRect(rect), gradient);
309
310  // Draw inner border corner point.
311  canvas->drawPoint(rect.x(), rect.y(), kScrollerTrackInnerBorderColor);
312
313  // Draw outer borders.
314  SkPaint paint;
315  paint.setColor(kScrollerTrackOuterBorderColor);
316  canvas->drawRectCoords(rect.right() - kScrollerTrackBorderWidth,
317                         rect.y(),
318                         rect.right(),
319                         rect.bottom(),
320                         paint);
321  canvas->drawRectCoords(rect.x(),
322                         rect.bottom() - kScrollerTrackBorderWidth,
323                         rect.right(),
324                         rect.bottom(),
325                         paint);
326}
327
328void NativeThemeMac::PaintMenuPopupBackground(
329    SkCanvas* canvas,
330    const gfx::Size& size,
331    const MenuBackgroundExtraParams& menu_background) const {
332  canvas->drawColor(kMenuPopupBackgroundColor, SkXfermode::kSrc_Mode);
333}
334
335void NativeThemeMac::PaintMenuItemBackground(
336    SkCanvas* canvas,
337    State state,
338    const gfx::Rect& rect,
339    const MenuListExtraParams& menu_list) const {
340  SkPaint paint;
341  switch (state) {
342    case NativeTheme::kNormal:
343    case NativeTheme::kDisabled:
344      // Draw nothing over the regular background.
345      break;
346    case NativeTheme::kHovered:
347      // TODO(tapted): Draw a gradient, and use [NSColor currentControlTint] to
348      // pick colors. The System color "selectedMenuItemColor" is actually still
349      // blue for Graphite. And while "keyboardFocusIndicatorColor" does change,
350      // and is a good shade of gray, it's not blue enough for the Blue theme.
351      paint.setColor(GetSystemColor(kColorId_HoverMenuItemBackgroundColor));
352      canvas->drawRect(gfx::RectToSkRect(rect), paint);
353      break;
354    default:
355      NOTREACHED();
356      break;
357  }
358}
359
360NativeThemeMac::NativeThemeMac() {
361  set_scrollbar_button_length(0);
362  SetScrollbarColors(kScrollerThumbColor,
363                     kScrollerThumbHoverColor,
364                     kScrollerTrackGradientColors[0]);
365}
366
367NativeThemeMac::~NativeThemeMac() {
368}
369
370}  // namespace ui
371