1/*
2 * Copyright (C) 2008 Apple Inc. All Rights Reserved.
3 * Copyright (C) 2009 Google Inc. All Rights Reserved.
4 *
5 * Redistribution and use in source and binary forms, with or without
6 * modification, are permitted provided that the following conditions
7 * are met:
8 * 1. Redistributions of source code must retain the above copyright
9 *    notice, this list of conditions and the following disclaimer.
10 * 2. Redistributions in binary form must reproduce the above copyright
11 *    notice, this list of conditions and the following disclaimer in the
12 *    documentation and/or other materials provided with the distribution.
13 *
14 * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY
15 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
16 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
17 * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL APPLE INC. OR
18 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
19 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
20 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
21 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
22 * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
23 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
24 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
25 */
26
27#import "config.h"
28#import "ThemeChromiumMac.h"
29
30#import "BlockExceptions.h"
31#import "GraphicsContext.h"
32#import "LocalCurrentGraphicsContext.h"
33#import "ScrollView.h"
34#import "WebCoreSystemInterface.h"
35#import <Carbon/Carbon.h>
36#include <wtf/StdLibExtras.h>
37#import <objc/runtime.h>
38
39using namespace std;
40
41// This file (and its associated .h file) is a clone of ThemeMac.mm.
42// Because the original file is designed to run in-process inside a Cocoa view,
43// we must maintain a fork. Please maintain this file by performing parallel
44// changes to it.
45//
46// The only changes from ThemeMac should be:
47// - The classname change from ThemeMac to ThemeChromiumMac.
48// - The import of FlippedView() and its use as the parent view for cell
49//   rendering.
50// - In updateStates() the code to update the cells' inactive state.
51// - In paintButton() the code to save/restore the window's default button cell.
52// - The Snow Leopard focus ring bug fix and its use around every call to
53//   -[NSButtonCell drawWithFrame:inView:].
54//
55// For all other differences, if it was introduced in this file, then the
56// maintainer forgot to include it in the list; otherwise it is an update that
57// should have been applied to this file but was not.
58
59// FIXME: Default buttons really should be more like push buttons and not like buttons.
60
61// --- START fix for Snow Leopard focus ring bug ---
62
63// There is a bug in the Cocoa focus ring drawing code. The code calls +[NSView
64// focusView] (to get the currently focused view) and then calls an NSRect-
65// returning method on that view to obtain a clipping rect. However, if there is
66// no focused view (as there won't be if the destination is a context), the rect
67// returned from the method invocation on nil is garbage.
68//
69// The garbage fortunately does not clip the focus ring on Leopard, but
70// unfortunately does so on Snow Leopard. Therefore, if a runtime test shows
71// that focus ring drawing fails, we swizzle NSView to ensure it returns a valid
72// view with a valid clipping rectangle.
73//
74// FIXME: After the referenced bug is fixed on all supported platforms, remove
75// this code.
76//
77// References:
78//  <http://crbug.com/27493>
79//  <rdar://problem/7604051> (<http://openradar.appspot.com/7604051>)
80
81@interface TCMVisibleView : NSView
82
83@end
84
85@implementation TCMVisibleView
86
87- (struct CGRect)_focusRingVisibleRect
88{
89    return CGRectZero;
90}
91
92- (id)_focusRingClipAncestor
93{
94    return self;
95}
96
97@end
98
99@interface NSView (TCMInterposing)
100+ (NSView *)TCMInterposing_focusView;
101@end
102
103namespace FocusIndicationFix {
104
105bool currentOSHasSetFocusRingStyleInBitmapBug()
106{
107    UInt32 pixel = 0;
108    UInt32* pixelPlane = &pixel;
109    UInt32** pixelPlanes = &pixelPlane;
110    NSBitmapImageRep *bitmap = [[NSBitmapImageRep alloc] initWithBitmapDataPlanes:(UInt8**)pixelPlanes
111                                                                       pixelsWide:1
112                                                                       pixelsHigh:1
113                                                                    bitsPerSample:8
114                                                                  samplesPerPixel:4
115                                                                         hasAlpha:YES
116                                                                         isPlanar:NO
117                                                                   colorSpaceName:NSCalibratedRGBColorSpace
118                                                                     bitmapFormat:NSAlphaFirstBitmapFormat
119                                                                      bytesPerRow:4
120                                                                     bitsPerPixel:32];
121    [NSGraphicsContext saveGraphicsState];
122    [NSGraphicsContext setCurrentContext:[NSGraphicsContext graphicsContextWithBitmapImageRep:bitmap]];
123    NSSetFocusRingStyle(NSFocusRingOnly);
124    NSRectFill(NSMakeRect(0, 0, 1, 1));
125    [NSGraphicsContext restoreGraphicsState];
126    [bitmap release];
127
128    return !pixel;
129}
130
131bool swizzleFocusView()
132{
133    if (!currentOSHasSetFocusRingStyleInBitmapBug())
134        return false;
135
136    Class nsview = [NSView class];
137    Method m1 = class_getClassMethod(nsview, @selector(focusView));
138    Method m2 = class_getClassMethod(nsview, @selector(TCMInterposing_focusView));
139    if (m1 && m2) {
140        method_exchangeImplementations(m1, m2);
141        return true;
142    }
143
144    return false;
145}
146
147static bool interpose = false;
148
149// A class to restrict the amount of time spent messing with interposing. It
150// only stacks one-deep.
151class ScopedFixer {
152public:
153    ScopedFixer()
154    {
155        static bool swizzled = swizzleFocusView();
156        interpose = swizzled;
157    }
158
159    ~ScopedFixer()
160    {
161        interpose = false;
162    }
163};
164
165}  // namespace FocusIndicationFix
166
167@implementation NSView (TCMInterposing)
168
169+ (NSView *)TCMInterposing_focusView
170{
171    NSView *view = [self TCMInterposing_focusView];  // call original (was swizzled)
172    if (!view && FocusIndicationFix::interpose) {
173        static TCMVisibleView* fixedView = [[TCMVisibleView alloc] init];
174        view = fixedView;
175    }
176
177    return view;
178}
179
180@end
181
182// --- END fix for Snow Leopard focus ring bug ---
183
184namespace WebCore {
185
186// Pick up utility function from RenderThemeChromiumMac.
187extern NSView* FlippedView();
188
189enum {
190    topMargin,
191    rightMargin,
192    bottomMargin,
193    leftMargin
194};
195
196Theme* platformTheme()
197{
198    DEFINE_STATIC_LOCAL(ThemeChromiumMac, themeMac, ());
199    return &themeMac;
200}
201
202// Helper functions used by a bunch of different control parts.
203
204static NSControlSize controlSizeForFont(const Font& font)
205{
206    int fontSize = font.pixelSize();
207    if (fontSize >= 16)
208        return NSRegularControlSize;
209    if (fontSize >= 11)
210        return NSSmallControlSize;
211    return NSMiniControlSize;
212}
213
214static LengthSize sizeFromNSControlSize(NSControlSize nsControlSize, const LengthSize& zoomedSize, float zoomFactor, const IntSize* sizes)
215{
216    IntSize controlSize = sizes[nsControlSize];
217    if (zoomFactor != 1.0f)
218        controlSize = IntSize(controlSize.width() * zoomFactor, controlSize.height() * zoomFactor);
219    LengthSize result = zoomedSize;
220    if (zoomedSize.width().isIntrinsicOrAuto() && controlSize.width() > 0)
221        result.setWidth(Length(controlSize.width(), Fixed));
222    if (zoomedSize.height().isIntrinsicOrAuto() && controlSize.height() > 0)
223        result.setHeight(Length(controlSize.height(), Fixed));
224    return result;
225}
226
227static LengthSize sizeFromFont(const Font& font, const LengthSize& zoomedSize, float zoomFactor, const IntSize* sizes)
228{
229    return sizeFromNSControlSize(controlSizeForFont(font), zoomedSize, zoomFactor, sizes);
230}
231
232static ControlSize controlSizeFromPixelSize(const IntSize* sizes, const IntSize& minZoomedSize, float zoomFactor)
233{
234    if (minZoomedSize.width() >= static_cast<int>(sizes[NSRegularControlSize].width() * zoomFactor) &&
235        minZoomedSize.height() >= static_cast<int>(sizes[NSRegularControlSize].height() * zoomFactor))
236        return NSRegularControlSize;
237    if (minZoomedSize.width() >= static_cast<int>(sizes[NSSmallControlSize].width() * zoomFactor) &&
238        minZoomedSize.height() >= static_cast<int>(sizes[NSSmallControlSize].height() * zoomFactor))
239        return NSSmallControlSize;
240    return NSMiniControlSize;
241}
242
243static void setControlSize(NSCell* cell, const IntSize* sizes, const IntSize& minZoomedSize, float zoomFactor)
244{
245    ControlSize size = controlSizeFromPixelSize(sizes, minZoomedSize, zoomFactor);
246    if (size != [cell controlSize]) // Only update if we have to, since AppKit does work even if the size is the same.
247        [cell setControlSize:(NSControlSize)size];
248}
249
250static void updateStates(NSCell* cell, ControlStates states)
251{
252    // Hover state is not supported by Aqua.
253
254    // Pressed state
255    bool oldPressed = [cell isHighlighted];
256    bool pressed = states & PressedState;
257    if (pressed != oldPressed)
258        [cell setHighlighted:pressed];
259
260    // Enabled state
261    bool oldEnabled = [cell isEnabled];
262    bool enabled = states & EnabledState;
263    if (enabled != oldEnabled)
264        [cell setEnabled:enabled];
265
266    // Focused state
267    bool oldFocused = [cell showsFirstResponder];
268    bool focused = states & FocusState;
269    if (focused != oldFocused)
270        [cell setShowsFirstResponder:focused];
271
272    // Checked and Indeterminate
273    bool oldIndeterminate = [cell state] == NSMixedState;
274    bool indeterminate = (states & IndeterminateState);
275    bool checked = states & CheckedState;
276    bool oldChecked = [cell state] == NSOnState;
277    if (oldIndeterminate != indeterminate || checked != oldChecked)
278        [cell setState:indeterminate ? NSMixedState : (checked ? NSOnState : NSOffState)];
279
280    // Window Inactive state
281    NSControlTint oldTint = [cell controlTint];
282    bool windowInactive = (states & WindowInactiveState);
283    NSControlTint tint = windowInactive ? static_cast<NSControlTint>(NSClearControlTint)
284                                        : [NSColor currentControlTint];
285    if (tint != oldTint)
286        [cell setControlTint:tint];
287}
288
289static ThemeDrawState convertControlStatesToThemeDrawState(ThemeButtonKind kind, ControlStates states)
290{
291    if (states & ReadOnlyState)
292        return kThemeStateUnavailableInactive;
293    if (!(states & EnabledState))
294        return kThemeStateUnavailableInactive;
295
296    // Do not process PressedState if !EnabledState or ReadOnlyState.
297    if (states & PressedState) {
298        if (kind == kThemeIncDecButton || kind == kThemeIncDecButtonSmall || kind == kThemeIncDecButtonMini)
299            return states & SpinUpState ? kThemeStatePressedUp : kThemeStatePressedDown;
300        return kThemeStatePressed;
301    }
302    return kThemeStateActive;
303}
304
305static IntRect inflateRect(const IntRect& zoomedRect, const IntSize& zoomedSize, const int* margins, float zoomFactor)
306{
307    // Only do the inflation if the available width/height are too small.  Otherwise try to
308    // fit the glow/check space into the available box's width/height.
309    int widthDelta = zoomedRect.width() - (zoomedSize.width() + margins[leftMargin] * zoomFactor + margins[rightMargin] * zoomFactor);
310    int heightDelta = zoomedRect.height() - (zoomedSize.height() + margins[topMargin] * zoomFactor + margins[bottomMargin] * zoomFactor);
311    IntRect result(zoomedRect);
312    if (widthDelta < 0) {
313        result.setX(result.x() - margins[leftMargin] * zoomFactor);
314        result.setWidth(result.width() - widthDelta);
315    }
316    if (heightDelta < 0) {
317        result.setY(result.y() - margins[topMargin] * zoomFactor);
318        result.setHeight(result.height() - heightDelta);
319    }
320    return result;
321}
322
323// Checkboxes
324
325static const IntSize* checkboxSizes()
326{
327    static const IntSize sizes[3] = { IntSize(14, 14), IntSize(12, 12), IntSize(10, 10) };
328    return sizes;
329}
330
331static const int* checkboxMargins(NSControlSize controlSize)
332{
333    static const int margins[3][4] =
334    {
335        { 3, 4, 4, 2 },
336        { 4, 3, 3, 3 },
337        { 4, 3, 3, 3 },
338    };
339    return margins[controlSize];
340}
341
342static LengthSize checkboxSize(const Font& font, const LengthSize& zoomedSize, float zoomFactor)
343{
344    // If the width and height are both specified, then we have nothing to do.
345    if (!zoomedSize.width().isIntrinsicOrAuto() && !zoomedSize.height().isIntrinsicOrAuto())
346        return zoomedSize;
347
348    // Use the font size to determine the intrinsic width of the control.
349    return sizeFromFont(font, zoomedSize, zoomFactor, checkboxSizes());
350}
351
352static NSButtonCell *checkbox(ControlStates states, const IntRect& zoomedRect, float zoomFactor)
353{
354    static NSButtonCell *checkboxCell;
355    if (!checkboxCell) {
356        checkboxCell = [[NSButtonCell alloc] init];
357        [checkboxCell setButtonType:NSSwitchButton];
358        [checkboxCell setTitle:nil];
359        [checkboxCell setAllowsMixedState:YES];
360        [checkboxCell setFocusRingType:NSFocusRingTypeExterior];
361    }
362
363    // Set the control size based off the rectangle we're painting into.
364    setControlSize(checkboxCell, checkboxSizes(), zoomedRect.size(), zoomFactor);
365
366    // Update the various states we respond to.
367    updateStates(checkboxCell, states);
368
369    return checkboxCell;
370}
371
372// FIXME: Share more code with radio buttons.
373static void paintCheckbox(ControlStates states, GraphicsContext* context, const IntRect& zoomedRect, float zoomFactor, ScrollView* scrollView)
374{
375    BEGIN_BLOCK_OBJC_EXCEPTIONS
376
377    // Determine the width and height needed for the control and prepare the cell for painting.
378    NSButtonCell *checkboxCell = checkbox(states, zoomedRect, zoomFactor);
379    LocalCurrentGraphicsContext localContext(context);
380
381    context->save();
382
383    NSControlSize controlSize = [checkboxCell controlSize];
384    IntSize zoomedSize = checkboxSizes()[controlSize];
385    zoomedSize.setWidth(zoomedSize.width() * zoomFactor);
386    zoomedSize.setHeight(zoomedSize.height() * zoomFactor);
387    IntRect inflatedRect = inflateRect(zoomedRect, zoomedSize, checkboxMargins(controlSize), zoomFactor);
388
389    if (zoomFactor != 1.0f) {
390        inflatedRect.setWidth(inflatedRect.width() / zoomFactor);
391        inflatedRect.setHeight(inflatedRect.height() / zoomFactor);
392        context->translate(inflatedRect.x(), inflatedRect.y());
393        context->scale(FloatSize(zoomFactor, zoomFactor));
394        context->translate(-inflatedRect.x(), -inflatedRect.y());
395    }
396
397    {
398        FocusIndicationFix::ScopedFixer fix;
399        [checkboxCell drawWithFrame:NSRect(inflatedRect) inView:FlippedView()];
400    }
401    [checkboxCell setControlView:nil];
402
403    context->restore();
404
405    END_BLOCK_OBJC_EXCEPTIONS
406}
407
408// Radio Buttons
409
410static const IntSize* radioSizes()
411{
412    static const IntSize sizes[3] = { IntSize(14, 15), IntSize(12, 13), IntSize(10, 10) };
413    return sizes;
414}
415
416static const int* radioMargins(NSControlSize controlSize)
417{
418    static const int margins[3][4] =
419    {
420        { 2, 2, 4, 2 },
421        { 3, 2, 3, 2 },
422        { 1, 0, 2, 0 },
423    };
424    return margins[controlSize];
425}
426
427static LengthSize radioSize(const Font& font, const LengthSize& zoomedSize, float zoomFactor)
428{
429    // If the width and height are both specified, then we have nothing to do.
430    if (!zoomedSize.width().isIntrinsicOrAuto() && !zoomedSize.height().isIntrinsicOrAuto())
431        return zoomedSize;
432
433    // Use the font size to determine the intrinsic width of the control.
434    return sizeFromFont(font, zoomedSize, zoomFactor, radioSizes());
435}
436
437static NSButtonCell *radio(ControlStates states, const IntRect& zoomedRect, float zoomFactor)
438{
439    static NSButtonCell *radioCell;
440    if (!radioCell) {
441        radioCell = [[NSButtonCell alloc] init];
442        [radioCell setButtonType:NSRadioButton];
443        [radioCell setTitle:nil];
444        [radioCell setFocusRingType:NSFocusRingTypeExterior];
445    }
446
447    // Set the control size based off the rectangle we're painting into.
448    setControlSize(radioCell, radioSizes(), zoomedRect.size(), zoomFactor);
449
450    // Update the various states we respond to.
451    updateStates(radioCell, states);
452
453    return radioCell;
454}
455
456static void paintRadio(ControlStates states, GraphicsContext* context, const IntRect& zoomedRect, float zoomFactor, ScrollView* scrollView)
457{
458    // Determine the width and height needed for the control and prepare the cell for painting.
459    NSButtonCell *radioCell = radio(states, zoomedRect, zoomFactor);
460    LocalCurrentGraphicsContext localContext(context);
461
462    context->save();
463
464    NSControlSize controlSize = [radioCell controlSize];
465    IntSize zoomedSize = radioSizes()[controlSize];
466    zoomedSize.setWidth(zoomedSize.width() * zoomFactor);
467    zoomedSize.setHeight(zoomedSize.height() * zoomFactor);
468    IntRect inflatedRect = inflateRect(zoomedRect, zoomedSize, radioMargins(controlSize), zoomFactor);
469
470    if (zoomFactor != 1.0f) {
471        inflatedRect.setWidth(inflatedRect.width() / zoomFactor);
472        inflatedRect.setHeight(inflatedRect.height() / zoomFactor);
473        context->translate(inflatedRect.x(), inflatedRect.y());
474        context->scale(FloatSize(zoomFactor, zoomFactor));
475        context->translate(-inflatedRect.x(), -inflatedRect.y());
476    }
477
478    BEGIN_BLOCK_OBJC_EXCEPTIONS
479    {
480        FocusIndicationFix::ScopedFixer fix;
481        [radioCell drawWithFrame:NSRect(inflatedRect) inView:FlippedView()];
482    }
483    [radioCell setControlView:nil];
484    END_BLOCK_OBJC_EXCEPTIONS
485
486    context->restore();
487}
488
489// Buttons
490
491// Buttons really only constrain height. They respect width.
492static const IntSize* buttonSizes()
493{
494    static const IntSize sizes[3] = { IntSize(0, 21), IntSize(0, 18), IntSize(0, 15) };
495    return sizes;
496}
497
498#if ENABLE(DATALIST)
499static const IntSize* listButtonSizes()
500{
501    static const IntSize sizes[3] = { IntSize(21, 21), IntSize(19, 18), IntSize(17, 16) };
502    return sizes;
503}
504#endif
505
506static const int* buttonMargins(NSControlSize controlSize)
507{
508    static const int margins[3][4] =
509    {
510        { 4, 6, 7, 6 },
511        { 4, 5, 6, 5 },
512        { 0, 1, 1, 1 },
513    };
514    return margins[controlSize];
515}
516
517static void setupButtonCell(NSButtonCell *&buttonCell, ControlPart part, ControlStates states, const IntRect& zoomedRect, float zoomFactor)
518{
519    if (!buttonCell) {
520        buttonCell = [[NSButtonCell alloc] init];
521        [buttonCell setTitle:nil];
522        [buttonCell setButtonType:NSMomentaryPushInButton];
523        if (states & DefaultState)
524            [buttonCell setKeyEquivalent:@"\r"];
525    }
526
527    // Set the control size based off the rectangle we're painting into.
528    const IntSize* sizes = buttonSizes();
529#if ENABLE(DATALIST)
530    if (part == ListButtonPart) {
531        [buttonCell setBezelStyle:NSRoundedDisclosureBezelStyle];
532        sizes = listButtonSizes();
533    } else
534#endif
535    if (part == SquareButtonPart || zoomedRect.height() > buttonSizes()[NSRegularControlSize].height() * zoomFactor) {
536        // Use the square button
537        if ([buttonCell bezelStyle] != NSShadowlessSquareBezelStyle)
538            [buttonCell setBezelStyle:NSShadowlessSquareBezelStyle];
539    } else if ([buttonCell bezelStyle] != NSRoundedBezelStyle)
540        [buttonCell setBezelStyle:NSRoundedBezelStyle];
541
542    setControlSize(buttonCell, sizes, zoomedRect.size(), zoomFactor);
543
544    // Update the various states we respond to.
545    updateStates(buttonCell, states);
546}
547
548static NSButtonCell *button(ControlPart part, ControlStates states, const IntRect& zoomedRect, float zoomFactor)
549{
550    bool isDefault = states & DefaultState;
551    static NSButtonCell *cells[2];
552    setupButtonCell(cells[isDefault], part, states, zoomedRect, zoomFactor);
553    return cells[isDefault];
554}
555
556static void paintButton(ControlPart part, ControlStates states, GraphicsContext* context, const IntRect& zoomedRect, float zoomFactor, ScrollView* scrollView)
557{
558    BEGIN_BLOCK_OBJC_EXCEPTIONS
559
560    // Determine the width and height needed for the control and prepare the cell for painting.
561    NSButtonCell *buttonCell = button(part, states, zoomedRect, zoomFactor);
562    LocalCurrentGraphicsContext localContext(context);
563
564    NSControlSize controlSize = [buttonCell controlSize];
565#if ENABLE(DATALIST)
566    IntSize zoomedSize = (part == ListButtonPart ? listButtonSizes() : buttonSizes())[controlSize];
567#else
568    IntSize zoomedSize = buttonSizes()[controlSize];
569#endif
570    zoomedSize.setWidth(zoomedRect.width()); // Buttons don't ever constrain width, so the zoomed width can just be honored.
571    zoomedSize.setHeight(zoomedSize.height() * zoomFactor);
572    IntRect inflatedRect = zoomedRect;
573    if ([buttonCell bezelStyle] == NSRoundedBezelStyle) {
574        // Center the button within the available space.
575        if (inflatedRect.height() > zoomedSize.height()) {
576            inflatedRect.setY(inflatedRect.y() + (inflatedRect.height() - zoomedSize.height()) / 2);
577            inflatedRect.setHeight(zoomedSize.height());
578        }
579
580        // Now inflate it to account for the shadow.
581        inflatedRect = inflateRect(inflatedRect, zoomedSize, buttonMargins(controlSize), zoomFactor);
582
583        if (zoomFactor != 1.0f) {
584            inflatedRect.setWidth(inflatedRect.width() / zoomFactor);
585            inflatedRect.setHeight(inflatedRect.height() / zoomFactor);
586            context->translate(inflatedRect.x(), inflatedRect.y());
587            context->scale(FloatSize(zoomFactor, zoomFactor));
588            context->translate(-inflatedRect.x(), -inflatedRect.y());
589        }
590    }
591
592    {
593        FocusIndicationFix::ScopedFixer fix;
594        [buttonCell drawWithFrame:NSRect(inflatedRect) inView:FlippedView()];
595    }
596    [buttonCell setControlView:nil];
597
598    END_BLOCK_OBJC_EXCEPTIONS
599}
600
601// Stepper
602
603static const IntSize* stepperSizes()
604{
605    static const IntSize sizes[3] = { IntSize(19, 27), IntSize(15, 22), IntSize(13, 15) };
606    return sizes;
607}
608
609// We don't use controlSizeForFont() for steppers because the stepper height
610// should be equal to or less than the corresponding text field height,
611static NSControlSize stepperControlSizeForFont(const Font& font)
612{
613    int fontSize = font.pixelSize();
614    if (fontSize >= 18)
615        return NSRegularControlSize;
616    if (fontSize >= 13)
617        return NSSmallControlSize;
618    return NSMiniControlSize;
619}
620
621static void paintStepper(ControlStates states, GraphicsContext* context, const IntRect& zoomedRect, float zoomFactor, ScrollView*)
622{
623    // We don't use NSStepperCell because there are no ways to draw an
624    // NSStepperCell with the up button highlighted.
625
626    HIThemeButtonDrawInfo drawInfo;
627    drawInfo.version = 0;
628    drawInfo.state = convertControlStatesToThemeDrawState(kThemeIncDecButton, states);
629    drawInfo.adornment = kThemeAdornmentDefault;
630    ControlSize controlSize = controlSizeFromPixelSize(stepperSizes(), zoomedRect.size(), zoomFactor);
631    if (controlSize == NSSmallControlSize)
632        drawInfo.kind = kThemeIncDecButtonSmall;
633    else if (controlSize == NSMiniControlSize)
634        drawInfo.kind = kThemeIncDecButtonMini;
635    else
636        drawInfo.kind = kThemeIncDecButton;
637
638    IntRect rect(zoomedRect);
639    context->save();
640    if (zoomFactor != 1.0f) {
641        rect.setWidth(rect.width() / zoomFactor);
642        rect.setHeight(rect.height() / zoomFactor);
643        context->translate(rect.x(), rect.y());
644        context->scale(FloatSize(zoomFactor, zoomFactor));
645        context->translate(-rect.x(), -rect.y());
646    }
647    CGRect bounds(rect);
648    // Adjust 'bounds' so that HIThemeDrawButton(bounds,...) draws exactly on 'rect'.
649    CGRect backgroundBounds;
650    HIThemeGetButtonBackgroundBounds(&bounds, &drawInfo, &backgroundBounds);
651    if (bounds.origin.x != backgroundBounds.origin.x)
652        bounds.origin.x += bounds.origin.x - backgroundBounds.origin.x;
653    if (bounds.origin.y != backgroundBounds.origin.y)
654        bounds.origin.y += bounds.origin.y - backgroundBounds.origin.y;
655    HIThemeDrawButton(&bounds, &drawInfo, context->platformContext(), kHIThemeOrientationNormal, 0);
656    context->restore();
657}
658
659// Theme overrides
660
661int ThemeChromiumMac::baselinePositionAdjustment(ControlPart part) const
662{
663    if (part == CheckboxPart || part == RadioPart)
664        return -2;
665    return Theme::baselinePositionAdjustment(part);
666}
667
668FontDescription ThemeChromiumMac::controlFont(ControlPart part, const Font& font, float zoomFactor) const
669{
670    switch (part) {
671        case PushButtonPart: {
672            FontDescription fontDescription;
673            fontDescription.setIsAbsoluteSize(true);
674            fontDescription.setGenericFamily(FontDescription::SerifFamily);
675
676            NSFont* nsFont = [NSFont systemFontOfSize:[NSFont systemFontSizeForControlSize:controlSizeForFont(font)]];
677            fontDescription.firstFamily().setFamily([nsFont familyName]);
678            fontDescription.setComputedSize([nsFont pointSize] * zoomFactor);
679            fontDescription.setSpecifiedSize([nsFont pointSize] * zoomFactor);
680            return fontDescription;
681        }
682        default:
683            return Theme::controlFont(part, font, zoomFactor);
684    }
685}
686
687LengthSize ThemeChromiumMac::controlSize(ControlPart part, const Font& font, const LengthSize& zoomedSize, float zoomFactor) const
688{
689    switch (part) {
690        case CheckboxPart:
691            return checkboxSize(font, zoomedSize, zoomFactor);
692        case RadioPart:
693            return radioSize(font, zoomedSize, zoomFactor);
694        case PushButtonPart:
695            // Height is reset to auto so that specified heights can be ignored.
696            return sizeFromFont(font, LengthSize(zoomedSize.width(), Length()), zoomFactor, buttonSizes());
697#if ENABLE(DATALIST)
698        case ListButtonPart:
699            return sizeFromFont(font, LengthSize(zoomedSize.width(), Length()), zoomFactor, listButtonSizes());
700#endif
701        case InnerSpinButtonPart:
702            // We don't use inner spin buttons on Mac.
703            return LengthSize(Length(Fixed), Length(Fixed));
704        case OuterSpinButtonPart:
705            if (!zoomedSize.width().isIntrinsicOrAuto() && !zoomedSize.height().isIntrinsicOrAuto())
706                return zoomedSize;
707            return sizeFromNSControlSize(stepperControlSizeForFont(font), zoomedSize, zoomFactor, stepperSizes());
708        default:
709            return zoomedSize;
710    }
711}
712
713LengthSize ThemeChromiumMac::minimumControlSize(ControlPart part, const Font& font, float zoomFactor) const
714{
715    switch (part) {
716        case SquareButtonPart:
717        case DefaultButtonPart:
718        case ButtonPart:
719        case ListButtonPart:
720            return LengthSize(Length(0, Fixed), Length(static_cast<int>(15 * zoomFactor), Fixed));
721        case InnerSpinButtonPart:
722            // We don't use inner spin buttons on Mac.
723            return LengthSize(Length(Fixed), Length(Fixed));
724        case OuterSpinButtonPart: {
725            IntSize base = stepperSizes()[NSMiniControlSize];
726            return LengthSize(Length(static_cast<int>(base.width() * zoomFactor), Fixed),
727                              Length(static_cast<int>(base.height() * zoomFactor), Fixed));
728        }
729        default:
730            return Theme::minimumControlSize(part, font, zoomFactor);
731    }
732}
733
734LengthBox ThemeChromiumMac::controlBorder(ControlPart part, const Font& font, const LengthBox& zoomedBox, float zoomFactor) const
735{
736    switch (part) {
737        case SquareButtonPart:
738        case DefaultButtonPart:
739        case ButtonPart:
740        case ListButtonPart:
741            return LengthBox(0, zoomedBox.right().value(), 0, zoomedBox.left().value());
742        default:
743            return Theme::controlBorder(part, font, zoomedBox, zoomFactor);
744    }
745}
746
747LengthBox ThemeChromiumMac::controlPadding(ControlPart part, const Font& font, const LengthBox& zoomedBox, float zoomFactor) const
748{
749    switch (part) {
750        case PushButtonPart: {
751            // Just use 8px.  AppKit wants to use 11px for mini buttons, but that padding is just too large
752            // for real-world Web sites (creating a huge necessary minimum width for buttons whose space is
753            // by definition constrained, since we select mini only for small cramped environments.
754            // This also guarantees the HTML <button> will match our rendering by default, since we're using a consistent
755            // padding.
756            const int padding = 8 * zoomFactor;
757            return LengthBox(0, padding, 0, padding);
758        }
759        default:
760            return Theme::controlPadding(part, font, zoomedBox, zoomFactor);
761    }
762}
763
764void ThemeChromiumMac::inflateControlPaintRect(ControlPart part, ControlStates states, IntRect& zoomedRect, float zoomFactor) const
765{
766    BEGIN_BLOCK_OBJC_EXCEPTIONS
767    switch (part) {
768        case CheckboxPart: {
769            // We inflate the rect as needed to account for padding included in the cell to accommodate the checkbox
770            // shadow" and the check.  We don't consider this part of the bounds of the control in WebKit.
771            NSCell *cell = checkbox(states, zoomedRect, zoomFactor);
772            NSControlSize controlSize = [cell controlSize];
773            IntSize zoomedSize = checkboxSizes()[controlSize];
774            zoomedSize.setHeight(zoomedSize.height() * zoomFactor);
775            zoomedSize.setWidth(zoomedSize.width() * zoomFactor);
776            zoomedRect = inflateRect(zoomedRect, zoomedSize, checkboxMargins(controlSize), zoomFactor);
777            break;
778        }
779        case RadioPart: {
780            // We inflate the rect as needed to account for padding included in the cell to accommodate the radio button
781            // shadow".  We don't consider this part of the bounds of the control in WebKit.
782            NSCell *cell = radio(states, zoomedRect, zoomFactor);
783            NSControlSize controlSize = [cell controlSize];
784            IntSize zoomedSize = radioSizes()[controlSize];
785            zoomedSize.setHeight(zoomedSize.height() * zoomFactor);
786            zoomedSize.setWidth(zoomedSize.width() * zoomFactor);
787            zoomedRect = inflateRect(zoomedRect, zoomedSize, radioMargins(controlSize), zoomFactor);
788            break;
789        }
790        case PushButtonPart:
791        case DefaultButtonPart:
792        case ButtonPart: {
793            NSButtonCell *cell = button(part, states, zoomedRect, zoomFactor);
794            NSControlSize controlSize = [cell controlSize];
795
796            // We inflate the rect as needed to account for the Aqua button's shadow.
797            if ([cell bezelStyle] == NSRoundedBezelStyle) {
798                IntSize zoomedSize = buttonSizes()[controlSize];
799                zoomedSize.setHeight(zoomedSize.height() * zoomFactor);
800                zoomedSize.setWidth(zoomedRect.width()); // Buttons don't ever constrain width, so the zoomed width can just be honored.
801                zoomedRect = inflateRect(zoomedRect, zoomedSize, buttonMargins(controlSize), zoomFactor);
802            }
803            break;
804        }
805        case OuterSpinButtonPart: {
806            static const int stepperMargin[4] = { 0, 0, 0, 0 };
807            ControlSize controlSize = controlSizeFromPixelSize(stepperSizes(), zoomedRect.size(), zoomFactor);
808            IntSize zoomedSize = stepperSizes()[controlSize];
809            zoomedSize.setHeight(zoomedSize.height() * zoomFactor);
810            zoomedSize.setWidth(zoomedSize.width() * zoomFactor);
811            zoomedRect = inflateRect(zoomedRect, zoomedSize, stepperMargin, zoomFactor);
812            break;
813        }
814        default:
815            break;
816    }
817    END_BLOCK_OBJC_EXCEPTIONS
818}
819
820void ThemeChromiumMac::paint(ControlPart part, ControlStates states, GraphicsContext* context, const IntRect& zoomedRect, float zoomFactor, ScrollView* scrollView) const
821{
822    switch (part) {
823        case CheckboxPart:
824            paintCheckbox(states, context, zoomedRect, zoomFactor, scrollView);
825            break;
826        case RadioPart:
827            paintRadio(states, context, zoomedRect, zoomFactor, scrollView);
828            break;
829        case PushButtonPart:
830        case DefaultButtonPart:
831        case ButtonPart:
832        case SquareButtonPart:
833        case ListButtonPart:
834            paintButton(part, states, context, zoomedRect, zoomFactor, scrollView);
835            break;
836        case OuterSpinButtonPart:
837            paintStepper(states, context, zoomedRect, zoomFactor, scrollView);
838            break;
839        default:
840            break;
841    }
842}
843
844}
845