blob: 338adb5dd82bc453a7474a3c7ce3b3f864537d1b [file] [log] [blame]
// Copyright 2017 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/harmony_button.h"
#import "base/mac/scoped_cftyperef.h"
#import "chrome/browser/themes/theme_properties.h"
#import "chrome/browser/ui/cocoa/themed_window.h"
#include "skia/ext/skia_utils_mac.h"
#import "ui/base/cocoa/nsview_additions.h"
#import "ui/base/theme_provider.h"
namespace {
NSColor* GetBackgroundColor(HoverState state, BOOL dark_theme) {
if (dark_theme) {
switch (state) {
case kHoverStateNone:
return [NSColor colorWithCalibratedWhite:1 alpha:0.12];
case kHoverStateMouseOver:
return [NSColor colorWithCalibratedWhite:1 alpha:0.18];
case kHoverStateMouseDown:
return [NSColor colorWithCalibratedWhite:1 alpha:0.22];
}
} else {
switch (state) {
case kHoverStateNone:
return [NSColor colorWithCalibratedWhite:1 alpha:1];
case kHoverStateMouseOver:
return [NSColor colorWithCalibratedWhite:0.98 alpha:1];
case kHoverStateMouseDown:
return [NSColor colorWithCalibratedWhite:0.95 alpha:1];
}
}
}
NSColor* GetBorderColor(BOOL dark_theme) {
return [NSColor colorWithCalibratedWhite:dark_theme ? 1 : 0 alpha:0.3];
}
NSColor* GetShadowColor() {
return NSColor.blackColor;
}
constexpr CGFloat kFontSize = 12;
constexpr CGFloat kTextAlpha = 0x8A / (CGFloat)0xFF;
constexpr CGSize kNormalShadowOffset{0, 0};
constexpr CGSize kMouseOverShadowOffset{0, 1};
constexpr CGFloat kNormalShadowOpacity = 0;
constexpr CGFloat kMouseOverShadowOpacity = 0.1;
constexpr CGFloat kNormalShadowRadius = 0;
constexpr CGFloat kMouseOverShadowRadius = 2;
constexpr CGFloat kCornerRadius = 3;
constexpr CGFloat kXPadding = 16;
constexpr CGFloat kMinWidth = 64;
constexpr CGFloat kHeight = 28;
// The text is a bit too low by default; offset the title rect to center it.
constexpr CGFloat kTextYOffset = 1;
constexpr NSTimeInterval kTransitionDuration = 0.25;
} // namespace
@interface HarmonyButtonCell : NSButtonCell
@end
@implementation HarmonyButtonCell
- (NSRect)titleRectForBounds:(NSRect)rect {
rect.origin.y -= kTextYOffset;
return rect;
}
@end
@implementation HarmonyButton
+ (instancetype)buttonWithTitle:(NSString*)title
target:(id)target
action:(SEL)action {
HarmonyButton* button = [[[self alloc] initWithFrame:NSZeroRect] autorelease];
button.title = title;
button.target = target;
button.action = action;
[button sizeToFit];
return button;
}
+ (Class)cellClass {
return [HarmonyButtonCell class];
}
- (instancetype)initWithFrame:(NSRect)frameRect {
if ((self = [super initWithFrame:frameRect])) {
self.bezelStyle = NSRoundedBezelStyle;
self.bordered = NO;
self.wantsLayer = YES;
CALayer* layer = self.layer;
layer.shadowColor = GetShadowColor().CGColor;
layer.cornerRadius = kCornerRadius;
[self updateBorderWidth];
[self updateHoverButtonAppearanceAnimated:NO];
}
return self;
}
- (void)updateBorderWidth {
self.layer.borderWidth = [self cr_lineWidth];
}
- (void)updateHoverButtonAppearanceAnimated:(BOOL)animated {
if (!self.window)
return;
const ui::ThemeProvider* themeProvider = [self.window themeProvider];
const BOOL darkTheme = [self.window hasDarkTheme];
CALayer* layer = self.layer;
if (animated) {
[NSAnimationContext beginGrouping];
NSAnimationContext* context = [NSAnimationContext currentContext];
context.allowsImplicitAnimation = YES;
context.duration = kTransitionDuration;
} else {
[layer removeAllAnimations];
}
switch (self.hoverState) {
case kHoverStateNone:
layer.shadowOffset = kNormalShadowOffset;
layer.shadowOpacity = kNormalShadowOpacity;
layer.shadowRadius = kNormalShadowRadius;
break;
case kHoverStateMouseOver:
case kHoverStateMouseDown:
layer.shadowOffset = kMouseOverShadowOffset;
layer.shadowOpacity = kMouseOverShadowOpacity;
layer.shadowRadius = kMouseOverShadowRadius;
break;
}
layer.borderColor =
(themeProvider && themeProvider->ShouldIncreaseContrast())
? CGColorGetConstantColor(darkTheme ? kCGColorWhite : kCGColorBlack)
: GetBorderColor(darkTheme).CGColor;
layer.backgroundColor =
GetBackgroundColor(self.hoverState, darkTheme).CGColor;
if (animated) {
[NSAnimationContext endGrouping];
}
}
// HoverButton overrides.
- (void)setHoverState:(HoverState)state {
if (state == hoverState_) {
return;
}
const BOOL animated =
state != kHoverStateMouseDown && hoverState_ != kHoverStateMouseDown;
hoverState_ = state;
[self updateHoverButtonAppearanceAnimated:animated];
}
// NSButton overrides.
- (void)setTitle:(NSString*)title {
NSColor* textColor;
if (const ui::ThemeProvider* themeProvider = [self.window themeProvider]) {
textColor = themeProvider->GetNSColor(ThemeProperties::COLOR_TAB_TEXT);
if (!themeProvider->ShouldIncreaseContrast())
textColor = [textColor colorWithAlphaComponent:kTextAlpha];
} else {
textColor = [NSColor controlTextColor];
}
NSFont* font;
if (@available(macOS 10.11, *)) {
font = [NSFont systemFontOfSize:kFontSize weight:NSFontWeightMedium];
} else {
font = [[NSFontManager sharedFontManager]
convertWeight:YES
ofFont:[NSFont systemFontOfSize:kFontSize]];
}
base::scoped_nsobject<NSMutableParagraphStyle> paragraphStyle(
[[NSParagraphStyle defaultParagraphStyle] mutableCopy]);
paragraphStyle.get().alignment = NSCenterTextAlignment;
base::scoped_nsobject<NSAttributedString> attributedTitle(
[[NSAttributedString alloc]
initWithString:title
attributes:@{
NSFontAttributeName : font,
NSForegroundColorAttributeName : textColor,
NSParagraphStyleAttributeName : paragraphStyle,
}]);
if (![self.attributedTitle isEqual:attributedTitle])
self.attributedTitle = attributedTitle;
}
// NSControl overrides.
- (void)sizeToFit {
NSSize size = self.attributedTitle.size;
size.width += kXPadding * 2;
size = [self backingAlignedRect:NSMakeRect(0, 0, size.width, size.height)
options:NSAlignAllEdgesOutward]
.size;
[self setFrameSize:NSMakeSize(size.width > kMinWidth ? size.width : kMinWidth,
kHeight)];
}
// NSView overrides.
- (void)layout {
CALayer* layer = self.layer;
layer.shadowPath =
base::ScopedCFTypeRef<CGPathRef>(CGPathCreateWithRoundedRect(
layer.bounds,
MIN(layer.cornerRadius, CGRectGetWidth(layer.bounds) / 2),
MIN(layer.cornerRadius, CGRectGetHeight(layer.bounds) / 2), nullptr));
[self updateHoverButtonAppearanceAnimated:NO];
self.title = self.title; // Match the theme.
[super layout];
}
- (void)drawFocusRingMask {
CGFloat radius = self.layer.cornerRadius;
[[NSBezierPath bezierPathWithRoundedRect:self.bounds
xRadius:radius
yRadius:radius] fill];
}
- (void)viewDidChangeBackingProperties {
[super viewDidChangeBackingProperties];
[self updateBorderWidth];
}
// This is undocumented. Without it, NSView sets `self.layer.masksToBounds =
// YES` whenever the view is resized, clipping the shadow.
- (BOOL)clipsToBounds {
return NO;
}
@end