/* * 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 "RenderThemeMac.h" #import "Document.h" #import "FoundationExtras.h" #import "FrameView.h" #import "GraphicsContext.h" #import "Image.h" #import "RenderView.h" #import "WebCoreSystemInterface.h" #import "cssstyleselector.h" #import "RenderPopupMenuMac.h" // The methods in this file are specific to the Mac OS X platform. namespace WebCore { enum { topMargin, rightMargin, bottomMargin, leftMargin }; enum { topPadding, rightPadding, bottomPadding, leftPadding }; RenderTheme* theme() { static RenderThemeMac macTheme; return &macTheme; } RenderThemeMac::RenderThemeMac() : checkbox(nil) , radio(nil) , button(nil) , resizeCornerImage(0) { } Color RenderThemeMac::platformActiveSelectionBackgroundColor() const { NSColor* color = [[NSColor selectedTextBackgroundColor] colorUsingColorSpaceName:NSDeviceRGBColorSpace]; return Color((int)(255 * [color redComponent]), (int)(255 * [color greenComponent]), (int)(255 * [color blueComponent])); } Color RenderThemeMac::platformInactiveSelectionBackgroundColor() const { NSColor* color = [[NSColor secondarySelectedControlColor] colorUsingColorSpaceName:NSDeviceRGBColorSpace]; return Color((int)(255 * [color redComponent]), (int)(255 * [color greenComponent]), (int)(255 * [color blueComponent])); } bool RenderThemeMac::isControlStyled(const RenderStyle* style, const BorderData& border, const BackgroundLayer& background, const Color& backgroundColor) const { if (style->appearance() == TextFieldAppearance || style->appearance() == TextAreaAppearance) return style->border() != border; // FIXME: We should allow menulists to be styled- but not until we have a good design for styled popups. if (style->appearance() == MenulistAppearance) return false; return RenderTheme::isControlStyled(style, border, background, backgroundColor); } void RenderThemeMac::paintResizeControl(GraphicsContext* c, const IntRect& r) { if (!resizeCornerImage) resizeCornerImage = Image::loadResource("textAreaResizeCorner"); IntPoint imagePoint(r.right() - resizeCornerImage->width(), r.bottom() - resizeCornerImage->height()); c->drawImage(resizeCornerImage, imagePoint); } 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 MenulistAppearance: { setPopupButtonCellState(o, r); r = inflateRect(r, popupButtonSizes()[[popupButton controlSize]], popupButtonMargins()); 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. if (o->style()->appearance() == MenulistAppearance) return o->marginTop() + o->height() - 5; // This is to match AppKit. There might be a better way to calculate this though. 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([font familyName]); 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->view()->frameView()->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 = HardRetainWithNSRelease([[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(), r.size()); // 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->view()->frameView()->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 = HardRetainWithNSRelease([[NSButtonCell alloc] init]); [radio setButtonType:NSRadioButton]; [radio setTitle:nil]; } // Set the control size based off the rectangle we're painting into. setControlSize(radio, radioSizes(), r.size()); // 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