| /* |
| * 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 "third_party/blink/renderer/platform/mac/theme_mac.h" |
| |
| #import <Carbon/Carbon.h> |
| #import "third_party/blink/renderer/platform/graphics/graphics_context_state_saver.h" |
| #import "third_party/blink/renderer/platform/mac/block_exceptions.h" |
| #import "third_party/blink/renderer/platform/mac/local_current_graphics_context.h" |
| #import "third_party/blink/renderer/platform/mac/version_util_mac.h" |
| #import "third_party/blink/renderer/platform/mac/web_core_ns_cell_extras.h" |
| #import "third_party/blink/renderer/platform/scroll/scrollable_area.h" |
| #include "third_party/blink/renderer/platform/wtf/std_lib_extras.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, theme_mac, ()); |
| return &theme_mac; |
| } |
| |
| // Helper functions used by a bunch of different control parts. |
| |
| static NSControlSize ControlSizeForFont( |
| const FontDescription& font_description) { |
| int font_size = font_description.ComputedPixelSize(); |
| if (font_size >= 16) |
| return NSRegularControlSize; |
| if (font_size >= 11) |
| return NSSmallControlSize; |
| return NSMiniControlSize; |
| } |
| |
| static LengthSize SizeFromNSControlSize(NSControlSize ns_control_size, |
| const LengthSize& zoomed_size, |
| float zoom_factor, |
| const IntSize* sizes) { |
| IntSize control_size = sizes[ns_control_size]; |
| if (zoom_factor != 1.0f) |
| control_size = IntSize(control_size.Width() * zoom_factor, |
| control_size.Height() * zoom_factor); |
| LengthSize result = zoomed_size; |
| if (zoomed_size.Width().IsIntrinsicOrAuto() && control_size.Width() > 0) |
| result.SetWidth(Length(control_size.Width(), kFixed)); |
| if (zoomed_size.Height().IsIntrinsicOrAuto() && control_size.Height() > 0) |
| result.SetHeight(Length(control_size.Height(), kFixed)); |
| return result; |
| } |
| |
| static LengthSize SizeFromFont(const FontDescription& font_description, |
| const LengthSize& zoomed_size, |
| float zoom_factor, |
| const IntSize* sizes) { |
| return SizeFromNSControlSize(ControlSizeForFont(font_description), |
| zoomed_size, zoom_factor, sizes); |
| } |
| |
| NSControlSize ThemeMac::ControlSizeFromPixelSize(const IntSize* sizes, |
| const IntSize& min_zoomed_size, |
| float zoom_factor) { |
| if (min_zoomed_size.Width() >= |
| static_cast<int>(sizes[NSRegularControlSize].Width() * zoom_factor) && |
| min_zoomed_size.Height() >= |
| static_cast<int>(sizes[NSRegularControlSize].Height() * zoom_factor)) |
| return NSRegularControlSize; |
| if (min_zoomed_size.Width() >= |
| static_cast<int>(sizes[NSSmallControlSize].Width() * zoom_factor) && |
| min_zoomed_size.Height() >= |
| static_cast<int>(sizes[NSSmallControlSize].Height() * zoom_factor)) |
| return NSSmallControlSize; |
| return NSMiniControlSize; |
| } |
| |
| static void SetControlSize(NSCell* cell, |
| const IntSize* sizes, |
| const IntSize& min_zoomed_size, |
| float zoom_factor) { |
| ControlSize size = |
| ThemeMac::ControlSizeFromPixelSize(sizes, min_zoomed_size, zoom_factor); |
| // 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 old_pressed = [cell isHighlighted]; |
| bool pressed = states & kPressedControlState; |
| if (pressed != old_pressed) |
| [cell setHighlighted:pressed]; |
| |
| // Enabled state |
| bool old_enabled = [cell isEnabled]; |
| bool enabled = states & kEnabledControlState; |
| if (enabled != old_enabled) |
| [cell setEnabled:enabled]; |
| |
| // Checked and Indeterminate |
| bool old_indeterminate = [cell state] == NSMixedState; |
| bool indeterminate = (states & kIndeterminateControlState); |
| bool checked = states & kCheckedControlState; |
| bool old_checked = [cell state] == NSOnState; |
| if (old_indeterminate != indeterminate || checked != old_checked) |
| [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(const IntSize& size) { |
| // Use a fake flipped view. |
| static NSView* flipped_view = [[BlinkFlippedControl alloc] init]; |
| [flipped_view setFrameSize:NSSizeFromCGSize(CGSize(size))]; |
| |
| return flipped_view; |
| } |
| |
| // static |
| IntRect ThemeMac::InflateRect(const IntRect& zoomed_rect, |
| const IntSize& zoomed_size, |
| const int* margins, |
| float zoom_factor) { |
| // 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 width_delta = zoomed_rect.Width() - |
| (zoomed_size.Width() + margins[kLeftMargin] * zoom_factor + |
| margins[kRightMargin] * zoom_factor); |
| int height_delta = zoomed_rect.Height() - |
| (zoomed_size.Height() + margins[kTopMargin] * zoom_factor + |
| margins[kBottomMargin] * zoom_factor); |
| IntRect result(zoomed_rect); |
| if (width_delta < 0) { |
| result.SetX(result.X() - margins[kLeftMargin] * zoom_factor); |
| result.SetWidth(result.Width() - width_delta); |
| } |
| if (height_delta < 0) { |
| result.SetY(result.Y() - margins[kTopMargin] * zoom_factor); |
| result.SetHeight(result.Height() - height_delta); |
| } |
| return result; |
| } |
| |
| // static |
| IntRect ThemeMac::InflateRectForAA(const IntRect& rect) { |
| const int kMargin = 2; |
| return IntRect(rect.X() - kMargin, rect.Y() - kMargin, |
| rect.Width() + 2 * kMargin, rect.Height() + 2 * kMargin); |
| } |
| |
| // 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 kMargin = 16; |
| IntRect result; |
| result.SetX(rect.X() - kMargin); |
| result.SetY(rect.Y() - kMargin); |
| result.SetWidth(rect.Width() + 2 * kMargin); |
| result.SetHeight(rect.Height() + 2 * kMargin); |
| return result; |
| } |
| |
| // Checkboxes |
| |
| const IntSize* ThemeMac::CheckboxSizes() { |
| static const IntSize kSizes[3] = {IntSize(14, 14), IntSize(12, 12), |
| IntSize(10, 10)}; |
| return kSizes; |
| } |
| |
| const int* ThemeMac::CheckboxMargins(NSControlSize control_size) { |
| static const int kMargins[3][4] = { |
| {3, 4, 4, 2}, {4, 3, 3, 3}, {4, 3, 3, 3}, |
| }; |
| return kMargins[control_size]; |
| } |
| |
| LengthSize ThemeMac::CheckboxSize(const FontDescription& font_description, |
| const LengthSize& zoomed_size, |
| float zoom_factor) { |
| // If the width and height are both specified, then we have nothing to do. |
| if (!zoomed_size.Width().IsIntrinsicOrAuto() && |
| !zoomed_size.Height().IsIntrinsicOrAuto()) |
| return zoomed_size; |
| |
| // Use the font size to determine the intrinsic width of the control. |
| return SizeFromFont(font_description, zoomed_size, zoom_factor, |
| CheckboxSizes()); |
| } |
| |
| NSButtonCell* ThemeMac::Checkbox(ControlStates states, |
| const IntRect& zoomed_rect, |
| float zoom_factor) { |
| static NSButtonCell* checkbox_cell; |
| if (!checkbox_cell) { |
| checkbox_cell = [[NSButtonCell alloc] init]; |
| [checkbox_cell setButtonType:NSSwitchButton]; |
| [checkbox_cell setTitle:nil]; |
| [checkbox_cell setAllowsMixedState:YES]; |
| [checkbox_cell setFocusRingType:NSFocusRingTypeExterior]; |
| } |
| |
| // Set the control size based off the rectangle we're painting into. |
| SetControlSize(checkbox_cell, CheckboxSizes(), zoomed_rect.Size(), |
| zoom_factor); |
| |
| // Update the various states we respond to. |
| UpdateStates(checkbox_cell, states); |
| |
| return checkbox_cell; |
| } |
| |
| const IntSize* ThemeMac::RadioSizes() { |
| static const IntSize kSizes[3] = {IntSize(14, 15), IntSize(12, 13), |
| IntSize(10, 10)}; |
| return kSizes; |
| } |
| |
| const int* ThemeMac::RadioMargins(NSControlSize control_size) { |
| static const int kMargins[3][4] = { |
| {2, 2, 4, 2}, {3, 2, 3, 2}, {1, 0, 2, 0}, |
| }; |
| return kMargins[control_size]; |
| } |
| |
| LengthSize ThemeMac::RadioSize(const FontDescription& font_description, |
| const LengthSize& zoomed_size, |
| float zoom_factor) { |
| // If the width and height are both specified, then we have nothing to do. |
| if (!zoomed_size.Width().IsIntrinsicOrAuto() && |
| !zoomed_size.Height().IsIntrinsicOrAuto()) |
| return zoomed_size; |
| |
| // Use the font size to determine the intrinsic width of the control. |
| return SizeFromFont(font_description, zoomed_size, zoom_factor, RadioSizes()); |
| } |
| |
| NSButtonCell* ThemeMac::Radio(ControlStates states, |
| const IntRect& zoomed_rect, |
| float zoom_factor) { |
| static NSButtonCell* radio_cell; |
| if (!radio_cell) { |
| radio_cell = [[NSButtonCell alloc] init]; |
| [radio_cell setButtonType:NSRadioButton]; |
| [radio_cell setTitle:nil]; |
| [radio_cell setFocusRingType:NSFocusRingTypeExterior]; |
| } |
| |
| // Set the control size based off the rectangle we're painting into. |
| SetControlSize(radio_cell, RadioSizes(), zoomed_rect.Size(), zoom_factor); |
| |
| // Update the various states we respond to. |
| // Cocoa draws NSMixedState NSRadioButton as NSOnState so we don't want that. |
| states &= ~kIndeterminateControlState; |
| UpdateStates(radio_cell, states); |
| |
| return radio_cell; |
| } |
| |
| // Buttons really only constrain height. They respect width. |
| const IntSize* ThemeMac::ButtonSizes() { |
| static const IntSize kSizes[3] = {IntSize(0, 21), IntSize(0, 18), |
| IntSize(0, 15)}; |
| return kSizes; |
| } |
| |
| const int* ThemeMac::ButtonMargins(NSControlSize control_size) { |
| static const int kMargins[3][4] = { |
| {4, 6, 7, 6}, {4, 5, 6, 5}, {0, 1, 1, 1}, |
| }; |
| return kMargins[control_size]; |
| } |
| |
| static void SetUpButtonCell(NSButtonCell* cell, |
| ControlPart part, |
| ControlStates states, |
| const IntRect& zoomed_rect, |
| float zoom_factor) { |
| // Set the control size based off the rectangle we're painting into. |
| const IntSize* sizes = ThemeMac::ButtonSizes(); |
| if (part == kSquareButtonPart || |
| zoomed_rect.Height() > |
| ThemeMac::ButtonSizes()[NSRegularControlSize].Height() * |
| zoom_factor) { |
| // Use the square button |
| if ([cell bezelStyle] != NSShadowlessSquareBezelStyle) |
| [cell setBezelStyle:NSShadowlessSquareBezelStyle]; |
| } else if ([cell bezelStyle] != NSRoundedBezelStyle) |
| [cell setBezelStyle:NSRoundedBezelStyle]; |
| |
| SetControlSize(cell, sizes, zoomed_rect.Size(), zoom_factor); |
| |
| // Update the various states we respond to. |
| UpdateStates(cell, states); |
| } |
| |
| NSButtonCell* ThemeMac::Button(ControlPart part, |
| ControlStates states, |
| const IntRect& zoomed_rect, |
| float zoom_factor) { |
| static NSButtonCell* cell = nil; |
| if (!cell) { |
| cell = [[NSButtonCell alloc] init]; |
| [cell setTitle:nil]; |
| [cell setButtonType:NSMomentaryPushInButton]; |
| } |
| SetUpButtonCell(cell, part, states, zoomed_rect, zoom_factor); |
| return cell; |
| } |
| |
| const IntSize* ThemeMac::StepperSizes() { |
| static const IntSize kSizes[3] = {IntSize(19, 27), IntSize(15, 22), |
| IntSize(13, 15)}; |
| return kSizes; |
| } |
| |
| // 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& font_description) { |
| int font_size = font_description.ComputedPixelSize(); |
| if (font_size >= 27) |
| return NSRegularControlSize; |
| if (font_size >= 22) |
| return NSSmallControlSize; |
| return NSMiniControlSize; |
| } |
| |
| // Theme overrides |
| |
| int ThemeMac::BaselinePositionAdjustment(ControlPart part) const { |
| if (part == kCheckboxPart || part == kRadioPart) |
| return -2; |
| return Theme::BaselinePositionAdjustment(part); |
| } |
| |
| FontDescription ThemeMac::ControlFont(ControlPart part, |
| const FontDescription& font_description, |
| float zoom_factor) const { |
| switch (part) { |
| case kPushButtonPart: { |
| FontDescription result; |
| result.SetIsAbsoluteSize(true); |
| result.SetGenericFamily(FontDescription::kSerifFamily); |
| |
| NSFont* ns_font = [NSFont |
| systemFontOfSize:[NSFont systemFontSizeForControlSize: |
| ControlSizeForFont(font_description)]]; |
| result.FirstFamily().SetFamily(FontFamilyNames::system_ui); |
| result.SetComputedSize([ns_font pointSize] * zoom_factor); |
| result.SetSpecifiedSize([ns_font pointSize] * zoom_factor); |
| return result; |
| } |
| default: |
| return Theme::ControlFont(part, font_description, zoom_factor); |
| } |
| } |
| |
| LengthSize ThemeMac::GetControlSize(ControlPart part, |
| const FontDescription& font_description, |
| const LengthSize& zoomed_size, |
| float zoom_factor) const { |
| switch (part) { |
| case kCheckboxPart: |
| return CheckboxSize(font_description, zoomed_size, zoom_factor); |
| case kRadioPart: |
| return RadioSize(font_description, zoomed_size, zoom_factor); |
| case kPushButtonPart: |
| // Height is reset to auto so that specified heights can be ignored. |
| return SizeFromFont(font_description, |
| LengthSize(zoomed_size.Width(), Length()), |
| zoom_factor, ButtonSizes()); |
| case kInnerSpinButtonPart: |
| if (!zoomed_size.Width().IsIntrinsicOrAuto() && |
| !zoomed_size.Height().IsIntrinsicOrAuto()) |
| return zoomed_size; |
| return SizeFromNSControlSize(StepperControlSizeForFont(font_description), |
| zoomed_size, zoom_factor, StepperSizes()); |
| default: |
| return zoomed_size; |
| } |
| } |
| |
| LengthSize ThemeMac::MinimumControlSize(ControlPart part, |
| const FontDescription& font_description, |
| float zoom_factor) const { |
| switch (part) { |
| case kSquareButtonPart: |
| case kButtonPart: |
| return LengthSize(Length(0, kFixed), |
| Length(static_cast<int>(15 * zoom_factor), kFixed)); |
| case kInnerSpinButtonPart: { |
| IntSize base = StepperSizes()[NSMiniControlSize]; |
| return LengthSize( |
| Length(static_cast<int>(base.Width() * zoom_factor), kFixed), |
| Length(static_cast<int>(base.Height() * zoom_factor), kFixed)); |
| } |
| default: |
| return Theme::MinimumControlSize(part, font_description, zoom_factor); |
| } |
| } |
| |
| LengthBox ThemeMac::ControlBorder(ControlPart part, |
| const FontDescription& font_description, |
| const LengthBox& zoomed_box, |
| float zoom_factor) const { |
| switch (part) { |
| case kSquareButtonPart: |
| return LengthBox(0, zoomed_box.Right().Value(), 0, |
| zoomed_box.Left().Value()); |
| default: |
| return Theme::ControlBorder(part, font_description, zoomed_box, |
| zoom_factor); |
| } |
| } |
| |
| LengthBox ThemeMac::ControlPadding(ControlPart part, |
| const FontDescription& font_description, |
| const Length& zoomed_box_top, |
| const Length& zoomed_box_right, |
| const Length& zoomed_box_bottom, |
| const Length& zoomed_box_left, |
| float zoom_factor) const { |
| switch (part) { |
| case kPushButtonPart: { |
| // 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 * zoom_factor; |
| return LengthBox(2, padding, 3, padding); |
| } |
| default: |
| return Theme::ControlPadding(part, font_description, zoomed_box_top, |
| zoomed_box_right, zoomed_box_bottom, |
| zoomed_box_left, zoom_factor); |
| } |
| } |
| |
| void ThemeMac::AddVisualOverflow(ControlPart part, |
| ControlStates states, |
| float zoom_factor, |
| IntRect& zoomed_rect) const { |
| BEGIN_BLOCK_OBJC_EXCEPTIONS |
| switch (part) { |
| case kCheckboxPart: { |
| // 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, zoomed_rect, zoom_factor); |
| NSControlSize control_size = [cell controlSize]; |
| IntSize zoomed_size = CheckboxSizes()[control_size]; |
| zoomed_size.SetHeight(zoomed_size.Height() * zoom_factor); |
| zoomed_size.SetWidth(zoomed_size.Width() * zoom_factor); |
| zoomed_rect = InflateRect(zoomed_rect, zoomed_size, |
| CheckboxMargins(control_size), zoom_factor); |
| break; |
| } |
| case kRadioPart: { |
| // 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, zoomed_rect, zoom_factor); |
| NSControlSize control_size = [cell controlSize]; |
| IntSize zoomed_size = RadioSizes()[control_size]; |
| zoomed_size.SetHeight(zoomed_size.Height() * zoom_factor); |
| zoomed_size.SetWidth(zoomed_size.Width() * zoom_factor); |
| zoomed_rect = InflateRect(zoomed_rect, zoomed_size, |
| RadioMargins(control_size), zoom_factor); |
| break; |
| } |
| case kPushButtonPart: |
| case kButtonPart: { |
| NSButtonCell* cell = Button(part, states, zoomed_rect, zoom_factor); |
| NSControlSize control_size = [cell controlSize]; |
| |
| // We inflate the rect as needed to account for the Aqua button's shadow. |
| if ([cell bezelStyle] == NSRoundedBezelStyle) { |
| IntSize zoomed_size = ButtonSizes()[control_size]; |
| zoomed_size.SetHeight(zoomed_size.Height() * zoom_factor); |
| // Buttons don't ever constrain width, so the zoomed width can just be |
| // honored. |
| zoomed_size.SetWidth(zoomed_rect.Width()); |
| zoomed_rect = InflateRect(zoomed_rect, zoomed_size, |
| ButtonMargins(control_size), zoom_factor); |
| } |
| break; |
| } |
| case kInnerSpinButtonPart: { |
| static const int kStepperMargin[4] = {0, 0, 0, 0}; |
| ControlSize control_size = ControlSizeFromPixelSize( |
| StepperSizes(), zoomed_rect.Size(), zoom_factor); |
| IntSize zoomed_size = StepperSizes()[control_size]; |
| zoomed_size.SetHeight(zoomed_size.Height() * zoom_factor); |
| zoomed_size.SetWidth(zoomed_size.Width() * zoom_factor); |
| zoomed_rect = |
| InflateRect(zoomed_rect, zoomed_size, kStepperMargin, zoom_factor); |
| break; |
| } |
| default: |
| break; |
| } |
| END_BLOCK_OBJC_EXCEPTIONS |
| } |
| } |