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.
5 #import "chrome/browser/ui/cocoa/tabs/tab_view.h"
7 #include "base/i18n/rtl.h"
8 #include "base/logging.h"
9 #include "base/mac/sdk_forward_declarations.h"
10 #include "base/strings/sys_string_conversions.h"
11 #include "chrome/browser/themes/theme_service.h"
12 #import "chrome/browser/ui/cocoa/nsview_additions.h"
13 #import "chrome/browser/ui/cocoa/tabs/tab_controller.h"
14 #import "chrome/browser/ui/cocoa/tabs/tab_window_controller.h"
15 #import "chrome/browser/ui/cocoa/themed_window.h"
16 #import "chrome/browser/ui/cocoa/view_id_util.h"
17 #include "grit/generated_resources.h"
18 #include "grit/theme_resources.h"
19 #import "third_party/google_toolbox_for_mac/src/AppKit/GTMFadeTruncatingTextFieldCell.h"
20 #import "ui/base/cocoa/nsgraphics_context_additions.h"
21 #include "ui/base/l10n/l10n_util.h"
22 #include "ui/base/resource/resource_bundle.h"
23 #include "ui/gfx/scoped_ns_graphics_context_save_gstate_mac.h"
26 const int kMaskHeight = 29; // Height of the mask bitmap.
27 const int kFillHeight = 25; // Height of the "mask on" part of the mask bitmap.
29 // Constants for inset and control points for tab shape.
30 const CGFloat kInsetMultiplier = 2.0/3.0;
32 // The amount of time in seconds during which each type of glow increases, holds
33 // steady, and decreases, respectively.
34 const NSTimeInterval kHoverShowDuration = 0.2;
35 const NSTimeInterval kHoverHoldDuration = 0.02;
36 const NSTimeInterval kHoverHideDuration = 0.4;
37 const NSTimeInterval kAlertShowDuration = 0.4;
38 const NSTimeInterval kAlertHoldDuration = 0.4;
39 const NSTimeInterval kAlertHideDuration = 0.4;
41 // The default time interval in seconds between glow updates (when
42 // increasing/decreasing).
43 const NSTimeInterval kGlowUpdateInterval = 0.025;
45 // This is used to judge whether the mouse has moved during rapid closure; if it
46 // has moved less than the threshold, we want to close the tab.
47 const CGFloat kRapidCloseDist = 2.5;
49 @interface TabView(Private)
51 - (void)resetLastGlowUpdateTime;
52 - (NSTimeInterval)timeElapsedSinceLastGlowUpdate;
53 - (void)adjustGlowValue;
54 - (CGImageRef)tabClippingMask;
56 @end // TabView(Private)
58 @implementation TabView
60 @synthesize state = state_;
61 @synthesize hoverAlpha = hoverAlpha_;
62 @synthesize alertAlpha = alertAlpha_;
63 @synthesize closing = closing_;
65 + (CGFloat)insetMultiplier {
66 return kInsetMultiplier;
69 - (id)initWithFrame:(NSRect)frame
70 controller:(TabController*)controller
71 closeButton:(HoverCloseButton*)closeButton {
72 self = [super initWithFrame:frame];
74 controller_ = controller;
75 closeButton_ = closeButton;
77 // Make a text field for the title, but don't add it as a subview.
78 // We will use the cell to draw the text directly into our layer,
79 // so that we can get font smoothing enabled.
80 titleView_.reset([[NSTextField alloc] init]);
81 [titleView_ setAutoresizingMask:NSViewWidthSizable];
82 base::scoped_nsobject<GTMFadeTruncatingTextFieldCell> labelCell(
83 [[GTMFadeTruncatingTextFieldCell alloc] initTextCell:@"Label"]);
84 [labelCell setControlSize:NSSmallControlSize];
85 CGFloat fontSize = [NSFont systemFontSizeForControlSize:NSSmallControlSize];
86 NSFont* font = [NSFont fontWithName:[[labelCell font] fontName]
88 [labelCell setFont:font];
89 [titleView_ setCell:labelCell];
90 titleViewCell_ = labelCell;
96 // Cancel any delayed requests that may still be pending (drags or hover).
97 [NSObject cancelPreviousPerformRequestsWithTarget:self];
101 // Called to obtain the context menu for when the user hits the right mouse
102 // button (or control-clicks). (Note that -rightMouseDown: is *not* called for
105 if ([self isClosing])
108 // Sheets, being window-modal, should block contextual menus. For some reason
109 // they do not. Disallow them ourselves.
110 if ([[self window] attachedSheet])
113 return [controller_ menu];
116 - (void)resizeSubviewsWithOldSize:(NSSize)oldBoundsSize {
117 [super resizeSubviewsWithOldSize:oldBoundsSize];
118 // Called when our view is resized. If it gets too small, start by hiding
119 // the close button and only show it if tab is selected. Eventually, hide the
121 [controller_ updateVisibility];
124 // Overridden so that mouse clicks come to this view (the parent of the
125 // hierarchy) first. We want to handle clicks and drags in this class and
126 // leave the background button for display purposes only.
127 - (BOOL)acceptsFirstMouse:(NSEvent*)theEvent {
131 - (void)mouseEntered:(NSEvent*)theEvent {
132 isMouseInside_ = YES;
133 [self resetLastGlowUpdateTime];
134 [self adjustGlowValue];
137 - (void)mouseMoved:(NSEvent*)theEvent {
138 hoverPoint_ = [self convertPoint:[theEvent locationInWindow]
140 [self setNeedsDisplay:YES];
143 - (void)mouseExited:(NSEvent*)theEvent {
146 [NSDate timeIntervalSinceReferenceDate] + kHoverHoldDuration;
147 [self resetLastGlowUpdateTime];
148 [self adjustGlowValue];
151 - (void)setTrackingEnabled:(BOOL)enabled {
152 if (![closeButton_ isHidden]) {
153 [closeButton_ setTrackingEnabled:enabled];
157 // Determines which view a click in our frame actually hit. It's either this
158 // view or our child close button.
159 - (NSView*)hitTest:(NSPoint)aPoint {
160 NSPoint viewPoint = [self convertPoint:aPoint fromView:[self superview]];
161 if (![closeButton_ isHidden])
162 if (NSPointInRect(viewPoint, [closeButton_ frame])) return closeButton_;
164 NSRect pointRect = NSMakeRect(viewPoint.x, viewPoint.y, 1, 1);
166 ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();
167 NSImage* left = rb.GetNativeImageNamed(IDR_TAB_ALPHA_LEFT).ToNSImage();
168 if (viewPoint.x < [left size].width) {
169 NSRect imageRect = NSMakeRect(0, 0, [left size].width, [left size].height);
170 if ([left hitTestRect:pointRect withImageDestinationRect:imageRect
171 context:nil hints:nil flipped:NO]) {
177 NSImage* right = rb.GetNativeImageNamed(IDR_TAB_ALPHA_RIGHT).ToNSImage();
178 CGFloat rightX = NSWidth([self bounds]) - [right size].width;
179 if (viewPoint.x > rightX) {
180 NSRect imageRect = NSMakeRect(
181 rightX, 0, [right size].width, [right size].height);
182 if ([right hitTestRect:pointRect withImageDestinationRect:imageRect
183 context:nil hints:nil flipped:NO]) {
189 if (viewPoint.y < kFillHeight)
194 // Returns |YES| if this tab can be torn away into a new window.
195 - (BOOL)canBeDragged {
196 return [controller_ tabCanBeDragged:controller_];
199 // Handle clicks and drags in this button. We get here because we have
200 // overridden acceptsFirstMouse: and the click is within our bounds.
201 - (void)mouseDown:(NSEvent*)theEvent {
202 if ([self isClosing])
205 // Record the point at which this event happened. This is used by other mouse
206 // events that are dispatched from |-maybeStartDrag::|.
207 mouseDownPoint_ = [theEvent locationInWindow];
209 // Record the state of the close button here, because selecting the tab will
211 BOOL closeButtonActive = ![closeButton_ isHidden];
213 // During the tab closure animation (in particular, during rapid tab closure),
214 // we may get incorrectly hit with a mouse down. If it should have gone to the
215 // close button, we send it there -- it should then track the mouse, so we
216 // don't have to worry about mouse ups.
217 if (closeButtonActive && [controller_ inRapidClosureMode]) {
218 NSPoint hitLocation = [[self superview] convertPoint:mouseDownPoint_
220 if ([self hitTest:hitLocation] == closeButton_) {
221 [closeButton_ mouseDown:theEvent];
226 // If the tab gets torn off, the tab controller will be removed from the tab
227 // strip and then deallocated. This will also result in *us* being
228 // deallocated. Both these are bad, so we prevent this by retaining the
230 base::scoped_nsobject<TabController> controller([controller_ retain]);
232 // Try to initiate a drag. This will spin a custom event loop and may
233 // dispatch other mouse events.
234 [controller_ maybeStartDrag:theEvent forTab:controller];
236 // The custom loop has ended, so clear the point.
237 mouseDownPoint_ = NSZeroPoint;
240 - (void)mouseUp:(NSEvent*)theEvent {
241 // Check for rapid tab closure.
242 if ([theEvent type] == NSLeftMouseUp) {
243 NSPoint upLocation = [theEvent locationInWindow];
244 CGFloat dx = upLocation.x - mouseDownPoint_.x;
245 CGFloat dy = upLocation.y - mouseDownPoint_.y;
247 // During rapid tab closure (mashing tab close buttons), we may get hit
248 // with a mouse down. As long as the mouse up is over the close button,
249 // and the mouse hasn't moved too much, we close the tab.
250 if (![closeButton_ isHidden] &&
251 (dx*dx + dy*dy) <= kRapidCloseDist*kRapidCloseDist &&
252 [controller_ inRapidClosureMode]) {
253 NSPoint hitLocation =
254 [[self superview] convertPoint:[theEvent locationInWindow]
256 if ([self hitTest:hitLocation] == closeButton_) {
257 [controller_ closeTab:self];
263 // Fire the action to select the tab.
264 [controller_ selectTab:self];
266 // Messaging the drag controller with |-endDrag:| would seem like the right
267 // thing to do here. But, when a tab has been detached, the controller's
268 // target is nil until the drag is finalized. Since |-mouseUp:| gets called
269 // via the manual event loop inside -[TabStripDragController
270 // maybeStartDrag:forTab:], the drag controller can end the dragging session
271 // itself directly after calling this.
274 - (void)otherMouseUp:(NSEvent*)theEvent {
275 if ([self isClosing])
278 // Support middle-click-to-close.
279 if ([theEvent buttonNumber] == 2) {
280 // |-hitTest:| takes a location in the superview's coordinates.
282 [[self superview] convertPoint:[theEvent locationInWindow]
284 // If the mouse up occurred in our view or over the close button, then
286 if ([self hitTest:upLocation])
287 [controller_ closeTab:self];
291 // Returns the color used to draw the background of a tab. |selected| selects
292 // between the foreground and background tabs.
293 - (NSColor*)backgroundColorForSelected:(bool)selected {
294 ThemeService* themeProvider =
295 static_cast<ThemeService*>([[self window] themeProvider]);
297 return [[self window] backgroundColor];
299 int bitmapResources[2][2] = {
300 // Background window.
302 IDR_THEME_TAB_BACKGROUND_INACTIVE, // Background tab.
303 IDR_THEME_TOOLBAR_INACTIVE, // Active tab.
305 // Currently focused window.
307 IDR_THEME_TAB_BACKGROUND, // Background tab.
308 IDR_THEME_TOOLBAR, // Active tab.
312 // Themes don't have an inactive image so only look for one if there's no
314 bool active = [[self window] isKeyWindow] || [[self window] isMainWindow] ||
315 !themeProvider->UsingDefaultTheme();
316 return themeProvider->GetNSImageColorNamed(bitmapResources[active][selected]);
319 // Draws the active tab background.
320 - (void)drawFillForActiveTab:(NSRect)dirtyRect {
321 NSColor* backgroundImageColor = [self backgroundColorForSelected:YES];
322 [backgroundImageColor set];
324 // Themes can have partially transparent images. NSRectFill() is measurably
325 // faster though, so call it for the known-safe default theme.
326 ThemeService* themeProvider =
327 static_cast<ThemeService*>([[self window] themeProvider]);
328 if (themeProvider && themeProvider->UsingDefaultTheme())
329 NSRectFill(dirtyRect);
331 NSRectFillUsingOperation(dirtyRect, NSCompositeSourceOver);
334 // Draws the tab background.
335 - (void)drawFill:(NSRect)dirtyRect {
336 gfx::ScopedNSGraphicsContextSaveGState scopedGState;
337 NSGraphicsContext* context = [NSGraphicsContext currentContext];
338 CGContextRef cgContext = static_cast<CGContextRef>([context graphicsPort]);
340 ThemeService* themeProvider =
341 static_cast<ThemeService*>([[self window] themeProvider]);
342 NSPoint position = [[self window]
343 themeImagePositionForAlignment: THEME_IMAGE_ALIGN_WITH_TAB_STRIP];
344 [context cr_setPatternPhase:position forView:self];
346 CGImageRef mask([self tabClippingMask]);
347 CGRect maskBounds = CGRectMake(0, 0, maskCacheWidth_, kMaskHeight);
348 CGContextClipToMask(cgContext, maskBounds, mask);
350 // There is only 1 active tab at a time.
351 // It has a different fill color which draws over the separator line.
352 if (state_ == NSOnState) {
353 [self drawFillForActiveTab:dirtyRect];
357 // Background tabs should not paint over the tab strip separator, which is
358 // two pixels high in both lodpi and hidpi.
359 if (dirtyRect.origin.y < 1)
360 dirtyRect.origin.y = 2 * [self cr_lineWidth];
362 // There can be multiple selected tabs.
363 // They have the same fill color as the active tab, but do not draw over
365 if (state_ == NSMixedState) {
366 [self drawFillForActiveTab:dirtyRect];
370 // Draw the tab background.
371 NSColor* backgroundImageColor = [self backgroundColorForSelected:NO];
372 [backgroundImageColor set];
374 // Themes can have partially transparent images. NSRectFill() is measurably
375 // faster though, so call it for the known-safe default theme.
376 bool usingDefaultTheme = themeProvider && themeProvider->UsingDefaultTheme();
377 if (usingDefaultTheme)
378 NSRectFill(dirtyRect);
380 NSRectFillUsingOperation(dirtyRect, NSCompositeSourceOver);
382 // Draw the glow for hover and the overlay for alerts.
383 CGFloat hoverAlpha = [self hoverAlpha];
384 CGFloat alertAlpha = [self alertAlpha];
385 if (hoverAlpha > 0 || alertAlpha > 0) {
386 gfx::ScopedNSGraphicsContextSaveGState contextSave;
387 CGContextBeginTransparencyLayer(cgContext, 0);
389 // The alert glow overlay is like the selected state but at most at most 80%
390 // opaque. The hover glow brings up the overlay's opacity at most 50%.
391 CGFloat backgroundAlpha = 0.8 * alertAlpha;
392 backgroundAlpha += (1 - backgroundAlpha) * 0.5 * hoverAlpha;
393 CGContextSetAlpha(cgContext, backgroundAlpha);
395 [self drawFillForActiveTab:dirtyRect];
397 // ui::ThemeProvider::HasCustomImage is true only if the theme provides the
398 // image. However, even if the theme doesn't provide a tab background, the
399 // theme machinery will make one if given a frame image. See
400 // BrowserThemePack::GenerateTabBackgroundImages for details.
401 BOOL hasCustomTheme = themeProvider &&
402 (themeProvider->HasCustomImage(IDR_THEME_TAB_BACKGROUND) ||
403 themeProvider->HasCustomImage(IDR_THEME_FRAME));
404 // Draw a mouse hover gradient for the default themes.
405 if (hoverAlpha > 0) {
406 if (themeProvider && !hasCustomTheme) {
407 base::scoped_nsobject<NSGradient> glow([NSGradient alloc]);
408 [glow initWithStartingColor:[NSColor colorWithCalibratedWhite:1.0
409 alpha:1.0 * hoverAlpha]
410 endingColor:[NSColor colorWithCalibratedWhite:1.0
412 NSRect rect = [self bounds];
413 NSPoint point = hoverPoint_;
414 point.y = NSHeight(rect);
415 [glow drawFromCenter:point
418 radius:NSWidth(rect) / 3.0
419 options:NSGradientDrawsBeforeStartingLocation];
423 CGContextEndTransparencyLayer(cgContext);
427 // Draws the tab outline.
428 - (void)drawStroke:(NSRect)dirtyRect {
429 BOOL focused = [[self window] isKeyWindow] || [[self window] isMainWindow];
430 CGFloat alpha = focused ? 1.0 : tabs::kImageNoFocusAlpha;
432 ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();
434 [rb.GetNativeImageNamed(IDR_TAB_ACTIVE_LEFT).ToNSImage() size].height;
435 if (state_ == NSOnState) {
436 NSDrawThreePartImage(NSMakeRect(0, 0, NSWidth([self bounds]), height),
437 rb.GetNativeImageNamed(IDR_TAB_ACTIVE_LEFT).ToNSImage(),
438 rb.GetNativeImageNamed(IDR_TAB_ACTIVE_CENTER).ToNSImage(),
439 rb.GetNativeImageNamed(IDR_TAB_ACTIVE_RIGHT).ToNSImage(),
441 NSCompositeSourceOver,
445 NSDrawThreePartImage(NSMakeRect(0, 0, NSWidth([self bounds]), height),
446 rb.GetNativeImageNamed(IDR_TAB_INACTIVE_LEFT).ToNSImage(),
447 rb.GetNativeImageNamed(IDR_TAB_INACTIVE_CENTER).ToNSImage(),
448 rb.GetNativeImageNamed(IDR_TAB_INACTIVE_RIGHT).ToNSImage(),
450 NSCompositeSourceOver,
456 - (void)drawRect:(NSRect)dirtyRect {
457 // Close button and image are drawn by subviews.
458 [self drawFill:dirtyRect];
459 [self drawStroke:dirtyRect];
461 // We draw the title string directly instead of using a NSTextField subview.
462 // This is so that we can get font smoothing to work on earlier OS, and even
463 // when the tab background is a pattern image (when using themes).
464 if (![titleView_ isHidden]) {
465 gfx::ScopedNSGraphicsContextSaveGState scopedGState;
466 NSGraphicsContext* context = [NSGraphicsContext currentContext];
467 CGContextRef cgContext = static_cast<CGContextRef>([context graphicsPort]);
468 CGContextSetShouldSmoothFonts(cgContext, true);
469 [[titleView_ cell] drawWithFrame:[titleView_ frame] inView:self];
473 - (void)setFrameOrigin:(NSPoint)origin {
474 // The background color depends on the view's vertical position.
475 if (NSMinY([self frame]) != origin.y)
476 [self setNeedsDisplay:YES];
477 [super setFrameOrigin:origin];
480 // Override this to catch the text so that we can choose when to display it.
481 - (void)setToolTip:(NSString*)string {
482 toolTipText_.reset([string retain]);
485 - (NSString*)toolTipText {
486 if (!toolTipText_.get()) {
489 return toolTipText_.get();
492 - (void)viewDidMoveToWindow {
493 [super viewDidMoveToWindow];
495 [controller_ updateTitleColor];
500 return [titleView_ stringValue];
503 - (void)setTitle:(NSString*)title {
504 [titleView_ setStringValue:title];
506 base::string16 title16 = base::SysNSStringToUTF16(title);
507 bool isRTL = base::i18n::GetFirstStrongCharacterDirection(title16) ==
508 base::i18n::RIGHT_TO_LEFT;
509 titleViewCell_.truncateMode = isRTL ? GTMFadeTruncatingHead
510 : GTMFadeTruncatingTail;
512 [self setNeedsDisplayInRect:[titleView_ frame]];
515 - (NSRect)titleFrame {
516 return [titleView_ frame];
519 - (void)setTitleFrame:(NSRect)titleFrame {
520 [titleView_ setFrame:titleFrame];
521 [self setNeedsDisplayInRect:titleFrame];
524 - (NSColor*)titleColor {
525 return [titleView_ textColor];
528 - (void)setTitleColor:(NSColor*)titleColor {
529 [titleView_ setTextColor:titleColor];
530 [self setNeedsDisplayInRect:[titleView_ frame]];
533 - (BOOL)titleHidden {
534 return [titleView_ isHidden];
537 - (void)setTitleHidden:(BOOL)titleHidden {
538 [titleView_ setHidden:titleHidden];
539 [self setNeedsDisplayInRect:[titleView_ frame]];
542 - (void)setState:(NSCellStateValue)state {
546 [self setNeedsDisplay:YES];
549 - (void)setClosing:(BOOL)closing {
550 closing_ = closing; // Safe because the property is nonatomic.
551 // When closing, ensure clicks to the close button go nowhere.
553 [closeButton_ setTarget:nil];
554 [closeButton_ setAction:nil];
559 // Do not start a new alert while already alerting or while in a decay cycle.
560 if (alertState_ == tabs::kAlertNone) {
561 alertState_ = tabs::kAlertRising;
562 [self resetLastGlowUpdateTime];
563 [self adjustGlowValue];
567 - (void)cancelAlert {
568 if (alertState_ != tabs::kAlertNone) {
569 alertState_ = tabs::kAlertFalling;
571 [NSDate timeIntervalSinceReferenceDate] + kGlowUpdateInterval;
572 [self resetLastGlowUpdateTime];
573 [self adjustGlowValue];
577 - (BOOL)accessibilityIsIgnored {
581 - (NSArray*)accessibilityActionNames {
582 NSArray* parentActions = [super accessibilityActionNames];
584 return [parentActions arrayByAddingObject:NSAccessibilityPressAction];
587 - (NSArray*)accessibilityAttributeNames {
588 NSMutableArray* attributes =
589 [[super accessibilityAttributeNames] mutableCopy];
590 [attributes addObject:NSAccessibilityTitleAttribute];
591 [attributes addObject:NSAccessibilityEnabledAttribute];
592 [attributes addObject:NSAccessibilityValueAttribute];
594 return [attributes autorelease];
597 - (BOOL)accessibilityIsAttributeSettable:(NSString*)attribute {
598 if ([attribute isEqual:NSAccessibilityTitleAttribute])
601 if ([attribute isEqual:NSAccessibilityEnabledAttribute])
604 if ([attribute isEqual:NSAccessibilityValueAttribute])
607 return [super accessibilityIsAttributeSettable:attribute];
610 - (void)accessibilityPerformAction:(NSString*)action {
611 if ([action isEqual:NSAccessibilityPressAction] &&
612 [[controller_ target] respondsToSelector:[controller_ action]]) {
613 [[controller_ target] performSelector:[controller_ action]
615 NSAccessibilityPostNotification(self,
616 NSAccessibilityValueChangedNotification);
618 [super accessibilityPerformAction:action];
622 - (id)accessibilityAttributeValue:(NSString*)attribute {
623 if ([attribute isEqual:NSAccessibilityRoleAttribute])
624 return NSAccessibilityRadioButtonRole;
625 if ([attribute isEqual:NSAccessibilityRoleDescriptionAttribute])
626 return l10n_util::GetNSStringWithFixup(IDS_ACCNAME_TAB);
627 if ([attribute isEqual:NSAccessibilityTitleAttribute])
628 return [controller_ title];
629 if ([attribute isEqual:NSAccessibilityValueAttribute])
630 return [NSNumber numberWithInt:[controller_ selected]];
631 if ([attribute isEqual:NSAccessibilityEnabledAttribute])
632 return [NSNumber numberWithBool:YES];
634 return [super accessibilityAttributeValue:attribute];
641 @end // @implementation TabView
643 @implementation TabView (TabControllerInterface)
645 - (void)setController:(TabController*)controller {
646 controller_ = controller;
649 @end // @implementation TabView (TabControllerInterface)
651 @implementation TabView(Private)
653 - (void)resetLastGlowUpdateTime {
654 lastGlowUpdate_ = [NSDate timeIntervalSinceReferenceDate];
657 - (NSTimeInterval)timeElapsedSinceLastGlowUpdate {
658 return [NSDate timeIntervalSinceReferenceDate] - lastGlowUpdate_;
661 - (void)adjustGlowValue {
662 // A time interval long enough to represent no update.
663 const NSTimeInterval kNoUpdate = 1000000;
665 // Time until next update for either glow.
666 NSTimeInterval nextUpdate = kNoUpdate;
668 NSTimeInterval elapsed = [self timeElapsedSinceLastGlowUpdate];
669 NSTimeInterval currentTime = [NSDate timeIntervalSinceReferenceDate];
671 // TODO(viettrungluu): <http://crbug.com/30617> -- split off the stuff below
672 // into a pure function and add a unit test.
674 CGFloat hoverAlpha = [self hoverAlpha];
675 if (isMouseInside_) {
676 // Increase hover glow until it's 1.
677 if (hoverAlpha < 1) {
678 hoverAlpha = MIN(hoverAlpha + elapsed / kHoverShowDuration, 1);
679 [self setHoverAlpha:hoverAlpha];
680 nextUpdate = MIN(kGlowUpdateInterval, nextUpdate);
681 } // Else already 1 (no update needed).
683 if (currentTime >= hoverHoldEndTime_) {
684 // No longer holding, so decrease hover glow until it's 0.
685 if (hoverAlpha > 0) {
686 hoverAlpha = MAX(hoverAlpha - elapsed / kHoverHideDuration, 0);
687 [self setHoverAlpha:hoverAlpha];
688 nextUpdate = MIN(kGlowUpdateInterval, nextUpdate);
689 } // Else already 0 (no update needed).
691 // Schedule update for end of hold time.
692 nextUpdate = MIN(hoverHoldEndTime_ - currentTime, nextUpdate);
696 CGFloat alertAlpha = [self alertAlpha];
697 if (alertState_ == tabs::kAlertRising) {
698 // Increase alert glow until it's 1 ...
699 alertAlpha = MIN(alertAlpha + elapsed / kAlertShowDuration, 1);
700 [self setAlertAlpha:alertAlpha];
702 // ... and having reached 1, switch to holding.
703 if (alertAlpha >= 1) {
704 alertState_ = tabs::kAlertHolding;
705 alertHoldEndTime_ = currentTime + kAlertHoldDuration;
706 nextUpdate = MIN(kAlertHoldDuration, nextUpdate);
708 nextUpdate = MIN(kGlowUpdateInterval, nextUpdate);
710 } else if (alertState_ != tabs::kAlertNone) {
711 if (alertAlpha > 0) {
712 if (currentTime >= alertHoldEndTime_) {
713 // Stop holding, then decrease alert glow (until it's 0).
714 if (alertState_ == tabs::kAlertHolding) {
715 alertState_ = tabs::kAlertFalling;
716 nextUpdate = MIN(kGlowUpdateInterval, nextUpdate);
718 DCHECK_EQ(tabs::kAlertFalling, alertState_);
719 alertAlpha = MAX(alertAlpha - elapsed / kAlertHideDuration, 0);
720 [self setAlertAlpha:alertAlpha];
721 nextUpdate = MIN(kGlowUpdateInterval, nextUpdate);
724 // Schedule update for end of hold time.
725 nextUpdate = MIN(alertHoldEndTime_ - currentTime, nextUpdate);
728 // Done the alert decay cycle.
729 alertState_ = tabs::kAlertNone;
733 if (nextUpdate < kNoUpdate)
734 [self performSelector:_cmd withObject:nil afterDelay:nextUpdate];
736 [self resetLastGlowUpdateTime];
737 [self setNeedsDisplay:YES];
740 - (CGImageRef)tabClippingMask {
741 // NOTE: NSHeight([self bounds]) doesn't match the height of the bitmaps.
743 if ([[self window] respondsToSelector:@selector(backingScaleFactor)])
744 scale = [[self window] backingScaleFactor];
746 NSRect bounds = [self bounds];
747 CGFloat tabWidth = NSWidth(bounds);
748 if (tabWidth == maskCacheWidth_ && scale == maskCacheScale_)
749 return maskCache_.get();
751 maskCacheWidth_ = tabWidth;
752 maskCacheScale_ = scale;
754 ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();
755 NSImage* leftMask = rb.GetNativeImageNamed(IDR_TAB_ALPHA_LEFT).ToNSImage();
756 NSImage* rightMask = rb.GetNativeImageNamed(IDR_TAB_ALPHA_RIGHT).ToNSImage();
758 CGFloat leftWidth = leftMask.size.width;
759 CGFloat rightWidth = rightMask.size.width;
761 // Image masks must be in the DeviceGray colorspace. Create a context and
762 // draw the mask into it.
763 base::ScopedCFTypeRef<CGColorSpaceRef> colorspace(
764 CGColorSpaceCreateDeviceGray());
765 base::ScopedCFTypeRef<CGContextRef> maskContext(
766 CGBitmapContextCreate(NULL, tabWidth * scale, kMaskHeight * scale,
767 8, tabWidth * scale, colorspace, 0));
768 CGContextScaleCTM(maskContext, scale, scale);
769 NSGraphicsContext* maskGraphicsContext =
770 [NSGraphicsContext graphicsContextWithGraphicsPort:maskContext
773 gfx::ScopedNSGraphicsContextSaveGState scopedGState;
774 [NSGraphicsContext setCurrentContext:maskGraphicsContext];
777 [[NSColor blackColor] setFill];
778 CGContextFillRect(maskContext, CGRectMake(0, 0, tabWidth, kMaskHeight));
780 NSDrawThreePartImage(NSMakeRect(0, 0, tabWidth, kMaskHeight),
781 leftMask, nil, rightMask, /*vertical=*/NO, NSCompositeSourceOver, 1.0,
784 CGFloat middleWidth = tabWidth - leftWidth - rightWidth;
785 NSRect middleRect = NSMakeRect(leftWidth, 0, middleWidth, kFillHeight);
786 [[NSColor whiteColor] setFill];
787 NSRectFill(middleRect);
789 maskCache_.reset(CGBitmapContextCreateImage(maskContext));
793 @end // @implementation TabView(Private)