blob: aedd5baaad7432d41d8a44413cdb2fda2730ff14 [file] [log] [blame]
/*
* Copyright (C) 2008, 2010, 2011, 2012 Apple Inc. All Rights Reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
* OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#import "platform/mac/ThemeMac.h"
#import <Carbon/Carbon.h>
#import "platform/graphics/GraphicsContextStateSaver.h"
#import "platform/mac/BlockExceptions.h"
#import "platform/mac/LocalCurrentGraphicsContext.h"
#import "platform/mac/VersionUtilMac.h"
#import "platform/mac/WebCoreNSCellExtras.h"
#import "platform/scroll/ScrollableArea.h"
#include "wtf/StdLibExtras.h"
// This is a view whose sole purpose is to tell AppKit that it's flipped.
@interface BlinkFlippedControl : NSControl
@end
@implementation BlinkFlippedControl
- (BOOL)isFlipped {
return YES;
}
- (NSText*)currentEditor {
return nil;
}
- (BOOL)_automaticFocusRingDisabled {
return YES;
}
@end
namespace blink {
Theme* platformTheme() {
DEFINE_STATIC_LOCAL(ThemeMac, themeMac, ());
return &themeMac;
}
// Helper functions used by a bunch of different control parts.
static NSControlSize controlSizeForFont(
const FontDescription& fontDescription) {
int fontSize = fontDescription.computedPixelSize();
if (fontSize >= 16)
return NSRegularControlSize;
if (fontSize >= 11)
return NSSmallControlSize;
return NSMiniControlSize;
}
static LengthSize sizeFromNSControlSize(NSControlSize nsControlSize,
const LengthSize& zoomedSize,
float zoomFactor,
const IntSize* sizes) {
IntSize controlSize = sizes[nsControlSize];
if (zoomFactor != 1.0f)
controlSize = IntSize(controlSize.width() * zoomFactor,
controlSize.height() * zoomFactor);
LengthSize result = zoomedSize;
if (zoomedSize.width().isIntrinsicOrAuto() && controlSize.width() > 0)
result.setWidth(Length(controlSize.width(), Fixed));
if (zoomedSize.height().isIntrinsicOrAuto() && controlSize.height() > 0)
result.setHeight(Length(controlSize.height(), Fixed));
return result;
}
static LengthSize sizeFromFont(const FontDescription& fontDescription,
const LengthSize& zoomedSize,
float zoomFactor,
const IntSize* sizes) {
return sizeFromNSControlSize(controlSizeForFont(fontDescription), zoomedSize,
zoomFactor, sizes);
}
NSControlSize ThemeMac::controlSizeFromPixelSize(const IntSize* sizes,
const IntSize& minZoomedSize,
float zoomFactor) {
if (minZoomedSize.width() >=
static_cast<int>(sizes[NSRegularControlSize].width() * zoomFactor) &&
minZoomedSize.height() >=
static_cast<int>(sizes[NSRegularControlSize].height() * zoomFactor))
return NSRegularControlSize;
if (minZoomedSize.width() >=
static_cast<int>(sizes[NSSmallControlSize].width() * zoomFactor) &&
minZoomedSize.height() >=
static_cast<int>(sizes[NSSmallControlSize].height() * zoomFactor))
return NSSmallControlSize;
return NSMiniControlSize;
}
static void setControlSize(NSCell* cell,
const IntSize* sizes,
const IntSize& minZoomedSize,
float zoomFactor) {
ControlSize size =
ThemeMac::controlSizeFromPixelSize(sizes, minZoomedSize, zoomFactor);
// Only update if we have to, since AppKit does work even if the size is the
// same.
if (size != [cell controlSize])
[cell setControlSize:(NSControlSize)size];
}
static void updateStates(NSCell* cell, ControlStates states) {
// Hover state is not supported by Aqua.
// Pressed state
bool oldPressed = [cell isHighlighted];
bool pressed = states & PressedControlState;
if (pressed != oldPressed)
[cell setHighlighted:pressed];
// Enabled state
bool oldEnabled = [cell isEnabled];
bool enabled = states & EnabledControlState;
if (enabled != oldEnabled)
[cell setEnabled:enabled];
// Checked and Indeterminate
bool oldIndeterminate = [cell state] == NSMixedState;
bool indeterminate = (states & IndeterminateControlState);
bool checked = states & CheckedControlState;
bool oldChecked = [cell state] == NSOnState;
if (oldIndeterminate != indeterminate || checked != oldChecked)
[cell setState:indeterminate ? NSMixedState
: (checked ? NSOnState : NSOffState)];
// Window inactive state does not need to be checked explicitly, since we
// paint parented to a view in a window whose key state can be detected.
}
// Return a fake NSView whose sole purpose is to tell AppKit that it's flipped.
NSView* ThemeMac::ensuredView(ScrollableArea* scrollableArea) {
// Use a fake flipped view.
static NSView* flippedView = [[BlinkFlippedControl alloc] init];
[flippedView setFrameSize:NSSizeFromCGSize(scrollableArea->contentsSize())];
return flippedView;
}
// static
IntRect ThemeMac::inflateRect(const IntRect& zoomedRect,
const IntSize& zoomedSize,
const int* margins,
float zoomFactor) {
// Only do the inflation if the available width/height are too small.
// Otherwise try to fit the glow/check space into the available box's
// width/height.
int widthDelta = zoomedRect.width() -
(zoomedSize.width() + margins[LeftMargin] * zoomFactor +
margins[RightMargin] * zoomFactor);
int heightDelta = zoomedRect.height() -
(zoomedSize.height() + margins[TopMargin] * zoomFactor +
margins[BottomMargin] * zoomFactor);
IntRect result(zoomedRect);
if (widthDelta < 0) {
result.setX(result.x() - margins[LeftMargin] * zoomFactor);
result.setWidth(result.width() - widthDelta);
}
if (heightDelta < 0) {
result.setY(result.y() - margins[TopMargin] * zoomFactor);
result.setHeight(result.height() - heightDelta);
}
return result;
}
// static
IntRect ThemeMac::inflateRectForAA(const IntRect& rect) {
const int margin = 2;
return IntRect(rect.x() - margin, rect.y() - margin,
rect.width() + 2 * margin, rect.height() + 2 * margin);
}
// static
IntRect ThemeMac::inflateRectForFocusRing(const IntRect& rect) {
// Just put a margin of 16 units around the rect. The UI elements that use
// this don't appropriately scale their focus rings appropriately (e.g, paint
// pickers), or switch to non-native widgets when scaled (e.g, check boxes
// and radio buttons).
const int margin = 16;
IntRect result;
result.setX(rect.x() - margin);
result.setY(rect.y() - margin);
result.setWidth(rect.width() + 2 * margin);
result.setHeight(rect.height() + 2 * margin);
return result;
}
// Checkboxes
const IntSize* ThemeMac::checkboxSizes() {
static const IntSize sizes[3] = {IntSize(14, 14), IntSize(12, 12),
IntSize(10, 10)};
return sizes;
}
const int* ThemeMac::checkboxMargins(NSControlSize controlSize) {
static const int margins[3][4] = {
{3, 4, 4, 2}, {4, 3, 3, 3}, {4, 3, 3, 3},
};
return margins[controlSize];
}
LengthSize ThemeMac::checkboxSize(const FontDescription& fontDescription,
const LengthSize& zoomedSize,
float zoomFactor) {
// If the width and height are both specified, then we have nothing to do.
if (!zoomedSize.width().isIntrinsicOrAuto() &&
!zoomedSize.height().isIntrinsicOrAuto())
return zoomedSize;
// Use the font size to determine the intrinsic width of the control.
return sizeFromFont(fontDescription, zoomedSize, zoomFactor, checkboxSizes());
}
NSButtonCell* ThemeMac::checkbox(ControlStates states,
const IntRect& zoomedRect,
float zoomFactor) {
static NSButtonCell* checkboxCell;
if (!checkboxCell) {
checkboxCell = [[NSButtonCell alloc] init];
[checkboxCell setButtonType:NSSwitchButton];
[checkboxCell setTitle:nil];
[checkboxCell setAllowsMixedState:YES];
[checkboxCell setFocusRingType:NSFocusRingTypeExterior];
}
// Set the control size based off the rectangle we're painting into.
setControlSize(checkboxCell, checkboxSizes(), zoomedRect.size(), zoomFactor);
// Update the various states we respond to.
updateStates(checkboxCell, states);
return checkboxCell;
}
const IntSize* ThemeMac::radioSizes() {
static const IntSize sizes[3] = {IntSize(14, 15), IntSize(12, 13),
IntSize(10, 10)};
return sizes;
}
const int* ThemeMac::radioMargins(NSControlSize controlSize) {
static const int margins[3][4] = {
{2, 2, 4, 2}, {3, 2, 3, 2}, {1, 0, 2, 0},
};
return margins[controlSize];
}
LengthSize ThemeMac::radioSize(const FontDescription& fontDescription,
const LengthSize& zoomedSize,
float zoomFactor) {
// If the width and height are both specified, then we have nothing to do.
if (!zoomedSize.width().isIntrinsicOrAuto() &&
!zoomedSize.height().isIntrinsicOrAuto())
return zoomedSize;
// Use the font size to determine the intrinsic width of the control.
return sizeFromFont(fontDescription, zoomedSize, zoomFactor, radioSizes());
}
NSButtonCell* ThemeMac::radio(ControlStates states,
const IntRect& zoomedRect,
float zoomFactor) {
static NSButtonCell* radioCell;
if (!radioCell) {
radioCell = [[NSButtonCell alloc] init];
[radioCell setButtonType:NSRadioButton];
[radioCell setTitle:nil];
[radioCell setFocusRingType:NSFocusRingTypeExterior];
}
// Set the control size based off the rectangle we're painting into.
setControlSize(radioCell, radioSizes(), zoomedRect.size(), zoomFactor);
// Update the various states we respond to.
// Cocoa draws NSMixedState NSRadioButton as NSOnState so we don't want that.
states &= ~IndeterminateControlState;
updateStates(radioCell, states);
return radioCell;
}
// Buttons really only constrain height. They respect width.
const IntSize* ThemeMac::buttonSizes() {
static const IntSize sizes[3] = {IntSize(0, 21), IntSize(0, 18),
IntSize(0, 15)};
return sizes;
}
const int* ThemeMac::buttonMargins(NSControlSize controlSize) {
static const int margins[3][4] = {
{4, 6, 7, 6}, {4, 5, 6, 5}, {0, 1, 1, 1},
};
return margins[controlSize];
}
static void setUpButtonCell(NSButtonCell* cell,
ControlPart part,
ControlStates states,
const IntRect& zoomedRect,
float zoomFactor) {
// Set the control size based off the rectangle we're painting into.
const IntSize* sizes = ThemeMac::buttonSizes();
if (part == SquareButtonPart ||
zoomedRect.height() >
ThemeMac::buttonSizes()[NSRegularControlSize].height() * zoomFactor) {
// Use the square button
if ([cell bezelStyle] != NSShadowlessSquareBezelStyle)
[cell setBezelStyle:NSShadowlessSquareBezelStyle];
} else if ([cell bezelStyle] != NSRoundedBezelStyle)
[cell setBezelStyle:NSRoundedBezelStyle];
setControlSize(cell, sizes, zoomedRect.size(), zoomFactor);
// Update the various states we respond to.
updateStates(cell, states);
}
NSButtonCell* ThemeMac::button(ControlPart part,
ControlStates states,
const IntRect& zoomedRect,
float zoomFactor) {
static NSButtonCell* cell = nil;
if (!cell) {
cell = [[NSButtonCell alloc] init];
[cell setTitle:nil];
[cell setButtonType:NSMomentaryPushInButton];
}
setUpButtonCell(cell, part, states, zoomedRect, zoomFactor);
return cell;
}
const IntSize* ThemeMac::stepperSizes() {
static const IntSize sizes[3] = {IntSize(19, 27), IntSize(15, 22),
IntSize(13, 15)};
return sizes;
}
// We don't use controlSizeForFont() for steppers because the stepper height
// should be equal to or less than the corresponding text field height,
static NSControlSize stepperControlSizeForFont(
const FontDescription& fontDescription) {
int fontSize = fontDescription.computedPixelSize();
if (fontSize >= 27)
return NSRegularControlSize;
if (fontSize >= 22)
return NSSmallControlSize;
return NSMiniControlSize;
}
// Theme overrides
int ThemeMac::baselinePositionAdjustment(ControlPart part) const {
if (part == CheckboxPart || part == RadioPart)
return -2;
return Theme::baselinePositionAdjustment(part);
}
FontDescription ThemeMac::controlFont(ControlPart part,
const FontDescription& fontDescription,
float zoomFactor) const {
switch (part) {
case PushButtonPart: {
FontDescription result;
result.setIsAbsoluteSize(true);
result.setGenericFamily(FontDescription::SerifFamily);
NSFont* nsFont = [NSFont
systemFontOfSize:[NSFont systemFontSizeForControlSize:
controlSizeForFont(fontDescription)]];
result.firstFamily().setFamily(@"BlinkMacSystemFont");
result.setComputedSize([nsFont pointSize] * zoomFactor);
result.setSpecifiedSize([nsFont pointSize] * zoomFactor);
return result;
}
default:
return Theme::controlFont(part, fontDescription, zoomFactor);
}
}
LengthSize ThemeMac::controlSize(ControlPart part,
const FontDescription& fontDescription,
const LengthSize& zoomedSize,
float zoomFactor) const {
switch (part) {
case CheckboxPart:
return checkboxSize(fontDescription, zoomedSize, zoomFactor);
case RadioPart:
return radioSize(fontDescription, zoomedSize, zoomFactor);
case PushButtonPart:
// Height is reset to auto so that specified heights can be ignored.
return sizeFromFont(fontDescription,
LengthSize(zoomedSize.width(), Length()), zoomFactor,
buttonSizes());
case InnerSpinButtonPart:
if (!zoomedSize.width().isIntrinsicOrAuto() &&
!zoomedSize.height().isIntrinsicOrAuto())
return zoomedSize;
return sizeFromNSControlSize(stepperControlSizeForFont(fontDescription),
zoomedSize, zoomFactor, stepperSizes());
default:
return zoomedSize;
}
}
LengthSize ThemeMac::minimumControlSize(ControlPart part,
const FontDescription& fontDescription,
float zoomFactor) const {
switch (part) {
case SquareButtonPart:
case ButtonPart:
return LengthSize(Length(0, Fixed),
Length(static_cast<int>(15 * zoomFactor), Fixed));
case InnerSpinButtonPart: {
IntSize base = stepperSizes()[NSMiniControlSize];
return LengthSize(
Length(static_cast<int>(base.width() * zoomFactor), Fixed),
Length(static_cast<int>(base.height() * zoomFactor), Fixed));
}
default:
return Theme::minimumControlSize(part, fontDescription, zoomFactor);
}
}
LengthBox ThemeMac::controlBorder(ControlPart part,
const FontDescription& fontDescription,
const LengthBox& zoomedBox,
float zoomFactor) const {
switch (part) {
case SquareButtonPart:
case ButtonPart:
return LengthBox(0, zoomedBox.right().value(), 0,
zoomedBox.left().value());
default:
return Theme::controlBorder(part, fontDescription, zoomedBox, zoomFactor);
}
}
LengthBox ThemeMac::controlPadding(ControlPart part,
const FontDescription& fontDescription,
const LengthBox& zoomedBox,
float zoomFactor) const {
switch (part) {
case PushButtonPart: {
// Just use 8px. AppKit wants to use 11px for mini buttons, but that
// padding is just too large for real-world Web sites (creating a huge
// necessary minimum width for buttons whose space is by definition
// constrained, since we select mini only for small cramped environments.
// This also guarantees the HTML <button> will match our rendering by
// default, since we're using a consistent padding.
const int padding = 8 * zoomFactor;
return LengthBox(2, padding, 3, padding);
}
default:
return Theme::controlPadding(part, fontDescription, zoomedBox,
zoomFactor);
}
}
void ThemeMac::addVisualOverflow(ControlPart part,
ControlStates states,
float zoomFactor,
IntRect& zoomedRect) const {
BEGIN_BLOCK_OBJC_EXCEPTIONS
switch (part) {
case CheckboxPart: {
// We inflate the rect as needed to account for padding included in the
// cell to accommodate the checkbox shadow" and the check. We don't
// consider this part of the bounds of the control in WebKit.
NSCell* cell = checkbox(states, zoomedRect, zoomFactor);
NSControlSize controlSize = [cell controlSize];
IntSize zoomedSize = checkboxSizes()[controlSize];
zoomedSize.setHeight(zoomedSize.height() * zoomFactor);
zoomedSize.setWidth(zoomedSize.width() * zoomFactor);
zoomedRect = inflateRect(zoomedRect, zoomedSize,
checkboxMargins(controlSize), zoomFactor);
break;
}
case RadioPart: {
// We inflate the rect as needed to account for padding included in the
// cell to accommodate the radio button shadow". We don't consider this
// part of the bounds of the control in WebKit.
NSCell* cell = radio(states, zoomedRect, zoomFactor);
NSControlSize controlSize = [cell controlSize];
IntSize zoomedSize = radioSizes()[controlSize];
zoomedSize.setHeight(zoomedSize.height() * zoomFactor);
zoomedSize.setWidth(zoomedSize.width() * zoomFactor);
zoomedRect = inflateRect(zoomedRect, zoomedSize,
radioMargins(controlSize), zoomFactor);
break;
}
case PushButtonPart:
case ButtonPart: {
NSButtonCell* cell = button(part, states, zoomedRect, zoomFactor);
NSControlSize controlSize = [cell controlSize];
// We inflate the rect as needed to account for the Aqua button's shadow.
if ([cell bezelStyle] == NSRoundedBezelStyle) {
IntSize zoomedSize = buttonSizes()[controlSize];
zoomedSize.setHeight(zoomedSize.height() * zoomFactor);
// Buttons don't ever constrain width, so the zoomed width can just be
// honored.
zoomedSize.setWidth(zoomedRect.width());
zoomedRect = inflateRect(zoomedRect, zoomedSize,
buttonMargins(controlSize), zoomFactor);
}
break;
}
case InnerSpinButtonPart: {
static const int stepperMargin[4] = {0, 0, 0, 0};
ControlSize controlSize = controlSizeFromPixelSize(
stepperSizes(), zoomedRect.size(), zoomFactor);
IntSize zoomedSize = stepperSizes()[controlSize];
zoomedSize.setHeight(zoomedSize.height() * zoomFactor);
zoomedSize.setWidth(zoomedSize.width() * zoomFactor);
zoomedRect =
inflateRect(zoomedRect, zoomedSize, stepperMargin, zoomFactor);
break;
}
default:
break;
}
END_BLOCK_OBJC_EXCEPTIONS
}
}