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/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"
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.
26 // Constants for inset and control points for tab shape.
27 const CGFloat kInsetMultiplier = 2.0/3.0;
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;
38 // The default time interval in seconds between glow updates (when
39 // increasing/decreasing).
40 const NSTimeInterval kGlowUpdateInterval = 0.025;
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;
46 @interface TabView(Private)
48 - (void)resetLastGlowUpdateTime;
49 - (NSTimeInterval)timeElapsedSinceLastGlowUpdate;
50 - (void)adjustGlowValue;
51 - (CGImageRef)tabClippingMask;
53 @end // TabView(Private)
55 @implementation TabView
57 @synthesize state = state_;
58 @synthesize hoverAlpha = hoverAlpha_;
59 @synthesize alertAlpha = alertAlpha_;
60 @synthesize closing = closing_;
62 + (CGFloat)insetMultiplier {
63 return kInsetMultiplier;
66 - (id)initWithFrame:(NSRect)frame
67 controller:(TabController*)controller
68 closeButton:(HoverCloseButton*)closeButton {
69 self = [super initWithFrame:frame];
71 controller_ = controller;
72 closeButton_ = closeButton;
78 // Cancel any delayed requests that may still be pending (drags or hover).
79 [NSObject cancelPreviousPerformRequestsWithTarget:self];
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
90 // Sheets, being window-modal, should block contextual menus. For some reason
91 // they do not. Disallow them ourselves.
92 if ([[self window] attachedSheet])
95 return [controller_ menu];
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
103 [controller_ updateVisibility];
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 {
113 - (void)mouseEntered:(NSEvent*)theEvent {
114 isMouseInside_ = YES;
115 [self resetLastGlowUpdateTime];
116 [self adjustGlowValue];
119 - (void)mouseMoved:(NSEvent*)theEvent {
120 hoverPoint_ = [self convertPoint:[theEvent locationInWindow]
122 [self setNeedsDisplay:YES];
125 - (void)mouseExited:(NSEvent*)theEvent {
128 [NSDate timeIntervalSinceReferenceDate] + kHoverHoldDuration;
129 [self resetLastGlowUpdateTime];
130 [self adjustGlowValue];
133 - (void)setTrackingEnabled:(BOOL)enabled {
134 if (![closeButton_ isHidden]) {
135 [closeButton_ setTrackingEnabled:enabled];
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_;
146 NSRect pointRect = NSMakeRect(viewPoint.x, viewPoint.y, 1, 1);
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]) {
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]) {
171 if (viewPoint.y < kFillHeight)
176 // Returns |YES| if this tab can be torn away into a new window.
177 - (BOOL)canBeDragged {
178 return [controller_ tabCanBeDragged:controller_];
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])
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];
191 // Record the state of the close button here, because selecting the tab will
193 BOOL closeButtonActive = ![closeButton_ isHidden];
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_
202 if ([self hitTest:hitLocation] == closeButton_) {
203 [closeButton_ mouseDown:theEvent];
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
212 base::scoped_nsobject<TabController> controller([controller_ retain]);
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];
218 // The custom loop has ended, so clear the point.
219 mouseDownPoint_ = NSZeroPoint;
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;
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]
238 if ([self hitTest:hitLocation] == closeButton_) {
239 [controller_ closeTab:self];
245 // Fire the action to select the tab.
246 [controller_ selectTab:self];
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.
256 - (void)otherMouseUp:(NSEvent*)theEvent {
257 if ([self isClosing])
260 // Support middle-click-to-close.
261 if ([theEvent buttonNumber] == 2) {
262 // |-hitTest:| takes a location in the superview's coordinates.
264 [[self superview] convertPoint:[theEvent locationInWindow]
266 // If the mouse up occurred in our view or over the close button, then
268 if ([self hitTest:upLocation])
269 [controller_ closeTab:self];
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]);
279 return [[self window] backgroundColor];
281 int bitmapResources[2][2] = {
282 // Background window.
284 IDR_THEME_TAB_BACKGROUND_INACTIVE, // Background tab.
285 IDR_THEME_TOOLBAR_INACTIVE, // Active tab.
287 // Currently focused window.
289 IDR_THEME_TAB_BACKGROUND, // Background tab.
290 IDR_THEME_TOOLBAR, // Active tab.
294 // Themes don't have an inactive image so only look for one if there's no
296 bool active = [[self window] isKeyWindow] || [[self window] isMainWindow] ||
297 !themeProvider->UsingDefaultTheme();
298 return themeProvider->GetNSImageColorNamed(bitmapResources[active][selected]);
301 // Draws the active tab background.
302 - (void)drawFillForActiveTab:(NSRect)dirtyRect {
303 NSColor* backgroundImageColor = [self backgroundColorForSelected:YES];
304 [backgroundImageColor set];
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);
313 NSRectFillUsingOperation(dirtyRect, NSCompositeSourceOver);
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]);
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];
328 CGImageRef mask([self tabClippingMask]);
329 CGRect maskBounds = CGRectMake(0, 0, maskCacheWidth_, kMaskHeight);
330 CGContextClipToMask(cgContext, maskBounds, mask);
332 bool selected = [self state];
334 [self drawFillForActiveTab:dirtyRect];
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];
343 // Draw the tab background.
344 NSColor* backgroundImageColor = [self backgroundColorForSelected:NO];
345 [backgroundImageColor set];
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);
353 NSRectFillUsingOperation(dirtyRect, NSCompositeSourceOver);
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);
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);
368 [self drawFillForActiveTab:dirtyRect];
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
385 NSRect rect = [self bounds];
386 NSPoint point = hoverPoint_;
387 point.y = NSHeight(rect);
388 [glow drawFromCenter:point
391 radius:NSWidth(rect) / 3.0
392 options:NSGradientDrawsBeforeStartingLocation];
396 CGContextEndTransparencyLayer(cgContext);
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;
405 ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();
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(),
414 NSCompositeSourceOver,
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(),
423 NSCompositeSourceOver,
429 - (void)drawRect:(NSRect)dirtyRect {
430 // Text, close button, and image are drawn by subviews.
431 [self drawFill:dirtyRect];
432 [self drawStroke:dirtyRect];
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];
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]);
447 - (NSString*)toolTipText {
448 if (!toolTipText_.get()) {
451 return toolTipText_.get();
454 - (void)viewDidMoveToWindow {
455 [super viewDidMoveToWindow];
457 [controller_ updateTitleColor];
461 - (void)setState:(NSCellStateValue)state {
465 [self setNeedsDisplay:YES];
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.
472 [closeButton_ setTarget:nil];
473 [closeButton_ setAction:nil];
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];
486 - (void)cancelAlert {
487 if (alertState_ != tabs::kAlertNone) {
488 alertState_ = tabs::kAlertFalling;
490 [NSDate timeIntervalSinceReferenceDate] + kGlowUpdateInterval;
491 [self resetLastGlowUpdateTime];
492 [self adjustGlowValue];
496 - (BOOL)accessibilityIsIgnored {
500 - (NSArray*)accessibilityActionNames {
501 NSArray* parentActions = [super accessibilityActionNames];
503 return [parentActions arrayByAddingObject:NSAccessibilityPressAction];
506 - (NSArray*)accessibilityAttributeNames {
507 NSMutableArray* attributes =
508 [[super accessibilityAttributeNames] mutableCopy];
509 [attributes addObject:NSAccessibilityTitleAttribute];
510 [attributes addObject:NSAccessibilityEnabledAttribute];
511 [attributes addObject:NSAccessibilityValueAttribute];
513 return [attributes autorelease];
516 - (BOOL)accessibilityIsAttributeSettable:(NSString*)attribute {
517 if ([attribute isEqual:NSAccessibilityTitleAttribute])
520 if ([attribute isEqual:NSAccessibilityEnabledAttribute])
523 if ([attribute isEqual:NSAccessibilityValueAttribute])
526 return [super accessibilityIsAttributeSettable:attribute];
529 - (void)accessibilityPerformAction:(NSString*)action {
530 if ([action isEqual:NSAccessibilityPressAction] &&
531 [[controller_ target] respondsToSelector:[controller_ action]]) {
532 [[controller_ target] performSelector:[controller_ action]
534 NSAccessibilityPostNotification(self,
535 NSAccessibilityValueChangedNotification);
537 [super accessibilityPerformAction:action];
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];
553 return [super accessibilityAttributeValue:attribute];
560 @end // @implementation TabView
562 @implementation TabView (TabControllerInterface)
564 - (void)setController:(TabController*)controller {
565 controller_ = controller;
568 @end // @implementation TabView (TabControllerInterface)
570 @implementation TabView(Private)
572 - (void)resetLastGlowUpdateTime {
573 lastGlowUpdate_ = [NSDate timeIntervalSinceReferenceDate];
576 - (NSTimeInterval)timeElapsedSinceLastGlowUpdate {
577 return [NSDate timeIntervalSinceReferenceDate] - lastGlowUpdate_;
580 - (void)adjustGlowValue {
581 // A time interval long enough to represent no update.
582 const NSTimeInterval kNoUpdate = 1000000;
584 // Time until next update for either glow.
585 NSTimeInterval nextUpdate = kNoUpdate;
587 NSTimeInterval elapsed = [self timeElapsedSinceLastGlowUpdate];
588 NSTimeInterval currentTime = [NSDate timeIntervalSinceReferenceDate];
590 // TODO(viettrungluu): <http://crbug.com/30617> -- split off the stuff below
591 // into a pure function and add a unit test.
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).
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).
610 // Schedule update for end of hold time.
611 nextUpdate = MIN(hoverHoldEndTime_ - currentTime, nextUpdate);
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];
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);
627 nextUpdate = MIN(kGlowUpdateInterval, nextUpdate);
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);
637 DCHECK_EQ(tabs::kAlertFalling, alertState_);
638 alertAlpha = MAX(alertAlpha - elapsed / kAlertHideDuration, 0);
639 [self setAlertAlpha:alertAlpha];
640 nextUpdate = MIN(kGlowUpdateInterval, nextUpdate);
643 // Schedule update for end of hold time.
644 nextUpdate = MIN(alertHoldEndTime_ - currentTime, nextUpdate);
647 // Done the alert decay cycle.
648 alertState_ = tabs::kAlertNone;
652 if (nextUpdate < kNoUpdate)
653 [self performSelector:_cmd withObject:nil afterDelay:nextUpdate];
655 [self resetLastGlowUpdateTime];
656 [self setNeedsDisplay:YES];
659 - (CGImageRef)tabClippingMask {
660 // NOTE: NSHeight([self bounds]) doesn't match the height of the bitmaps.
662 if ([[self window] respondsToSelector:@selector(backingScaleFactor)])
663 scale = [[self window] backingScaleFactor];
665 NSRect bounds = [self bounds];
666 CGFloat tabWidth = NSWidth(bounds);
667 if (tabWidth == maskCacheWidth_ && scale == maskCacheScale_)
668 return maskCache_.get();
670 maskCacheWidth_ = tabWidth;
671 maskCacheScale_ = scale;
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();
677 CGFloat leftWidth = leftMask.size.width;
678 CGFloat rightWidth = rightMask.size.width;
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
692 gfx::ScopedNSGraphicsContextSaveGState scopedGState;
693 [NSGraphicsContext setCurrentContext:maskGraphicsContext];
696 [[NSColor blackColor] setFill];
697 CGContextFillRect(maskContext, CGRectMake(0, 0, tabWidth, kMaskHeight));
699 NSDrawThreePartImage(NSMakeRect(0, 0, tabWidth, kMaskHeight),
700 leftMask, nil, rightMask, /*vertical=*/NO, NSCompositeSourceOver, 1.0,
703 CGFloat middleWidth = tabWidth - leftWidth - rightWidth;
704 NSRect middleRect = NSMakeRect(leftWidth, 0, middleWidth, kFillHeight);
705 [[NSColor whiteColor] setFill];
706 NSRectFill(middleRect);
708 maskCache_.reset(CGBitmapContextCreateImage(maskContext));
712 @end // @implementation TabView(Private)