Upstream version 5.34.104.0
[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   // There is only 1 active tab at a time.
333   // It has a different fill color which draws over the separator line.
334   if ([controller_ active]) {
335     [self drawFillForActiveTab:dirtyRect];
336     return;
337   }
338
339   // Background tabs should not paint over the tab strip separator, which is
340   // two pixels high in both lodpi and hidpi.
341   if (dirtyRect.origin.y < 1)
342     dirtyRect.origin.y = 2 * [self cr_lineWidth];
343
344   // There can be multiple selected tabs.
345   // They have the same fill color as the active tab, but do not draw over
346   // the separator.
347   if ([controller_ selected]) {
348     [self drawFillForActiveTab:dirtyRect];
349     return;
350   }
351
352   // Draw the tab background.
353   NSColor* backgroundImageColor = [self backgroundColorForSelected:NO];
354   [backgroundImageColor set];
355
356   // Themes can have partially transparent images. NSRectFill() is measurably
357   // faster though, so call it for the known-safe default theme.
358   bool usingDefaultTheme = themeProvider && themeProvider->UsingDefaultTheme();
359   if (usingDefaultTheme)
360     NSRectFill(dirtyRect);
361   else
362     NSRectFillUsingOperation(dirtyRect, NSCompositeSourceOver);
363
364   // Draw the glow for hover and the overlay for alerts.
365   CGFloat hoverAlpha = [self hoverAlpha];
366   CGFloat alertAlpha = [self alertAlpha];
367   if (hoverAlpha > 0 || alertAlpha > 0) {
368     gfx::ScopedNSGraphicsContextSaveGState contextSave;
369     CGContextBeginTransparencyLayer(cgContext, 0);
370
371     // The alert glow overlay is like the selected state but at most at most 80%
372     // opaque. The hover glow brings up the overlay's opacity at most 50%.
373     CGFloat backgroundAlpha = 0.8 * alertAlpha;
374     backgroundAlpha += (1 - backgroundAlpha) * 0.5 * hoverAlpha;
375     CGContextSetAlpha(cgContext, backgroundAlpha);
376
377     [self drawFillForActiveTab:dirtyRect];
378
379     // ui::ThemeProvider::HasCustomImage is true only if the theme provides the
380     // image. However, even if the theme doesn't provide a tab background, the
381     // theme machinery will make one if given a frame image. See
382     // BrowserThemePack::GenerateTabBackgroundImages for details.
383     BOOL hasCustomTheme = themeProvider &&
384         (themeProvider->HasCustomImage(IDR_THEME_TAB_BACKGROUND) ||
385          themeProvider->HasCustomImage(IDR_THEME_FRAME));
386     // Draw a mouse hover gradient for the default themes.
387     if (hoverAlpha > 0) {
388       if (themeProvider && !hasCustomTheme) {
389         base::scoped_nsobject<NSGradient> glow([NSGradient alloc]);
390         [glow initWithStartingColor:[NSColor colorWithCalibratedWhite:1.0
391                                         alpha:1.0 * hoverAlpha]
392                         endingColor:[NSColor colorWithCalibratedWhite:1.0
393                                                                 alpha:0.0]];
394         NSRect rect = [self bounds];
395         NSPoint point = hoverPoint_;
396         point.y = NSHeight(rect);
397         [glow drawFromCenter:point
398                       radius:0.0
399                     toCenter:point
400                       radius:NSWidth(rect) / 3.0
401                      options:NSGradientDrawsBeforeStartingLocation];
402       }
403     }
404
405     CGContextEndTransparencyLayer(cgContext);
406   }
407 }
408
409 // Draws the tab outline.
410 - (void)drawStroke:(NSRect)dirtyRect {
411   BOOL focused = [[self window] isKeyWindow] || [[self window] isMainWindow];
412   CGFloat alpha = focused ? 1.0 : tabs::kImageNoFocusAlpha;
413
414   ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();
415   float height =
416       [rb.GetNativeImageNamed(IDR_TAB_ACTIVE_LEFT).ToNSImage() size].height;
417   if ([controller_ active]) {
418     NSDrawThreePartImage(NSMakeRect(0, 0, NSWidth([self bounds]), height),
419         rb.GetNativeImageNamed(IDR_TAB_ACTIVE_LEFT).ToNSImage(),
420         rb.GetNativeImageNamed(IDR_TAB_ACTIVE_CENTER).ToNSImage(),
421         rb.GetNativeImageNamed(IDR_TAB_ACTIVE_RIGHT).ToNSImage(),
422         /*vertical=*/NO,
423         NSCompositeSourceOver,
424         alpha,
425         /*flipped=*/NO);
426   } else {
427     NSDrawThreePartImage(NSMakeRect(0, 0, NSWidth([self bounds]), height),
428         rb.GetNativeImageNamed(IDR_TAB_INACTIVE_LEFT).ToNSImage(),
429         rb.GetNativeImageNamed(IDR_TAB_INACTIVE_CENTER).ToNSImage(),
430         rb.GetNativeImageNamed(IDR_TAB_INACTIVE_RIGHT).ToNSImage(),
431         /*vertical=*/NO,
432         NSCompositeSourceOver,
433         alpha,
434         /*flipped=*/NO);
435   }
436 }
437
438 - (void)drawRect:(NSRect)dirtyRect {
439   // Text, close button, and image are drawn by subviews.
440   [self drawFill:dirtyRect];
441   [self drawStroke:dirtyRect];
442 }
443
444 - (void)setFrameOrigin:(NSPoint)origin {
445   // The background color depends on the view's vertical position.
446   if (NSMinY([self frame]) != origin.y)
447     [self setNeedsDisplay:YES];
448   [super setFrameOrigin:origin];
449 }
450
451 // Override this to catch the text so that we can choose when to display it.
452 - (void)setToolTip:(NSString*)string {
453   toolTipText_.reset([string retain]);
454 }
455
456 - (NSString*)toolTipText {
457   if (!toolTipText_.get()) {
458     return @"";
459   }
460   return toolTipText_.get();
461 }
462
463 - (void)viewDidMoveToWindow {
464   [super viewDidMoveToWindow];
465   if ([self window]) {
466     [controller_ updateTitleColor];
467   }
468 }
469
470 - (void)setState:(NSCellStateValue)state {
471   if (state_ == state)
472     return;
473   state_ = state;
474   [self setNeedsDisplay:YES];
475 }
476
477 - (void)setClosing:(BOOL)closing {
478   closing_ = closing;  // Safe because the property is nonatomic.
479   // When closing, ensure clicks to the close button go nowhere.
480   if (closing) {
481     [closeButton_ setTarget:nil];
482     [closeButton_ setAction:nil];
483   }
484 }
485
486 - (void)startAlert {
487   // Do not start a new alert while already alerting or while in a decay cycle.
488   if (alertState_ == tabs::kAlertNone) {
489     alertState_ = tabs::kAlertRising;
490     [self resetLastGlowUpdateTime];
491     [self adjustGlowValue];
492   }
493 }
494
495 - (void)cancelAlert {
496   if (alertState_ != tabs::kAlertNone) {
497     alertState_ = tabs::kAlertFalling;
498     alertHoldEndTime_ =
499         [NSDate timeIntervalSinceReferenceDate] + kGlowUpdateInterval;
500     [self resetLastGlowUpdateTime];
501     [self adjustGlowValue];
502   }
503 }
504
505 - (BOOL)accessibilityIsIgnored {
506   return NO;
507 }
508
509 - (NSArray*)accessibilityActionNames {
510   NSArray* parentActions = [super accessibilityActionNames];
511
512   return [parentActions arrayByAddingObject:NSAccessibilityPressAction];
513 }
514
515 - (NSArray*)accessibilityAttributeNames {
516   NSMutableArray* attributes =
517       [[super accessibilityAttributeNames] mutableCopy];
518   [attributes addObject:NSAccessibilityTitleAttribute];
519   [attributes addObject:NSAccessibilityEnabledAttribute];
520   [attributes addObject:NSAccessibilityValueAttribute];
521
522   return [attributes autorelease];
523 }
524
525 - (BOOL)accessibilityIsAttributeSettable:(NSString*)attribute {
526   if ([attribute isEqual:NSAccessibilityTitleAttribute])
527     return NO;
528
529   if ([attribute isEqual:NSAccessibilityEnabledAttribute])
530     return NO;
531
532   if ([attribute isEqual:NSAccessibilityValueAttribute])
533     return YES;
534
535   return [super accessibilityIsAttributeSettable:attribute];
536 }
537
538 - (void)accessibilityPerformAction:(NSString*)action {
539   if ([action isEqual:NSAccessibilityPressAction] &&
540       [[controller_ target] respondsToSelector:[controller_ action]]) {
541     [[controller_ target] performSelector:[controller_ action]
542         withObject:self];
543     NSAccessibilityPostNotification(self,
544                                     NSAccessibilityValueChangedNotification);
545   } else {
546     [super accessibilityPerformAction:action];
547   }
548 }
549
550 - (id)accessibilityAttributeValue:(NSString*)attribute {
551   if ([attribute isEqual:NSAccessibilityRoleAttribute])
552     return NSAccessibilityRadioButtonRole;
553   if ([attribute isEqual:NSAccessibilityRoleDescriptionAttribute])
554     return l10n_util::GetNSStringWithFixup(IDS_ACCNAME_TAB);
555   if ([attribute isEqual:NSAccessibilityTitleAttribute])
556     return [controller_ title];
557   if ([attribute isEqual:NSAccessibilityValueAttribute])
558     return [NSNumber numberWithInt:[controller_ selected]];
559   if ([attribute isEqual:NSAccessibilityEnabledAttribute])
560     return [NSNumber numberWithBool:YES];
561
562   return [super accessibilityAttributeValue:attribute];
563 }
564
565 - (ViewID)viewID {
566   return VIEW_ID_TAB;
567 }
568
569 @end  // @implementation TabView
570
571 @implementation TabView (TabControllerInterface)
572
573 - (void)setController:(TabController*)controller {
574   controller_ = controller;
575 }
576
577 @end  // @implementation TabView (TabControllerInterface)
578
579 @implementation TabView(Private)
580
581 - (void)resetLastGlowUpdateTime {
582   lastGlowUpdate_ = [NSDate timeIntervalSinceReferenceDate];
583 }
584
585 - (NSTimeInterval)timeElapsedSinceLastGlowUpdate {
586   return [NSDate timeIntervalSinceReferenceDate] - lastGlowUpdate_;
587 }
588
589 - (void)adjustGlowValue {
590   // A time interval long enough to represent no update.
591   const NSTimeInterval kNoUpdate = 1000000;
592
593   // Time until next update for either glow.
594   NSTimeInterval nextUpdate = kNoUpdate;
595
596   NSTimeInterval elapsed = [self timeElapsedSinceLastGlowUpdate];
597   NSTimeInterval currentTime = [NSDate timeIntervalSinceReferenceDate];
598
599   // TODO(viettrungluu): <http://crbug.com/30617> -- split off the stuff below
600   // into a pure function and add a unit test.
601
602   CGFloat hoverAlpha = [self hoverAlpha];
603   if (isMouseInside_) {
604     // Increase hover glow until it's 1.
605     if (hoverAlpha < 1) {
606       hoverAlpha = MIN(hoverAlpha + elapsed / kHoverShowDuration, 1);
607       [self setHoverAlpha:hoverAlpha];
608       nextUpdate = MIN(kGlowUpdateInterval, nextUpdate);
609     }  // Else already 1 (no update needed).
610   } else {
611     if (currentTime >= hoverHoldEndTime_) {
612       // No longer holding, so decrease hover glow until it's 0.
613       if (hoverAlpha > 0) {
614         hoverAlpha = MAX(hoverAlpha - elapsed / kHoverHideDuration, 0);
615         [self setHoverAlpha:hoverAlpha];
616         nextUpdate = MIN(kGlowUpdateInterval, nextUpdate);
617       }  // Else already 0 (no update needed).
618     } else {
619       // Schedule update for end of hold time.
620       nextUpdate = MIN(hoverHoldEndTime_ - currentTime, nextUpdate);
621     }
622   }
623
624   CGFloat alertAlpha = [self alertAlpha];
625   if (alertState_ == tabs::kAlertRising) {
626     // Increase alert glow until it's 1 ...
627     alertAlpha = MIN(alertAlpha + elapsed / kAlertShowDuration, 1);
628     [self setAlertAlpha:alertAlpha];
629
630     // ... and having reached 1, switch to holding.
631     if (alertAlpha >= 1) {
632       alertState_ = tabs::kAlertHolding;
633       alertHoldEndTime_ = currentTime + kAlertHoldDuration;
634       nextUpdate = MIN(kAlertHoldDuration, nextUpdate);
635     } else {
636       nextUpdate = MIN(kGlowUpdateInterval, nextUpdate);
637     }
638   } else if (alertState_ != tabs::kAlertNone) {
639     if (alertAlpha > 0) {
640       if (currentTime >= alertHoldEndTime_) {
641         // Stop holding, then decrease alert glow (until it's 0).
642         if (alertState_ == tabs::kAlertHolding) {
643           alertState_ = tabs::kAlertFalling;
644           nextUpdate = MIN(kGlowUpdateInterval, nextUpdate);
645         } else {
646           DCHECK_EQ(tabs::kAlertFalling, alertState_);
647           alertAlpha = MAX(alertAlpha - elapsed / kAlertHideDuration, 0);
648           [self setAlertAlpha:alertAlpha];
649           nextUpdate = MIN(kGlowUpdateInterval, nextUpdate);
650         }
651       } else {
652         // Schedule update for end of hold time.
653         nextUpdate = MIN(alertHoldEndTime_ - currentTime, nextUpdate);
654       }
655     } else {
656       // Done the alert decay cycle.
657       alertState_ = tabs::kAlertNone;
658     }
659   }
660
661   if (nextUpdate < kNoUpdate)
662     [self performSelector:_cmd withObject:nil afterDelay:nextUpdate];
663
664   [self resetLastGlowUpdateTime];
665   [self setNeedsDisplay:YES];
666 }
667
668 - (CGImageRef)tabClippingMask {
669   // NOTE: NSHeight([self bounds]) doesn't match the height of the bitmaps.
670   CGFloat scale = 1;
671   if ([[self window] respondsToSelector:@selector(backingScaleFactor)])
672     scale = [[self window] backingScaleFactor];
673
674   NSRect bounds = [self bounds];
675   CGFloat tabWidth = NSWidth(bounds);
676   if (tabWidth == maskCacheWidth_ && scale == maskCacheScale_)
677     return maskCache_.get();
678
679   maskCacheWidth_ = tabWidth;
680   maskCacheScale_ = scale;
681
682   ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();
683   NSImage* leftMask = rb.GetNativeImageNamed(IDR_TAB_ALPHA_LEFT).ToNSImage();
684   NSImage* rightMask = rb.GetNativeImageNamed(IDR_TAB_ALPHA_RIGHT).ToNSImage();
685
686   CGFloat leftWidth = leftMask.size.width;
687   CGFloat rightWidth = rightMask.size.width;
688
689   // Image masks must be in the DeviceGray colorspace. Create a context and
690   // draw the mask into it.
691   base::ScopedCFTypeRef<CGColorSpaceRef> colorspace(
692       CGColorSpaceCreateDeviceGray());
693   base::ScopedCFTypeRef<CGContextRef> maskContext(
694       CGBitmapContextCreate(NULL, tabWidth * scale, kMaskHeight * scale,
695                             8, tabWidth * scale, colorspace, 0));
696   CGContextScaleCTM(maskContext, scale, scale);
697   NSGraphicsContext* maskGraphicsContext =
698       [NSGraphicsContext graphicsContextWithGraphicsPort:maskContext
699                                                  flipped:NO];
700
701   gfx::ScopedNSGraphicsContextSaveGState scopedGState;
702   [NSGraphicsContext setCurrentContext:maskGraphicsContext];
703
704   // Draw mask image.
705   [[NSColor blackColor] setFill];
706   CGContextFillRect(maskContext, CGRectMake(0, 0, tabWidth, kMaskHeight));
707
708   NSDrawThreePartImage(NSMakeRect(0, 0, tabWidth, kMaskHeight),
709       leftMask, nil, rightMask, /*vertical=*/NO, NSCompositeSourceOver, 1.0,
710       /*flipped=*/NO);
711
712   CGFloat middleWidth = tabWidth - leftWidth - rightWidth;
713   NSRect middleRect = NSMakeRect(leftWidth, 0, middleWidth, kFillHeight);
714   [[NSColor whiteColor] setFill];
715   NSRectFill(middleRect);
716
717   maskCache_.reset(CGBitmapContextCreateImage(maskContext));
718   return maskCache_;
719 }
720
721 @end  // @implementation TabView(Private)