| // 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<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) 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<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; |
| } |
| |
| // 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<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; |
| 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<NewTabButtonCustomImageRep> 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<CGContextRef>( |
| [[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<CGContextRef>( |
| [[NSGraphicsContext currentContext] graphicsPort]); |
| CGFloat lineWidth = LineWidthFromContext(context); |
| [[NewTabButton newTabButtonBezierPathWithInset:0 lineWidth:lineWidth] fill]; |
| [image unlockFocus]; |
| return image; |
| } |
| |
| @end |