blob: be39575384f7a3e574d141a78ce5f688064b3e27 [file] [log] [blame]
/*
* Copyright (C) 2008 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 "config.h"
#import "ThemeMac.h"
#import "BlockExceptions.h"
#import "GraphicsContext.h"
#import "LocalCurrentGraphicsContext.h"
#import "ScrollView.h"
#import "WebCoreSystemInterface.h"
#include <wtf/StdLibExtras.h>
using namespace std;
// FIXME: Default buttons really should be more like push buttons and not like buttons.
namespace WebCore {
enum {
topMargin,
rightMargin,
bottomMargin,
leftMargin
};
Theme* platformTheme()
{
DEFINE_STATIC_LOCAL(ThemeMac, themeMac, ());
return &themeMac;
}
// Helper functions used by a bunch of different control parts.
static NSControlSize controlSizeForFont(const Font& font)
{
int fontSize = font.pixelSize();
if (fontSize >= 16)
return NSRegularControlSize;
if (fontSize >= 11)
return NSSmallControlSize;
return NSMiniControlSize;
}
static LengthSize sizeFromFont(const Font& font, const LengthSize& zoomedSize, float zoomFactor, const IntSize* sizes)
{
IntSize controlSize = sizes[controlSizeForFont(font)];
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 void setControlSize(NSCell* cell, const IntSize* sizes, const IntSize& minZoomedSize, float zoomFactor)
{
NSControlSize size;
if (minZoomedSize.width() >= static_cast<int>(sizes[NSRegularControlSize].width() * zoomFactor) &&
minZoomedSize.height() >= static_cast<int>(sizes[NSRegularControlSize].height() * zoomFactor))
size = NSRegularControlSize;
else if (minZoomedSize.width() >= static_cast<int>(sizes[NSSmallControlSize].width() * zoomFactor) &&
minZoomedSize.height() >= static_cast<int>(sizes[NSSmallControlSize].height() * zoomFactor))
size = NSSmallControlSize;
else
size = NSMiniControlSize;
if (size != [cell controlSize]) // Only update if we have to, since AppKit does work even if the size is the same.
[cell setControlSize:size];
}
static void updateStates(NSCell* cell, ControlStates states)
{
// Hover state is not supported by Aqua.
// Pressed state
bool oldPressed = [cell isHighlighted];
bool pressed = states & PressedState;
if (pressed != oldPressed)
[cell setHighlighted:pressed];
// Enabled state
bool oldEnabled = [cell isEnabled];
bool enabled = states & EnabledState;
if (enabled != oldEnabled)
[cell setEnabled:enabled];
// Focused state
bool oldFocused = [cell showsFirstResponder];
bool focused = states & FocusState;
if (focused != oldFocused)
[cell setShowsFirstResponder:focused];
// Checked and Indeterminate
bool oldIndeterminate = [cell state] == NSMixedState;
bool indeterminate = (states & IndeterminateState);
bool checked = states & CheckedState;
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.
}
static IntRect 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;
}
// Checkboxes
static const IntSize* checkboxSizes()
{
static const IntSize sizes[3] = { IntSize(14, 14), IntSize(12, 12), IntSize(10, 10) };
return sizes;
}
static const int* checkboxMargins(NSControlSize controlSize)
{
static const int margins[3][4] =
{
{ 3, 4, 4, 2 },
{ 4, 3, 3, 3 },
{ 4, 3, 3, 3 },
};
return margins[controlSize];
}
static LengthSize checkboxSize(const Font& font, 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(font, zoomedSize, zoomFactor, checkboxSizes());
}
static NSButtonCell *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;
}
// FIXME: Share more code with radio buttons.
static void paintCheckbox(ControlStates states, GraphicsContext* context, const IntRect& zoomedRect, float zoomFactor, ScrollView* scrollView)
{
BEGIN_BLOCK_OBJC_EXCEPTIONS
// Determine the width and height needed for the control and prepare the cell for painting.
NSButtonCell *checkboxCell = checkbox(states, zoomedRect, zoomFactor);
context->save();
NSControlSize controlSize = [checkboxCell controlSize];
IntSize zoomedSize = checkboxSizes()[controlSize];
zoomedSize.setWidth(zoomedSize.width() * zoomFactor);
zoomedSize.setHeight(zoomedSize.height() * zoomFactor);
IntRect inflatedRect = inflateRect(zoomedRect, zoomedSize, checkboxMargins(controlSize), zoomFactor);
if (zoomFactor != 1.0f) {
inflatedRect.setWidth(inflatedRect.width() / zoomFactor);
inflatedRect.setHeight(inflatedRect.height() / zoomFactor);
context->translate(inflatedRect.x(), inflatedRect.y());
context->scale(FloatSize(zoomFactor, zoomFactor));
context->translate(-inflatedRect.x(), -inflatedRect.y());
}
[checkboxCell drawWithFrame:NSRect(inflatedRect) inView:scrollView->documentView()];
[checkboxCell setControlView:nil];
context->restore();
END_BLOCK_OBJC_EXCEPTIONS
}
// Radio Buttons
static const IntSize* radioSizes()
{
static const IntSize sizes[3] = { IntSize(14, 15), IntSize(12, 13), IntSize(10, 10) };
return sizes;
}
static const int* radioMargins(NSControlSize controlSize)
{
static const int margins[3][4] =
{
{ 2, 2, 4, 2 },
{ 3, 2, 3, 2 },
{ 1, 0, 2, 0 },
};
return margins[controlSize];
}
static LengthSize radioSize(const Font& font, 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(font, zoomedSize, zoomFactor, radioSizes());
}
static NSButtonCell *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.
updateStates(radioCell, states);
return radioCell;
}
static void paintRadio(ControlStates states, GraphicsContext* context, const IntRect& zoomedRect, float zoomFactor, ScrollView* scrollView)
{
// Determine the width and height needed for the control and prepare the cell for painting.
NSButtonCell *radioCell = radio(states, zoomedRect, zoomFactor);
context->save();
NSControlSize controlSize = [radioCell controlSize];
IntSize zoomedSize = radioSizes()[controlSize];
zoomedSize.setWidth(zoomedSize.width() * zoomFactor);
zoomedSize.setHeight(zoomedSize.height() * zoomFactor);
IntRect inflatedRect = inflateRect(zoomedRect, zoomedSize, radioMargins(controlSize), zoomFactor);
if (zoomFactor != 1.0f) {
inflatedRect.setWidth(inflatedRect.width() / zoomFactor);
inflatedRect.setHeight(inflatedRect.height() / zoomFactor);
context->translate(inflatedRect.x(), inflatedRect.y());
context->scale(FloatSize(zoomFactor, zoomFactor));
context->translate(-inflatedRect.x(), -inflatedRect.y());
}
BEGIN_BLOCK_OBJC_EXCEPTIONS
[radioCell drawWithFrame:NSRect(inflatedRect) inView:scrollView->documentView()];
[radioCell setControlView:nil];
END_BLOCK_OBJC_EXCEPTIONS
context->restore();
}
// Buttons
// Buttons really only constrain height. They respect width.
static const IntSize* buttonSizes()
{
static const IntSize sizes[3] = { IntSize(0, 21), IntSize(0, 18), IntSize(0, 15) };
return sizes;
}
#if ENABLE(DATALIST)
static const IntSize* listButtonSizes()
{
static const IntSize sizes[3] = { IntSize(21, 21), IntSize(19, 18), IntSize(17, 16) };
return sizes;
}
#endif
static const int* 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 *&buttonCell, ControlPart part, ControlStates states, const IntRect& zoomedRect, float zoomFactor)
{
if (!buttonCell) {
buttonCell = [[NSButtonCell alloc] init];
[buttonCell setTitle:nil];
[buttonCell setButtonType:NSMomentaryPushInButton];
if (states & DefaultState)
[buttonCell setKeyEquivalent:@"\r"];
}
// Set the control size based off the rectangle we're painting into.
const IntSize* sizes = buttonSizes();
#if ENABLE(DATALIST)
if (part == ListButtonPart) {
[buttonCell setBezelStyle:NSRoundedDisclosureBezelStyle];
sizes = listButtonSizes();
} else
#endif
if (part == SquareButtonPart || zoomedRect.height() > buttonSizes()[NSRegularControlSize].height() * zoomFactor) {
// Use the square button
if ([buttonCell bezelStyle] != NSShadowlessSquareBezelStyle)
[buttonCell setBezelStyle:NSShadowlessSquareBezelStyle];
} else if ([buttonCell bezelStyle] != NSRoundedBezelStyle)
[buttonCell setBezelStyle:NSRoundedBezelStyle];
setControlSize(buttonCell, sizes, zoomedRect.size(), zoomFactor);
// Update the various states we respond to.
updateStates(buttonCell, states);
}
static NSButtonCell *button(ControlPart part, ControlStates states, const IntRect& zoomedRect, float zoomFactor)
{
bool isDefault = states & DefaultState;
static NSButtonCell *cells[2];
setupButtonCell(cells[isDefault], part, states, zoomedRect, zoomFactor);
return cells[isDefault];
}
static void paintButton(ControlPart part, ControlStates states, GraphicsContext* context, const IntRect& zoomedRect, float zoomFactor, ScrollView* scrollView)
{
BEGIN_BLOCK_OBJC_EXCEPTIONS
// Determine the width and height needed for the control and prepare the cell for painting.
NSButtonCell *buttonCell = button(part, states, zoomedRect, zoomFactor);
LocalCurrentGraphicsContext localContext(context);
NSControlSize controlSize = [buttonCell controlSize];
#if ENABLE(DATALIST)
IntSize zoomedSize = (part == ListButtonPart ? listButtonSizes() : buttonSizes())[controlSize];
#else
IntSize zoomedSize = buttonSizes()[controlSize];
#endif
zoomedSize.setWidth(zoomedRect.width()); // Buttons don't ever constrain width, so the zoomed width can just be honored.
zoomedSize.setHeight(zoomedSize.height() * zoomFactor);
IntRect inflatedRect = zoomedRect;
if ([buttonCell bezelStyle] == NSRoundedBezelStyle) {
// Center the button within the available space.
if (inflatedRect.height() > zoomedSize.height()) {
inflatedRect.setY(inflatedRect.y() + (inflatedRect.height() - zoomedSize.height()) / 2);
inflatedRect.setHeight(zoomedSize.height());
}
// Now inflate it to account for the shadow.
inflatedRect = inflateRect(inflatedRect, zoomedSize, buttonMargins(controlSize), zoomFactor);
if (zoomFactor != 1.0f) {
inflatedRect.setWidth(inflatedRect.width() / zoomFactor);
inflatedRect.setHeight(inflatedRect.height() / zoomFactor);
context->translate(inflatedRect.x(), inflatedRect.y());
context->scale(FloatSize(zoomFactor, zoomFactor));
context->translate(-inflatedRect.x(), -inflatedRect.y());
}
}
NSView *view = scrollView->documentView();
NSWindow *window = [view window];
NSButtonCell *previousDefaultButtonCell = [window defaultButtonCell];
if (states & DefaultState) {
[window setDefaultButtonCell:buttonCell];
wkAdvanceDefaultButtonPulseAnimation(buttonCell);
} else if ([previousDefaultButtonCell isEqual:buttonCell])
[window setDefaultButtonCell:nil];
if (!view) {
context->save();
context->translate(inflatedRect.x(), inflatedRect.y());
context->scale(FloatSize(1, -1));
context->translate(0, -inflatedRect.height());
inflatedRect.setLocation(IntPoint());
}
[buttonCell drawWithFrame:NSRect(inflatedRect) inView:view];
[buttonCell setControlView:nil];
if (!view)
context->restore();
if (![previousDefaultButtonCell isEqual:buttonCell])
[window setDefaultButtonCell:previousDefaultButtonCell];
END_BLOCK_OBJC_EXCEPTIONS
}
// 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 Font& font, float zoomFactor) const
{
switch (part) {
case PushButtonPart: {
FontDescription fontDescription;
fontDescription.setIsAbsoluteSize(true);
fontDescription.setGenericFamily(FontDescription::SerifFamily);
NSFont* nsFont = [NSFont systemFontOfSize:[NSFont systemFontSizeForControlSize:controlSizeForFont(font)]];
fontDescription.firstFamily().setFamily([nsFont familyName]);
fontDescription.setComputedSize([nsFont pointSize] * zoomFactor);
fontDescription.setSpecifiedSize([nsFont pointSize] * zoomFactor);
return fontDescription;
}
default:
return Theme::controlFont(part, font, zoomFactor);
}
}
LengthSize ThemeMac::controlSize(ControlPart part, const Font& font, const LengthSize& zoomedSize, float zoomFactor) const
{
switch (part) {
case CheckboxPart:
return checkboxSize(font, zoomedSize, zoomFactor);
case RadioPart:
return radioSize(font, zoomedSize, zoomFactor);
case PushButtonPart:
// Height is reset to auto so that specified heights can be ignored.
return sizeFromFont(font, LengthSize(zoomedSize.width(), Length()), zoomFactor, buttonSizes());
#if ENABLE(DATALIST)
case ListButtonPart:
return sizeFromFont(font, LengthSize(zoomedSize.width(), Length()), zoomFactor, listButtonSizes());
#endif
default:
return zoomedSize;
}
}
LengthSize ThemeMac::minimumControlSize(ControlPart part, const Font& font, float zoomFactor) const
{
switch (part) {
case SquareButtonPart:
case DefaultButtonPart:
case ButtonPart:
case ListButtonPart:
return LengthSize(Length(0, Fixed), Length(static_cast<int>(15 * zoomFactor), Fixed));
default:
return Theme::minimumControlSize(part, font, zoomFactor);
}
}
LengthBox ThemeMac::controlBorder(ControlPart part, const Font& font, const LengthBox& zoomedBox, float zoomFactor) const
{
switch (part) {
case SquareButtonPart:
case DefaultButtonPart:
case ButtonPart:
case ListButtonPart:
return LengthBox(0, zoomedBox.right().value(), 0, zoomedBox.left().value());
default:
return Theme::controlBorder(part, font, zoomedBox, zoomFactor);
}
}
LengthBox ThemeMac::controlPadding(ControlPart part, const Font& font, 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(0, padding, 0, padding);
}
default:
return Theme::controlPadding(part, font, zoomedBox, zoomFactor);
}
}
void ThemeMac::inflateControlPaintRect(ControlPart part, ControlStates states, IntRect& zoomedRect, float zoomFactor) 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 DefaultButtonPart:
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);
zoomedSize.setWidth(zoomedRect.width()); // Buttons don't ever constrain width, so the zoomed width can just be honored.
zoomedRect = inflateRect(zoomedRect, zoomedSize, buttonMargins(controlSize), zoomFactor);
}
break;
}
default:
break;
}
END_BLOCK_OBJC_EXCEPTIONS
}
void ThemeMac::paint(ControlPart part, ControlStates states, GraphicsContext* context, const IntRect& zoomedRect, float zoomFactor, ScrollView* scrollView) const
{
switch (part) {
case CheckboxPart:
paintCheckbox(states, context, zoomedRect, zoomFactor, scrollView);
break;
case RadioPart:
paintRadio(states, context, zoomedRect, zoomFactor, scrollView);
break;
case PushButtonPart:
case DefaultButtonPart:
case ButtonPart:
case SquareButtonPart:
case ListButtonPart:
paintButton(part, states, context, zoomedRect, zoomFactor, scrollView);
break;
default:
break;
}
}
}