blob: b544a5adef8f6b9c84f737d654e2ec4704e3924a [file] [log] [blame]
// 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/l10n_util.h"
#include "chrome/browser/ui/cocoa/tabs/tab_view.h"
#include "chrome/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 OverlayOption {
NONE,
LIGHTEN,
DARKEN,
};
const NSSize newTabButtonImageSize = {34, 18};
NSImage* GetMaskImageFromCell(NewTabButtonCell* aCell) {
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<NSImage> 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) OverlayOption overlayOption;
@end
@implementation NewTabButtonCustomImageRep
@synthesize destView = destView_;
@synthesize fillColor = fillColor_;
@synthesize patternPhasePosition = patternPhasePosition_;
@synthesize overlayOption = overlayOption_;
- (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 TabStripControllerCocoa.
}
- (void)drawFocusRingMaskWithFrame:(NSRect)cellFrame inView:(NSView*)view {
// Match the button's shape.
[self drawImage:GetMaskImageFromCell(self) withFrame:cellFrame inView:view];
}
@end
@interface NewTabButtonCocoa ()
// Returns a new tab button image appropriate for the specified button state
// (e.g. hover) and theme. In Material Design, the theme color affects the
// button color.
- (NSImage*)imageForState:(image_button_cell::ButtonState)state
theme:(const ui::ThemeProvider*)theme;
// Returns a new tab button image bezier path with the specified line width.
+ (NSBezierPath*)newTabButtonBezierPathWithLineWidth:(CGFloat)lineWidth;
// Draws the new tab button image to |imageRep|, with either a normal stroke or
// a heavy stroke for increased visibility.
+ (void)drawNewTabButtonImage:(NewTabButtonCustomImageRep*)imageRep
withHeavyStroke:(BOOL)heavyStroke;
// NSCustomImageRep custom drawing method shims for normal and heavy strokes
// respectively.
+ (void)drawNewTabButtonImageWithNormalStroke:
(NewTabButtonCustomImageRep*)imageRep;
+ (void)drawNewTabButtonImageWithHeavyStroke:
(NewTabButtonCustomImageRep*)imageRep;
// Returns a new tab button image filled with |fillColor|.
- (NSImage*)imageWithFillColor:(NSColor*)fillColor;
@end
@implementation NewTabButtonCocoa
+ (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<NewTabButtonCell>([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;
}
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<NewTabButtonCell>([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;
OverlayOption overlayOption = OverlayOption::NONE;
switch (state) {
case image_button_cell::kDefaultState:
// In the normal state, the NTP button looks like a background tab.
fillColor = theme->GetNSImageColorNamed(IDR_THEME_TAB_BACKGROUND);
break;
case image_button_cell::kHoverState:
fillColor = theme->GetNSImageColorNamed(IDR_THEME_TAB_BACKGROUND);
overlayOption = OverlayOption::LIGHTEN;
break;
case image_button_cell::kPressedState:
fillColor = theme->GetNSImageColorNamed(IDR_THEME_TAB_BACKGROUND);
overlayOption = OverlayOption::DARKEN;
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();
}
SEL drawSelector = @selector(drawNewTabButtonImageWithNormalStroke:);
if (theme && theme->ShouldIncreaseContrast())
drawSelector = @selector(drawNewTabButtonImageWithHeavyStroke:);
base::scoped_nsobject<NewTabButtonCustomImageRep> imageRep(
[[NewTabButtonCustomImageRep alloc]
initWithDrawSelector:drawSelector
delegate:[NewTabButtonCocoa class]]);
[imageRep setDestView:self];
[imageRep setFillColor:fillColor];
[imageRep setPatternPhasePosition:
[[self window]
themeImagePositionForAlignment:THEME_IMAGE_ALIGN_WITH_TAB_STRIP]];
[imageRep setOverlayOption:overlayOption];
NSImage* newTabButtonImage =
[[[NSImage alloc] initWithSize:newTabButtonImageSize] autorelease];
[newTabButtonImage addRepresentation:imageRep];
return newTabButtonImage;
}
+ (NSBezierPath*)newTabButtonBezierPathWithLineWidth:(CGFloat)lineWidth {
NSBezierPath* bezierPath = [NSBezierPath bezierPath];
// This data comes straight from the SVG.
[bezierPath moveToPoint:NSMakePoint(15.2762236,30)];
[bezierPath curveToPoint:NSMakePoint(11.0354216,27.1770115)
controlPoint1:NSMakePoint(13.3667706,30)
controlPoint2:NSMakePoint(11.7297681,28.8344828)];
[bezierPath curveToPoint:NSMakePoint(7.28528951e-08,2.01431416)
controlPoint1:NSMakePoint(11.0354216,27.1770115)
controlPoint2:NSMakePoint(0.000412425082,3.87955717)];
[bezierPath curveToPoint:NSMakePoint(1.70510791,0)
controlPoint1:NSMakePoint(-0.000270516213,0.790325707)
controlPoint2:NSMakePoint(0.753255356,0)];
[bezierPath lineToPoint:NSMakePoint(48.7033642,0)];
[bezierPath curveToPoint:NSMakePoint(52.9464653,2.82643678)
controlPoint1:NSMakePoint(50.6151163,0)
controlPoint2:NSMakePoint(52.2521188,1.16666667)];
[bezierPath curveToPoint:NSMakePoint(64.0268555,27.5961914)
controlPoint1:NSMakePoint(52.9464653,2.82643678)
controlPoint2:NSMakePoint(64.0268555,27.4111339)];
[bezierPath curveToPoint:NSMakePoint(62.2756294,30)
controlPoint1:NSMakePoint(64.0268555,28.5502144)
controlPoint2:NSMakePoint(63.227482,29.9977011)];
[bezierPath closePath];
// The SVG path is flipped for some reason, so flip it back. However, in RTL,
// we'd need to flip it again below, so when in RTL mode just leave the flip
// out altogether.
if (!cocoa_l10n_util::ShouldDoExperimentalRTLLayout()) {
const CGFloat kSVGHeight = 32;
NSAffineTransformStruct flipStruct = {1, 0, 0, -1, 0, kSVGHeight};
NSAffineTransform* flipTransform = [NSAffineTransform transform];
[flipTransform setTransformStruct:flipStruct];
[bezierPath transformUsingAffineTransform:flipTransform];
}
// The SVG data is for the 2x version so scale it down.
NSAffineTransform* scaleTransform = [NSAffineTransform transform];
const CGFloat k50PercentScale = 0.5;
[scaleTransform scaleBy:k50PercentScale];
[bezierPath transformUsingAffineTransform:scaleTransform];
// Adjust by half the line width to get crisp lines.
NSAffineTransform* transform = [NSAffineTransform transform];
[transform translateXBy:lineWidth / 2 yBy:lineWidth / 2];
[bezierPath transformUsingAffineTransform:transform];
[bezierPath setLineWidth:lineWidth];
return bezierPath;
}
+ (void)drawNewTabButtonImage:(NewTabButtonCustomImageRep*)imageRep
withHeavyStroke:(BOOL)heavyStroke {
[[NSGraphicsContext currentContext]
cr_setPatternPhase:[imageRep patternPhasePosition]
forView:[imageRep destView]];
CGContextRef context = static_cast<CGContextRef>(
[[NSGraphicsContext currentContext] graphicsPort]);
CGFloat lineWidth = LineWidthFromContext(context);
NSBezierPath* bezierPath =
[self newTabButtonBezierPathWithLineWidth:lineWidth];
if ([imageRep fillColor]) {
[[imageRep fillColor] set];
[bezierPath fill];
}
CGFloat alpha = heavyStroke ? 1.0 : 0.25;
static NSColor* strokeColor =
[[NSColor colorWithCalibratedWhite:0 alpha:alpha] retain];
[strokeColor set];
[bezierPath stroke];
BOOL isRTL = cocoa_l10n_util::ShouldDoExperimentalRTLLayout();
CGFloat buttonWidth = newTabButtonImageSize.width;
// Bottom edge.
const CGFloat kBottomEdgeX = 9;
const CGFloat kBottomEdgeY = 1.2825;
const CGFloat kBottomEdgeWidth = 22;
NSPoint bottomEdgeStart = NSMakePoint(kBottomEdgeX, kBottomEdgeY);
NSPoint bottomEdgeEnd = NSMakePoint(kBottomEdgeX + kBottomEdgeWidth,
kBottomEdgeY);
if (isRTL) {
bottomEdgeStart.x = buttonWidth - bottomEdgeStart.x;
bottomEdgeEnd.x = buttonWidth - bottomEdgeEnd.x;
}
NSBezierPath* bottomEdgePath = [NSBezierPath bezierPath];
[bottomEdgePath moveToPoint:bottomEdgeStart];
[bottomEdgePath lineToPoint:bottomEdgeEnd];
static NSColor* bottomEdgeColor =
[[NSColor colorWithCalibratedWhite:0 alpha:0.07] retain];
[bottomEdgeColor set];
[bottomEdgePath setLineWidth:lineWidth];
[bottomEdgePath setLineCapStyle:NSRoundLineCapStyle];
[bottomEdgePath stroke];
CGPoint shadowStart = NSZeroPoint;
CGPoint shadowEnd = NSZeroPoint;
NSColor* overlayColor = nil;
const CGFloat kBottomShadowX = 8;
const CGFloat kBottomShadowY = kBottomEdgeY - lineWidth;
const CGFloat kTopShadowX = 1;
const CGFloat kTopShadowY = kBottomShadowY + 15;
const CGFloat kShadowWidth = 24;
static NSColor* lightOverlayColor =
[[NSColor colorWithCalibratedWhite:1 alpha:0.20] retain];
static NSColor* darkOverlayColor =
[[NSColor colorWithCalibratedWhite:0 alpha:0.08] retain];
switch ([imageRep overlayOption]) {
case OverlayOption::LIGHTEN:
overlayColor = lightOverlayColor;
break;
case OverlayOption::DARKEN:
overlayColor = darkOverlayColor;
shadowStart = NSMakePoint(kTopShadowX, kTopShadowY);
shadowEnd = NSMakePoint(kTopShadowX + kShadowWidth, kTopShadowY);
break;
case OverlayOption::NONE:
shadowStart = NSMakePoint(kBottomShadowX, kBottomShadowY);
shadowEnd = NSMakePoint(kBottomShadowX + kShadowWidth, kBottomShadowY);
break;
}
// Shadow beneath the bottom or top edge.
if (!NSEqualPoints(shadowStart, NSZeroPoint)) {
if (isRTL) {
shadowStart.x = buttonWidth - shadowStart.x;
shadowEnd.x = buttonWidth - shadowEnd.x;
}
NSBezierPath* shadowPath = [NSBezierPath bezierPath];
[shadowPath moveToPoint:shadowStart];
[shadowPath lineToPoint:shadowEnd];
[shadowPath setLineWidth:lineWidth];
[shadowPath setLineCapStyle:NSRoundLineCapStyle];
static NSColor* shadowColor =
[[NSColor colorWithCalibratedWhite:0 alpha:0.10] retain];
[shadowColor set];
[shadowPath stroke];
}
if (overlayColor) {
[overlayColor set];
[[self newTabButtonBezierPathWithLineWidth:lineWidth] fill];
}
}
+ (void)drawNewTabButtonImageWithNormalStroke:
(NewTabButtonCustomImageRep*)image {
[self drawNewTabButtonImage:image withHeavyStroke:NO];
}
+ (void)drawNewTabButtonImageWithHeavyStroke:
(NewTabButtonCustomImageRep*)image {
[self drawNewTabButtonImage:image withHeavyStroke:YES];
}
- (NSImage*)imageWithFillColor:(NSColor*)fillColor {
NSImage* image =
[[[NSImage alloc] initWithSize:newTabButtonImageSize] autorelease];
[image lockFocus];
[fillColor set];
CGContextRef context = static_cast<CGContextRef>(
[[NSGraphicsContext currentContext] graphicsPort]);
CGFloat lineWidth = LineWidthFromContext(context);
[[NewTabButtonCocoa newTabButtonBezierPathWithLineWidth:lineWidth] fill];
[image unlockFocus];
return image;
}
@end