blob: 27ea84870a54cbe4d065bdca99d072fec8d46ca6 [file] [log] [blame]
/*
* This file is part of the theme implementation for form controls in WebCore.
*
* Copyright (C) 2005, 2006 Apple Computer, Inc.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Library General Public
* License as published by the Free Software Foundation; either
* version 2 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Library General Public License for more details.
*
* You should have received a copy of the GNU Library General Public License
* along with this library; see the file COPYING.LIB. If not, write to
* the Free Software Foundation, Inc., 59 Temple Place - Suite 330,
* Boston, MA 02111-1307, USA.
*/
#import "config.h"
#import "render_theme_mac.h"
#import "DocumentImpl.h"
#import "FrameView.h"
#import "FoundationExtras.h"
#import "cssstyleselector.h"
#import "dom_elementimpl.h"
#import "Font.h"
#import "render_canvas.h"
#import "render_style.h"
#import "WebCoreGraphicsBridge.h"
// The methods in this file are specific to the Mac OS X platform.
namespace WebCore {
enum {
topMargin,
rightMargin,
bottomMargin,
leftMargin
};
RenderTheme* theme()
{
static RenderThemeMac macTheme;
return &macTheme;
}
RenderThemeMac::RenderThemeMac()
: checkbox(nil)
, radio(nil)
, button(nil)
, textField(nil)
{
}
void RenderThemeMac::adjustRepaintRect(const RenderObject* o, IntRect& r)
{
switch (o->style()->appearance()) {
case CheckboxAppearance: {
// Since we query the prototype cell, we need to update its state to match.
setCheckboxCellState(o, r);
// 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.
r = inflateRect(r, checkboxSizes()[[checkbox controlSize]], checkboxMargins());
break;
}
case RadioAppearance: {
// Since we query the prototype cell, we need to update its state to match.
setRadioCellState(o, r);
// 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.
r = inflateRect(r, radioSizes()[[radio controlSize]], radioMargins());
break;
}
case PushButtonAppearance:
case ButtonAppearance: {
// Since we query the prototype cell, we need to update its state to match.
setButtonCellState(o, r);
// 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.
if ([button bezelStyle] == NSRoundedBezelStyle)
r = inflateRect(r, buttonSizes()[[button controlSize]], buttonMargins());
break;
}
case TextFieldAppearance:
// Since we query the prototype cell, we need to update its state to match.
setTextFieldCellState(o, r);
break;
default:
break;
}
}
IntRect RenderThemeMac::inflateRect(const IntRect& r, const IntSize& size, const int* margins) const
{
// 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 = r.width() - (size.width() + margins[leftMargin] + margins[rightMargin]);
int heightDelta = r.height() - (size.height() + margins[topMargin] + margins[bottomMargin]);
IntRect result(r);
if (widthDelta < 0) {
result.setX(result.x() - margins[leftMargin]);
result.setWidth(result.width() - widthDelta);
}
if (heightDelta < 0) {
result.setY(result.y() - margins[topMargin]);
result.setHeight(result.height() - heightDelta);
}
return result;
}
void RenderThemeMac::updateCheckedState(NSCell* cell, const RenderObject* o)
{
bool oldIndeterminate = [cell state] == NSMixedState;
bool indeterminate = isIndeterminate(o);
bool checked = isChecked(o);
if (oldIndeterminate != indeterminate) {
[cell setState:indeterminate ? NSMixedState : (checked ? NSOnState : NSOffState)];
return;
}
bool oldChecked = [cell state] == NSOnState;
if (checked != oldChecked)
[cell setState:checked ? NSOnState : NSOffState];
}
void RenderThemeMac::updateEnabledState(NSCell* cell, const RenderObject* o)
{
bool oldEnabled = [cell isEnabled];
bool enabled = isEnabled(o);
if (enabled != oldEnabled)
[cell setEnabled:enabled];
}
void RenderThemeMac::updateFocusedState(NSCell* cell, const RenderObject* o)
{
// FIXME: Need to add a key window test here, or the element will look
// focused even when in the background.
bool oldFocused = [cell showsFirstResponder];
bool focused = (o->element() && o->document()->focusNode() == o->element()) && (o->style()->outlineStyleIsAuto());
if (focused != oldFocused)
[cell setShowsFirstResponder:focused];
}
void RenderThemeMac::updatePressedState(NSCell* cell, const RenderObject* o)
{
bool oldPressed = [cell isHighlighted];
bool pressed = (o->element() && o->element()->active());
if (pressed != oldPressed)
[cell setHighlighted:pressed];
}
short RenderThemeMac::baselinePosition(const RenderObject* o) const
{
if (o->style()->appearance() == CheckboxAppearance || o->style()->appearance() == RadioAppearance)
return o->marginTop() + o->height() - 2; // The baseline is 2px up from the bottom of the checkbox/radio in AppKit.
return RenderTheme::baselinePosition(o);
}
bool RenderThemeMac::controlSupportsTints(const RenderObject* o) const
{
if (!isEnabled(o))
return false;
// Checkboxes only have tint when checked.
if (o->style()->appearance() == CheckboxAppearance)
return isChecked(o);
// For now assume other controls have tint if enabled.
return true;
}
NSControlSize RenderThemeMac::controlSizeForFont(RenderStyle* style) const
{
int fontSize = style->fontSize();
if (fontSize >= 16)
return NSRegularControlSize;
if (fontSize >= 11)
return NSSmallControlSize;
return NSMiniControlSize;
}
void RenderThemeMac::setControlSize(NSCell* cell, const IntSize* sizes, const IntSize& minSize)
{
NSControlSize size;
if (minSize.width() >= sizes[NSRegularControlSize].width() &&
minSize.height() >= sizes[NSRegularControlSize].height())
size = NSRegularControlSize;
else if (minSize.width() >= sizes[NSSmallControlSize].width() &&
minSize.height() >= sizes[NSSmallControlSize].height())
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];
}
IntSize RenderThemeMac::sizeForFont(RenderStyle* style, const IntSize* sizes) const
{
return sizes[controlSizeForFont(style)];
}
void RenderThemeMac::setSizeFromFont(RenderStyle* style, const IntSize* sizes) const
{
// FIXME: Check is flawed, since it doesn't take min-width/max-width into account.
IntSize size = sizeForFont(style, sizes);
if (style->width().isIntrinsicOrAuto() && size.width() > 0)
style->setWidth(Length(size.width(), Fixed));
if (style->height().isAuto() && size.height() > 0)
style->setHeight(Length(size.height(), Fixed));
}
void RenderThemeMac::setFontFromControlSize(CSSStyleSelector* selector, RenderStyle* style, NSControlSize controlSize) const
{
FontDescription fontDescription;
fontDescription.setIsAbsoluteSize(true);
fontDescription.setGenericFamily(FontDescription::SerifFamily);
NSFont* font = [NSFont systemFontOfSize:[NSFont systemFontSizeForControlSize:controlSize]];
fontDescription.firstFamily().setFamily(QString::fromNSString([font fontName]));
fontDescription.setComputedSize([font pointSize]);
fontDescription.setSpecifiedSize([font pointSize]);
if (style->setFontDescription(fontDescription))
style->font().update();
}
void RenderThemeMac::addIntrinsicMargins(RenderStyle* style, NSControlSize size) const
{
// Cut out the intrinsic margins completely if we end up using mini controls.
if (size == NSMiniControlSize)
return;
// Intrinsic margin value.
const int m = 2;
// FIXME: Using width/height alone and not also dealing with min-width/max-width is flawed.
if (style->width().isIntrinsicOrAuto()) {
if (style->marginLeft().quirk())
style->setMarginLeft(Length(m, Fixed));
if (style->marginRight().quirk())
style->setMarginRight(Length(m, Fixed));
}
if (style->height().isAuto()) {
if (style->marginTop().quirk())
style->setMarginTop(Length(m, Fixed));
if (style->marginBottom().quirk())
style->setMarginBottom(Length(m, Fixed));
}
}
bool RenderThemeMac::paintCheckbox(RenderObject* o, const RenderObject::PaintInfo& i, const IntRect& r)
{
// Determine the width and height needed for the control and prepare the cell for painting.
setCheckboxCellState(o, r);
// 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.
IntRect inflatedRect = inflateRect(r, checkboxSizes()[[checkbox controlSize]], checkboxMargins());
[checkbox drawWithFrame:NSRect(inflatedRect) inView:o->canvas()->view()->getDocumentView()];
[checkbox setControlView: nil];
return false;
}
const IntSize* RenderThemeMac::checkboxSizes() const
{
static const IntSize sizes[3] = { IntSize(14, 14), IntSize(12, 12), IntSize(10, 10) };
return sizes;
}
const int* RenderThemeMac::checkboxMargins() const
{
static const int margins[3][4] =
{
{ 3, 4, 4, 2 },
{ 4, 3, 3, 3 },
{ 4, 3, 3, 3 },
};
return margins[[checkbox controlSize]];
}
void RenderThemeMac::setCheckboxCellState(const RenderObject* o, const IntRect& r)
{
if (!checkbox) {
checkbox = KWQRetainNSRelease([[NSButtonCell alloc] init]);
[checkbox setButtonType:NSSwitchButton];
[checkbox setTitle:nil];
[checkbox setAllowsMixedState:YES];
}
// Set the control size based off the rectangle we're painting into.
setControlSize(checkbox, checkboxSizes(), IntSize(r.width(), r.height()));
// Update the various states we respond to.
updateCheckedState(checkbox, o);
updateEnabledState(checkbox, o);
updatePressedState(checkbox, o);
updateFocusedState(checkbox, o);
}
void RenderThemeMac::setCheckboxSize(RenderStyle* style) const
{
// If the width and height are both specified, then we have nothing to do.
if (!style->width().isIntrinsicOrAuto() && !style->height().isAuto())
return;
// Use the font size to determine the intrinsic width of the control.
setSizeFromFont(style, checkboxSizes());
}
bool RenderThemeMac::paintRadio(RenderObject* o, const RenderObject::PaintInfo& i, const IntRect& r)
{
// Determine the width and height needed for the control and prepare the cell for painting.
setRadioCellState(o, r);
// 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.
IntRect inflatedRect = inflateRect(r, radioSizes()[[radio controlSize]], radioMargins());
[radio drawWithFrame:NSRect(inflatedRect) inView:o->canvas()->view()->getDocumentView()];
[radio setControlView: nil];
return false;
}
const IntSize* RenderThemeMac::radioSizes() const
{
static const IntSize sizes[3] = { IntSize(14, 15), IntSize(12, 13), IntSize(10, 10) };
return sizes;
}
const int* RenderThemeMac::radioMargins() const
{
static const int margins[3][4] =
{
{ 2, 2, 4, 2 },
{ 3, 2, 3, 2 },
{ 1, 0, 2, 0 },
};
return margins[[radio controlSize]];
}
void RenderThemeMac::setRadioCellState(const RenderObject* o, const IntRect& r)
{
if (!radio) {
radio = KWQRetainNSRelease([[NSButtonCell alloc] init]);
[radio setButtonType:NSRadioButton];
[radio setTitle:nil];
}
// Set the control size based off the rectangle we're painting into.
setControlSize(radio, radioSizes(), IntSize(r.width(), r.height()));
// Update the various states we respond to.
updateCheckedState(radio, o);
updateEnabledState(radio, o);
updatePressedState(radio, o);
updateFocusedState(radio, o);
}
void RenderThemeMac::setRadioSize(RenderStyle* style) const
{
// If the width and height are both specified, then we have nothing to do.
if (!style->width().isIntrinsicOrAuto() && !style->height().isAuto())
return;
// Use the font size to determine the intrinsic width of the control.
setSizeFromFont(style, radioSizes());
}
void RenderThemeMac::setButtonPaddingFromControlSize(RenderStyle* style, NSControlSize size) const
{
// 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 HTML4 <button> will match our rendering by default, since we're using a consistent
// padding.
const int padding = 8;
style->setPaddingLeft(Length(padding, Fixed));
style->setPaddingRight(Length(padding, Fixed));
style->setPaddingTop(Length(0, Fixed));
style->setPaddingBottom(Length(0, Fixed));
}
void RenderThemeMac::adjustButtonStyle(CSSStyleSelector* selector, RenderStyle* style, ElementImpl* e) const
{
// There are three appearance constants for buttons.
// (1) Push-button is the constant for the default Aqua system button. Push buttons will not scale vertically and will not allow
// custom fonts or colors. <input>s use this constant. This button will allow custom colors and font weights/variants but won't
// scale vertically.
// (2) square-button is the constant for the square button. This button will allow custom fonts and colors and will scale vertically.
// (3) Button is the constant that means "pick the best button as appropriate." <button>s use this constant. This button will
// also scale vertically and allow custom fonts and colors. It will attempt to use Aqua if possible and will make this determination
// solely on the rectangle of the control.
// Determine our control size based off our font.
NSControlSize controlSize = controlSizeForFont(style);
// Add in intrinsic margins
addIntrinsicMargins(style, controlSize);
if (style->appearance() == PushButtonAppearance) {
// Ditch the border.
style->resetBorder();
// Height is locked to auto.
style->setHeight(Length(Auto));
// White-space is locked to pre
style->setWhiteSpace(PRE);
// Set the button's vertical size.
setButtonSize(style);
// Add in the padding that we'd like to use.
setButtonPaddingFromControlSize(style, controlSize);
// Our font is locked to the appropriate system font size for the control. To clarify, we first use the CSS-specified font to figure out
// a reasonable control size, but once that control size is determined, we throw that font away and use the appropriate
// system font for the control size instead.
setFontFromControlSize(selector, style, controlSize);
} else {
// Set a min-height so that we can't get smaller than the mini button.
style->setMinHeight(Length(15, Fixed));
// Reset the top and bottom borders.
style->resetBorderTop();
style->resetBorderBottom();
}
}
const IntSize* RenderThemeMac::buttonSizes() const
{
static const IntSize sizes[3] = { IntSize(0, 21), IntSize(0, 18), IntSize(0, 15) };
return sizes;
}
const int* RenderThemeMac::buttonMargins() const
{
static const int margins[3][4] =
{
{ 4, 6, 7, 6 },
{ 4, 5, 6, 5 },
{ 0, 1, 1, 1 },
};
return margins[[button controlSize]];
}
void RenderThemeMac::setButtonSize(RenderStyle* style) const
{
// If the width and height are both specified, then we have nothing to do.
if (!style->width().isIntrinsicOrAuto() && !style->height().isAuto())
return;
// Use the font size to determine the intrinsic width of the control.
setSizeFromFont(style, buttonSizes());
}
void RenderThemeMac::setButtonCellState(const RenderObject* o, const IntRect& r)
{
if (!button) {
button = KWQRetainNSRelease([[NSButtonCell alloc] init]);
[button setTitle:nil];
[button setButtonType:NSMomentaryPushInButton];
}
// Set the control size based off the rectangle we're painting into.
if (o->style()->appearance() == SquareButtonAppearance ||
r.height() > buttonSizes()[NSRegularControlSize].height()) {
// Use the square button
if ([button bezelStyle] != NSShadowlessSquareBezelStyle)
[button setBezelStyle:NSShadowlessSquareBezelStyle];
} else if ([button bezelStyle] != NSRoundedBezelStyle)
[button setBezelStyle:NSRoundedBezelStyle];
setControlSize(button, buttonSizes(), IntSize(r.width(), r.height()));
// Update the various states we respond to.
updateCheckedState(button, o);
updateEnabledState(button, o);
updatePressedState(button, o);
updateFocusedState(button, o);
}
bool RenderThemeMac::paintButton(RenderObject* o, const RenderObject::PaintInfo& i, const IntRect& r)
{
// Determine the width and height needed for the control and prepare the cell for painting.
setButtonCellState(o, r);
// We inflate the rect as needed to account for padding included in the cell to accommodate the button
// shadow. We don't consider this part of the bounds of the control in WebKit.
IntSize size = buttonSizes()[[button controlSize]];
size.setWidth(r.width());
IntRect inflatedRect = r;
if ([button bezelStyle] == NSRoundedBezelStyle) {
// Center the button within the available space.
if (inflatedRect.height() > size.height()) {
inflatedRect.setY(inflatedRect.y() + (inflatedRect.height() - size.height()) / 2);
inflatedRect.setHeight(size.height());
}
// Now inflate it to account for the shadow.
inflatedRect = inflateRect(inflatedRect, size, buttonMargins());
}
[button drawWithFrame:NSRect(inflatedRect) inView:o->canvas()->view()->getDocumentView()];
[button setControlView: nil];
return false;
}
bool RenderThemeMac::paintTextField(RenderObject* o, const RenderObject::PaintInfo& i, const IntRect& r)
{
// Initialize text field and update state.
setTextFieldCellState(o, r);
[[WebCoreGraphicsBridge sharedBridge] drawBezeledTextFieldCell:NSRect(r) enabled:[textField isEnabled]];
[textField setControlView: nil];
return true;
}
void RenderThemeMac::setTextFieldCellState(const RenderObject* o, const IntRect& r)
{
if (!textField)
textField = KWQRetainNSRelease([[NSTextFieldCell alloc] initTextCell:@""]);
updateEnabledState(textField, o);
}
void RenderThemeMac::adjustTextFieldStyle(CSSStyleSelector* selector, RenderStyle* style, ElementImpl* e) const
{
// FIXME: If the style has a border, then turn off the aqua appearance.
// FIXME: This should be in html4.css when we flip the switch
style->setPaddingTop(Length(3, Fixed));
style->setPaddingBottom(Length(3, Fixed));
style->setPaddingRight(Length(3, Fixed));
style->setPaddingLeft(Length(3, Fixed));
style->setMarginTop(Length(2, Fixed));
style->setMarginBottom(Length(2, Fixed));
style->setMarginLeft(Length(2, Fixed));
style->setMarginRight(Length(2, Fixed));
if (!style->hasBackground())
style->setBackgroundColor(Color(Color::white));
}
}