// Copyright (c) 2011 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #import "chrome/browser/ui/cocoa/new_tab_button.h" #include "base/mac/foundation_util.h" #include "base/mac/sdk_forward_declarations.h" #import "chrome/browser/ui/cocoa/image_button_cell.h" #include "chrome/browser/ui/cocoa/tabs/tab_view.h" #include "grit/theme_resources.h" #include "ui/base/cocoa/nsgraphics_context_additions.h" #include "ui/base/material_design/material_design_controller.h" #include "ui/base/resource/resource_bundle.h" #include "ui/base/theme_provider.h" @class NewTabButtonCell; namespace { enum class RenderingOption { NORMAL, OVERLAY_LIGHTEN, OVERLAY_LIGHTEN_INCOGNITO, OVERLAY_DARKEN, INLAY_LIGHTEN, }; NSImage* GetMaskImageFromCell(NewTabButtonCell* aCell) { if (!ui::MaterialDesignController::IsModeMaterial()) { ui::ResourceBundle& bundle = ui::ResourceBundle::GetSharedInstance(); return bundle.GetNativeImageNamed(IDR_NEWTAB_BUTTON_MASK).ToNSImage(); } return [aCell imageForState:image_button_cell::kDefaultState view:nil]; } // Creates an NSImage with size |size| and bitmap image representations for both // 1x and 2x scale factors. |drawingHandler| is called once for every scale // factor. This is similar to -[NSImage imageWithSize:flipped:drawingHandler:], // but this function always evaluates drawingHandler eagerly, and it works on // 10.6 and 10.7. NSImage* CreateImageWithSize(NSSize size, void (^drawingHandler)(NSSize)) { base::scoped_nsobject result([[NSImage alloc] initWithSize:size]); [NSGraphicsContext saveGraphicsState]; for (ui::ScaleFactor scale_factor : ui::GetSupportedScaleFactors()) { float scale = GetScaleForScaleFactor(scale_factor); NSBitmapImageRep *bmpImageRep = [[[NSBitmapImageRep alloc] initWithBitmapDataPlanes:NULL pixelsWide:size.width * scale pixelsHigh:size.height * scale bitsPerSample:8 samplesPerPixel:4 hasAlpha:YES isPlanar:NO colorSpaceName:NSDeviceRGBColorSpace bytesPerRow:0 bitsPerPixel:0] autorelease]; [bmpImageRep setSize:size]; [NSGraphicsContext setCurrentContext: [NSGraphicsContext graphicsContextWithBitmapImageRep:bmpImageRep]]; drawingHandler(size); [result addRepresentation:bmpImageRep]; } [NSGraphicsContext restoreGraphicsState]; return result.release(); } // Takes a normal bitmap and a mask image and returns an image the size of the // mask that has pixels from |image| but alpha information from |mask|. NSImage* ApplyMask(NSImage* image, NSImage* mask) { return [CreateImageWithSize([mask size], ^(NSSize size) { // Skip a few pixels from the top of the tab background gradient, because // the new tab button is not drawn at the very top of the browser window. const int kYOffset = 10; CGFloat width = size.width; CGFloat height = size.height; // In some themes, the tab background image is narrower than the // new tab button, so tile the background image. CGFloat x = 0; // The floor() is to make sure images with odd widths don't draw to the // same pixel twice on retina displays. (Using NSDrawThreePartImage() // caused a startup perf regression, so that cannot be used.) CGFloat tileWidth = floor(std::min(width, [image size].width)); while (x < width) { [image drawAtPoint:NSMakePoint(x, 0) fromRect:NSMakeRect(0, [image size].height - height - kYOffset, tileWidth, height) operation:NSCompositeCopy fraction:1.0]; x += tileWidth; } [mask drawAtPoint:NSZeroPoint fromRect:NSMakeRect(0, 0, width, height) operation:NSCompositeDestinationIn fraction:1.0]; }) autorelease]; } // Paints |overlay| on top of |ground|. NSImage* Overlay(NSImage* ground, NSImage* overlay, CGFloat alpha) { DCHECK_EQ([ground size].width, [overlay size].width); DCHECK_EQ([ground size].height, [overlay size].height); return [CreateImageWithSize([ground size], ^(NSSize size) { CGFloat width = size.width; CGFloat height = size.height; [ground drawAtPoint:NSZeroPoint fromRect:NSMakeRect(0, 0, width, height) operation:NSCompositeCopy fraction:1.0]; [overlay drawAtPoint:NSZeroPoint fromRect:NSMakeRect(0, 0, width, height) operation:NSCompositeSourceOver fraction:alpha]; }) autorelease]; } CGFloat LineWidthFromContext(CGContextRef context) { CGRect unitRect = CGRectMake(0.0, 0.0, 1.0, 1.0); CGRect deviceRect = CGContextConvertRectToDeviceSpace(context, unitRect); return 1.0 / deviceRect.size.height; } } // namespace @interface NewTabButtonCustomImageRep : NSCustomImageRep @property (assign, nonatomic) NSView* destView; @property (copy, nonatomic) NSColor* fillColor; @property (assign, nonatomic) NSPoint patternPhasePosition; @property (assign, nonatomic) RenderingOption renderingOption; @end @implementation NewTabButtonCustomImageRep @synthesize destView = destView_; @synthesize fillColor = fillColor_; @synthesize patternPhasePosition = patternPhasePosition_; @synthesize renderingOption = renderingOption_; - (void)dealloc { [fillColor_ release]; [super dealloc]; } @end // A simple override of the ImageButtonCell to disable handling of // -mouseEntered. @interface NewTabButtonCell : ImageButtonCell - (void)mouseEntered:(NSEvent*)theEvent; @end @implementation NewTabButtonCell - (void)mouseEntered:(NSEvent*)theEvent { // Ignore this since the NTB enter is handled by the TabStripController. } - (void)drawFocusRingMaskWithFrame:(NSRect)cellFrame inView:(NSView*)view { // Match the button's shape. [self drawImage:GetMaskImageFromCell(self) withFrame:cellFrame inView:view]; } @end @implementation NewTabButton + (Class)cellClass { return [NewTabButtonCell class]; } - (BOOL)pointIsOverButton:(NSPoint)point { NSPoint localPoint = [self convertPoint:point fromView:[self superview]]; NSRect pointRect = NSMakeRect(localPoint.x, localPoint.y, 1, 1); NSImage* buttonMask = GetMaskImageFromCell([self cell]); NSRect bounds = self.bounds; NSSize buttonMaskSize = [buttonMask size]; NSRect destinationRect = NSMakeRect( (NSWidth(bounds) - buttonMaskSize.width) / 2, (NSHeight(bounds) - buttonMaskSize.height) / 2, buttonMaskSize.width, buttonMaskSize.height); return [buttonMask hitTestRect:pointRect withImageDestinationRect:destinationRect context:nil hints:nil flipped:YES]; } // Override to only accept clicks within the bounds of the defined path, not // the entire bounding box. |aPoint| is in the superview's coordinate system. - (NSView*)hitTest:(NSPoint)aPoint { if ([self pointIsOverButton:aPoint]) return [super hitTest:aPoint]; return nil; } // ThemedWindowDrawing implementation. - (void)windowDidChangeTheme { [self setNeedsDisplay:YES]; } - (void)windowDidChangeActive { [self setNeedsDisplay:YES]; } - (void)viewDidMoveToWindow { NewTabButtonCell* cell = base::mac::ObjCCast([self cell]); if ([self window] && ![cell imageForState:image_button_cell::kDefaultState view:self]) { [self setImages]; } } - (void)setImages { const ui::ThemeProvider* theme = [[self window] themeProvider]; if (!theme) { return; } // The old way of doing things. if (!ui::MaterialDesignController::IsModeMaterial()) { ResourceBundle& rb = ResourceBundle::GetSharedInstance(); NSImage* mask = rb.GetNativeImageNamed(IDR_NEWTAB_BUTTON_MASK).ToNSImage(); NSImage* normal = rb.GetNativeImageNamed(IDR_NEWTAB_BUTTON).ToNSImage(); NSImage* hover = rb.GetNativeImageNamed(IDR_NEWTAB_BUTTON_H).ToNSImage(); NSImage* pressed = rb.GetNativeImageNamed(IDR_NEWTAB_BUTTON_P).ToNSImage(); NSImage* foreground = ApplyMask( theme->GetNSImageNamed(IDR_THEME_TAB_BACKGROUND), mask); [[self cell] setImage:Overlay(foreground, normal, 1.0) forButtonState:image_button_cell::kDefaultState]; [[self cell] setImage:Overlay(foreground, hover, 1.0) forButtonState:image_button_cell::kHoverState]; [[self cell] setImage:Overlay(foreground, pressed, 1.0) forButtonState:image_button_cell::kPressedState]; // IDR_THEME_TAB_BACKGROUND_INACTIVE is only used with the default theme. if (theme->UsingSystemTheme()) { const CGFloat alpha = tabs::kImageNoFocusAlpha; NSImage* background = ApplyMask( theme->GetNSImageNamed(IDR_THEME_TAB_BACKGROUND_INACTIVE), mask); [[self cell] setImage:Overlay(background, normal, alpha) forButtonState: image_button_cell::kDefaultStateBackground]; [[self cell] setImage:Overlay(background, hover, alpha) forButtonState:image_button_cell::kHoverStateBackground]; } else { [[self cell] setImage:nil forButtonState: image_button_cell::kDefaultStateBackground]; [[self cell] setImage:nil forButtonState:image_button_cell::kHoverStateBackground]; } return; } NSImage* mask = [self imageWithFillColor:[NSColor whiteColor]]; NSImage* normal = [self imageForState:image_button_cell::kDefaultState theme:theme]; NSImage* hover = [self imageForState:image_button_cell::kHoverState theme:theme]; NSImage* pressed = [self imageForState:image_button_cell::kPressedState theme:theme]; NSImage* normalBackground = nil; NSImage* hoverBackground = nil; // If using a custom theme, overlay the default image with the theme's custom // tab background image. if (!theme->UsingSystemTheme()) { NSImage* foreground = ApplyMask(theme->GetNSImageNamed(IDR_THEME_TAB_BACKGROUND), mask); normal = Overlay(foreground, normal, 1.0); hover = Overlay(foreground, hover, 1.0); pressed = Overlay(foreground, pressed, 1.0); NSImage* background = ApplyMask( theme->GetNSImageNamed(IDR_THEME_TAB_BACKGROUND_INACTIVE), mask); normalBackground = Overlay(background, normal, tabs::kImageNoFocusAlpha); hoverBackground = Overlay(background, hover, tabs::kImageNoFocusAlpha); } NewTabButtonCell* cell = base::mac::ObjCCast([self cell]); [cell setImage:normal forButtonState:image_button_cell::kDefaultState]; [cell setImage:hover forButtonState:image_button_cell::kHoverState]; [cell setImage:pressed forButtonState:image_button_cell::kPressedState]; [cell setImage:normalBackground forButtonState:image_button_cell::kDefaultStateBackground]; [cell setImage:hoverBackground forButtonState:image_button_cell::kHoverStateBackground]; } - (NSImage*)imageForState:(image_button_cell::ButtonState)state theme:(const ui::ThemeProvider*)theme { NSColor* fillColor = nil; RenderingOption renderingOption = RenderingOption::NORMAL; switch (state) { case image_button_cell::kDefaultState: fillColor = theme->GetNSImageColorNamed(IDR_THEME_TAB_BACKGROUND); break; case image_button_cell::kHoverState: fillColor = theme->GetNSImageColorNamed(IDR_THEME_TAB_BACKGROUND); // When a custom theme highlight the entire area, otherwise only // highlight the interior and not the border. if (theme->HasCustomImage(IDR_THEME_TAB_BACKGROUND)) { renderingOption = RenderingOption::OVERLAY_LIGHTEN; } else if (theme->InIncognitoMode()) { renderingOption = RenderingOption::OVERLAY_LIGHTEN_INCOGNITO; } else { renderingOption = RenderingOption::INLAY_LIGHTEN; } break; case image_button_cell::kPressedState: fillColor = theme->GetNSImageColorNamed(IDR_THEME_TAB_BACKGROUND); break; case image_button_cell::kDefaultStateBackground: case image_button_cell::kHoverStateBackground: fillColor = theme->GetNSImageColorNamed(IDR_THEME_TAB_BACKGROUND_INACTIVE); break; default: fillColor = [NSColor redColor]; // All states should be accounted for above. NOTREACHED(); } base::scoped_nsobject imageRep = [[NewTabButtonCustomImageRep alloc] initWithDrawSelector:@selector(drawNewTabButtonImage:) delegate:[NewTabButton class]]; [imageRep setDestView:self]; [imageRep setFillColor:fillColor]; [imageRep setPatternPhasePosition: [[self window] themeImagePositionForAlignment:THEME_IMAGE_ALIGN_WITH_TAB_STRIP]]; [imageRep setRenderingOption:renderingOption]; NSImage* newTabButtonImage = [[[NSImage alloc] initWithSize:NSMakeSize(34, 17)] autorelease]; [newTabButtonImage setCacheMode:NSImageCacheAlways]; [newTabButtonImage addRepresentation:imageRep]; return newTabButtonImage; } + (NSBezierPath*)newTabButtonBezierPathWithInset:(int)inset lineWidth:(CGFloat)lineWidth { NSBezierPath* bezierPath = [NSBezierPath bezierPath]; // Bottom edge. [bezierPath moveToPoint:NSMakePoint(19 - inset, 1.2825 + inset)]; [bezierPath lineToPoint:NSMakePoint(10.45 + inset, 1.2825 + inset)]; // Lower-left corner. [bezierPath curveToPoint:NSMakePoint(6.08 + inset, 2.85 + inset) controlPoint1:NSMakePoint(10.1664 + inset, 1.3965 + inset) controlPoint2:NSMakePoint(7.89222 + inset, 0.787708 + inset)]; // Left side. [bezierPath lineToPoint:NSMakePoint(0.7125 + inset, 14.25 - inset)]; // Upper-left corner. const float topEdgeY = 16.2688; [bezierPath curveToPoint:NSMakePoint(1.71 + inset, topEdgeY - inset) controlPoint1:NSMakePoint(0.246496 + inset, 15.2613 - inset) controlPoint2:NSMakePoint(0.916972 + inset, 16.3489 - inset)]; // Top edge. [bezierPath lineToPoint:NSMakePoint(23.275 - inset, topEdgeY - inset)]; // Upper right corner. [bezierPath curveToPoint:NSMakePoint(27.645 - inset, 14.7012 - inset) controlPoint1:NSMakePoint(26.4376 - inset, 16.3305 - inset) controlPoint2:NSMakePoint(26.9257 - inset, 15.8059 - inset)]; // Right side. [bezierPath lineToPoint:NSMakePoint(32.9543 - inset, 3.62561 + inset)]; // Lower right corner. [bezierPath curveToPoint:NSMakePoint(32.015 - inset, 1.2825 + inset) controlPoint1:NSMakePoint(34.069 - inset, 1.45303 + inset) controlPoint2:NSMakePoint(31.0348 - inset, 1.31455 + inset)]; [bezierPath closePath]; // On non-Retina machines, adjust the path so that the top line rests along // a pixel line (to get a crisp line on the display). if (lineWidth == 1) { NSAffineTransform* translateTransform = [NSAffineTransform transform]; [translateTransform translateXBy:0 yBy:0.5 - (topEdgeY - trunc(topEdgeY))]; [bezierPath transformUsingAffineTransform:translateTransform]; } [bezierPath setLineWidth:lineWidth]; return bezierPath; } + (void)drawNewTabButtonImage:(NewTabButtonCustomImageRep*)imageRep { [[NSGraphicsContext currentContext] cr_setPatternPhase:[imageRep patternPhasePosition] forView:[imageRep destView]]; CGContextRef context = static_cast( [[NSGraphicsContext currentContext] graphicsPort]); CGFloat lineWidth = LineWidthFromContext(context); NSBezierPath* bezierPath = [self newTabButtonBezierPathWithInset:0 lineWidth:lineWidth]; if ([imageRep fillColor]) { [[imageRep fillColor] set]; [bezierPath fill]; } static NSColor* strokeColor = [[NSColor colorWithCalibratedWhite:0 alpha:0.4] retain]; [strokeColor set]; [bezierPath stroke]; // Bottom edge. bezierPath = [NSBezierPath bezierPath]; [bezierPath moveToPoint:NSMakePoint(31, 1.2825)]; [bezierPath lineToPoint:NSMakePoint(9, 1.2825)]; static NSColor* bottomEdgeColor = [[NSColor colorWithCalibratedWhite:0.25 alpha:0.3] retain]; [bottomEdgeColor set]; [bezierPath setLineWidth:lineWidth]; [bezierPath setLineCapStyle:NSRoundLineCapStyle]; [bezierPath stroke]; // Shadow beneath the bottom edge. NSAffineTransform* translateTransform = [NSAffineTransform transform]; [translateTransform translateXBy:0 yBy:-lineWidth]; [bezierPath transformUsingAffineTransform:translateTransform]; static NSColor* shadowColor = [[NSColor colorWithCalibratedWhite:0.5 alpha:0.3] retain]; [shadowColor set]; [bezierPath stroke]; static NSColor* lightColor = [[NSColor colorWithCalibratedWhite:1 alpha:0.35] retain]; static NSColor* lightIncognitoColor = [[NSColor colorWithCalibratedWhite:1 alpha:0.15] retain]; static NSColor* darkColor = [[NSColor colorWithCalibratedWhite:0 alpha:0.08] retain]; CGFloat inset = -1; switch ([imageRep renderingOption]) { case RenderingOption::OVERLAY_LIGHTEN: [lightColor set]; inset = 0; break; case RenderingOption::OVERLAY_LIGHTEN_INCOGNITO: [lightIncognitoColor set]; inset = 0; break; case RenderingOption::OVERLAY_DARKEN: [darkColor set]; NSRectFillUsingOperation(NSMakeRect(0, 0, 34, 17), NSCompositeSourceAtop); break; case RenderingOption::INLAY_LIGHTEN: [lightColor set]; inset = 1; break; case RenderingOption::NORMAL: break; } if (inset != -1) { bezierPath = [self newTabButtonBezierPathWithInset:inset lineWidth:lineWidth]; [bezierPath fill]; } } - (NSImage*)imageWithFillColor:(NSColor*)fillColor { NSImage* image = [[[NSImage alloc] initWithSize:NSMakeSize(34, 17)] autorelease]; [image lockFocus]; [fillColor set]; CGContextRef context = static_cast( [[NSGraphicsContext currentContext] graphicsPort]); CGFloat lineWidth = LineWidthFromContext(context); [[NewTabButton newTabButtonBezierPathWithInset:0 lineWidth:lineWidth] fill]; [image unlockFocus]; return image; } @end