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