- add sources.
[platform/framework/web/crosswalk.git] / src / chrome / browser / ui / cocoa / tabs / tab_view.mm
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 #import "chrome/browser/ui/cocoa/tabs/tab_view.h"
6
7 #include "base/logging.h"
8 #include "base/mac/sdk_forward_declarations.h"
9 #include "chrome/browser/themes/theme_service.h"
10 #import "chrome/browser/ui/cocoa/nsview_additions.h"
11 #import "chrome/browser/ui/cocoa/tabs/tab_controller.h"
12 #import "chrome/browser/ui/cocoa/tabs/tab_window_controller.h"
13 #import "chrome/browser/ui/cocoa/themed_window.h"
14 #import "chrome/browser/ui/cocoa/view_id_util.h"
15 #include "grit/generated_resources.h"
16 #include "grit/theme_resources.h"
17 #import "ui/base/cocoa/nsgraphics_context_additions.h"
18 #include "ui/base/l10n/l10n_util.h"
19 #include "ui/base/resource/resource_bundle.h"
20 #include "ui/gfx/scoped_ns_graphics_context_save_gstate_mac.h"
21
22
23 const int kMaskHeight = 29;  // Height of the mask bitmap.
24 const int kFillHeight = 25;  // Height of the "mask on" part of the mask bitmap.
25
26 // Constants for inset and control points for tab shape.
27 const CGFloat kInsetMultiplier = 2.0/3.0;
28
29 // The amount of time in seconds during which each type of glow increases, holds
30 // steady, and decreases, respectively.
31 const NSTimeInterval kHoverShowDuration = 0.2;
32 const NSTimeInterval kHoverHoldDuration = 0.02;
33 const NSTimeInterval kHoverHideDuration = 0.4;
34 const NSTimeInterval kAlertShowDuration = 0.4;
35 const NSTimeInterval kAlertHoldDuration = 0.4;
36 const NSTimeInterval kAlertHideDuration = 0.4;
37
38 // The default time interval in seconds between glow updates (when
39 // increasing/decreasing).
40 const NSTimeInterval kGlowUpdateInterval = 0.025;
41
42 // This is used to judge whether the mouse has moved during rapid closure; if it
43 // has moved less than the threshold, we want to close the tab.
44 const CGFloat kRapidCloseDist = 2.5;
45
46 @interface TabView(Private)
47
48 - (void)resetLastGlowUpdateTime;
49 - (NSTimeInterval)timeElapsedSinceLastGlowUpdate;
50 - (void)adjustGlowValue;
51 - (CGImageRef)tabClippingMask;
52
53 @end  // TabView(Private)
54
55 @implementation TabView
56
57 @synthesize state = state_;
58 @synthesize hoverAlpha = hoverAlpha_;
59 @synthesize alertAlpha = alertAlpha_;
60 @synthesize closing = closing_;
61
62 + (CGFloat)insetMultiplier {
63   return kInsetMultiplier;
64 }
65
66 - (id)initWithFrame:(NSRect)frame
67          controller:(TabController*)controller
68         closeButton:(HoverCloseButton*)closeButton {
69   self = [super initWithFrame:frame];
70   if (self) {
71     controller_ = controller;
72     closeButton_ = closeButton;
73   }
74   return self;
75 }
76
77 - (void)dealloc {
78   // Cancel any delayed requests that may still be pending (drags or hover).
79   [NSObject cancelPreviousPerformRequestsWithTarget:self];
80   [super dealloc];
81 }
82
83 // Called to obtain the context menu for when the user hits the right mouse
84 // button (or control-clicks). (Note that -rightMouseDown: is *not* called for
85 // control-click.)
86 - (NSMenu*)menu {
87   if ([self isClosing])
88     return nil;
89
90   // Sheets, being window-modal, should block contextual menus. For some reason
91   // they do not. Disallow them ourselves.
92   if ([[self window] attachedSheet])
93     return nil;
94
95   return [controller_ menu];
96 }
97
98 - (void)resizeSubviewsWithOldSize:(NSSize)oldBoundsSize {
99   [super resizeSubviewsWithOldSize:oldBoundsSize];
100   // Called when our view is resized. If it gets too small, start by hiding
101   // the close button and only show it if tab is selected. Eventually, hide the
102   // icon as well.
103   [controller_ updateVisibility];
104 }
105
106 // Overridden so that mouse clicks come to this view (the parent of the
107 // hierarchy) first. We want to handle clicks and drags in this class and
108 // leave the background button for display purposes only.
109 - (BOOL)acceptsFirstMouse:(NSEvent*)theEvent {
110   return YES;
111 }
112
113 - (void)mouseEntered:(NSEvent*)theEvent {
114   isMouseInside_ = YES;
115   [self resetLastGlowUpdateTime];
116   [self adjustGlowValue];
117 }
118
119 - (void)mouseMoved:(NSEvent*)theEvent {
120   hoverPoint_ = [self convertPoint:[theEvent locationInWindow]
121                           fromView:nil];
122   [self setNeedsDisplay:YES];
123 }
124
125 - (void)mouseExited:(NSEvent*)theEvent {
126   isMouseInside_ = NO;
127   hoverHoldEndTime_ =
128       [NSDate timeIntervalSinceReferenceDate] + kHoverHoldDuration;
129   [self resetLastGlowUpdateTime];
130   [self adjustGlowValue];
131 }
132
133 - (void)setTrackingEnabled:(BOOL)enabled {
134   if (![closeButton_ isHidden]) {
135     [closeButton_ setTrackingEnabled:enabled];
136   }
137 }
138
139 // Determines which view a click in our frame actually hit. It's either this
140 // view or our child close button.
141 - (NSView*)hitTest:(NSPoint)aPoint {
142   NSPoint viewPoint = [self convertPoint:aPoint fromView:[self superview]];
143   if (![closeButton_ isHidden])
144     if (NSPointInRect(viewPoint, [closeButton_ frame])) return closeButton_;
145
146   NSRect pointRect = NSMakeRect(viewPoint.x, viewPoint.y, 1, 1);
147
148   ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();
149   NSImage* left = rb.GetNativeImageNamed(IDR_TAB_ALPHA_LEFT).ToNSImage();
150   if (viewPoint.x < [left size].width) {
151     NSRect imageRect = NSMakeRect(0, 0, [left size].width, [left size].height);
152     if ([left hitTestRect:pointRect withImageDestinationRect:imageRect
153           context:nil hints:nil flipped:NO]) {
154       return self;
155     }
156     return nil;
157   }
158
159   NSImage* right = rb.GetNativeImageNamed(IDR_TAB_ALPHA_RIGHT).ToNSImage();
160   CGFloat rightX = NSWidth([self bounds]) - [right size].width;
161   if (viewPoint.x > rightX) {
162     NSRect imageRect = NSMakeRect(
163         rightX, 0, [right size].width, [right size].height);
164     if ([right hitTestRect:pointRect withImageDestinationRect:imageRect
165           context:nil hints:nil flipped:NO]) {
166       return self;
167     }
168     return nil;
169   }
170
171   if (viewPoint.y < kFillHeight)
172     return self;
173   return nil;
174 }
175
176 // Returns |YES| if this tab can be torn away into a new window.
177 - (BOOL)canBeDragged {
178   return [controller_ tabCanBeDragged:controller_];
179 }
180
181 // Handle clicks and drags in this button. We get here because we have
182 // overridden acceptsFirstMouse: and the click is within our bounds.
183 - (void)mouseDown:(NSEvent*)theEvent {
184   if ([self isClosing])
185     return;
186
187   // Record the point at which this event happened. This is used by other mouse
188   // events that are dispatched from |-maybeStartDrag::|.
189   mouseDownPoint_ = [theEvent locationInWindow];
190
191   // Record the state of the close button here, because selecting the tab will
192   // unhide it.
193   BOOL closeButtonActive = ![closeButton_ isHidden];
194
195   // During the tab closure animation (in particular, during rapid tab closure),
196   // we may get incorrectly hit with a mouse down. If it should have gone to the
197   // close button, we send it there -- it should then track the mouse, so we
198   // don't have to worry about mouse ups.
199   if (closeButtonActive && [controller_ inRapidClosureMode]) {
200     NSPoint hitLocation = [[self superview] convertPoint:mouseDownPoint_
201                                                 fromView:nil];
202     if ([self hitTest:hitLocation] == closeButton_) {
203       [closeButton_ mouseDown:theEvent];
204       return;
205     }
206   }
207
208   // If the tab gets torn off, the tab controller will be removed from the tab
209   // strip and then deallocated. This will also result in *us* being
210   // deallocated. Both these are bad, so we prevent this by retaining the
211   // controller.
212   base::scoped_nsobject<TabController> controller([controller_ retain]);
213
214   // Try to initiate a drag. This will spin a custom event loop and may
215   // dispatch other mouse events.
216   [controller_ maybeStartDrag:theEvent forTab:controller];
217
218   // The custom loop has ended, so clear the point.
219   mouseDownPoint_ = NSZeroPoint;
220 }
221
222 - (void)mouseUp:(NSEvent*)theEvent {
223   // Check for rapid tab closure.
224   if ([theEvent type] == NSLeftMouseUp) {
225     NSPoint upLocation = [theEvent locationInWindow];
226     CGFloat dx = upLocation.x - mouseDownPoint_.x;
227     CGFloat dy = upLocation.y - mouseDownPoint_.y;
228
229     // During rapid tab closure (mashing tab close buttons), we may get hit
230     // with a mouse down. As long as the mouse up is over the close button,
231     // and the mouse hasn't moved too much, we close the tab.
232     if (![closeButton_ isHidden] &&
233         (dx*dx + dy*dy) <= kRapidCloseDist*kRapidCloseDist &&
234         [controller_ inRapidClosureMode]) {
235       NSPoint hitLocation =
236           [[self superview] convertPoint:[theEvent locationInWindow]
237                                 fromView:nil];
238       if ([self hitTest:hitLocation] == closeButton_) {
239         [controller_ closeTab:self];
240         return;
241       }
242     }
243   }
244
245   // Fire the action to select the tab.
246   [controller_ selectTab:self];
247
248   // Messaging the drag controller with |-endDrag:| would seem like the right
249   // thing to do here. But, when a tab has been detached, the controller's
250   // target is nil until the drag is finalized. Since |-mouseUp:| gets called
251   // via the manual event loop inside -[TabStripDragController
252   // maybeStartDrag:forTab:], the drag controller can end the dragging session
253   // itself directly after calling this.
254 }
255
256 - (void)otherMouseUp:(NSEvent*)theEvent {
257   if ([self isClosing])
258     return;
259
260   // Support middle-click-to-close.
261   if ([theEvent buttonNumber] == 2) {
262     // |-hitTest:| takes a location in the superview's coordinates.
263     NSPoint upLocation =
264         [[self superview] convertPoint:[theEvent locationInWindow]
265                               fromView:nil];
266     // If the mouse up occurred in our view or over the close button, then
267     // close.
268     if ([self hitTest:upLocation])
269       [controller_ closeTab:self];
270   }
271 }
272
273 // Returns the color used to draw the background of a tab. |selected| selects
274 // between the foreground and background tabs.
275 - (NSColor*)backgroundColorForSelected:(bool)selected {
276   ThemeService* themeProvider =
277       static_cast<ThemeService*>([[self window] themeProvider]);
278   if (!themeProvider)
279     return [[self window] backgroundColor];
280
281   int bitmapResources[2][2] = {
282     // Background window.
283     {
284       IDR_THEME_TAB_BACKGROUND_INACTIVE,  // Background tab.
285       IDR_THEME_TOOLBAR_INACTIVE,         // Active tab.
286     },
287     // Currently focused window.
288     {
289       IDR_THEME_TAB_BACKGROUND,  // Background tab.
290       IDR_THEME_TOOLBAR,         // Active tab.
291     },
292   };
293
294   // Themes don't have an inactive image so only look for one if there's no
295   // theme.
296   bool active = [[self window] isKeyWindow] || [[self window] isMainWindow] ||
297                 !themeProvider->UsingDefaultTheme();
298   return themeProvider->GetNSImageColorNamed(bitmapResources[active][selected]);
299 }
300
301 // Draws the active tab background.
302 - (void)drawFillForActiveTab:(NSRect)dirtyRect {
303   NSColor* backgroundImageColor = [self backgroundColorForSelected:YES];
304   [backgroundImageColor set];
305
306   // Themes can have partially transparent images. NSRectFill() is measurably
307   // faster though, so call it for the known-safe default theme.
308   ThemeService* themeProvider =
309       static_cast<ThemeService*>([[self window] themeProvider]);
310   if (themeProvider && themeProvider->UsingDefaultTheme())
311     NSRectFill(dirtyRect);
312   else
313     NSRectFillUsingOperation(dirtyRect, NSCompositeSourceOver);
314 }
315
316 // Draws the tab background.
317 - (void)drawFill:(NSRect)dirtyRect {
318   gfx::ScopedNSGraphicsContextSaveGState scopedGState;
319   NSGraphicsContext* context = [NSGraphicsContext currentContext];
320   CGContextRef cgContext = static_cast<CGContextRef>([context graphicsPort]);
321
322   ThemeService* themeProvider =
323       static_cast<ThemeService*>([[self window] themeProvider]);
324   NSPoint position = [[self window]
325       themeImagePositionForAlignment: THEME_IMAGE_ALIGN_WITH_TAB_STRIP];
326   [context cr_setPatternPhase:position forView:self];
327
328   CGImageRef mask([self tabClippingMask]);
329   CGRect maskBounds = CGRectMake(0, 0, maskCacheWidth_, kMaskHeight);
330   CGContextClipToMask(cgContext, maskBounds, mask);
331
332   bool selected = [self state];
333   if (selected) {
334     [self drawFillForActiveTab:dirtyRect];
335     return;
336   }
337
338   // Background tabs should not paint over the tab strip separator, which is
339   // two pixels high in both lodpi and hidpi.
340   if (dirtyRect.origin.y < 1)
341     dirtyRect.origin.y = 2 * [self cr_lineWidth];
342
343   // Draw the tab background.
344   NSColor* backgroundImageColor = [self backgroundColorForSelected:NO];
345   [backgroundImageColor set];
346
347   // Themes can have partially transparent images. NSRectFill() is measurably
348   // faster though, so call it for the known-safe default theme.
349   bool usingDefaultTheme = themeProvider && themeProvider->UsingDefaultTheme();
350   if (usingDefaultTheme)
351     NSRectFill(dirtyRect);
352   else
353     NSRectFillUsingOperation(dirtyRect, NSCompositeSourceOver);
354
355   // Draw the glow for hover and the overlay for alerts.
356   CGFloat hoverAlpha = [self hoverAlpha];
357   CGFloat alertAlpha = [self alertAlpha];
358   if (hoverAlpha > 0 || alertAlpha > 0) {
359     gfx::ScopedNSGraphicsContextSaveGState contextSave;
360     CGContextBeginTransparencyLayer(cgContext, 0);
361
362     // The alert glow overlay is like the selected state but at most at most 80%
363     // opaque. The hover glow brings up the overlay's opacity at most 50%.
364     CGFloat backgroundAlpha = 0.8 * alertAlpha;
365     backgroundAlpha += (1 - backgroundAlpha) * 0.5 * hoverAlpha;
366     CGContextSetAlpha(cgContext, backgroundAlpha);
367
368     [self drawFillForActiveTab:dirtyRect];
369
370     // ui::ThemeProvider::HasCustomImage is true only if the theme provides the
371     // image. However, even if the theme doesn't provide a tab background, the
372     // theme machinery will make one if given a frame image. See
373     // BrowserThemePack::GenerateTabBackgroundImages for details.
374     BOOL hasCustomTheme = themeProvider &&
375         (themeProvider->HasCustomImage(IDR_THEME_TAB_BACKGROUND) ||
376          themeProvider->HasCustomImage(IDR_THEME_FRAME));
377     // Draw a mouse hover gradient for the default themes.
378     if (hoverAlpha > 0) {
379       if (themeProvider && !hasCustomTheme) {
380         base::scoped_nsobject<NSGradient> glow([NSGradient alloc]);
381         [glow initWithStartingColor:[NSColor colorWithCalibratedWhite:1.0
382                                         alpha:1.0 * hoverAlpha]
383                         endingColor:[NSColor colorWithCalibratedWhite:1.0
384                                                                 alpha:0.0]];
385         NSRect rect = [self bounds];
386         NSPoint point = hoverPoint_;
387         point.y = NSHeight(rect);
388         [glow drawFromCenter:point
389                       radius:0.0
390                     toCenter:point
391                       radius:NSWidth(rect) / 3.0
392                      options:NSGradientDrawsBeforeStartingLocation];
393       }
394     }
395
396     CGContextEndTransparencyLayer(cgContext);
397   }
398 }
399
400 // Draws the tab outline.
401 - (void)drawStroke:(NSRect)dirtyRect {
402   BOOL focused = [[self window] isKeyWindow] || [[self window] isMainWindow];
403   CGFloat alpha = focused ? 1.0 : tabs::kImageNoFocusAlpha;
404
405   ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();
406   float height =
407       [rb.GetNativeImageNamed(IDR_TAB_ACTIVE_LEFT).ToNSImage() size].height;
408   if ([controller_ active]) {
409     NSDrawThreePartImage(NSMakeRect(0, 0, NSWidth([self bounds]), height),
410         rb.GetNativeImageNamed(IDR_TAB_ACTIVE_LEFT).ToNSImage(),
411         rb.GetNativeImageNamed(IDR_TAB_ACTIVE_CENTER).ToNSImage(),
412         rb.GetNativeImageNamed(IDR_TAB_ACTIVE_RIGHT).ToNSImage(),
413         /*vertical=*/NO,
414         NSCompositeSourceOver,
415         alpha,
416         /*flipped=*/NO);
417   } else {
418     NSDrawThreePartImage(NSMakeRect(0, 0, NSWidth([self bounds]), height),
419         rb.GetNativeImageNamed(IDR_TAB_INACTIVE_LEFT).ToNSImage(),
420         rb.GetNativeImageNamed(IDR_TAB_INACTIVE_CENTER).ToNSImage(),
421         rb.GetNativeImageNamed(IDR_TAB_INACTIVE_RIGHT).ToNSImage(),
422         /*vertical=*/NO,
423         NSCompositeSourceOver,
424         alpha,
425         /*flipped=*/NO);
426   }
427 }
428
429 - (void)drawRect:(NSRect)dirtyRect {
430   // Text, close button, and image are drawn by subviews.
431   [self drawFill:dirtyRect];
432   [self drawStroke:dirtyRect];
433 }
434
435 - (void)setFrameOrigin:(NSPoint)origin {
436   // The background color depends on the view's vertical position.
437   if (NSMinY([self frame]) != origin.y)
438     [self setNeedsDisplay:YES];
439   [super setFrameOrigin:origin];
440 }
441
442 // Override this to catch the text so that we can choose when to display it.
443 - (void)setToolTip:(NSString*)string {
444   toolTipText_.reset([string retain]);
445 }
446
447 - (NSString*)toolTipText {
448   if (!toolTipText_.get()) {
449     return @"";
450   }
451   return toolTipText_.get();
452 }
453
454 - (void)viewDidMoveToWindow {
455   [super viewDidMoveToWindow];
456   if ([self window]) {
457     [controller_ updateTitleColor];
458   }
459 }
460
461 - (void)setState:(NSCellStateValue)state {
462   if (state_ == state)
463     return;
464   state_ = state;
465   [self setNeedsDisplay:YES];
466 }
467
468 - (void)setClosing:(BOOL)closing {
469   closing_ = closing;  // Safe because the property is nonatomic.
470   // When closing, ensure clicks to the close button go nowhere.
471   if (closing) {
472     [closeButton_ setTarget:nil];
473     [closeButton_ setAction:nil];
474   }
475 }
476
477 - (void)startAlert {
478   // Do not start a new alert while already alerting or while in a decay cycle.
479   if (alertState_ == tabs::kAlertNone) {
480     alertState_ = tabs::kAlertRising;
481     [self resetLastGlowUpdateTime];
482     [self adjustGlowValue];
483   }
484 }
485
486 - (void)cancelAlert {
487   if (alertState_ != tabs::kAlertNone) {
488     alertState_ = tabs::kAlertFalling;
489     alertHoldEndTime_ =
490         [NSDate timeIntervalSinceReferenceDate] + kGlowUpdateInterval;
491     [self resetLastGlowUpdateTime];
492     [self adjustGlowValue];
493   }
494 }
495
496 - (BOOL)accessibilityIsIgnored {
497   return NO;
498 }
499
500 - (NSArray*)accessibilityActionNames {
501   NSArray* parentActions = [super accessibilityActionNames];
502
503   return [parentActions arrayByAddingObject:NSAccessibilityPressAction];
504 }
505
506 - (NSArray*)accessibilityAttributeNames {
507   NSMutableArray* attributes =
508       [[super accessibilityAttributeNames] mutableCopy];
509   [attributes addObject:NSAccessibilityTitleAttribute];
510   [attributes addObject:NSAccessibilityEnabledAttribute];
511   [attributes addObject:NSAccessibilityValueAttribute];
512
513   return [attributes autorelease];
514 }
515
516 - (BOOL)accessibilityIsAttributeSettable:(NSString*)attribute {
517   if ([attribute isEqual:NSAccessibilityTitleAttribute])
518     return NO;
519
520   if ([attribute isEqual:NSAccessibilityEnabledAttribute])
521     return NO;
522
523   if ([attribute isEqual:NSAccessibilityValueAttribute])
524     return YES;
525
526   return [super accessibilityIsAttributeSettable:attribute];
527 }
528
529 - (void)accessibilityPerformAction:(NSString*)action {
530   if ([action isEqual:NSAccessibilityPressAction] &&
531       [[controller_ target] respondsToSelector:[controller_ action]]) {
532     [[controller_ target] performSelector:[controller_ action]
533         withObject:self];
534     NSAccessibilityPostNotification(self,
535                                     NSAccessibilityValueChangedNotification);
536   } else {
537     [super accessibilityPerformAction:action];
538   }
539 }
540
541 - (id)accessibilityAttributeValue:(NSString*)attribute {
542   if ([attribute isEqual:NSAccessibilityRoleAttribute])
543     return NSAccessibilityRadioButtonRole;
544   if ([attribute isEqual:NSAccessibilityRoleDescriptionAttribute])
545     return l10n_util::GetNSStringWithFixup(IDS_ACCNAME_TAB);
546   if ([attribute isEqual:NSAccessibilityTitleAttribute])
547     return [controller_ title];
548   if ([attribute isEqual:NSAccessibilityValueAttribute])
549     return [NSNumber numberWithInt:[controller_ selected]];
550   if ([attribute isEqual:NSAccessibilityEnabledAttribute])
551     return [NSNumber numberWithBool:YES];
552
553   return [super accessibilityAttributeValue:attribute];
554 }
555
556 - (ViewID)viewID {
557   return VIEW_ID_TAB;
558 }
559
560 @end  // @implementation TabView
561
562 @implementation TabView (TabControllerInterface)
563
564 - (void)setController:(TabController*)controller {
565   controller_ = controller;
566 }
567
568 @end  // @implementation TabView (TabControllerInterface)
569
570 @implementation TabView(Private)
571
572 - (void)resetLastGlowUpdateTime {
573   lastGlowUpdate_ = [NSDate timeIntervalSinceReferenceDate];
574 }
575
576 - (NSTimeInterval)timeElapsedSinceLastGlowUpdate {
577   return [NSDate timeIntervalSinceReferenceDate] - lastGlowUpdate_;
578 }
579
580 - (void)adjustGlowValue {
581   // A time interval long enough to represent no update.
582   const NSTimeInterval kNoUpdate = 1000000;
583
584   // Time until next update for either glow.
585   NSTimeInterval nextUpdate = kNoUpdate;
586
587   NSTimeInterval elapsed = [self timeElapsedSinceLastGlowUpdate];
588   NSTimeInterval currentTime = [NSDate timeIntervalSinceReferenceDate];
589
590   // TODO(viettrungluu): <http://crbug.com/30617> -- split off the stuff below
591   // into a pure function and add a unit test.
592
593   CGFloat hoverAlpha = [self hoverAlpha];
594   if (isMouseInside_) {
595     // Increase hover glow until it's 1.
596     if (hoverAlpha < 1) {
597       hoverAlpha = MIN(hoverAlpha + elapsed / kHoverShowDuration, 1);
598       [self setHoverAlpha:hoverAlpha];
599       nextUpdate = MIN(kGlowUpdateInterval, nextUpdate);
600     }  // Else already 1 (no update needed).
601   } else {
602     if (currentTime >= hoverHoldEndTime_) {
603       // No longer holding, so decrease hover glow until it's 0.
604       if (hoverAlpha > 0) {
605         hoverAlpha = MAX(hoverAlpha - elapsed / kHoverHideDuration, 0);
606         [self setHoverAlpha:hoverAlpha];
607         nextUpdate = MIN(kGlowUpdateInterval, nextUpdate);
608       }  // Else already 0 (no update needed).
609     } else {
610       // Schedule update for end of hold time.
611       nextUpdate = MIN(hoverHoldEndTime_ - currentTime, nextUpdate);
612     }
613   }
614
615   CGFloat alertAlpha = [self alertAlpha];
616   if (alertState_ == tabs::kAlertRising) {
617     // Increase alert glow until it's 1 ...
618     alertAlpha = MIN(alertAlpha + elapsed / kAlertShowDuration, 1);
619     [self setAlertAlpha:alertAlpha];
620
621     // ... and having reached 1, switch to holding.
622     if (alertAlpha >= 1) {
623       alertState_ = tabs::kAlertHolding;
624       alertHoldEndTime_ = currentTime + kAlertHoldDuration;
625       nextUpdate = MIN(kAlertHoldDuration, nextUpdate);
626     } else {
627       nextUpdate = MIN(kGlowUpdateInterval, nextUpdate);
628     }
629   } else if (alertState_ != tabs::kAlertNone) {
630     if (alertAlpha > 0) {
631       if (currentTime >= alertHoldEndTime_) {
632         // Stop holding, then decrease alert glow (until it's 0).
633         if (alertState_ == tabs::kAlertHolding) {
634           alertState_ = tabs::kAlertFalling;
635           nextUpdate = MIN(kGlowUpdateInterval, nextUpdate);
636         } else {
637           DCHECK_EQ(tabs::kAlertFalling, alertState_);
638           alertAlpha = MAX(alertAlpha - elapsed / kAlertHideDuration, 0);
639           [self setAlertAlpha:alertAlpha];
640           nextUpdate = MIN(kGlowUpdateInterval, nextUpdate);
641         }
642       } else {
643         // Schedule update for end of hold time.
644         nextUpdate = MIN(alertHoldEndTime_ - currentTime, nextUpdate);
645       }
646     } else {
647       // Done the alert decay cycle.
648       alertState_ = tabs::kAlertNone;
649     }
650   }
651
652   if (nextUpdate < kNoUpdate)
653     [self performSelector:_cmd withObject:nil afterDelay:nextUpdate];
654
655   [self resetLastGlowUpdateTime];
656   [self setNeedsDisplay:YES];
657 }
658
659 - (CGImageRef)tabClippingMask {
660   // NOTE: NSHeight([self bounds]) doesn't match the height of the bitmaps.
661   CGFloat scale = 1;
662   if ([[self window] respondsToSelector:@selector(backingScaleFactor)])
663     scale = [[self window] backingScaleFactor];
664
665   NSRect bounds = [self bounds];
666   CGFloat tabWidth = NSWidth(bounds);
667   if (tabWidth == maskCacheWidth_ && scale == maskCacheScale_)
668     return maskCache_.get();
669
670   maskCacheWidth_ = tabWidth;
671   maskCacheScale_ = scale;
672
673   ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();
674   NSImage* leftMask = rb.GetNativeImageNamed(IDR_TAB_ALPHA_LEFT).ToNSImage();
675   NSImage* rightMask = rb.GetNativeImageNamed(IDR_TAB_ALPHA_RIGHT).ToNSImage();
676
677   CGFloat leftWidth = leftMask.size.width;
678   CGFloat rightWidth = rightMask.size.width;
679
680   // Image masks must be in the DeviceGray colorspace. Create a context and
681   // draw the mask into it.
682   base::ScopedCFTypeRef<CGColorSpaceRef> colorspace(
683       CGColorSpaceCreateDeviceGray());
684   base::ScopedCFTypeRef<CGContextRef> maskContext(
685       CGBitmapContextCreate(NULL, tabWidth * scale, kMaskHeight * scale,
686                             8, tabWidth * scale, colorspace, 0));
687   CGContextScaleCTM(maskContext, scale, scale);
688   NSGraphicsContext* maskGraphicsContext =
689       [NSGraphicsContext graphicsContextWithGraphicsPort:maskContext
690                                                  flipped:NO];
691
692   gfx::ScopedNSGraphicsContextSaveGState scopedGState;
693   [NSGraphicsContext setCurrentContext:maskGraphicsContext];
694
695   // Draw mask image.
696   [[NSColor blackColor] setFill];
697   CGContextFillRect(maskContext, CGRectMake(0, 0, tabWidth, kMaskHeight));
698
699   NSDrawThreePartImage(NSMakeRect(0, 0, tabWidth, kMaskHeight),
700       leftMask, nil, rightMask, /*vertical=*/NO, NSCompositeSourceOver, 1.0,
701       /*flipped=*/NO);
702
703   CGFloat middleWidth = tabWidth - leftWidth - rightWidth;
704   NSRect middleRect = NSMakeRect(leftWidth, 0, middleWidth, kFillHeight);
705   [[NSColor whiteColor] setFill];
706   NSRectFill(middleRect);
707
708   maskCache_.reset(CGBitmapContextCreateImage(maskContext));
709   return maskCache_;
710 }
711
712 @end  // @implementation TabView(Private)