Home | History | Annotate | Download | only in cocoa
      1 // Copyright (c) 2012 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 "chrome/browser/ui/cocoa/gradient_button_cell.h"
      6 
      7 #include <cmath>
      8 
      9 #include "base/logging.h"
     10 #import "base/mac/scoped_nsobject.h"
     11 #import "chrome/browser/themes/theme_properties.h"
     12 #import "chrome/browser/themes/theme_service.h"
     13 #import "chrome/browser/ui/cocoa/rect_path_utils.h"
     14 #import "chrome/browser/ui/cocoa/themed_window.h"
     15 #include "grit/theme_resources.h"
     16 #import "third_party/google_toolbox_for_mac/src/AppKit/GTMNSColor+Luminance.h"
     17 #import "ui/base/cocoa/nsgraphics_context_additions.h"
     18 #import "ui/base/cocoa/nsview_additions.h"
     19 #include "ui/gfx/scoped_ns_graphics_context_save_gstate_mac.h"
     20 
     21 @interface GradientButtonCell (Private)
     22 - (void)sharedInit;
     23 
     24 // Get drawing parameters for a given cell frame in a given view. The inner
     25 // frame is the one required by |-drawInteriorWithFrame:inView:|. The inner and
     26 // outer paths are the ones required by |-drawBorderAndFillForTheme:...|. The
     27 // outer path also gives the area in which to clip. Any of the |return...|
     28 // arguments may be NULL (in which case the given parameter won't be returned).
     29 // If |returnInnerPath| or |returnOuterPath|, |*returnInnerPath| or
     30 // |*returnOuterPath| should be nil, respectively.
     31 - (void)getDrawParamsForFrame:(NSRect)cellFrame
     32                        inView:(NSView*)controlView
     33                    innerFrame:(NSRect*)returnInnerFrame
     34                     innerPath:(NSBezierPath**)returnInnerPath
     35                      clipPath:(NSBezierPath**)returnClipPath;
     36 
     37 - (void)updateTrackingAreas;
     38 
     39 @end
     40 
     41 
     42 static const NSTimeInterval kAnimationShowDuration = 0.2;
     43 
     44 // Note: due to a bug (?), drawWithFrame:inView: does not call
     45 // drawBorderAndFillForTheme::::: unless the mouse is inside.  The net
     46 // effect is that our "fade out" when the mouse leaves becaumes
     47 // instantaneous.  When I "fixed" it things looked horrible; the
     48 // hover-overed bookmark button would stay highlit for 0.4 seconds
     49 // which felt like latency/lag.  I'm leaving the "bug" in place for
     50 // now so we don't suck.  -jrg
     51 static const NSTimeInterval kAnimationHideDuration = 0.4;
     52 
     53 static const NSTimeInterval kAnimationContinuousCycleDuration = 0.4;
     54 
     55 @implementation GradientButtonCell
     56 
     57 @synthesize hoverAlpha = hoverAlpha_;
     58 
     59 // For nib instantiations
     60 - (id)initWithCoder:(NSCoder*)decoder {
     61   if ((self = [super initWithCoder:decoder])) {
     62     [self sharedInit];
     63   }
     64   return self;
     65 }
     66 
     67 // For programmatic instantiations
     68 - (id)initTextCell:(NSString*)string {
     69   if ((self = [super initTextCell:string])) {
     70     [self sharedInit];
     71   }
     72   return self;
     73 }
     74 
     75 - (void)dealloc {
     76   if (trackingArea_) {
     77     [[self controlView] removeTrackingArea:trackingArea_];
     78     trackingArea_.reset();
     79   }
     80   [super dealloc];
     81 }
     82 
     83 // Return YES if we are pulsing (towards another state or continuously).
     84 - (BOOL)pulsing {
     85   if ((pulseState_ == gradient_button_cell::kPulsingOn) ||
     86       (pulseState_ == gradient_button_cell::kPulsingOff) ||
     87       (pulseState_ == gradient_button_cell::kPulsingContinuous))
     88     return YES;
     89   return NO;
     90 }
     91 
     92 // Perform one pulse step when animating a pulse.
     93 - (void)performOnePulseStep {
     94   NSTimeInterval thisUpdate = [NSDate timeIntervalSinceReferenceDate];
     95   NSTimeInterval elapsed = thisUpdate - lastHoverUpdate_;
     96   CGFloat opacity = [self hoverAlpha];
     97 
     98   // Update opacity based on state.
     99   // Adjust state if we have finished.
    100   switch (pulseState_) {
    101   case gradient_button_cell::kPulsingOn:
    102     opacity += elapsed / kAnimationShowDuration;
    103     if (opacity > 1.0) {
    104       [self setPulseState:gradient_button_cell::kPulsedOn];
    105       return;
    106     }
    107     break;
    108   case gradient_button_cell::kPulsingOff:
    109     opacity -= elapsed / kAnimationHideDuration;
    110     if (opacity < 0.0) {
    111       [self setPulseState:gradient_button_cell::kPulsedOff];
    112       return;
    113     }
    114     break;
    115   case gradient_button_cell::kPulsingContinuous:
    116     opacity += elapsed / kAnimationContinuousCycleDuration * pulseMultiplier_;
    117     if (opacity > 1.0) {
    118       opacity = 1.0;
    119       pulseMultiplier_ *= -1.0;
    120     } else if (opacity < 0.0) {
    121       opacity = 0.0;
    122       pulseMultiplier_ *= -1.0;
    123     }
    124     outerStrokeAlphaMult_ = opacity;
    125     break;
    126   default:
    127     NOTREACHED() << "unknown pulse state";
    128   }
    129 
    130   // Update our control.
    131   lastHoverUpdate_ = thisUpdate;
    132   [self setHoverAlpha:opacity];
    133   [[self controlView] setNeedsDisplay:YES];
    134 
    135   // If our state needs it, keep going.
    136   if ([self pulsing]) {
    137     [self performSelector:_cmd withObject:nil afterDelay:0.02];
    138   }
    139 }
    140 
    141 - (gradient_button_cell::PulseState)pulseState {
    142   return pulseState_;
    143 }
    144 
    145 // Set the pulsing state.  This can either set the pulse to on or off
    146 // immediately (e.g. kPulsedOn, kPulsedOff) or initiate an animated
    147 // state change.
    148 - (void)setPulseState:(gradient_button_cell::PulseState)pstate {
    149   pulseState_ = pstate;
    150   pulseMultiplier_ = 0.0;
    151   [NSObject cancelPreviousPerformRequestsWithTarget:self];
    152   lastHoverUpdate_ = [NSDate timeIntervalSinceReferenceDate];
    153 
    154   switch (pstate) {
    155   case gradient_button_cell::kPulsedOn:
    156   case gradient_button_cell::kPulsedOff:
    157     outerStrokeAlphaMult_ = 1.0;
    158     [self setHoverAlpha:((pulseState_ == gradient_button_cell::kPulsedOn) ?
    159                          1.0 : 0.0)];
    160     [[self controlView] setNeedsDisplay:YES];
    161     break;
    162   case gradient_button_cell::kPulsingOn:
    163   case gradient_button_cell::kPulsingOff:
    164     outerStrokeAlphaMult_ = 1.0;
    165     // Set initial value then engage timer.
    166     [self setHoverAlpha:((pulseState_ == gradient_button_cell::kPulsingOn) ?
    167                          0.0 : 1.0)];
    168     [self performOnePulseStep];
    169     break;
    170   case gradient_button_cell::kPulsingContinuous:
    171     // Semantics of continuous pulsing are that we pulse independent
    172     // of mouse position.
    173     pulseMultiplier_ = 1.0;
    174     [self performOnePulseStep];
    175     break;
    176   default:
    177     CHECK(0);
    178     break;
    179   }
    180 }
    181 
    182 - (void)safelyStopPulsing {
    183   [NSObject cancelPreviousPerformRequestsWithTarget:self];
    184 }
    185 
    186 - (void)setIsContinuousPulsing:(BOOL)continuous {
    187   if (!continuous && pulseState_ != gradient_button_cell::kPulsingContinuous)
    188     return;
    189   if (continuous) {
    190     [self setPulseState:gradient_button_cell::kPulsingContinuous];
    191   } else {
    192     [self setPulseState:(isMouseInside_ ? gradient_button_cell::kPulsedOn :
    193                          gradient_button_cell::kPulsedOff)];
    194   }
    195 }
    196 
    197 - (BOOL)isContinuousPulsing {
    198   return (pulseState_ == gradient_button_cell::kPulsingContinuous) ?
    199       YES : NO;
    200 }
    201 
    202 #if 1
    203 // If we are not continuously pulsing, perform a pulse animation to
    204 // reflect our new state.
    205 - (void)setMouseInside:(BOOL)flag animate:(BOOL)animated {
    206   isMouseInside_ = flag;
    207   if (pulseState_ != gradient_button_cell::kPulsingContinuous) {
    208     if (animated) {
    209       [self setPulseState:(isMouseInside_ ? gradient_button_cell::kPulsingOn :
    210                            gradient_button_cell::kPulsingOff)];
    211     } else {
    212       [self setPulseState:(isMouseInside_ ? gradient_button_cell::kPulsedOn :
    213                            gradient_button_cell::kPulsedOff)];
    214     }
    215   }
    216 }
    217 #else
    218 
    219 - (void)adjustHoverValue {
    220   NSTimeInterval thisUpdate = [NSDate timeIntervalSinceReferenceDate];
    221 
    222   NSTimeInterval elapsed = thisUpdate - lastHoverUpdate_;
    223 
    224   CGFloat opacity = [self hoverAlpha];
    225   if (isMouseInside_) {
    226     opacity += elapsed / kAnimationShowDuration;
    227   } else {
    228     opacity -= elapsed / kAnimationHideDuration;
    229   }
    230 
    231   if (!isMouseInside_ && opacity < 0) {
    232     opacity = 0;
    233   } else if (isMouseInside_ && opacity > 1) {
    234     opacity = 1;
    235   } else {
    236     [self performSelector:_cmd withObject:nil afterDelay:0.02];
    237   }
    238   lastHoverUpdate_ = thisUpdate;
    239   [self setHoverAlpha:opacity];
    240 
    241   [[self controlView] setNeedsDisplay:YES];
    242 }
    243 
    244 - (void)setMouseInside:(BOOL)flag animate:(BOOL)animated {
    245   isMouseInside_ = flag;
    246   if (animated) {
    247     lastHoverUpdate_ = [NSDate timeIntervalSinceReferenceDate];
    248     [self adjustHoverValue];
    249   } else {
    250     [NSObject cancelPreviousPerformRequestsWithTarget:self];
    251     [self setHoverAlpha:flag ? 1.0 : 0.0];
    252   }
    253   [[self controlView] setNeedsDisplay:YES];
    254 }
    255 
    256 
    257 
    258 #endif
    259 
    260 - (NSGradient*)gradientForHoverAlpha:(CGFloat)hoverAlpha
    261                             isThemed:(BOOL)themed {
    262   CGFloat startAlpha = 0.6 + 0.3 * hoverAlpha;
    263   CGFloat endAlpha = 0.333 * hoverAlpha;
    264 
    265   if (themed) {
    266     startAlpha = 0.2 + 0.35 * hoverAlpha;
    267     endAlpha = 0.333 * hoverAlpha;
    268   }
    269 
    270   NSColor* startColor =
    271       [NSColor colorWithCalibratedWhite:1.0
    272                                   alpha:startAlpha];
    273   NSColor* endColor =
    274       [NSColor colorWithCalibratedWhite:1.0 - 0.15 * hoverAlpha
    275                                   alpha:endAlpha];
    276   NSGradient* gradient = [[NSGradient alloc] initWithColorsAndLocations:
    277                           startColor, hoverAlpha * 0.33,
    278                           endColor, 1.0, nil];
    279 
    280   return [gradient autorelease];
    281 }
    282 
    283 - (void)sharedInit {
    284   shouldTheme_ = YES;
    285   pulseState_ = gradient_button_cell::kPulsedOff;
    286   pulseMultiplier_ = 1.0;
    287   outerStrokeAlphaMult_ = 1.0;
    288   gradient_.reset([[self gradientForHoverAlpha:0.0 isThemed:NO] retain]);
    289 }
    290 
    291 - (void)setShouldTheme:(BOOL)shouldTheme {
    292   shouldTheme_ = shouldTheme;
    293 }
    294 
    295 - (NSImage*)overlayImage {
    296   return overlayImage_.get();
    297 }
    298 
    299 - (void)setOverlayImage:(NSImage*)image {
    300   overlayImage_.reset([image retain]);
    301   [[self controlView] setNeedsDisplay:YES];
    302 }
    303 
    304 - (NSBackgroundStyle)interiorBackgroundStyle {
    305   // Never lower the interior, since that just leads to a weird shadow which can
    306   // often interact badly with the theme.
    307   return NSBackgroundStyleRaised;
    308 }
    309 
    310 - (void)mouseEntered:(NSEvent*)theEvent {
    311   [self setMouseInside:YES animate:YES];
    312 }
    313 
    314 - (void)mouseExited:(NSEvent*)theEvent {
    315   [self setMouseInside:NO animate:YES];
    316 }
    317 
    318 - (BOOL)isMouseInside {
    319   return trackingArea_ && isMouseInside_;
    320 }
    321 
    322 // Since we have our own drawWithFrame:, we need to also have our own
    323 // logic for determining when the mouse is inside for honoring this
    324 // request.
    325 - (void)setShowsBorderOnlyWhileMouseInside:(BOOL)showOnly {
    326   [super setShowsBorderOnlyWhileMouseInside:showOnly];
    327   if (showOnly) {
    328     [self updateTrackingAreas];
    329   } else {
    330     if (trackingArea_) {
    331       [[self controlView] removeTrackingArea:trackingArea_];
    332       trackingArea_.reset(nil);
    333       if (isMouseInside_) {
    334         isMouseInside_ = NO;
    335         [[self controlView] setNeedsDisplay:YES];
    336       }
    337     }
    338   }
    339 }
    340 
    341 // TODO(viettrungluu): clean up/reorganize.
    342 - (void)drawBorderAndFillForTheme:(ui::ThemeProvider*)themeProvider
    343                       controlView:(NSView*)controlView
    344                         innerPath:(NSBezierPath*)innerPath
    345               showClickedGradient:(BOOL)showClickedGradient
    346             showHighlightGradient:(BOOL)showHighlightGradient
    347                        hoverAlpha:(CGFloat)hoverAlpha
    348                            active:(BOOL)active
    349                         cellFrame:(NSRect)cellFrame
    350                   defaultGradient:(NSGradient*)defaultGradient {
    351   BOOL isFlatButton = [self showsBorderOnlyWhileMouseInside];
    352 
    353   // For flat (unbordered when not hovered) buttons, never use the toolbar
    354   // button background image, but the modest gradient used for themed buttons.
    355   // To make things even more modest, scale the hover alpha down by 40 percent
    356   // unless clicked.
    357   NSColor* backgroundImageColor;
    358   BOOL useThemeGradient;
    359   if (isFlatButton) {
    360     backgroundImageColor = nil;
    361     useThemeGradient = YES;
    362     if (!showClickedGradient)
    363       hoverAlpha *= 0.6;
    364   } else {
    365     backgroundImageColor = nil;
    366     if (themeProvider &&
    367         themeProvider->HasCustomImage(IDR_THEME_BUTTON_BACKGROUND)) {
    368       backgroundImageColor =
    369           themeProvider->GetNSImageColorNamed(IDR_THEME_BUTTON_BACKGROUND);
    370     }
    371     useThemeGradient = backgroundImageColor ? YES : NO;
    372   }
    373 
    374   // The basic gradient shown inside; see above.
    375   NSGradient* gradient;
    376   if (hoverAlpha == 0 && !useThemeGradient) {
    377     gradient = defaultGradient ? defaultGradient
    378                                : gradient_;
    379   } else {
    380     gradient = [self gradientForHoverAlpha:hoverAlpha
    381                                   isThemed:useThemeGradient];
    382   }
    383 
    384   // If we're drawing a background image, show that; else possibly show the
    385   // clicked gradient.
    386   if (backgroundImageColor) {
    387     [backgroundImageColor set];
    388     // Set the phase to match window.
    389     NSRect trueRect = [controlView convertRect:cellFrame toView:nil];
    390     [[NSGraphicsContext currentContext]
    391         cr_setPatternPhase:NSMakePoint(NSMinX(trueRect), NSMaxY(trueRect))
    392                    forView:controlView];
    393     [innerPath fill];
    394   } else {
    395     if (showClickedGradient) {
    396       NSGradient* clickedGradient = nil;
    397       if (isFlatButton &&
    398           [self tag] == kStandardButtonTypeWithLimitedClickFeedback) {
    399         clickedGradient = gradient;
    400       } else {
    401         clickedGradient = themeProvider ? themeProvider->GetNSGradient(
    402             active ?
    403                 ThemeProperties::GRADIENT_TOOLBAR_BUTTON_PRESSED :
    404                 ThemeProperties::GRADIENT_TOOLBAR_BUTTON_PRESSED_INACTIVE) :
    405             nil;
    406       }
    407       [clickedGradient drawInBezierPath:innerPath angle:90.0];
    408     }
    409   }
    410 
    411   // Visually indicate unclicked, enabled buttons.
    412   if (!showClickedGradient && [self isEnabled]) {
    413     gfx::ScopedNSGraphicsContextSaveGState scopedGState;
    414     [innerPath addClip];
    415 
    416     // Draw the inner glow.
    417     if (hoverAlpha > 0) {
    418       [innerPath setLineWidth:2];
    419       [[NSColor colorWithCalibratedWhite:1.0 alpha:0.2 * hoverAlpha] setStroke];
    420       [innerPath stroke];
    421     }
    422 
    423     // Draw the top inner highlight.
    424     NSAffineTransform* highlightTransform = [NSAffineTransform transform];
    425     [highlightTransform translateXBy:1 yBy:1];
    426     base::scoped_nsobject<NSBezierPath> highlightPath([innerPath copy]);
    427     [highlightPath transformUsingAffineTransform:highlightTransform];
    428     [[NSColor colorWithCalibratedWhite:1.0 alpha:0.2] setStroke];
    429     [highlightPath stroke];
    430 
    431     // Draw the gradient inside.
    432     [gradient drawInBezierPath:innerPath angle:90.0];
    433   }
    434 
    435   // Don't draw anything else for disabled flat buttons.
    436   if (isFlatButton && ![self isEnabled])
    437     return;
    438 
    439   // Draw the outer stroke.
    440   NSColor* strokeColor = nil;
    441   if (showClickedGradient) {
    442     strokeColor = [NSColor
    443                     colorWithCalibratedWhite:0.0
    444                                        alpha:0.3 * outerStrokeAlphaMult_];
    445   } else {
    446     strokeColor = themeProvider ? themeProvider->GetNSColor(
    447         active ? ThemeProperties::COLOR_TOOLBAR_BUTTON_STROKE :
    448                  ThemeProperties::COLOR_TOOLBAR_BUTTON_STROKE_INACTIVE) :
    449         [NSColor colorWithCalibratedWhite:0.0
    450                                     alpha:0.3 * outerStrokeAlphaMult_];
    451   }
    452   [strokeColor setStroke];
    453 
    454   [innerPath setLineWidth:1];
    455   [innerPath stroke];
    456 }
    457 
    458 // TODO(viettrungluu): clean this up.
    459 // (Private)
    460 - (void)getDrawParamsForFrame:(NSRect)cellFrame
    461                        inView:(NSView*)controlView
    462                    innerFrame:(NSRect*)returnInnerFrame
    463                     innerPath:(NSBezierPath**)returnInnerPath
    464                      clipPath:(NSBezierPath**)returnClipPath {
    465   const CGFloat lineWidth = [controlView cr_lineWidth];
    466   const CGFloat halfLineWidth = lineWidth / 2.0;
    467 
    468   // Constants from Cole.  Will kConstant them once the feedback loop
    469   // is complete.
    470   NSRect drawFrame = NSInsetRect(cellFrame, 1.5 * lineWidth, 1.5 * lineWidth);
    471   NSRect innerFrame = NSInsetRect(cellFrame, lineWidth, lineWidth);
    472   const CGFloat radius = 3;
    473 
    474   ButtonType type = [[(NSControl*)controlView cell] tag];
    475   switch (type) {
    476     case kMiddleButtonType:
    477       drawFrame.size.width += 20;
    478       innerFrame.size.width += 2;
    479       // Fallthrough
    480     case kRightButtonType:
    481       drawFrame.origin.x -= 20;
    482       innerFrame.origin.x -= 2;
    483       // Fallthrough
    484     case kLeftButtonType:
    485     case kLeftButtonWithShadowType:
    486       drawFrame.size.width += 20;
    487       innerFrame.size.width += 2;
    488     default:
    489       break;
    490   }
    491   if (type == kLeftButtonWithShadowType)
    492     innerFrame.size.width -= 1.0;
    493 
    494   // Return results if |return...| not null.
    495   if (returnInnerFrame)
    496     *returnInnerFrame = innerFrame;
    497   if (returnInnerPath) {
    498     DCHECK(*returnInnerPath == nil);
    499     *returnInnerPath = [NSBezierPath bezierPathWithRoundedRect:drawFrame
    500                                                        xRadius:radius
    501                                                        yRadius:radius];
    502     [*returnInnerPath setLineWidth:lineWidth];
    503   }
    504   if (returnClipPath) {
    505     DCHECK(*returnClipPath == nil);
    506     NSRect clipPathRect =
    507         NSInsetRect(drawFrame, -halfLineWidth, -halfLineWidth);
    508     *returnClipPath = [NSBezierPath
    509         bezierPathWithRoundedRect:clipPathRect
    510                           xRadius:radius + halfLineWidth
    511                           yRadius:radius + halfLineWidth];
    512   }
    513 }
    514 
    515 // TODO(viettrungluu): clean this up.
    516 - (void)drawWithFrame:(NSRect)cellFrame inView:(NSView*)controlView {
    517   NSRect innerFrame;
    518   NSBezierPath* innerPath = nil;
    519   [self getDrawParamsForFrame:cellFrame
    520                        inView:controlView
    521                    innerFrame:&innerFrame
    522                     innerPath:&innerPath
    523                      clipPath:NULL];
    524 
    525   BOOL pressed = ([((NSControl*)[self controlView]) isEnabled] &&
    526                   [self isHighlighted]);
    527   NSWindow* window = [controlView window];
    528   ui::ThemeProvider* themeProvider = [window themeProvider];
    529   BOOL active = [window isKeyWindow] || [window isMainWindow];
    530 
    531   // Stroke the borders and appropriate fill gradient. If we're borderless, the
    532   // only time we want to draw the inner gradient is if we're highlighted or if
    533   // we're the first responder (when "Full Keyboard Access" is turned on).
    534   if (([self isBordered] && ![self showsBorderOnlyWhileMouseInside]) ||
    535       pressed ||
    536       [self isMouseInside] ||
    537       [self isContinuousPulsing] ||
    538       [self showsFirstResponder]) {
    539 
    540     // When pulsing we want the bookmark to stand out a little more.
    541     BOOL showClickedGradient = pressed ||
    542         (pulseState_ == gradient_button_cell::kPulsingContinuous);
    543 
    544     [self drawBorderAndFillForTheme:themeProvider
    545                         controlView:controlView
    546                           innerPath:innerPath
    547                 showClickedGradient:showClickedGradient
    548               showHighlightGradient:[self isHighlighted]
    549                          hoverAlpha:[self hoverAlpha]
    550                              active:active
    551                           cellFrame:cellFrame
    552                     defaultGradient:nil];
    553   }
    554 
    555   // If this is the left side of a segmented button, draw a slight shadow.
    556   ButtonType type = [[(NSControl*)controlView cell] tag];
    557   if (type == kLeftButtonWithShadowType) {
    558     const CGFloat lineWidth = [controlView cr_lineWidth];
    559     NSRect borderRect, contentRect;
    560     NSDivideRect(cellFrame, &borderRect, &contentRect, lineWidth, NSMaxXEdge);
    561     NSColor* stroke = themeProvider ? themeProvider->GetNSColor(
    562         active ? ThemeProperties::COLOR_TOOLBAR_BUTTON_STROKE :
    563                  ThemeProperties::COLOR_TOOLBAR_BUTTON_STROKE_INACTIVE) :
    564         [NSColor blackColor];
    565 
    566     [[stroke colorWithAlphaComponent:0.2] set];
    567     NSRectFillUsingOperation(NSInsetRect(borderRect, 0, 2),
    568                              NSCompositeSourceOver);
    569   }
    570   [self drawInteriorWithFrame:innerFrame inView:controlView];
    571 
    572   // Draws the blue focus ring.
    573   if ([self showsFirstResponder]) {
    574     gfx::ScopedNSGraphicsContextSaveGState scoped_state;
    575     const CGFloat lineWidth = [controlView cr_lineWidth];
    576     // insetX = 1.0 is used for the drawing of blue highlight so that this
    577     // highlight won't be too near the bookmark toolbar itself, in case we
    578     // draw bookmark buttons in bookmark toolbar.
    579     rect_path_utils::FrameRectWithInset(rect_path_utils::RoundedCornerAll,
    580                                         NSInsetRect(cellFrame, 0, lineWidth),
    581                                         1.0,            // insetX
    582                                         0.0,            // insetY
    583                                         3.0,            // outerRadius
    584                                         lineWidth * 2,  // lineWidth
    585                                         [controlView
    586                                             cr_keyboardFocusIndicatorColor]);
    587   }
    588 }
    589 
    590 - (void)drawInteriorWithFrame:(NSRect)cellFrame inView:(NSView*)controlView {
    591   const CGFloat lineWidth = [controlView cr_lineWidth];
    592 
    593   if (shouldTheme_) {
    594     BOOL isTemplate = [[self image] isTemplate];
    595 
    596     gfx::ScopedNSGraphicsContextSaveGState scopedGState;
    597 
    598     CGContextRef context =
    599         (CGContextRef)([[NSGraphicsContext currentContext] graphicsPort]);
    600 
    601     ThemeService* themeProvider = static_cast<ThemeService*>(
    602         [[controlView window] themeProvider]);
    603     NSColor* color = themeProvider ?
    604         themeProvider->GetNSColorTint(ThemeProperties::TINT_BUTTONS) :
    605         [NSColor blackColor];
    606 
    607     if (isTemplate && themeProvider && themeProvider->UsingDefaultTheme()) {
    608       base::scoped_nsobject<NSShadow> shadow([[NSShadow alloc] init]);
    609       [shadow.get() setShadowColor:themeProvider->GetNSColor(
    610           ThemeProperties::COLOR_TOOLBAR_BEZEL)];
    611       [shadow.get() setShadowOffset:NSMakeSize(0.0, -lineWidth)];
    612       [shadow setShadowBlurRadius:lineWidth];
    613       [shadow set];
    614     }
    615 
    616     CGContextBeginTransparencyLayer(context, 0);
    617     NSRect imageRect = NSZeroRect;
    618     imageRect.size = [[self image] size];
    619     NSRect drawRect = [self imageRectForBounds:cellFrame];
    620     [[self image] drawInRect:drawRect
    621                     fromRect:imageRect
    622                    operation:NSCompositeSourceOver
    623                     fraction:[self isEnabled] ? 1.0 : 0.5
    624               respectFlipped:YES
    625                        hints:nil];
    626     if (isTemplate && color) {
    627       [color set];
    628       NSRectFillUsingOperation(cellFrame, NSCompositeSourceAtop);
    629     }
    630     CGContextEndTransparencyLayer(context);
    631   } else {
    632     // NSCell draws these off-center for some reason, probably because of the
    633     // positioning of the control in the xib.
    634     [super drawInteriorWithFrame:NSOffsetRect(cellFrame, 0, lineWidth)
    635                           inView:controlView];
    636   }
    637 
    638   if (overlayImage_) {
    639     NSRect imageRect = NSZeroRect;
    640     imageRect.size = [overlayImage_ size];
    641     [overlayImage_ drawInRect:[self imageRectForBounds:cellFrame]
    642                      fromRect:imageRect
    643                     operation:NSCompositeSourceOver
    644                      fraction:[self isEnabled] ? 1.0 : 0.5
    645                respectFlipped:YES
    646                         hints:nil];
    647   }
    648 }
    649 
    650 - (int)verticalTextOffset {
    651   return 1;
    652 }
    653 
    654 // Overriden from NSButtonCell so we can display a nice fadeout effect for
    655 // button titles that overflow.
    656 // This method is copied in the most part from GTMFadeTruncatingTextFieldCell,
    657 // the only difference is that here we draw the text ourselves rather than
    658 // calling the super to do the work.
    659 // We can't use GTMFadeTruncatingTextFieldCell because there's no easy way to
    660 // get it to work with NSButtonCell.
    661 // TODO(jeremy): Move this to GTM.
    662 - (NSRect)drawTitle:(NSAttributedString*)title
    663           withFrame:(NSRect)cellFrame
    664              inView:(NSView*)controlView {
    665   NSSize size = [title size];
    666 
    667   // Empirically, Cocoa will draw an extra 2 pixels past NSWidth(cellFrame)
    668   // before it clips the text.
    669   const CGFloat kOverflowBeforeClip = 2;
    670   BOOL clipping = YES;
    671   if (std::floor(size.width) <= (NSWidth(cellFrame) + kOverflowBeforeClip)) {
    672     cellFrame.origin.y += ([self verticalTextOffset] - 1);
    673     clipping = NO;
    674   }
    675 
    676   // Gradient is about twice our line height long.
    677   CGFloat gradientWidth = MIN(size.height * 2, NSWidth(cellFrame) / 4);
    678 
    679   NSRect solidPart, gradientPart;
    680   NSDivideRect(cellFrame, &gradientPart, &solidPart, gradientWidth, NSMaxXEdge);
    681 
    682   // Draw non-gradient part without transparency layer, as light text on a dark
    683   // background looks bad with a gradient layer.
    684   NSPoint textOffset = NSZeroPoint;
    685   {
    686     gfx::ScopedNSGraphicsContextSaveGState scopedGState;
    687     if (clipping)
    688       [NSBezierPath clipRect:solidPart];
    689 
    690     // 11 is the magic number needed to make this match the native
    691     // NSButtonCell's label display.
    692     CGFloat textLeft = [[self image] size].width + 11;
    693 
    694     // For some reason, the height of cellFrame as passed in is totally bogus.
    695     // For vertical centering purposes, we need the bounds of the containing
    696     // view.
    697     NSRect buttonFrame = [[self controlView] frame];
    698 
    699     // Call the vertical offset to match native NSButtonCell's version.
    700     textOffset = NSMakePoint(textLeft,
    701                              (NSHeight(buttonFrame) - size.height) / 2 +
    702                              [self verticalTextOffset]);
    703     [title drawAtPoint:textOffset];
    704   }
    705 
    706   if (!clipping)
    707     return cellFrame;
    708 
    709   // Draw the gradient part with a transparency layer. This makes the text look
    710   // suboptimal, but since it fades out, that's ok.
    711   gfx::ScopedNSGraphicsContextSaveGState scopedGState;
    712   [NSBezierPath clipRect:gradientPart];
    713   CGContextRef context = static_cast<CGContextRef>(
    714       [[NSGraphicsContext currentContext] graphicsPort]);
    715   CGContextBeginTransparencyLayerWithRect(context,
    716                                           NSRectToCGRect(gradientPart), 0);
    717   [title drawAtPoint:textOffset];
    718 
    719   NSColor *color = [NSColor textColor];
    720   NSColor *alphaColor = [color colorWithAlphaComponent:0.0];
    721   NSGradient *mask = [[NSGradient alloc] initWithStartingColor:color
    722                                                    endingColor:alphaColor];
    723 
    724   // Draw the gradient mask
    725   CGContextSetBlendMode(context, kCGBlendModeDestinationIn);
    726   [mask drawFromPoint:NSMakePoint(NSMaxX(cellFrame) - gradientWidth,
    727                                   NSMinY(cellFrame))
    728               toPoint:NSMakePoint(NSMaxX(cellFrame),
    729                                   NSMinY(cellFrame))
    730               options:NSGradientDrawsBeforeStartingLocation];
    731   [mask release];
    732   CGContextEndTransparencyLayer(context);
    733 
    734   return cellFrame;
    735 }
    736 
    737 - (NSBezierPath*)clipPathForFrame:(NSRect)cellFrame
    738                            inView:(NSView*)controlView {
    739   NSBezierPath* boundingPath = nil;
    740   [self getDrawParamsForFrame:cellFrame
    741                        inView:controlView
    742                    innerFrame:NULL
    743                     innerPath:NULL
    744                      clipPath:&boundingPath];
    745   return boundingPath;
    746 }
    747 
    748 - (void)resetCursorRect:(NSRect)cellFrame inView:(NSView*)controlView {
    749   [super resetCursorRect:cellFrame inView:controlView];
    750   if (trackingArea_)
    751     [self updateTrackingAreas];
    752 }
    753 
    754 - (BOOL)isMouseReallyInside {
    755   BOOL mouseInView = NO;
    756   NSView* controlView = [self controlView];
    757   NSWindow* window = [controlView window];
    758   NSRect bounds = [controlView bounds];
    759   if (window) {
    760     NSPoint mousePoint = [window mouseLocationOutsideOfEventStream];
    761     mousePoint = [controlView convertPoint:mousePoint fromView:nil];
    762     mouseInView = [controlView mouse:mousePoint inRect:bounds];
    763   }
    764   return mouseInView;
    765 }
    766 
    767 - (void)updateTrackingAreas {
    768   NSView* controlView = [self controlView];
    769   BOOL mouseInView = [self isMouseReallyInside];
    770 
    771   if (trackingArea_.get())
    772     [controlView removeTrackingArea:trackingArea_];
    773 
    774   NSTrackingAreaOptions options = NSTrackingMouseEnteredAndExited |
    775                                   NSTrackingActiveInActiveApp;
    776   if (mouseInView)
    777     options |= NSTrackingAssumeInside;
    778 
    779   trackingArea_.reset([[NSTrackingArea alloc]
    780                         initWithRect:[controlView bounds]
    781                              options:options
    782                                owner:self
    783                             userInfo:nil]);
    784   if (isMouseInside_ != mouseInView) {
    785     [self setMouseInside:mouseInView animate:NO];
    786     [controlView setNeedsDisplay:YES];
    787   }
    788 }
    789 
    790 @end
    791