/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "nsNativeThemeCocoa.h" #include "mozilla/gfx/2D.h" #include "mozilla/gfx/Helpers.h" #include "nsChildView.h" #include "nsDeviceContext.h" #include "nsLayoutUtils.h" #include "nsObjCExceptions.h" #include "nsNumberControlFrame.h" #include "nsRangeFrame.h" #include "nsRenderingContext.h" #include "nsRect.h" #include "nsSize.h" #include "nsThemeConstants.h" #include "nsIPresShell.h" #include "nsPresContext.h" #include "nsIContent.h" #include "nsIDocument.h" #include "nsIFrame.h" #include "nsIAtom.h" #include "nsNameSpaceManager.h" #include "nsPresContext.h" #include "nsGkAtoms.h" #include "nsCocoaFeatures.h" #include "nsCocoaWindow.h" #include "nsNativeThemeColors.h" #include "nsIScrollableFrame.h" #include "mozilla/EventStates.h" #include "mozilla/dom/Element.h" #include "mozilla/dom/HTMLMeterElement.h" #include "nsLookAndFeel.h" #include "VibrancyManager.h" #include "gfxContext.h" #include "gfxQuartzSurface.h" #include "gfxQuartzNativeDrawing.h" #include using namespace mozilla; using namespace mozilla::gfx; using mozilla::dom::HTMLMeterElement; #define DRAW_IN_FRAME_DEBUG 0 #define SCROLLBARS_VISUAL_DEBUG 0 // private Quartz routines needed here extern "C" { CG_EXTERN void CGContextSetCTM(CGContextRef, CGAffineTransform); CG_EXTERN void CGContextSetBaseCTM(CGContextRef, CGAffineTransform); typedef CFTypeRef CUIRendererRef; void CUIDraw(CUIRendererRef r, CGRect rect, CGContextRef ctx, CFDictionaryRef options, CFDictionaryRef* result); } // Workaround for NSCell control tint drawing // Without this workaround, NSCells are always drawn with the clear control tint // as long as they're not attached to an NSControl which is a subview of an active window. // XXXmstange Why doesn't Webkit need this? @implementation NSCell (ControlTintWorkaround) - (int)_realControlTint { return [self controlTint]; } @end // The purpose of this class is to provide objects that can be used when drawing // NSCells using drawWithFrame:inView: without causing any harm. The only // messages that will be sent to such an object are "isFlipped" and // "currentEditor": isFlipped needs to return YES in order to avoid drawing bugs // on 10.4 (see bug 465069); currentEditor (which isn't even a method of // NSView) will be called when drawing search fields, and we only provide it in // order to prevent "unrecognized selector" exceptions. // There's no need to pass the actual NSView that we're drawing into to // drawWithFrame:inView:. What's more, doing so even causes unnecessary // invalidations as soon as we draw a focusring! @interface CellDrawView : NSView @end; @implementation CellDrawView - (BOOL)isFlipped { return YES; } - (NSText*)currentEditor { return nil; } @end // These two classes don't actually add any behavior over NSButtonCell. Their // purpose is to make it easy to distinguish NSCell objects that are used for // drawing radio buttons / checkboxes from other cell types. // The class names are made up, there are no classes with these names in AppKit. // The reason we need them is that calling [cell setButtonType:NSRadioButton] // doesn't leave an easy-to-check "marker" on the cell object - there is no // -[NSButtonCell buttonType] method. @interface RadioButtonCell : NSButtonCell @end; @implementation RadioButtonCell @end; @interface CheckboxCell : NSButtonCell @end; @implementation CheckboxCell @end; static void DrawFocusRingForCellIfNeeded(NSCell* aCell, NSRect aWithFrame, NSView* aInView) { if ([aCell showsFirstResponder]) { CGContextRef cgContext = (CGContextRef)[[NSGraphicsContext currentContext] graphicsPort]; CGContextSaveGState(cgContext); // It's important to set the focus ring style before we enter the // transparency layer so that the transparency layer only contains // the normal button mask without the focus ring, and the conversion // to the focus ring shape happens only when the transparency layer is // ended. NSSetFocusRingStyle(NSFocusRingOnly); // We need to draw the whole button into a transparency layer because // many button types are composed of multiple parts, and if these parts // were drawn while the focus ring style was active, each individual part // would produce a focus ring for itself. But we only want one focus ring // for the whole button. The transparency layer is a way to merge the // individual button parts together before the focus ring shape is // calculated. CGContextBeginTransparencyLayerWithRect(cgContext, NSRectToCGRect(aWithFrame), 0); [aCell drawFocusRingMaskWithFrame:aWithFrame inView:aInView]; CGContextEndTransparencyLayer(cgContext); CGContextRestoreGState(cgContext); } } static bool FocusIsDrawnByDrawWithFrame(NSCell* aCell) { #if defined(MAC_OS_X_VERSION_10_8) && MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_8 // When building with the 10.8 SDK or higher, focus rings don't draw as part // of -[NSCell drawWithFrame:inView:] and must be drawn by a separate call // to -[NSCell drawFocusRingMaskWithFrame:inView:]; . // See the NSButtonCell section under // https://developer.apple.com/library/mac/releasenotes/AppKit/RN-AppKitOlderNotes/#X10_8Notes return false; #else if (!nsCocoaFeatures::OnYosemiteOrLater()) { // When building with the 10.7 SDK or lower, focus rings always draw as // part of -[NSCell drawWithFrame:inView:] if the build is run on 10.9 or // lower. return true; } // On 10.10, whether the focus ring is drawn as part of // -[NSCell drawWithFrame:inView:] depends on the cell type. // Radio buttons and checkboxes draw their own focus rings, other cell // types need -[NSCell drawFocusRingMaskWithFrame:inView:]. return [aCell isKindOfClass:[RadioButtonCell class]] || [aCell isKindOfClass:[CheckboxCell class]]; #endif } static void DrawCellIncludingFocusRing(NSCell* aCell, NSRect aWithFrame, NSView* aInView) { [aCell drawWithFrame:aWithFrame inView:aInView]; if (!FocusIsDrawnByDrawWithFrame(aCell)) { DrawFocusRingForCellIfNeeded(aCell, aWithFrame, aInView); } } /** * NSProgressBarCell is used to draw progress bars of any size. */ @interface NSProgressBarCell : NSCell { /*All instance variables are private*/ double mValue; double mMax; bool mIsIndeterminate; bool mIsHorizontal; } - (void)setValue:(double)value; - (double)value; - (void)setMax:(double)max; - (double)max; - (void)setIndeterminate:(bool)aIndeterminate; - (bool)isIndeterminate; - (void)setHorizontal:(bool)aIsHorizontal; - (bool)isHorizontal; - (void)drawWithFrame:(NSRect)cellFrame inView:(NSView *)controlView; @end @implementation NSProgressBarCell - (void)setMax:(double)aMax { mMax = aMax; } - (double)max { return mMax; } - (void)setValue:(double)aValue { mValue = aValue; } - (double)value { return mValue; } - (void)setIndeterminate:(bool)aIndeterminate { mIsIndeterminate = aIndeterminate; } - (bool)isIndeterminate { return mIsIndeterminate; } - (void)setHorizontal:(bool)aIsHorizontal { mIsHorizontal = aIsHorizontal; } - (bool)isHorizontal { return mIsHorizontal; } - (void)drawWithFrame:(NSRect)cellFrame inView:(NSView *)controlView { CGContext* cgContext = (CGContextRef)[[NSGraphicsContext currentContext] graphicsPort]; HIThemeTrackDrawInfo tdi; tdi.version = 0; tdi.min = 0; tdi.value = INT32_MAX * (mValue / mMax); tdi.max = INT32_MAX; tdi.bounds = NSRectToCGRect(cellFrame); tdi.attributes = mIsHorizontal ? kThemeTrackHorizontal : 0; tdi.enableState = [self controlTint] == NSClearControlTint ? kThemeTrackInactive : kThemeTrackActive; NSControlSize size = [self controlSize]; if (size == NSRegularControlSize) { tdi.kind = mIsIndeterminate ? kThemeLargeIndeterminateBar : kThemeLargeProgressBar; } else { NS_ASSERTION(size == NSSmallControlSize, "We shouldn't have another size than small and regular for the moment"); tdi.kind = mIsIndeterminate ? kThemeMediumIndeterminateBar : kThemeMediumProgressBar; } int32_t stepsPerSecond = mIsIndeterminate ? 60 : 30; int32_t milliSecondsPerStep = 1000 / stepsPerSecond; tdi.trackInfo.progress.phase = uint8_t(PR_IntervalToMilliseconds(PR_IntervalNow()) / milliSecondsPerStep); HIThemeDrawTrack(&tdi, NULL, cgContext, kHIThemeOrientationNormal); } @end @interface ContextAwareSearchFieldCell : NSSearchFieldCell { nsIFrame* mContext; } // setContext: stores the searchfield nsIFrame so that it can be consulted // during painting. Please reset this by calling setContext:nullptr as soon as // you're done with painting because we don't want to keep a dangling pointer. - (void)setContext:(nsIFrame*)aContext; @end @implementation ContextAwareSearchFieldCell - (id)initTextCell:(NSString*)aString { if ((self = [super initTextCell:aString])) { mContext = nullptr; } return self; } - (void)setContext:(nsIFrame*)aContext { mContext = aContext; } static BOOL IsToolbarStyleContainer(nsIFrame* aFrame) { nsIContent* content = aFrame->GetContent(); if (!content) return NO; if (content->IsAnyOfXULElements(nsGkAtoms::toolbar, nsGkAtoms::toolbox, nsGkAtoms::statusbar)) return YES; switch (aFrame->StyleDisplay()->mAppearance) { case NS_THEME_TOOLBAR: case NS_THEME_STATUSBAR: return YES; default: return NO; } } - (BOOL)_isToolbarMode { // On 10.7, searchfields have two different styles, depending on whether // the searchfield is on top of of window chrome. This function is called on // 10.7 during drawing in order to determine which style to use. for (nsIFrame* frame = mContext; frame; frame = frame->GetParent()) { if (IsToolbarStyleContainer(frame)) { return YES; } } return NO; } @end // Workaround for Bug 542048 // On 64-bit, NSSearchFieldCells don't draw focus rings. #if defined(__x86_64__) @interface SearchFieldCellWithFocusRing : ContextAwareSearchFieldCell {} @end @implementation SearchFieldCellWithFocusRing - (void)drawWithFrame:(NSRect)rect inView:(NSView*)controlView { [super drawWithFrame:rect inView:controlView]; if (FocusIsDrawnByDrawWithFrame(self)) { // For some reason, -[NSSearchFieldCell drawWithFrame:inView] doesn't draw a // focus ring in 64 bit mode, no matter what SDK is used or what OS X version // we're running on. But if FocusIsDrawnByDrawWithFrame(self), then our // caller expects us to draw a focus ring. So we just do that here. DrawFocusRingForCellIfNeeded(self, rect, controlView); } } - (void)drawFocusRingMaskWithFrame:(NSRect)rect inView:(NSView*)controlView { // By default this draws nothing. I don't know why. // We just draw the search field again. It's a great mask shape for its own // focus ring. [super drawWithFrame:rect inView:controlView]; } @end #endif #define HITHEME_ORIENTATION kHIThemeOrientationNormal static CGFloat kMaxFocusRingWidth = 0; // initialized by the nsNativeThemeCocoa constructor // These enums are for indexing into the margin array. enum { leopardOSorlater = 0, // 10.6 - 10.9 yosemiteOSorlater = 1 // 10.10+ }; enum { miniControlSize, smallControlSize, regularControlSize }; enum { leftMargin, topMargin, rightMargin, bottomMargin }; static size_t EnumSizeForCocoaSize(NSControlSize cocoaControlSize) { if (cocoaControlSize == NSMiniControlSize) return miniControlSize; else if (cocoaControlSize == NSSmallControlSize) return smallControlSize; else return regularControlSize; } static NSControlSize CocoaSizeForEnum(int32_t enumControlSize) { if (enumControlSize == miniControlSize) return NSMiniControlSize; else if (enumControlSize == smallControlSize) return NSSmallControlSize; else return NSRegularControlSize; } static NSString* CUIControlSizeForCocoaSize(NSControlSize aControlSize) { if (aControlSize == NSRegularControlSize) return @"regular"; else if (aControlSize == NSSmallControlSize) return @"small"; else return @"mini"; } static void InflateControlRect(NSRect* rect, NSControlSize cocoaControlSize, const float marginSet[][3][4]) { if (!marginSet) return; static int osIndex = nsCocoaFeatures::OnYosemiteOrLater() ? yosemiteOSorlater : leopardOSorlater; size_t controlSize = EnumSizeForCocoaSize(cocoaControlSize); const float* buttonMargins = marginSet[osIndex][controlSize]; rect->origin.x -= buttonMargins[leftMargin]; rect->origin.y -= buttonMargins[bottomMargin]; rect->size.width += buttonMargins[leftMargin] + buttonMargins[rightMargin]; rect->size.height += buttonMargins[bottomMargin] + buttonMargins[topMargin]; } static ChildView* ChildViewForFrame(nsIFrame* aFrame) { if (!aFrame) return nil; nsIWidget* widget = aFrame->GetNearestWidget(); if (!widget) return nil; NSWindow* window = (NSWindow*)widget->GetNativeData(NS_NATIVE_WINDOW); return [window isKindOfClass:[BaseWindow class]] ? [(BaseWindow*)window mainChildView] : nil; } static NSWindow* NativeWindowForFrame(nsIFrame* aFrame, nsIWidget** aTopLevelWidget = NULL) { if (!aFrame) return nil; nsIWidget* widget = aFrame->GetNearestWidget(); if (!widget) return nil; nsIWidget* topLevelWidget = widget->GetTopLevelWidget(); if (aTopLevelWidget) *aTopLevelWidget = topLevelWidget; return (NSWindow*)topLevelWidget->GetNativeData(NS_NATIVE_WINDOW); } static NSSize WindowButtonsSize(nsIFrame* aFrame) { NSWindow* window = NativeWindowForFrame(aFrame); if (!window) { // Return fallback values. return NSMakeSize(54, 16); } NSRect buttonBox = NSZeroRect; NSButton* closeButton = [window standardWindowButton:NSWindowCloseButton]; if (closeButton) { buttonBox = NSUnionRect(buttonBox, [closeButton frame]); } NSButton* minimizeButton = [window standardWindowButton:NSWindowMiniaturizeButton]; if (minimizeButton) { buttonBox = NSUnionRect(buttonBox, [minimizeButton frame]); } NSButton* zoomButton = [window standardWindowButton:NSWindowZoomButton]; if (zoomButton) { buttonBox = NSUnionRect(buttonBox, [zoomButton frame]); } return buttonBox.size; } static BOOL FrameIsInActiveWindow(nsIFrame* aFrame) { nsIWidget* topLevelWidget = NULL; NSWindow* win = NativeWindowForFrame(aFrame, &topLevelWidget); if (!topLevelWidget || !win) return YES; // XUL popups, e.g. the toolbar customization popup, can't become key windows, // but controls in these windows should still get the active look. if (topLevelWidget->WindowType() == eWindowType_popup) return YES; if ([win isSheet]) return [win isKeyWindow]; return [win isMainWindow] && ![win attachedSheet]; } // Toolbar controls and content controls respond to different window // activeness states. static BOOL IsActive(nsIFrame* aFrame, BOOL aIsToolbarControl) { if (aIsToolbarControl) return [NativeWindowForFrame(aFrame) isMainWindow]; return FrameIsInActiveWindow(aFrame); } static bool IsInSourceList(nsIFrame* aFrame) { for (nsIFrame* frame = aFrame->GetParent(); frame; frame = frame->GetParent()) { if (frame->StyleDisplay()->mAppearance == NS_THEME_MAC_SOURCE_LIST) { return true; } } return false; } NS_IMPL_ISUPPORTS_INHERITED(nsNativeThemeCocoa, nsNativeTheme, nsITheme) nsNativeThemeCocoa::nsNativeThemeCocoa() { NS_OBJC_BEGIN_TRY_ABORT_BLOCK; kMaxFocusRingWidth = nsCocoaFeatures::OnYosemiteOrLater() ? 7 : 4; // provide a local autorelease pool, as this is called during startup // before the main event-loop pool is in place nsAutoreleasePool pool; mDisclosureButtonCell = [[NSButtonCell alloc] initTextCell:@""]; [mDisclosureButtonCell setBezelStyle:NSRoundedDisclosureBezelStyle]; [mDisclosureButtonCell setButtonType:NSPushOnPushOffButton]; [mDisclosureButtonCell setHighlightsBy:NSPushInCellMask]; mHelpButtonCell = [[NSButtonCell alloc] initTextCell:@""]; [mHelpButtonCell setBezelStyle:NSHelpButtonBezelStyle]; [mHelpButtonCell setButtonType:NSMomentaryPushInButton]; [mHelpButtonCell setHighlightsBy:NSPushInCellMask]; mPushButtonCell = [[NSButtonCell alloc] initTextCell:@""]; [mPushButtonCell setButtonType:NSMomentaryPushInButton]; [mPushButtonCell setHighlightsBy:NSPushInCellMask]; mRadioButtonCell = [[RadioButtonCell alloc] initTextCell:@""]; [mRadioButtonCell setButtonType:NSRadioButton]; mCheckboxCell = [[CheckboxCell alloc] initTextCell:@""]; [mCheckboxCell setButtonType:NSSwitchButton]; [mCheckboxCell setAllowsMixedState:YES]; #if defined(__x86_64__) mSearchFieldCell = [[SearchFieldCellWithFocusRing alloc] initTextCell:@""]; #else mSearchFieldCell = [[ContextAwareSearchFieldCell alloc] initTextCell:@""]; #endif [mSearchFieldCell setBezelStyle:NSTextFieldRoundedBezel]; [mSearchFieldCell setBezeled:YES]; [mSearchFieldCell setEditable:YES]; [mSearchFieldCell setFocusRingType:NSFocusRingTypeExterior]; mDropdownCell = [[NSPopUpButtonCell alloc] initTextCell:@"" pullsDown:NO]; mComboBoxCell = [[NSComboBoxCell alloc] initTextCell:@""]; [mComboBoxCell setBezeled:YES]; [mComboBoxCell setEditable:YES]; [mComboBoxCell setFocusRingType:NSFocusRingTypeExterior]; mProgressBarCell = [[NSProgressBarCell alloc] init]; mMeterBarCell = [[NSLevelIndicatorCell alloc] initWithLevelIndicatorStyle:NSContinuousCapacityLevelIndicatorStyle]; mCellDrawView = [[CellDrawView alloc] init]; NS_OBJC_END_TRY_ABORT_BLOCK; } nsNativeThemeCocoa::~nsNativeThemeCocoa() { NS_OBJC_BEGIN_TRY_ABORT_BLOCK; [mMeterBarCell release]; [mProgressBarCell release]; [mDisclosureButtonCell release]; [mHelpButtonCell release]; [mPushButtonCell release]; [mRadioButtonCell release]; [mCheckboxCell release]; [mSearchFieldCell release]; [mDropdownCell release]; [mComboBoxCell release]; [mCellDrawView release]; NS_OBJC_END_TRY_ABORT_BLOCK; } // Limit on the area of the target rect (in pixels^2) in // DrawCellWithScaling() and DrawButton() and above which we // don't draw the object into a bitmap buffer. This is to avoid crashes in // [NSGraphicsContext graphicsContextWithGraphicsPort:flipped:] and // CGContextDrawImage(), and also to avoid very poor drawing performance in // CGContextDrawImage() when it scales the bitmap (particularly if xscale or // yscale is less than but near 1 -- e.g. 0.9). This value was determined // by trial and error, on OS X 10.4.11 and 10.5.4, and on systems with // different amounts of RAM. #define BITMAP_MAX_AREA 500000 static int GetBackingScaleFactorForRendering(CGContextRef cgContext) { CGAffineTransform ctm = CGContextGetUserSpaceToDeviceSpaceTransform(cgContext); CGRect transformedUserSpacePixel = CGRectApplyAffineTransform(CGRectMake(0, 0, 1, 1), ctm); float maxScale = std::max(fabs(transformedUserSpacePixel.size.width), fabs(transformedUserSpacePixel.size.height)); return maxScale > 1.0 ? 2 : 1; } /* * Draw the given NSCell into the given cgContext. * * destRect - the size and position of the resulting control rectangle * controlSize - the NSControlSize which will be given to the NSCell before * asking it to render * naturalSize - The natural dimensions of this control. * If the control rect size is not equal to either of these, a scale * will be applied to the context so that rendering the control at the * natural size will result in it filling the destRect space. * If a control has no natural dimensions in either/both axes, pass 0.0f. * minimumSize - The minimum dimensions of this control. * If the control rect size is less than the minimum for a given axis, * a scale will be applied to the context so that the minimum is used * for drawing. If a control has no minimum dimensions in either/both * axes, pass 0.0f. * marginSet - an array of margins; a multidimensional array of [2][3][4], * with the first dimension being the OS version (Tiger or Leopard), * the second being the control size (mini, small, regular), and the third * being the 4 margin values (left, top, right, bottom). * view - The NSView that we're drawing into. As far as I can tell, it doesn't * matter if this is really the right view; it just has to return YES when * asked for isFlipped. Otherwise we'll get drawing bugs on 10.4. * mirrorHorizontal - whether to mirror the cell horizontally */ static void DrawCellWithScaling(NSCell *cell, CGContextRef cgContext, const HIRect& destRect, NSControlSize controlSize, NSSize naturalSize, NSSize minimumSize, const float marginSet[][3][4], NSView* view, BOOL mirrorHorizontal) { NS_OBJC_BEGIN_TRY_ABORT_BLOCK; NSRect drawRect = NSMakeRect(destRect.origin.x, destRect.origin.y, destRect.size.width, destRect.size.height); if (naturalSize.width != 0.0f) drawRect.size.width = naturalSize.width; if (naturalSize.height != 0.0f) drawRect.size.height = naturalSize.height; // Keep aspect ratio when scaling if one dimension is free. if (naturalSize.width == 0.0f && naturalSize.height != 0.0f) drawRect.size.width = destRect.size.width * naturalSize.height / destRect.size.height; if (naturalSize.height == 0.0f && naturalSize.width != 0.0f) drawRect.size.height = destRect.size.height * naturalSize.width / destRect.size.width; // Honor minimum sizes. if (drawRect.size.width < minimumSize.width) drawRect.size.width = minimumSize.width; if (drawRect.size.height < minimumSize.height) drawRect.size.height = minimumSize.height; [NSGraphicsContext saveGraphicsState]; // Only skip the buffer if the area of our cell (in pixels^2) is too large. if (drawRect.size.width * drawRect.size.height > BITMAP_MAX_AREA) { // Inflate the rect Gecko gave us by the margin for the control. InflateControlRect(&drawRect, controlSize, marginSet); NSGraphicsContext* savedContext = [NSGraphicsContext currentContext]; [NSGraphicsContext setCurrentContext:[NSGraphicsContext graphicsContextWithGraphicsPort:cgContext flipped:YES]]; DrawCellIncludingFocusRing(cell, drawRect, view); [NSGraphicsContext setCurrentContext:savedContext]; } else { float w = ceil(drawRect.size.width); float h = ceil(drawRect.size.height); NSRect tmpRect = NSMakeRect(kMaxFocusRingWidth, kMaxFocusRingWidth, w, h); // inflate to figure out the frame we need to tell NSCell to draw in, to get something that's 0,0,w,h InflateControlRect(&tmpRect, controlSize, marginSet); // and then, expand by kMaxFocusRingWidth size to make sure we can capture any focus ring w += kMaxFocusRingWidth * 2.0; h += kMaxFocusRingWidth * 2.0; int backingScaleFactor = GetBackingScaleFactorForRendering(cgContext); CGColorSpaceRef rgb = CGColorSpaceCreateDeviceRGB(); CGContextRef ctx = CGBitmapContextCreate(NULL, (int) w * backingScaleFactor, (int) h * backingScaleFactor, 8, (int) w * backingScaleFactor * 4, rgb, kCGImageAlphaPremultipliedFirst); CGColorSpaceRelease(rgb); // We need to flip the image twice in order to avoid drawing bugs on 10.4, see bug 465069. // This is the first flip transform, applied to cgContext. CGContextScaleCTM(cgContext, 1.0f, -1.0f); CGContextTranslateCTM(cgContext, 0.0f, -(2.0 * destRect.origin.y + destRect.size.height)); if (mirrorHorizontal) { CGContextScaleCTM(cgContext, -1.0f, 1.0f); CGContextTranslateCTM(cgContext, -(2.0 * destRect.origin.x + destRect.size.width), 0.0f); } NSGraphicsContext* savedContext = [NSGraphicsContext currentContext]; [NSGraphicsContext setCurrentContext:[NSGraphicsContext graphicsContextWithGraphicsPort:ctx flipped:YES]]; CGContextScaleCTM(ctx, backingScaleFactor, backingScaleFactor); // Set the context's "base transform" to in order to get correctly-sized focus rings. CGContextSetBaseCTM(ctx, CGAffineTransformMakeScale(backingScaleFactor, backingScaleFactor)); // This is the second flip transform, applied to ctx. CGContextScaleCTM(ctx, 1.0f, -1.0f); CGContextTranslateCTM(ctx, 0.0f, -(2.0 * tmpRect.origin.y + tmpRect.size.height)); DrawCellIncludingFocusRing(cell, tmpRect, view); [NSGraphicsContext setCurrentContext:savedContext]; CGImageRef img = CGBitmapContextCreateImage(ctx); // Drop the image into the original destination rectangle, scaling to fit // Only scale kMaxFocusRingWidth by xscale/yscale when the resulting rect // doesn't extend beyond the overflow rect float xscale = destRect.size.width / drawRect.size.width; float yscale = destRect.size.height / drawRect.size.height; float scaledFocusRingX = xscale < 1.0f ? kMaxFocusRingWidth * xscale : kMaxFocusRingWidth; float scaledFocusRingY = yscale < 1.0f ? kMaxFocusRingWidth * yscale : kMaxFocusRingWidth; CGContextDrawImage(cgContext, CGRectMake(destRect.origin.x - scaledFocusRingX, destRect.origin.y - scaledFocusRingY, destRect.size.width + scaledFocusRingX * 2, destRect.size.height + scaledFocusRingY * 2), img); CGImageRelease(img); CGContextRelease(ctx); } [NSGraphicsContext restoreGraphicsState]; #if DRAW_IN_FRAME_DEBUG CGContextSetRGBFillColor(cgContext, 0.0, 0.0, 0.5, 0.25); CGContextFillRect(cgContext, destRect); #endif NS_OBJC_END_TRY_ABORT_BLOCK; } struct CellRenderSettings { // The natural dimensions of the control. // If a control has no natural dimensions in either/both axes, set to 0.0f. NSSize naturalSizes[3]; // The minimum dimensions of the control. // If a control has no minimum dimensions in either/both axes, set to 0.0f. NSSize minimumSizes[3]; // A three-dimensional array, // with the first dimension being the OS version ([0] 10.6-10.9, [1] 10.10 and above), // the second being the control size (mini, small, regular), and the third // being the 4 margin values (left, top, right, bottom). float margins[2][3][4]; }; /* * This is a helper method that returns the required NSControlSize given a size * and the size of the three controls plus a tolerance. * size - The width or the height of the element to draw. * sizes - An array with the all the width/height of the element for its * different sizes. * tolerance - The tolerance as passed to DrawCellWithSnapping. * NOTE: returns NSRegularControlSize if all values in 'sizes' are zero. */ static NSControlSize FindControlSize(CGFloat size, const CGFloat* sizes, CGFloat tolerance) { for (uint32_t i = miniControlSize; i <= regularControlSize; ++i) { if (sizes[i] == 0) { continue; } CGFloat next = 0; // Find next value. for (uint32_t j = i+1; j <= regularControlSize; ++j) { if (sizes[j] != 0) { next = sizes[j]; break; } } // If it's the latest value, we pick it. if (next == 0) { return CocoaSizeForEnum(i); } if (size <= sizes[i] + tolerance && size < next) { return CocoaSizeForEnum(i); } } // If we are here, that means sizes[] was an array with only empty values // or the algorithm above is wrong. // The former can happen but the later would be wrong. NS_ASSERTION(sizes[0] == 0 && sizes[1] == 0 && sizes[2] == 0, "We found no control! We shouldn't be there!"); return CocoaSizeForEnum(regularControlSize); } /* * Draw the given NSCell into the given cgContext with a nice control size. * * This function is similar to DrawCellWithScaling, but it decides what * control size to use based on the destRect's size. * Scaling is only applied when the difference between the destRect's size * and the next smaller natural size is greater than snapTolerance. Otherwise * it snaps to the next smaller control size without scaling because unscaled * controls look nicer. */ static void DrawCellWithSnapping(NSCell *cell, CGContextRef cgContext, const HIRect& destRect, const CellRenderSettings settings, float verticalAlignFactor, NSView* view, BOOL mirrorHorizontal, float snapTolerance = 2.0f) { NS_OBJC_BEGIN_TRY_ABORT_BLOCK; const float rectWidth = destRect.size.width, rectHeight = destRect.size.height; const NSSize *sizes = settings.naturalSizes; const NSSize miniSize = sizes[EnumSizeForCocoaSize(NSMiniControlSize)]; const NSSize smallSize = sizes[EnumSizeForCocoaSize(NSSmallControlSize)]; const NSSize regularSize = sizes[EnumSizeForCocoaSize(NSRegularControlSize)]; HIRect drawRect = destRect; CGFloat controlWidths[3] = { miniSize.width, smallSize.width, regularSize.width }; NSControlSize controlSizeX = FindControlSize(rectWidth, controlWidths, snapTolerance); CGFloat controlHeights[3] = { miniSize.height, smallSize.height, regularSize.height }; NSControlSize controlSizeY = FindControlSize(rectHeight, controlHeights, snapTolerance); NSControlSize controlSize = NSRegularControlSize; size_t sizeIndex = 0; // At some sizes, don't scale but snap. const NSControlSize smallerControlSize = EnumSizeForCocoaSize(controlSizeX) < EnumSizeForCocoaSize(controlSizeY) ? controlSizeX : controlSizeY; const size_t smallerControlSizeIndex = EnumSizeForCocoaSize(smallerControlSize); const NSSize size = sizes[smallerControlSizeIndex]; float diffWidth = size.width ? rectWidth - size.width : 0.0f; float diffHeight = size.height ? rectHeight - size.height : 0.0f; if (diffWidth >= 0.0f && diffHeight >= 0.0f && diffWidth <= snapTolerance && diffHeight <= snapTolerance) { // Snap to the smaller control size. controlSize = smallerControlSize; sizeIndex = smallerControlSizeIndex; MOZ_ASSERT(sizeIndex < ArrayLength(settings.naturalSizes)); // Resize and center the drawRect. if (sizes[sizeIndex].width) { drawRect.origin.x += ceil((destRect.size.width - sizes[sizeIndex].width) / 2); drawRect.size.width = sizes[sizeIndex].width; } if (sizes[sizeIndex].height) { drawRect.origin.y += floor((destRect.size.height - sizes[sizeIndex].height) * verticalAlignFactor); drawRect.size.height = sizes[sizeIndex].height; } } else { // Use the larger control size. controlSize = EnumSizeForCocoaSize(controlSizeX) > EnumSizeForCocoaSize(controlSizeY) ? controlSizeX : controlSizeY; sizeIndex = EnumSizeForCocoaSize(controlSize); } [cell setControlSize:controlSize]; MOZ_ASSERT(sizeIndex < ArrayLength(settings.minimumSizes)); const NSSize minimumSize = settings.minimumSizes[sizeIndex]; DrawCellWithScaling(cell, cgContext, drawRect, controlSize, sizes[sizeIndex], minimumSize, settings.margins, view, mirrorHorizontal); NS_OBJC_END_TRY_ABORT_BLOCK; } @interface NSWindow(CoreUIRendererPrivate) + (CUIRendererRef)coreUIRenderer; @end static id GetAquaAppearance() { // We only need NSAppearance on 10.10 and up. if (nsCocoaFeatures::OnYosemiteOrLater()) { Class NSAppearanceClass = NSClassFromString(@"NSAppearance"); if (NSAppearanceClass && [NSAppearanceClass respondsToSelector:@selector(appearanceNamed:)]) { return [NSAppearanceClass performSelector:@selector(appearanceNamed:) withObject:@"NSAppearanceNameAqua"]; } } return nil; } @interface NSObject(NSAppearanceCoreUIRendering) - (void)_drawInRect:(CGRect)rect context:(CGContextRef)cgContext options:(id)options; @end static void RenderWithCoreUI(CGRect aRect, CGContextRef cgContext, NSDictionary* aOptions, bool aSkipAreaCheck = false) { id appearance = GetAquaAppearance(); if (!aSkipAreaCheck && aRect.size.width * aRect.size.height > BITMAP_MAX_AREA) { return; } if (appearance && [appearance respondsToSelector:@selector(_drawInRect:context:options:)]) { // Render through NSAppearance on Mac OS 10.10 and up. This will call // CUIDraw with a CoreUI renderer that will give us the correct 10.10 // style. Calling CUIDraw directly with [NSWindow coreUIRenderer] still // renders 10.9-style widgets on 10.10. [appearance _drawInRect:aRect context:cgContext options:aOptions]; } else { // 10.9 and below CUIRendererRef renderer = [NSWindow respondsToSelector:@selector(coreUIRenderer)] ? [NSWindow coreUIRenderer] : nil; CUIDraw(renderer, aRect, cgContext, (CFDictionaryRef)aOptions, NULL); } } static float VerticalAlignFactor(nsIFrame *aFrame) { if (!aFrame) return 0.5f; // default: center const nsStyleCoord& va = aFrame->StyleDisplay()->mVerticalAlign; uint8_t intval = (va.GetUnit() == eStyleUnit_Enumerated) ? va.GetIntValue() : NS_STYLE_VERTICAL_ALIGN_MIDDLE; switch (intval) { case NS_STYLE_VERTICAL_ALIGN_TOP: case NS_STYLE_VERTICAL_ALIGN_TEXT_TOP: return 0.0f; case NS_STYLE_VERTICAL_ALIGN_SUB: case NS_STYLE_VERTICAL_ALIGN_SUPER: case NS_STYLE_VERTICAL_ALIGN_MIDDLE: case NS_STYLE_VERTICAL_ALIGN_MIDDLE_WITH_BASELINE: return 0.5f; case NS_STYLE_VERTICAL_ALIGN_BASELINE: case NS_STYLE_VERTICAL_ALIGN_TEXT_BOTTOM: case NS_STYLE_VERTICAL_ALIGN_BOTTOM: return 1.0f; default: NS_NOTREACHED("invalid vertical-align"); return 0.5f; } } // These are the sizes that Gecko needs to request to draw if it wants // to get a standard-sized Aqua radio button drawn. Note that the rects // that draw these are actually a little bigger. static const CellRenderSettings radioSettings = { { NSMakeSize(11, 11), // mini NSMakeSize(13, 13), // small NSMakeSize(16, 16) // regular }, { NSZeroSize, NSZeroSize, NSZeroSize }, { { // Leopard {0, 0, 0, 0}, // mini {0, 1, 1, 1}, // small {0, 0, 0, 0} // regular }, { // Yosemite {0, 0, 0, 0}, // mini {1, 1, 1, 2}, // small {0, 0, 0, 0} // regular } } }; static const CellRenderSettings checkboxSettings = { { NSMakeSize(11, 11), // mini NSMakeSize(13, 13), // small NSMakeSize(16, 16) // regular }, { NSZeroSize, NSZeroSize, NSZeroSize }, { { // Leopard {0, 1, 0, 0}, // mini {0, 1, 0, 1}, // small {0, 1, 0, 1} // regular }, { // Yosemite {0, 1, 0, 0}, // mini {0, 1, 0, 1}, // small {0, 1, 0, 1} // regular } } }; void nsNativeThemeCocoa::DrawCheckboxOrRadio(CGContextRef cgContext, bool inCheckbox, const HIRect& inBoxRect, bool inSelected, EventStates inState, nsIFrame* aFrame) { NS_OBJC_BEGIN_TRY_ABORT_BLOCK; NSButtonCell *cell = inCheckbox ? mCheckboxCell : mRadioButtonCell; NSCellStateValue state = inSelected ? NSOnState : NSOffState; // Check if we have an indeterminate checkbox if (inCheckbox && GetIndeterminate(aFrame)) state = NSMixedState; [cell setEnabled:!IsDisabled(aFrame, inState)]; [cell setShowsFirstResponder:inState.HasState(NS_EVENT_STATE_FOCUS)]; [cell setState:state]; [cell setHighlighted:inState.HasAllStates(NS_EVENT_STATE_ACTIVE | NS_EVENT_STATE_HOVER)]; [cell setControlTint:(FrameIsInActiveWindow(aFrame) ? [NSColor currentControlTint] : NSClearControlTint)]; // Ensure that the control is square. float length = std::min(inBoxRect.size.width, inBoxRect.size.height); HIRect drawRect = CGRectMake(inBoxRect.origin.x + (int)((inBoxRect.size.width - length) / 2.0f), inBoxRect.origin.y + (int)((inBoxRect.size.height - length) / 2.0f), length, length); DrawCellWithSnapping(cell, cgContext, drawRect, inCheckbox ? checkboxSettings : radioSettings, VerticalAlignFactor(aFrame), mCellDrawView, NO); NS_OBJC_END_TRY_ABORT_BLOCK; } static const CellRenderSettings searchFieldSettings = { { NSMakeSize(0, 16), // mini NSMakeSize(0, 19), // small NSMakeSize(0, 22) // regular }, { NSMakeSize(32, 0), // mini NSMakeSize(38, 0), // small NSMakeSize(44, 0) // regular }, { { // Leopard {0, 0, 0, 0}, // mini {0, 0, 0, 0}, // small {0, 0, 0, 0} // regular }, { // Yosemite {0, 0, 0, 0}, // mini {0, 0, 0, 0}, // small {0, 0, 0, 0} // regular } } }; void nsNativeThemeCocoa::DrawSearchField(CGContextRef cgContext, const HIRect& inBoxRect, nsIFrame* aFrame, EventStates inState) { NS_OBJC_BEGIN_TRY_ABORT_BLOCK; ContextAwareSearchFieldCell* cell = mSearchFieldCell; [cell setContext:aFrame]; [cell setEnabled:!IsDisabled(aFrame, inState)]; // NOTE: this could probably use inState [cell setShowsFirstResponder:IsFocused(aFrame)]; // When using the 10.11 SDK, the default string will be shown if we don't // set the placeholder string. [cell setPlaceholderString:@""]; DrawCellWithSnapping(cell, cgContext, inBoxRect, searchFieldSettings, VerticalAlignFactor(aFrame), mCellDrawView, IsFrameRTL(aFrame)); [cell setContext:nullptr]; NS_OBJC_END_TRY_ABORT_BLOCK; } static const NSSize kCheckmarkSize = NSMakeSize(11, 11); static const NSSize kMenuarrowSize = NSMakeSize(9, 10); static const NSSize kMenuScrollArrowSize = NSMakeSize(10, 8); static NSString* kCheckmarkImage = @"MenuOnState"; static NSString* kMenuarrowRightImage = @"MenuSubmenu"; static NSString* kMenuarrowLeftImage = @"MenuSubmenuLeft"; static NSString* kMenuDownScrollArrowImage = @"MenuScrollDown"; static NSString* kMenuUpScrollArrowImage = @"MenuScrollUp"; static const CGFloat kMenuIconIndent = 6.0f; void nsNativeThemeCocoa::DrawMenuIcon(CGContextRef cgContext, const CGRect& aRect, EventStates inState, nsIFrame* aFrame, const NSSize& aIconSize, NSString* aImageName, bool aCenterHorizontally) { NS_OBJC_BEGIN_TRY_ABORT_BLOCK; // Adjust size and position of our drawRect. CGFloat paddingX = std::max(CGFloat(0.0), aRect.size.width - aIconSize.width); CGFloat paddingY = std::max(CGFloat(0.0), aRect.size.height - aIconSize.height); CGFloat paddingStartX = std::min(paddingX, kMenuIconIndent); CGFloat paddingEndX = std::max(CGFloat(0.0), paddingX - kMenuIconIndent); CGRect drawRect = CGRectMake( aRect.origin.x + (aCenterHorizontally ? ceil(paddingX / 2) : IsFrameRTL(aFrame) ? paddingEndX : paddingStartX), aRect.origin.y + ceil(paddingY / 2), aIconSize.width, aIconSize.height); NSString* state = IsDisabled(aFrame, inState) ? @"disabled" : (CheckBooleanAttr(aFrame, nsGkAtoms::menuactive) ? @"pressed" : @"normal"); NSString* imageName = aImageName; if (!nsCocoaFeatures::OnElCapitanOrLater()) { // Pre-10.11, image names are prefixed with "image." imageName = [@"image." stringByAppendingString:aImageName]; } RenderWithCoreUI(drawRect, cgContext, [NSDictionary dictionaryWithObjectsAndKeys: @"kCUIBackgroundTypeMenu", @"backgroundTypeKey", imageName, @"imageNameKey", state, @"state", @"image", @"widget", [NSNumber numberWithBool:YES], @"is.flipped", nil]); #if DRAW_IN_FRAME_DEBUG CGContextSetRGBFillColor(cgContext, 0.0, 0.0, 0.5, 0.25); CGContextFillRect(cgContext, drawRect); #endif NS_OBJC_END_TRY_ABORT_BLOCK; } static const NSSize kHelpButtonSize = NSMakeSize(20, 20); static const NSSize kDisclosureButtonSize = NSMakeSize(21, 21); static const CellRenderSettings pushButtonSettings = { { NSMakeSize(0, 16), // mini NSMakeSize(0, 19), // small NSMakeSize(0, 22) // regular }, { NSMakeSize(18, 0), // mini NSMakeSize(26, 0), // small NSMakeSize(30, 0) // regular }, { { // Leopard {0, 0, 0, 0}, // mini {4, 0, 4, 1}, // small {5, 0, 5, 2} // regular }, { // Yosemite {0, 0, 0, 0}, // mini {4, 0, 4, 1}, // small {5, 0, 5, 2} // regular } } }; // The height at which we start doing square buttons instead of rounded buttons // Rounded buttons look bad if drawn at a height greater than 26, so at that point // we switch over to doing square buttons which looks fine at any size. #define DO_SQUARE_BUTTON_HEIGHT 26 void nsNativeThemeCocoa::DrawPushButton(CGContextRef cgContext, const HIRect& inBoxRect, EventStates inState, uint8_t aWidgetType, nsIFrame* aFrame, float aOriginalHeight) { NS_OBJC_BEGIN_TRY_ABORT_BLOCK; BOOL isActive = FrameIsInActiveWindow(aFrame); BOOL isDisabled = IsDisabled(aFrame, inState); NSButtonCell* cell = (aWidgetType == NS_THEME_BUTTON) ? mPushButtonCell : (aWidgetType == NS_THEME_MAC_HELP_BUTTON) ? mHelpButtonCell : mDisclosureButtonCell; [cell setEnabled:!isDisabled]; [cell setHighlighted:isActive && inState.HasAllStates(NS_EVENT_STATE_ACTIVE | NS_EVENT_STATE_HOVER)]; [cell setShowsFirstResponder:inState.HasState(NS_EVENT_STATE_FOCUS) && !isDisabled && isActive]; if (aWidgetType != NS_THEME_BUTTON) { // Help button or disclosure button. NSSize buttonSize = NSMakeSize(0, 0); if (aWidgetType == NS_THEME_MAC_HELP_BUTTON) { buttonSize = kHelpButtonSize; } else { // Disclosure button. buttonSize = kDisclosureButtonSize; [cell setState:(aWidgetType == NS_THEME_MAC_DISCLOSURE_BUTTON_CLOSED) ? NSOffState : NSOnState]; } DrawCellWithScaling(cell, cgContext, inBoxRect, NSRegularControlSize, NSZeroSize, buttonSize, NULL, mCellDrawView, false); // Don't mirror icon in RTL. } else { // If the button is tall enough, draw the square button style so that // buttons with non-standard content look good. Otherwise draw normal // rounded aqua buttons. // This comparison is done based on the height that is calculated without // the top, because the snapped height can be affected by the top of the // rect and that may result in different height depending on the top value. if (aOriginalHeight > DO_SQUARE_BUTTON_HEIGHT) { [cell setBezelStyle:NSShadowlessSquareBezelStyle]; DrawCellWithScaling(cell, cgContext, inBoxRect, NSRegularControlSize, NSZeroSize, NSMakeSize(14, 0), NULL, mCellDrawView, IsFrameRTL(aFrame)); } else { [cell setBezelStyle:NSRoundedBezelStyle]; DrawCellWithSnapping(cell, cgContext, inBoxRect, pushButtonSettings, 0.5f, mCellDrawView, IsFrameRTL(aFrame), 1.0f); } } #if DRAW_IN_FRAME_DEBUG CGContextSetRGBFillColor(cgContext, 0.0, 0.0, 0.5, 0.25); CGContextFillRect(cgContext, inBoxRect); #endif NS_OBJC_END_TRY_ABORT_BLOCK; } void nsNativeThemeCocoa::DrawFocusOutline(CGContextRef cgContext, const HIRect& inBoxRect, EventStates inState, uint8_t aWidgetType, nsIFrame* aFrame) { NS_OBJC_BEGIN_TRY_ABORT_BLOCK; HIThemeFrameDrawInfo fdi; fdi.version = 0; fdi.kind = kHIThemeFrameTextFieldSquare; fdi.state = kThemeStateActive; fdi.isFocused = TRUE; #if DRAW_IN_FRAME_DEBUG CGContextSetRGBFillColor(cgContext, 0.0, 0.0, 0.5, 0.25); CGContextFillRect(cgContext, inBoxRect); #endif HIThemeDrawFrame(&inBoxRect, &fdi, cgContext, HITHEME_ORIENTATION); NS_OBJC_END_TRY_ABORT_BLOCK; } typedef void (*RenderHIThemeControlFunction)(CGContextRef cgContext, const HIRect& aRenderRect, void* aData); static void RenderTransformedHIThemeControl(CGContextRef aCGContext, const HIRect& aRect, RenderHIThemeControlFunction aFunc, void* aData, BOOL mirrorHorizontally = NO) { CGAffineTransform savedCTM = CGContextGetCTM(aCGContext); CGContextTranslateCTM(aCGContext, aRect.origin.x, aRect.origin.y); bool drawDirect; HIRect drawRect = aRect; drawRect.origin = CGPointZero; if (!mirrorHorizontally && savedCTM.a == 1.0f && savedCTM.b == 0.0f && savedCTM.c == 0.0f && (savedCTM.d == 1.0f || savedCTM.d == -1.0f)) { drawDirect = TRUE; } else { drawDirect = FALSE; } // Fall back to no bitmap buffer if the area of our control (in pixels^2) // is too large. if (drawDirect || (aRect.size.width * aRect.size.height > BITMAP_MAX_AREA)) { aFunc(aCGContext, drawRect, aData); } else { // Inflate the buffer to capture focus rings. int w = ceil(drawRect.size.width) + 2 * kMaxFocusRingWidth; int h = ceil(drawRect.size.height) + 2 * kMaxFocusRingWidth; int backingScaleFactor = GetBackingScaleFactorForRendering(aCGContext); CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); CGContextRef bitmapctx = CGBitmapContextCreate(NULL, w * backingScaleFactor, h * backingScaleFactor, 8, w * backingScaleFactor * 4, colorSpace, kCGImageAlphaPremultipliedFirst); CGColorSpaceRelease(colorSpace); CGContextScaleCTM(bitmapctx, backingScaleFactor, backingScaleFactor); CGContextTranslateCTM(bitmapctx, kMaxFocusRingWidth, kMaxFocusRingWidth); // Set the context's "base transform" to in order to get correctly-sized focus rings. CGContextSetBaseCTM(bitmapctx, CGAffineTransformMakeScale(backingScaleFactor, backingScaleFactor)); // HITheme always wants to draw into a flipped context, or things // get confused. CGContextTranslateCTM(bitmapctx, 0.0f, aRect.size.height); CGContextScaleCTM(bitmapctx, 1.0f, -1.0f); aFunc(bitmapctx, drawRect, aData); CGImageRef bitmap = CGBitmapContextCreateImage(bitmapctx); CGAffineTransform ctm = CGContextGetCTM(aCGContext); // We need to unflip, so that we can do a DrawImage without getting a flipped image. CGContextTranslateCTM(aCGContext, 0.0f, aRect.size.height); CGContextScaleCTM(aCGContext, 1.0f, -1.0f); if (mirrorHorizontally) { CGContextTranslateCTM(aCGContext, aRect.size.width, 0); CGContextScaleCTM(aCGContext, -1.0f, 1.0f); } HIRect inflatedDrawRect = CGRectMake(-kMaxFocusRingWidth, -kMaxFocusRingWidth, w, h); CGContextDrawImage(aCGContext, inflatedDrawRect, bitmap); CGContextSetCTM(aCGContext, ctm); CGImageRelease(bitmap); CGContextRelease(bitmapctx); } CGContextSetCTM(aCGContext, savedCTM); } static void RenderButton(CGContextRef cgContext, const HIRect& aRenderRect, void* aData) { HIThemeButtonDrawInfo* bdi = (HIThemeButtonDrawInfo*)aData; HIThemeDrawButton(&aRenderRect, bdi, cgContext, kHIThemeOrientationNormal, NULL); } void nsNativeThemeCocoa::DrawButton(CGContextRef cgContext, ThemeButtonKind inKind, const HIRect& inBoxRect, bool inIsDefault, ThemeButtonValue inValue, ThemeButtonAdornment inAdornment, EventStates inState, nsIFrame* aFrame) { NS_OBJC_BEGIN_TRY_ABORT_BLOCK; BOOL isActive = FrameIsInActiveWindow(aFrame); BOOL isDisabled = IsDisabled(aFrame, inState); HIThemeButtonDrawInfo bdi; bdi.version = 0; bdi.kind = inKind; bdi.value = inValue; bdi.adornment = inAdornment; if (isDisabled) { bdi.state = kThemeStateUnavailable; } else if (inState.HasAllStates(NS_EVENT_STATE_ACTIVE | NS_EVENT_STATE_HOVER)) { bdi.state = kThemeStatePressed; } else { if (inKind == kThemeArrowButton) bdi.state = kThemeStateUnavailable; // these are always drawn as unavailable else if (!isActive && inKind == kThemeListHeaderButton) bdi.state = kThemeStateInactive; else bdi.state = kThemeStateActive; } if (inState.HasState(NS_EVENT_STATE_FOCUS) && isActive) bdi.adornment |= kThemeAdornmentFocus; if (inIsDefault && !isDisabled && !inState.HasState(NS_EVENT_STATE_ACTIVE)) { bdi.adornment |= kThemeAdornmentDefault; bdi.animation.time.start = 0; bdi.animation.time.current = CFAbsoluteTimeGetCurrent(); } HIRect drawFrame = inBoxRect; if (inKind == kThemePushButton) { drawFrame.size.height -= 2; if (inBoxRect.size.height < pushButtonSettings.naturalSizes[smallControlSize].height) { bdi.kind = kThemePushButtonMini; } else if (inBoxRect.size.height < pushButtonSettings.naturalSizes[regularControlSize].height) { bdi.kind = kThemePushButtonSmall; drawFrame.origin.y -= 1; drawFrame.origin.x += 1; drawFrame.size.width -= 2; } } else if (inKind == kThemeListHeaderButton) { CGContextClipToRect(cgContext, inBoxRect); // Always remove the top border. drawFrame.origin.y -= 1; drawFrame.size.height += 1; // Remove the left border in LTR mode and the right border in RTL mode. drawFrame.size.width += 1; bool isLast = IsLastTreeHeaderCell(aFrame); if (isLast) drawFrame.size.width += 1; // Also remove the other border. if (!IsFrameRTL(aFrame) || isLast) drawFrame.origin.x -= 1; } RenderTransformedHIThemeControl(cgContext, drawFrame, RenderButton, &bdi, IsFrameRTL(aFrame)); #if DRAW_IN_FRAME_DEBUG CGContextSetRGBFillColor(cgContext, 0.0, 0.0, 0.5, 0.25); CGContextFillRect(cgContext, inBoxRect); #endif NS_OBJC_END_TRY_ABORT_BLOCK; } static const CellRenderSettings dropdownSettings = { { NSMakeSize(0, 16), // mini NSMakeSize(0, 19), // small NSMakeSize(0, 22) // regular }, { NSMakeSize(18, 0), // mini NSMakeSize(38, 0), // small NSMakeSize(44, 0) // regular }, { { // Leopard {1, 1, 2, 1}, // mini {3, 0, 3, 1}, // small {3, 0, 3, 0} // regular }, { // Yosemite {1, 1, 2, 1}, // mini {3, 0, 3, 1}, // small {3, 0, 3, 0} // regular } } }; static const CellRenderSettings editableMenulistSettings = { { NSMakeSize(0, 15), // mini NSMakeSize(0, 18), // small NSMakeSize(0, 21) // regular }, { NSMakeSize(18, 0), // mini NSMakeSize(38, 0), // small NSMakeSize(44, 0) // regular }, { { // Leopard {0, 0, 2, 2}, // mini {0, 0, 3, 2}, // small {0, 1, 3, 3} // regular }, { // Yosemite {0, 0, 2, 2}, // mini {0, 0, 3, 2}, // small {0, 1, 3, 3} // regular } } }; void nsNativeThemeCocoa::DrawDropdown(CGContextRef cgContext, const HIRect& inBoxRect, EventStates inState, uint8_t aWidgetType, nsIFrame* aFrame) { NS_OBJC_BEGIN_TRY_ABORT_BLOCK; [mDropdownCell setPullsDown:(aWidgetType == NS_THEME_BUTTON)]; BOOL isEditable = (aWidgetType == NS_THEME_MENULIST_TEXTFIELD); NSCell* cell = isEditable ? (NSCell*)mComboBoxCell : (NSCell*)mDropdownCell; [cell setEnabled:!IsDisabled(aFrame, inState)]; [cell setShowsFirstResponder:(IsFocused(aFrame) || inState.HasState(NS_EVENT_STATE_FOCUS))]; [cell setHighlighted:IsOpenButton(aFrame)]; [cell setControlTint:(FrameIsInActiveWindow(aFrame) ? [NSColor currentControlTint] : NSClearControlTint)]; const CellRenderSettings& settings = isEditable ? editableMenulistSettings : dropdownSettings; DrawCellWithSnapping(cell, cgContext, inBoxRect, settings, 0.5f, mCellDrawView, IsFrameRTL(aFrame)); NS_OBJC_END_TRY_ABORT_BLOCK; } static const CellRenderSettings spinnerSettings = { { NSMakeSize(11, 16), // mini (width trimmed by 2px to reduce blank border) NSMakeSize(15, 22), // small NSMakeSize(19, 27) // regular }, { NSMakeSize(11, 16), // mini (width trimmed by 2px to reduce blank border) NSMakeSize(15, 22), // small NSMakeSize(19, 27) // regular }, { { // Leopard {0, 0, 0, 0}, // mini {0, 0, 0, 0}, // small {0, 0, 0, 0} // regular }, { // Yosemite {0, 0, 0, 0}, // mini {0, 0, 0, 0}, // small {0, 0, 0, 0} // regular } } }; void nsNativeThemeCocoa::DrawSpinButtons(CGContextRef cgContext, ThemeButtonKind inKind, const HIRect& inBoxRect, ThemeDrawState inDrawState, ThemeButtonAdornment inAdornment, EventStates inState, nsIFrame* aFrame) { NS_OBJC_BEGIN_TRY_ABORT_BLOCK; HIThemeButtonDrawInfo bdi; bdi.version = 0; bdi.kind = inKind; bdi.value = kThemeButtonOff; bdi.adornment = inAdornment; if (IsDisabled(aFrame, inState)) bdi.state = kThemeStateUnavailable; else bdi.state = FrameIsInActiveWindow(aFrame) ? inDrawState : kThemeStateActive; HIThemeDrawButton(&inBoxRect, &bdi, cgContext, HITHEME_ORIENTATION, NULL); NS_OBJC_END_TRY_ABORT_BLOCK; } void nsNativeThemeCocoa::DrawSpinButton(CGContextRef cgContext, ThemeButtonKind inKind, const HIRect& inBoxRect, ThemeDrawState inDrawState, ThemeButtonAdornment inAdornment, EventStates inState, nsIFrame* aFrame, uint8_t aWidgetType) { NS_OBJC_BEGIN_TRY_ABORT_BLOCK; MOZ_ASSERT(aWidgetType == NS_THEME_SPINNER_UPBUTTON || aWidgetType == NS_THEME_SPINNER_DOWNBUTTON); HIThemeButtonDrawInfo bdi; bdi.version = 0; bdi.kind = inKind; bdi.value = kThemeButtonOff; bdi.adornment = inAdornment; if (IsDisabled(aFrame, inState)) bdi.state = kThemeStateUnavailable; else bdi.state = FrameIsInActiveWindow(aFrame) ? inDrawState : kThemeStateActive; // Cocoa only allows kThemeIncDecButton to paint the up and down spin buttons // together as a single unit (presumably because when one button is active, // the appearance of both changes (in different ways)). Here we have to paint // both buttons, using clip to hide the one we don't want to paint. HIRect drawRect = inBoxRect; drawRect.size.height *= 2; if (aWidgetType == NS_THEME_SPINNER_DOWNBUTTON) { drawRect.origin.y -= inBoxRect.size.height; } // Shift the drawing a little to the left, since cocoa paints with more // blank space around the visual buttons than we'd like: drawRect.origin.x -= 1; CGContextSaveGState(cgContext); CGContextClipToRect(cgContext, inBoxRect); HIThemeDrawButton(&drawRect, &bdi, cgContext, HITHEME_ORIENTATION, NULL); CGContextRestoreGState(cgContext); NS_OBJC_END_TRY_ABORT_BLOCK; } void nsNativeThemeCocoa::DrawFrame(CGContextRef cgContext, HIThemeFrameKind inKind, const HIRect& inBoxRect, bool inDisabled, EventStates inState) { NS_OBJC_BEGIN_TRY_ABORT_BLOCK; HIThemeFrameDrawInfo fdi; fdi.version = 0; fdi.kind = inKind; // We don't ever set an inactive state for this because it doesn't // look right (see other apps). fdi.state = inDisabled ? kThemeStateUnavailable : kThemeStateActive; // for some reason focus rings on listboxes draw incorrectly if (inKind == kHIThemeFrameListBox) fdi.isFocused = 0; else fdi.isFocused = inState.HasState(NS_EVENT_STATE_FOCUS); // HIThemeDrawFrame takes the rect for the content area of the frame, not // the bounding rect for the frame. Here we reduce the size of the rect we // will pass to make it the size of the content. HIRect drawRect = inBoxRect; if (inKind == kHIThemeFrameTextFieldSquare) { SInt32 frameOutset = 0; ::GetThemeMetric(kThemeMetricEditTextFrameOutset, &frameOutset); drawRect.origin.x += frameOutset; drawRect.origin.y += frameOutset; drawRect.size.width -= frameOutset * 2; drawRect.size.height -= frameOutset * 2; } else if (inKind == kHIThemeFrameListBox) { SInt32 frameOutset = 0; ::GetThemeMetric(kThemeMetricListBoxFrameOutset, &frameOutset); drawRect.origin.x += frameOutset; drawRect.origin.y += frameOutset; drawRect.size.width -= frameOutset * 2; drawRect.size.height -= frameOutset * 2; } #if DRAW_IN_FRAME_DEBUG CGContextSetRGBFillColor(cgContext, 0.0, 0.0, 0.5, 0.25); CGContextFillRect(cgContext, inBoxRect); #endif HIThemeDrawFrame(&drawRect, &fdi, cgContext, HITHEME_ORIENTATION); NS_OBJC_END_TRY_ABORT_BLOCK; } static const CellRenderSettings progressSettings[2][2] = { // Vertical progress bar. { // Determined settings. { { NSZeroSize, // mini NSMakeSize(10, 0), // small NSMakeSize(16, 0) // regular }, { NSZeroSize, NSZeroSize, NSZeroSize }, { { // Leopard {0, 0, 0, 0}, // mini {1, 1, 1, 1}, // small {1, 1, 1, 1} // regular } } }, // There is no horizontal margin in regular undetermined size. { { NSZeroSize, // mini NSMakeSize(10, 0), // small NSMakeSize(16, 0) // regular }, { NSZeroSize, NSZeroSize, NSZeroSize }, { { // Leopard {0, 0, 0, 0}, // mini {1, 1, 1, 1}, // small {1, 0, 1, 0} // regular }, { // Yosemite {0, 0, 0, 0}, // mini {1, 1, 1, 1}, // small {1, 0, 1, 0} // regular } } } }, // Horizontal progress bar. { // Determined settings. { { NSZeroSize, // mini NSMakeSize(0, 10), // small NSMakeSize(0, 16) // regular }, { NSZeroSize, NSZeroSize, NSZeroSize }, { { // Leopard {0, 0, 0, 0}, // mini {1, 1, 1, 1}, // small {1, 1, 1, 1} // regular }, { // Yosemite {0, 0, 0, 0}, // mini {1, 1, 1, 1}, // small {1, 1, 1, 1} // regular } } }, // There is no horizontal margin in regular undetermined size. { { NSZeroSize, // mini NSMakeSize(0, 10), // small NSMakeSize(0, 16) // regular }, { NSZeroSize, NSZeroSize, NSZeroSize }, { { // Leopard {0, 0, 0, 0}, // mini {1, 1, 1, 1}, // small {0, 1, 0, 1} // regular }, { // Yosemite {0, 0, 0, 0}, // mini {1, 1, 1, 1}, // small {0, 1, 0, 1} // regular } } } } }; void nsNativeThemeCocoa::DrawProgress(CGContextRef cgContext, const HIRect& inBoxRect, bool inIsIndeterminate, bool inIsHorizontal, double inValue, double inMaxValue, nsIFrame* aFrame) { NS_OBJC_BEGIN_TRY_ABORT_BLOCK; NSProgressBarCell* cell = mProgressBarCell; [cell setValue:inValue]; [cell setMax:inMaxValue]; [cell setIndeterminate:inIsIndeterminate]; [cell setHorizontal:inIsHorizontal]; [cell setControlTint:(FrameIsInActiveWindow(aFrame) ? [NSColor currentControlTint] : NSClearControlTint)]; DrawCellWithSnapping(cell, cgContext, inBoxRect, progressSettings[inIsHorizontal][inIsIndeterminate], VerticalAlignFactor(aFrame), mCellDrawView, IsFrameRTL(aFrame)); NS_OBJC_END_TRY_ABORT_BLOCK; } static const CellRenderSettings meterSetting = { { NSMakeSize(0, 16), // mini NSMakeSize(0, 16), // small NSMakeSize(0, 16) // regular }, { NSZeroSize, NSZeroSize, NSZeroSize }, { { // Leopard {1, 1, 1, 1}, // mini {1, 1, 1, 1}, // small {1, 1, 1, 1} // regular }, { // Yosemite {1, 1, 1, 1}, // mini {1, 1, 1, 1}, // small {1, 1, 1, 1} // regular } } }; void nsNativeThemeCocoa::DrawMeter(CGContextRef cgContext, const HIRect& inBoxRect, nsIFrame* aFrame) { NS_OBJC_BEGIN_TRY_ABORT_BLOCK NS_PRECONDITION(aFrame, "aFrame should not be null here!"); // When using -moz-meterbar on an non meter element, we will not be able to // get all the needed information so we just draw an empty meter. nsIContent* content = aFrame->GetContent(); if (!(content && content->IsHTMLElement(nsGkAtoms::meter))) { DrawCellWithSnapping(mMeterBarCell, cgContext, inBoxRect, meterSetting, VerticalAlignFactor(aFrame), mCellDrawView, IsFrameRTL(aFrame)); return; } HTMLMeterElement* meterElement = static_cast(content); double value = meterElement->Value(); double min = meterElement->Min(); double max = meterElement->Max(); NSLevelIndicatorCell* cell = mMeterBarCell; [cell setMinValue:min]; [cell setMaxValue:max]; [cell setDoubleValue:value]; /** * The way HTML and Cocoa defines the meter/indicator widget are different. * So, we are going to use a trick to get the Cocoa widget showing what we * are expecting: we set the warningValue or criticalValue to the current * value when we want to have the widget to be in the warning or critical * state. */ EventStates states = aFrame->GetContent()->AsElement()->State(); // Reset previously set warning and critical values. [cell setWarningValue:max+1]; [cell setCriticalValue:max+1]; if (states.HasState(NS_EVENT_STATE_SUB_OPTIMUM)) { [cell setWarningValue:value]; } else if (states.HasState(NS_EVENT_STATE_SUB_SUB_OPTIMUM)) { [cell setCriticalValue:value]; } HIRect rect = CGRectStandardize(inBoxRect); BOOL vertical = IsVerticalMeter(aFrame); CGContextSaveGState(cgContext); if (vertical) { /** * Cocoa doesn't provide a vertical meter bar so to show one, we have to * show a rotated horizontal meter bar. * Given that we want to show a vertical meter bar, we assume that the rect * has vertical dimensions but we can't correctly draw a meter widget inside * such a rectangle so we need to inverse width and height (and re-position) * to get a rectangle with horizontal dimensions. * Finally, we want to show a vertical meter so we want to rotate the result * so it is vertical. We do that by changing the context. */ CGFloat tmp = rect.size.width; rect.size.width = rect.size.height; rect.size.height = tmp; rect.origin.x += rect.size.height / 2.f - rect.size.width / 2.f; rect.origin.y += rect.size.width / 2.f - rect.size.height / 2.f; CGContextTranslateCTM(cgContext, CGRectGetMidX(rect), CGRectGetMidY(rect)); CGContextRotateCTM(cgContext, -M_PI / 2.f); CGContextTranslateCTM(cgContext, -CGRectGetMidX(rect), -CGRectGetMidY(rect)); } DrawCellWithSnapping(cell, cgContext, rect, meterSetting, VerticalAlignFactor(aFrame), mCellDrawView, !vertical && IsFrameRTL(aFrame)); CGContextRestoreGState(cgContext); NS_OBJC_END_TRY_ABORT_BLOCK } void nsNativeThemeCocoa::DrawTabPanel(CGContextRef cgContext, const HIRect& inBoxRect, nsIFrame* aFrame) { NS_OBJC_BEGIN_TRY_ABORT_BLOCK; HIThemeTabPaneDrawInfo tpdi; tpdi.version = 1; tpdi.state = FrameIsInActiveWindow(aFrame) ? kThemeStateActive : kThemeStateInactive; tpdi.direction = kThemeTabNorth; tpdi.size = kHIThemeTabSizeNormal; tpdi.kind = kHIThemeTabKindNormal; HIThemeDrawTabPane(&inBoxRect, &tpdi, cgContext, HITHEME_ORIENTATION); NS_OBJC_END_TRY_ABORT_BLOCK; } void nsNativeThemeCocoa::DrawScale(CGContextRef cgContext, const HIRect& inBoxRect, EventStates inState, bool inIsVertical, bool inIsReverse, int32_t inCurrentValue, int32_t inMinValue, int32_t inMaxValue, nsIFrame* aFrame) { NS_OBJC_BEGIN_TRY_ABORT_BLOCK; HIThemeTrackDrawInfo tdi; tdi.version = 0; tdi.kind = kThemeMediumSlider; tdi.bounds = inBoxRect; tdi.min = inMinValue; tdi.max = inMaxValue; tdi.value = inCurrentValue; tdi.attributes = kThemeTrackShowThumb; if (!inIsVertical) tdi.attributes |= kThemeTrackHorizontal; if (inIsReverse) tdi.attributes |= kThemeTrackRightToLeft; if (inState.HasState(NS_EVENT_STATE_FOCUS)) tdi.attributes |= kThemeTrackHasFocus; if (IsDisabled(aFrame, inState)) tdi.enableState = kThemeTrackDisabled; else tdi.enableState = FrameIsInActiveWindow(aFrame) ? kThemeTrackActive : kThemeTrackInactive; tdi.trackInfo.slider.thumbDir = kThemeThumbPlain; tdi.trackInfo.slider.pressState = 0; HIThemeDrawTrack(&tdi, NULL, cgContext, HITHEME_ORIENTATION); NS_OBJC_END_TRY_ABORT_BLOCK; } nsIFrame* nsNativeThemeCocoa::SeparatorResponsibility(nsIFrame* aBefore, nsIFrame* aAfter) { // Usually a separator is drawn by the segment to the right of the // separator, but pressed and selected segments have higher priority. if (!aBefore || !aAfter) return nullptr; if (IsSelectedButton(aAfter)) return aAfter; if (IsSelectedButton(aBefore) || IsPressedButton(aBefore)) return aBefore; return aAfter; } CGRect nsNativeThemeCocoa::SeparatorAdjustedRect(CGRect aRect, nsIFrame* aLeft, nsIFrame* aCurrent, nsIFrame* aRight) { // A separator between two segments should always be located in the leftmost // pixel column of the segment to the right of the separator, regardless of // who ends up drawing it. // CoreUI draws the separators inside the drawing rect. if (aLeft && SeparatorResponsibility(aLeft, aCurrent) == aLeft) { // The left button draws the separator, so we need to make room for it. aRect.origin.x += 1; aRect.size.width -= 1; } if (SeparatorResponsibility(aCurrent, aRight) == aCurrent) { // We draw the right separator, so we need to extend the draw rect into the // segment to our right. aRect.size.width += 1; } return aRect; } static NSString* ToolbarButtonPosition(BOOL aIsFirst, BOOL aIsLast) { if (aIsFirst) { if (aIsLast) return @"kCUISegmentPositionOnly"; return @"kCUISegmentPositionFirst"; } if (aIsLast) return @"kCUISegmentPositionLast"; return @"kCUISegmentPositionMiddle"; } struct SegmentedControlRenderSettings { const CGFloat* heights; const NSString* widgetName; const BOOL ignoresPressedWhenSelected; const BOOL isToolbarControl; }; static const CGFloat tabHeights[3] = { 17, 20, 23 }; static const SegmentedControlRenderSettings tabRenderSettings = { tabHeights, @"tab", YES, NO }; static const CGFloat toolbarButtonHeights[3] = { 15, 18, 22 }; static const SegmentedControlRenderSettings toolbarButtonRenderSettings = { toolbarButtonHeights, @"kCUIWidgetButtonSegmentedSCurve", NO, YES }; void nsNativeThemeCocoa::DrawSegment(CGContextRef cgContext, const HIRect& inBoxRect, EventStates inState, nsIFrame* aFrame, const SegmentedControlRenderSettings& aSettings) { BOOL isActive = IsActive(aFrame, aSettings.isToolbarControl); BOOL isFocused = inState.HasState(NS_EVENT_STATE_FOCUS); BOOL isSelected = IsSelectedButton(aFrame); BOOL isPressed = IsPressedButton(aFrame); if (isSelected && aSettings.ignoresPressedWhenSelected) { isPressed = NO; } BOOL isRTL = IsFrameRTL(aFrame); nsIFrame* left = GetAdjacentSiblingFrameWithSameAppearance(aFrame, isRTL); nsIFrame* right = GetAdjacentSiblingFrameWithSameAppearance(aFrame, !isRTL); CGRect drawRect = SeparatorAdjustedRect(inBoxRect, left, aFrame, right); BOOL drawLeftSeparator = SeparatorResponsibility(left, aFrame) == aFrame; BOOL drawRightSeparator = SeparatorResponsibility(aFrame, right) == aFrame; NSControlSize controlSize = FindControlSize(drawRect.size.height, aSettings.heights, 4.0f); RenderWithCoreUI(drawRect, cgContext, [NSDictionary dictionaryWithObjectsAndKeys: aSettings.widgetName, @"widget", (isActive ? @"kCUIPresentationStateActiveKey" : @"kCUIPresentationStateInactive"), @"kCUIPresentationStateKey", ToolbarButtonPosition(!left, !right), @"kCUIPositionKey", [NSNumber numberWithBool:drawLeftSeparator], @"kCUISegmentLeadingSeparatorKey", [NSNumber numberWithBool:drawRightSeparator], @"kCUISegmentTrailingSeparatorKey", [NSNumber numberWithBool:isSelected], @"value", (isPressed ? @"pressed" : (isActive ? @"normal" : @"inactive")), @"state", [NSNumber numberWithBool:isFocused], @"focus", CUIControlSizeForCocoaSize(controlSize), @"size", [NSNumber numberWithBool:YES], @"is.flipped", @"up", @"direction", nil]); } void nsNativeThemeCocoa::GetScrollbarPressStates(nsIFrame* aFrame, EventStates aButtonStates[]) { static nsIContent::AttrValuesArray attributeValues[] = { &nsGkAtoms::scrollbarUpTop, &nsGkAtoms::scrollbarDownTop, &nsGkAtoms::scrollbarUpBottom, &nsGkAtoms::scrollbarDownBottom, nullptr }; // Get the state of any scrollbar buttons in our child frames for (nsIFrame *childFrame : aFrame->PrincipalChildList()) { nsIContent *childContent = childFrame->GetContent(); if (!childContent) continue; int32_t attrIndex = childContent->FindAttrValueIn(kNameSpaceID_None, nsGkAtoms::sbattr, attributeValues, eCaseMatters); if (attrIndex < 0) continue; aButtonStates[attrIndex] = GetContentState(childFrame, NS_THEME_BUTTON); } } nsIFrame* nsNativeThemeCocoa::GetParentScrollbarFrame(nsIFrame *aFrame) { // Walk our parents to find a scrollbar frame nsIFrame *scrollbarFrame = aFrame; do { if (scrollbarFrame->GetType() == nsGkAtoms::scrollbarFrame) break; } while ((scrollbarFrame = scrollbarFrame->GetParent())); // We return null if we can't find a parent scrollbar frame return scrollbarFrame; } static bool ToolbarCanBeUnified(CGContextRef cgContext, const HIRect& inBoxRect, NSWindow* aWindow) { if (![aWindow isKindOfClass:[ToolbarWindow class]]) return false; ToolbarWindow* win = (ToolbarWindow*)aWindow; float unifiedToolbarHeight = [win unifiedToolbarHeight]; return inBoxRect.origin.x == 0 && inBoxRect.size.width >= [win frame].size.width && CGRectGetMaxY(inBoxRect) <= unifiedToolbarHeight; } // By default, kCUIWidgetWindowFrame drawing draws rounded corners in the // upper corners. Depending on the context type, it fills the background in // the corners with black or leaves it transparent. Unfortunately, this corner // rounding interacts poorly with the window corner masking we apply during // titlebar drawing and results in small remnants of the corner background // appearing at the rounded edge. // So we draw square corners. static void DrawNativeTitlebarToolbarWithSquareCorners(CGContextRef aContext, const CGRect& aRect, CGFloat aUnifiedHeight, BOOL aIsMain, BOOL aIsFlipped) { // We extend the draw rect horizontally and clip away the rounded corners. const CGFloat extendHorizontal = 10; CGRect drawRect = CGRectInset(aRect, -extendHorizontal, 0); CGContextSaveGState(aContext); CGContextClipToRect(aContext, aRect); RenderWithCoreUI(drawRect, aContext, [NSDictionary dictionaryWithObjectsAndKeys: @"kCUIWidgetWindowFrame", @"widget", @"regularwin", @"windowtype", (aIsMain ? @"normal" : @"inactive"), @"state", [NSNumber numberWithDouble:aUnifiedHeight], @"kCUIWindowFrameUnifiedTitleBarHeightKey", [NSNumber numberWithBool:YES], @"kCUIWindowFrameDrawTitleSeparatorKey", [NSNumber numberWithBool:aIsFlipped], @"is.flipped", nil]); CGContextRestoreGState(aContext); } void nsNativeThemeCocoa::DrawUnifiedToolbar(CGContextRef cgContext, const HIRect& inBoxRect, NSWindow* aWindow) { NS_OBJC_BEGIN_TRY_ABORT_BLOCK; CGContextSaveGState(cgContext); CGContextClipToRect(cgContext, inBoxRect); CGFloat unifiedHeight = std::max([(ToolbarWindow*)aWindow unifiedToolbarHeight], inBoxRect.size.height); BOOL isMain = [aWindow isMainWindow]; CGFloat titlebarHeight = unifiedHeight - inBoxRect.size.height; CGRect drawRect = CGRectMake(inBoxRect.origin.x, inBoxRect.origin.y - titlebarHeight, inBoxRect.size.width, inBoxRect.size.height + titlebarHeight); DrawNativeTitlebarToolbarWithSquareCorners(cgContext, drawRect, unifiedHeight, isMain, YES); CGContextRestoreGState(cgContext); NS_OBJC_END_TRY_ABORT_BLOCK; } void nsNativeThemeCocoa::DrawStatusBar(CGContextRef cgContext, const HIRect& inBoxRect, nsIFrame *aFrame) { NS_OBJC_BEGIN_TRY_ABORT_BLOCK; if (inBoxRect.size.height < 2.0f) return; CGContextSaveGState(cgContext); CGContextClipToRect(cgContext, inBoxRect); // kCUIWidgetWindowFrame draws a complete window frame with both title bar // and bottom bar. We only want the bottom bar, so we extend the draw rect // upwards to make space for the title bar, and then we clip it away. CGRect drawRect = inBoxRect; const int extendUpwards = 40; drawRect.origin.y -= extendUpwards; drawRect.size.height += extendUpwards; RenderWithCoreUI(drawRect, cgContext, [NSDictionary dictionaryWithObjectsAndKeys: @"kCUIWidgetWindowFrame", @"widget", @"regularwin", @"windowtype", (IsActive(aFrame, YES) ? @"normal" : @"inactive"), @"state", [NSNumber numberWithInt:inBoxRect.size.height], @"kCUIWindowFrameBottomBarHeightKey", [NSNumber numberWithBool:YES], @"kCUIWindowFrameDrawBottomBarSeparatorKey", [NSNumber numberWithBool:YES], @"is.flipped", nil]); CGContextRestoreGState(cgContext); NS_OBJC_END_TRY_ABORT_BLOCK; } void nsNativeThemeCocoa::DrawNativeTitlebar(CGContextRef aContext, CGRect aTitlebarRect, CGFloat aUnifiedHeight, BOOL aIsMain, BOOL aIsFlipped) { CGFloat unifiedHeight = std::max(aUnifiedHeight, aTitlebarRect.size.height); DrawNativeTitlebarToolbarWithSquareCorners(aContext, aTitlebarRect, unifiedHeight, aIsMain, aIsFlipped); } static void RenderResizer(CGContextRef cgContext, const HIRect& aRenderRect, void* aData) { HIThemeGrowBoxDrawInfo* drawInfo = (HIThemeGrowBoxDrawInfo*)aData; HIThemeDrawGrowBox(&CGPointZero, drawInfo, cgContext, kHIThemeOrientationNormal); } void nsNativeThemeCocoa::DrawResizer(CGContextRef cgContext, const HIRect& aRect, nsIFrame *aFrame) { NS_OBJC_BEGIN_TRY_ABORT_BLOCK; HIThemeGrowBoxDrawInfo drawInfo; drawInfo.version = 0; drawInfo.state = kThemeStateActive; drawInfo.kind = kHIThemeGrowBoxKindNormal; drawInfo.direction = kThemeGrowRight | kThemeGrowDown; drawInfo.size = kHIThemeGrowBoxSizeNormal; RenderTransformedHIThemeControl(cgContext, aRect, RenderResizer, &drawInfo, IsFrameRTL(aFrame)); NS_OBJC_END_TRY_ABORT_BLOCK; } static void DrawVibrancyBackground(CGContextRef cgContext, CGRect inBoxRect, nsIFrame* aFrame, nsITheme::ThemeGeometryType aThemeGeometryType, int aCornerRadiusIfOpaque = 0) { ChildView* childView = ChildViewForFrame(aFrame); if (childView) { NSRect rect = NSRectFromCGRect(inBoxRect); NSGraphicsContext* savedContext = [NSGraphicsContext currentContext]; [NSGraphicsContext setCurrentContext:[NSGraphicsContext graphicsContextWithGraphicsPort:cgContext flipped:YES]]; [NSGraphicsContext saveGraphicsState]; NSColor* fillColor = [childView vibrancyFillColorForThemeGeometryType:aThemeGeometryType]; if ([fillColor alphaComponent] == 1.0 && aCornerRadiusIfOpaque > 0) { // The fillColor being opaque means that the system-wide pref "reduce // transparency" is set. In that scenario, we still go through all the // vibrancy rendering paths (VibrancyManager::SystemSupportsVibrancy() // will still return true), but the result just won't look "vibrant". // However, there's one unfortunate change of behavior that this pref // has: It stops the window server from applying window masks. We use // a window mask to get rounded corners on menus. So since the mask // doesn't work in "reduce vibrancy" mode, we need to do our own rounded // corner clipping here. [[NSBezierPath bezierPathWithRoundedRect:rect xRadius:aCornerRadiusIfOpaque yRadius:aCornerRadiusIfOpaque] addClip]; } [fillColor set]; NSRectFill(rect); [NSGraphicsContext restoreGraphicsState]; [NSGraphicsContext setCurrentContext:savedContext]; } } bool nsNativeThemeCocoa::IsParentScrollbarRolledOver(nsIFrame* aFrame) { nsIFrame* scrollbarFrame = GetParentScrollbarFrame(aFrame); return nsLookAndFeel::UseOverlayScrollbars() ? CheckBooleanAttr(scrollbarFrame, nsGkAtoms::hover) : GetContentState(scrollbarFrame, NS_THEME_NONE).HasState(NS_EVENT_STATE_HOVER); } static bool IsHiDPIContext(nsPresContext* aContext) { return nsPresContext::AppUnitsPerCSSPixel() >= 2 * aContext->DeviceContext()->AppUnitsPerDevPixelAtUnitFullZoom(); } static bool IsScrollbarWidthThin(nsIFrame* aFrame) { return aFrame->StyleUserInterface()->mScrollbarWidth == StyleScrollbarWidth::Thin; } NS_IMETHODIMP nsNativeThemeCocoa::DrawWidgetBackground(nsRenderingContext* aContext, nsIFrame* aFrame, uint8_t aWidgetType, const nsRect& aRect, const nsRect& aDirtyRect) { NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT; DrawTarget& aDrawTarget = *aContext->GetDrawTarget(); // setup to draw into the correct port int32_t p2a = aFrame->PresContext()->AppUnitsPerDevPixel(); gfx::Rect nativeDirtyRect = NSRectToRect(aDirtyRect, p2a); gfxRect nativeWidgetRect(aRect.x, aRect.y, aRect.width, aRect.height); nativeWidgetRect.ScaleInverse(gfxFloat(p2a)); float nativeWidgetHeight = round(nativeWidgetRect.Height()); nativeWidgetRect.Round(); if (nativeWidgetRect.IsEmpty()) return NS_OK; // Don't attempt to draw invisible widgets. AutoRestoreTransform autoRestoreTransform(&aDrawTarget); bool hidpi = IsHiDPIContext(aFrame->PresContext()); if (hidpi) { // Use high-resolution drawing. nativeWidgetRect.Scale(0.5f); nativeWidgetHeight *= 0.5f; nativeDirtyRect.Scale(0.5f); aDrawTarget.SetTransform(aDrawTarget.GetTransform().PreScale(2.0f, 2.0f)); } gfxQuartzNativeDrawing nativeDrawing(aDrawTarget, nativeDirtyRect); CGContextRef cgContext = nativeDrawing.BeginNativeDrawing(); if (cgContext == nullptr) { // The Quartz surface handles 0x0 surfaces by internally // making all operations no-ops; there's no cgcontext created for them. // Unfortunately, this means that callers that want to render // directly to the CGContext need to be aware of this quirk. return NS_OK; } if (hidpi) { // Set the context's "base transform" to in order to get correctly-sized focus rings. CGContextSetBaseCTM(cgContext, CGAffineTransformMakeScale(2, 2)); } #if 0 if (1 /*aWidgetType == NS_THEME_TEXTFIELD*/) { fprintf(stderr, "Native theme drawing widget %d [%p] dis:%d in rect [%d %d %d %d]\n", aWidgetType, aFrame, IsDisabled(aFrame), aRect.x, aRect.y, aRect.width, aRect.height); fprintf(stderr, "Cairo matrix: [%f %f %f %f %f %f]\n", mat._11, mat._12, mat._21, mat._22, mat._31, mat._32); fprintf(stderr, "Native theme xform[0]: [%f %f %f %f %f %f]\n", mm0.a, mm0.b, mm0.c, mm0.d, mm0.tx, mm0.ty); CGAffineTransform mm = CGContextGetCTM(cgContext); fprintf(stderr, "Native theme xform[1]: [%f %f %f %f %f %f]\n", mm.a, mm.b, mm.c, mm.d, mm.tx, mm.ty); } #endif CGRect macRect = CGRectMake(nativeWidgetRect.X(), nativeWidgetRect.Y(), nativeWidgetRect.Width(), nativeWidgetRect.Height()); #if 0 fprintf(stderr, " --> macRect %f %f %f %f\n", macRect.origin.x, macRect.origin.y, macRect.size.width, macRect.size.height); CGRect bounds = CGContextGetClipBoundingBox(cgContext); fprintf(stderr, " --> clip bounds: %f %f %f %f\n", bounds.origin.x, bounds.origin.y, bounds.size.width, bounds.size.height); //CGContextSetRGBFillColor(cgContext, 0.0, 0.0, 1.0, 0.1); //CGContextFillRect(cgContext, bounds); #endif EventStates eventState = GetContentState(aFrame, aWidgetType); switch (aWidgetType) { case NS_THEME_DIALOG: { if (IsWindowSheet(aFrame)) { if (VibrancyManager::SystemSupportsVibrancy()) { ThemeGeometryType type = ThemeGeometryTypeForWidget(aFrame, aWidgetType); DrawVibrancyBackground(cgContext, macRect, aFrame, type); } else { HIThemeSetFill(kThemeBrushSheetBackgroundTransparent, NULL, cgContext, HITHEME_ORIENTATION); CGContextFillRect(cgContext, macRect); } } else { HIThemeSetFill(kThemeBrushDialogBackgroundActive, NULL, cgContext, HITHEME_ORIENTATION); CGContextFillRect(cgContext, macRect); } } break; case NS_THEME_MENUPOPUP: if (VibrancyManager::SystemSupportsVibrancy()) { DrawVibrancyBackground(cgContext, macRect, aFrame, eThemeGeometryTypeMenu, 4); } else { HIThemeMenuDrawInfo mdi; memset(&mdi, 0, sizeof(mdi)); mdi.version = 0; mdi.menuType = IsDisabled(aFrame, eventState) ? static_cast(kThemeMenuTypeInactive) : static_cast(kThemeMenuTypePopUp); bool isLeftOfParent = false; if (IsSubmenu(aFrame, &isLeftOfParent) && !isLeftOfParent) { mdi.menuType = kThemeMenuTypeHierarchical; } // The rounded corners draw outside the frame. CGRect deflatedRect = CGRectMake(macRect.origin.x, macRect.origin.y + 4, macRect.size.width, macRect.size.height - 8); HIThemeDrawMenuBackground(&deflatedRect, &mdi, cgContext, HITHEME_ORIENTATION); } break; case NS_THEME_MENUARROW: { bool isRTL = IsFrameRTL(aFrame); DrawMenuIcon(cgContext, macRect, eventState, aFrame, kMenuarrowSize, isRTL ? kMenuarrowLeftImage : kMenuarrowRightImage, true); } break; case NS_THEME_MENUITEM: case NS_THEME_CHECKMENUITEM: { if (VibrancyManager::SystemSupportsVibrancy()) { ThemeGeometryType type = ThemeGeometryTypeForWidget(aFrame, aWidgetType); DrawVibrancyBackground(cgContext, macRect, aFrame, type); } else { bool isDisabled = IsDisabled(aFrame, eventState); bool isSelected = !isDisabled && CheckBooleanAttr(aFrame, nsGkAtoms::menuactive); // maybe use kThemeMenuItemHierBackground or PopUpBackground instead of just Plain? HIThemeMenuItemDrawInfo drawInfo; memset(&drawInfo, 0, sizeof(drawInfo)); drawInfo.version = 0; drawInfo.itemType = kThemeMenuItemPlain; drawInfo.state = (isDisabled ? static_cast(kThemeMenuDisabled) : isSelected ? static_cast(kThemeMenuSelected) : static_cast(kThemeMenuActive)); // XXX pass in the menu rect instead of always using the item rect HIRect ignored; HIThemeDrawMenuItem(&macRect, &macRect, &drawInfo, cgContext, HITHEME_ORIENTATION, &ignored); } if (aWidgetType == NS_THEME_CHECKMENUITEM) { DrawMenuIcon(cgContext, macRect, eventState, aFrame, kCheckmarkSize, kCheckmarkImage, false); } } break; case NS_THEME_MENUSEPARATOR: { // Workaround for visual artifacts issues with // HIThemeDrawMenuSeparator on macOS Big Sur. if (nsCocoaFeatures::OnBigSurOrLater()) { CGRect separatorRect = macRect; separatorRect.size.height = 1; separatorRect.size.width -= 42; separatorRect.origin.x += 21; // Use a gray color similar to the native separator CGContextSetRGBFillColor(cgContext, 0.816, 0.816, 0.816, 1.0); CGContextFillRect(cgContext, separatorRect); } else { ThemeMenuState menuState; if (IsDisabled(aFrame, eventState)) { menuState = kThemeMenuDisabled; } else { menuState = CheckBooleanAttr(aFrame, nsGkAtoms::menuactive) ? kThemeMenuSelected : kThemeMenuActive; } HIThemeMenuItemDrawInfo midi = { 0, kThemeMenuItemPlain, menuState }; HIThemeDrawMenuSeparator(&macRect, &macRect, &midi, cgContext, HITHEME_ORIENTATION); } } break; case NS_THEME_BUTTON_ARROW_UP: case NS_THEME_BUTTON_ARROW_DOWN: DrawMenuIcon(cgContext, macRect, eventState, aFrame, kMenuScrollArrowSize, aWidgetType == NS_THEME_BUTTON_ARROW_UP ? kMenuUpScrollArrowImage : kMenuDownScrollArrowImage, true); break; case NS_THEME_TOOLTIP: if (VibrancyManager::SystemSupportsVibrancy()) { DrawVibrancyBackground(cgContext, macRect, aFrame, ThemeGeometryTypeForWidget(aFrame, aWidgetType)); } else { CGContextSetRGBFillColor(cgContext, 0.996, 1.000, 0.792, 0.950); CGContextFillRect(cgContext, macRect); } break; case NS_THEME_CHECKBOX: case NS_THEME_RADIO: { bool isCheckbox = (aWidgetType == NS_THEME_CHECKBOX); DrawCheckboxOrRadio(cgContext, isCheckbox, macRect, GetCheckedOrSelected(aFrame, !isCheckbox), eventState, aFrame); } break; case NS_THEME_BUTTON: if (IsDefaultButton(aFrame)) { // Check whether the default button is in a document that does not // match the :-moz-window-inactive pseudoclass. This activeness check // is different from the other "active window" checks in this file // because we absolutely need the button's default button appearance to // be in sync with its text color, and the text color is changed by // such a :-moz-window-inactive rule. (That's because on 10.10 and up, // default buttons in active windows have blue background and white // text, and default buttons in inactive windows have white background // and black text.) EventStates docState = aFrame->GetContent()->OwnerDoc()->GetDocumentState(); bool isInActiveWindow = !docState.HasState(NS_DOCUMENT_STATE_WINDOW_INACTIVE); if (!IsDisabled(aFrame, eventState) && isInActiveWindow && !QueueAnimatedContentForRefresh(aFrame->GetContent(), 10)) { NS_WARNING("Unable to animate button!"); } DrawButton(cgContext, kThemePushButton, macRect, isInActiveWindow, kThemeButtonOff, kThemeAdornmentNone, eventState, aFrame); } else if (IsButtonTypeMenu(aFrame)) { DrawDropdown(cgContext, macRect, eventState, aWidgetType, aFrame); } else { DrawPushButton(cgContext, macRect, eventState, aWidgetType, aFrame, nativeWidgetHeight); } break; case NS_THEME_FOCUS_OUTLINE: DrawFocusOutline(cgContext, macRect, eventState, aWidgetType, aFrame); break; case NS_THEME_MAC_HELP_BUTTON: case NS_THEME_MAC_DISCLOSURE_BUTTON_OPEN: case NS_THEME_MAC_DISCLOSURE_BUTTON_CLOSED: DrawPushButton(cgContext, macRect, eventState, aWidgetType, aFrame, nativeWidgetHeight); break; case NS_THEME_BUTTON_BEVEL: DrawButton(cgContext, kThemeMediumBevelButton, macRect, IsDefaultButton(aFrame), kThemeButtonOff, kThemeAdornmentNone, eventState, aFrame); break; case NS_THEME_SPINNER: { nsIContent* content = aFrame->GetContent(); if (content->IsHTMLElement()) { // In HTML the theming for the spin buttons is drawn individually into // their own backgrounds instead of being drawn into the background of // their spinner parent as it is for XUL. break; } ThemeDrawState state = kThemeStateActive; if (content->AttrValueIs(kNameSpaceID_None, nsGkAtoms::state, NS_LITERAL_STRING("up"), eCaseMatters)) { state = kThemeStatePressedUp; } else if (content->AttrValueIs(kNameSpaceID_None, nsGkAtoms::state, NS_LITERAL_STRING("down"), eCaseMatters)) { state = kThemeStatePressedDown; } DrawSpinButtons(cgContext, kThemeIncDecButton, macRect, state, kThemeAdornmentNone, eventState, aFrame); } break; case NS_THEME_SPINNER_UPBUTTON: case NS_THEME_SPINNER_DOWNBUTTON: { nsNumberControlFrame* numberControlFrame = nsNumberControlFrame::GetNumberControlFrameForSpinButton(aFrame); if (numberControlFrame) { ThemeDrawState state = kThemeStateActive; if (numberControlFrame->SpinnerUpButtonIsDepressed()) { state = kThemeStatePressedUp; } else if (numberControlFrame->SpinnerDownButtonIsDepressed()) { state = kThemeStatePressedDown; } DrawSpinButton(cgContext, kThemeIncDecButtonMini, macRect, state, kThemeAdornmentNone, eventState, aFrame, aWidgetType); } } break; case NS_THEME_TOOLBARBUTTON: DrawSegment(cgContext, macRect, eventState, aFrame, toolbarButtonRenderSettings); break; case NS_THEME_SEPARATOR: { HIThemeSeparatorDrawInfo sdi = { 0, kThemeStateActive }; HIThemeDrawSeparator(&macRect, &sdi, cgContext, HITHEME_ORIENTATION); } break; case NS_THEME_TOOLBAR: { NSWindow* win = NativeWindowForFrame(aFrame); if (ToolbarCanBeUnified(cgContext, macRect, win)) { DrawUnifiedToolbar(cgContext, macRect, win); break; } BOOL isMain = [win isMainWindow]; CGRect drawRect = macRect; // top border drawRect.size.height = 1.0f; DrawNativeGreyColorInRect(cgContext, toolbarTopBorderGrey, drawRect, isMain); // background drawRect.origin.y += drawRect.size.height; drawRect.size.height = macRect.size.height - 2.0f; DrawNativeGreyColorInRect(cgContext, toolbarFillGrey, drawRect, isMain); // bottom border drawRect.origin.y += drawRect.size.height; drawRect.size.height = 1.0f; DrawNativeGreyColorInRect(cgContext, toolbarBottomBorderGrey, drawRect, isMain); } break; case NS_THEME_WINDOW_TITLEBAR: { NSWindow* win = NativeWindowForFrame(aFrame); BOOL isMain = [win isMainWindow]; float unifiedToolbarHeight = [win isKindOfClass:[ToolbarWindow class]] ? [(ToolbarWindow*)win unifiedToolbarHeight] : macRect.size.height; DrawNativeTitlebar(cgContext, macRect, unifiedToolbarHeight, isMain, YES); } break; case NS_THEME_STATUSBAR: DrawStatusBar(cgContext, macRect, aFrame); break; case NS_THEME_MENULIST: case NS_THEME_MENULIST_TEXTFIELD: DrawDropdown(cgContext, macRect, eventState, aWidgetType, aFrame); break; case NS_THEME_MENULIST_BUTTON: DrawButton(cgContext, kThemeArrowButton, macRect, false, kThemeButtonOn, kThemeAdornmentArrowDownArrow, eventState, aFrame); break; case NS_THEME_GROUPBOX: { HIThemeGroupBoxDrawInfo gdi = { 0, kThemeStateActive, kHIThemeGroupBoxKindPrimary }; HIThemeDrawGroupBox(&macRect, &gdi, cgContext, HITHEME_ORIENTATION); break; } case NS_THEME_TEXTFIELD: case NS_THEME_NUMBER_INPUT: // HIThemeSetFill is not available on 10.3 CGContextSetRGBFillColor(cgContext, 1.0, 1.0, 1.0, 1.0); CGContextFillRect(cgContext, macRect); // XUL textboxes set the native appearance on the containing box, while // concrete focus is set on the html:input element within it. We can // though, check the focused attribute of xul textboxes in this case. // On Mac, focus rings are always shown for textboxes, so we do not need // to check the window's focus ring state here if (aFrame->GetContent()->IsXULElement() && IsFocused(aFrame)) { eventState |= NS_EVENT_STATE_FOCUS; } DrawFrame(cgContext, kHIThemeFrameTextFieldSquare, macRect, IsDisabled(aFrame, eventState) || IsReadOnly(aFrame), eventState); break; case NS_THEME_SEARCHFIELD: DrawSearchField(cgContext, macRect, aFrame, eventState); break; case NS_THEME_PROGRESSBAR: { double value = GetProgressValue(aFrame); double maxValue = GetProgressMaxValue(aFrame); // Don't request repaints for scrollbars at 100% because those don't animate. if (value < maxValue) { if (!QueueAnimatedContentForRefresh(aFrame->GetContent(), 30)) { NS_WARNING("Unable to animate progressbar!"); } } DrawProgress(cgContext, macRect, IsIndeterminateProgress(aFrame, eventState), !IsVerticalProgress(aFrame), value, maxValue, aFrame); break; } case NS_THEME_PROGRESSBAR_VERTICAL: DrawProgress(cgContext, macRect, IsIndeterminateProgress(aFrame, eventState), false, GetProgressValue(aFrame), GetProgressMaxValue(aFrame), aFrame); break; case NS_THEME_METERBAR: DrawMeter(cgContext, macRect, aFrame); break; case NS_THEME_PROGRESSCHUNK: case NS_THEME_PROGRESSCHUNK_VERTICAL: case NS_THEME_METERCHUNK: // Do nothing: progress and meter bars cases will draw chunks. break; case NS_THEME_TREETWISTY: DrawButton(cgContext, kThemeDisclosureButton, macRect, false, kThemeDisclosureRight, kThemeAdornmentNone, eventState, aFrame); break; case NS_THEME_TREETWISTYOPEN: DrawButton(cgContext, kThemeDisclosureButton, macRect, false, kThemeDisclosureDown, kThemeAdornmentNone, eventState, aFrame); break; case NS_THEME_TREEHEADERCELL: { TreeSortDirection sortDirection = GetTreeSortDirection(aFrame); DrawButton(cgContext, kThemeListHeaderButton, macRect, false, sortDirection == eTreeSortDirection_Natural ? kThemeButtonOff : kThemeButtonOn, sortDirection == eTreeSortDirection_Ascending ? kThemeAdornmentHeaderButtonSortUp : kThemeAdornmentNone, eventState, aFrame); } break; case NS_THEME_TREEITEM: case NS_THEME_TREEVIEW: // HIThemeSetFill is not available on 10.3 // HIThemeSetFill(kThemeBrushWhite, NULL, cgContext, HITHEME_ORIENTATION); CGContextSetRGBFillColor(cgContext, 1.0, 1.0, 1.0, 1.0); CGContextFillRect(cgContext, macRect); break; case NS_THEME_TREEHEADER: // do nothing, taken care of by individual header cells case NS_THEME_TREEHEADERSORTARROW: // do nothing, taken care of by treeview header case NS_THEME_TREELINE: // do nothing, these lines don't exist on macos break; case NS_THEME_SCALE_HORIZONTAL: case NS_THEME_SCALE_VERTICAL: { int32_t curpos = CheckIntAttr(aFrame, nsGkAtoms::curpos, 0); int32_t minpos = CheckIntAttr(aFrame, nsGkAtoms::minpos, 0); int32_t maxpos = CheckIntAttr(aFrame, nsGkAtoms::maxpos, 100); if (!maxpos) maxpos = 100; bool reverse = aFrame->GetContent()-> AttrValueIs(kNameSpaceID_None, nsGkAtoms::dir, NS_LITERAL_STRING("reverse"), eCaseMatters); DrawScale(cgContext, macRect, eventState, (aWidgetType == NS_THEME_SCALE_VERTICAL), reverse, curpos, minpos, maxpos, aFrame); } break; case NS_THEME_SCALETHUMB_HORIZONTAL: case NS_THEME_SCALETHUMB_VERTICAL: // do nothing, drawn by scale break; case NS_THEME_RANGE: { nsRangeFrame *rangeFrame = do_QueryFrame(aFrame); if (!rangeFrame) { break; } // DrawScale requires integer min, max and value. This is purely for // drawing, so we normalize to a range 0-1000 here. int32_t value = int32_t(rangeFrame->GetValueAsFractionOfRange() * 1000); int32_t min = 0; int32_t max = 1000; bool isVertical = !IsRangeHorizontal(aFrame); bool reverseDir = isVertical || rangeFrame->IsRightToLeft(); DrawScale(cgContext, macRect, eventState, isVertical, reverseDir, value, min, max, aFrame); break; } case NS_THEME_SCROLLBAR_SMALL: case NS_THEME_SCROLLBAR: break; case NS_THEME_SCROLLBARTHUMB_VERTICAL: case NS_THEME_SCROLLBARTHUMB_HORIZONTAL: { BOOL isOverlay = nsLookAndFeel::UseOverlayScrollbars(); BOOL isHorizontal = (aWidgetType == NS_THEME_SCROLLBARTHUMB_HORIZONTAL); BOOL isRolledOver = IsParentScrollbarRolledOver(aFrame); nsIFrame* scrollbarFrame = GetParentScrollbarFrame(aFrame); bool isSmall = (scrollbarFrame && scrollbarFrame->StyleDisplay()->mAppearance == NS_THEME_SCROLLBAR_SMALL); if (isOverlay && !isRolledOver) { if (isHorizontal) { macRect.origin.y += 4; macRect.size.height -= 4; } else { if (aFrame->StyleVisibility()->mDirection != NS_STYLE_DIRECTION_RTL) { macRect.origin.x += 4; } macRect.size.width -= 4; } } const BOOL isOnTopOfDarkBackground = IsDarkBackground(aFrame); NSMutableDictionary* options = [NSMutableDictionary dictionaryWithObjectsAndKeys: (isOverlay ? @"kCUIWidgetOverlayScrollBar" : @"scrollbar"), @"widget", (isSmall ? @"small" : @"regular"), @"size", (isHorizontal ? @"kCUIOrientHorizontal" : @"kCUIOrientVertical"), @"kCUIOrientationKey", (isOverlay && isOnTopOfDarkBackground ? @"kCUIVariantWhite" : @""), @"kCUIVariantKey", [NSNumber numberWithBool:YES], @"indiconly", [NSNumber numberWithBool:YES], @"kCUIThumbProportionKey", [NSNumber numberWithBool:YES], @"is.flipped", nil]; if (isRolledOver) { [options setObject:@"rollover" forKey:@"state"]; } RenderWithCoreUI(macRect, cgContext, options, true); } break; case NS_THEME_SCROLLBARBUTTON_UP: case NS_THEME_SCROLLBARBUTTON_LEFT: #if SCROLLBARS_VISUAL_DEBUG CGContextSetRGBFillColor(cgContext, 1.0, 0, 0, 0.6); CGContextFillRect(cgContext, macRect); #endif break; case NS_THEME_SCROLLBARBUTTON_DOWN: case NS_THEME_SCROLLBARBUTTON_RIGHT: #if SCROLLBARS_VISUAL_DEBUG CGContextSetRGBFillColor(cgContext, 0, 1.0, 0, 0.6); CGContextFillRect(cgContext, macRect); #endif break; case NS_THEME_SCROLLBARTRACK_HORIZONTAL: case NS_THEME_SCROLLBARTRACK_VERTICAL: { BOOL isOverlay = nsLookAndFeel::UseOverlayScrollbars(); if (!isOverlay || IsParentScrollbarRolledOver(aFrame)) { BOOL isHorizontal = (aWidgetType == NS_THEME_SCROLLBARTRACK_HORIZONTAL); nsIFrame* scrollbarFrame = GetParentScrollbarFrame(aFrame); bool isSmall = (scrollbarFrame && scrollbarFrame->StyleDisplay()->mAppearance == NS_THEME_SCROLLBAR_SMALL); const BOOL isOnTopOfDarkBackground = IsDarkBackground(aFrame); RenderWithCoreUI(macRect, cgContext, [NSDictionary dictionaryWithObjectsAndKeys: (isOverlay ? @"kCUIWidgetOverlayScrollBar" : @"scrollbar"), @"widget", (isSmall ? @"small" : @"regular"), @"size", (isHorizontal ? @"kCUIOrientHorizontal" : @"kCUIOrientVertical"), @"kCUIOrientationKey", (isOnTopOfDarkBackground ? @"kCUIVariantWhite" : @""), @"kCUIVariantKey", [NSNumber numberWithBool:YES], @"noindicator", [NSNumber numberWithBool:YES], @"kCUIThumbProportionKey", [NSNumber numberWithBool:YES], @"is.flipped", nil], true); } } break; case NS_THEME_TEXTFIELD_MULTILINE: { // we have to draw this by hand because there is no HITheme value for it CGContextSetRGBFillColor(cgContext, 1.0, 1.0, 1.0, 1.0); CGContextFillRect(cgContext, macRect); // #737373 for the top border, #999999 for the rest. float x = macRect.origin.x, y = macRect.origin.y; float w = macRect.size.width, h = macRect.size.height; CGContextSetRGBFillColor(cgContext, 0.4510, 0.4510, 0.4510, 1.0); CGContextFillRect(cgContext, CGRectMake(x, y, w, 1)); CGContextSetRGBFillColor(cgContext, 0.6, 0.6, 0.6, 1.0); CGContextFillRect(cgContext, CGRectMake(x, y + 1, 1, h - 1)); CGContextFillRect(cgContext, CGRectMake(x + w - 1, y + 1, 1, h - 1)); CGContextFillRect(cgContext, CGRectMake(x + 1, y + h - 1, w - 2, 1)); // draw a focus ring if (eventState.HasState(NS_EVENT_STATE_FOCUS)) { NSGraphicsContext* savedContext = [NSGraphicsContext currentContext]; [NSGraphicsContext setCurrentContext:[NSGraphicsContext graphicsContextWithGraphicsPort:cgContext flipped:YES]]; CGContextSaveGState(cgContext); NSSetFocusRingStyle(NSFocusRingOnly); NSRectFill(NSRectFromCGRect(macRect)); CGContextRestoreGState(cgContext); [NSGraphicsContext setCurrentContext:savedContext]; } } break; case NS_THEME_LISTBOX: { // We have to draw this by hand because kHIThemeFrameListBox drawing // is buggy on 10.5, see bug 579259. CGContextSetRGBFillColor(cgContext, 1.0, 1.0, 1.0, 1.0); CGContextFillRect(cgContext, macRect); // #8E8E8E for the top border, #BEBEBE for the rest. float x = macRect.origin.x, y = macRect.origin.y; float w = macRect.size.width, h = macRect.size.height; CGContextSetRGBFillColor(cgContext, 0.557, 0.557, 0.557, 1.0); CGContextFillRect(cgContext, CGRectMake(x, y, w, 1)); CGContextSetRGBFillColor(cgContext, 0.745, 0.745, 0.745, 1.0); CGContextFillRect(cgContext, CGRectMake(x, y + 1, 1, h - 1)); CGContextFillRect(cgContext, CGRectMake(x + w - 1, y + 1, 1, h - 1)); CGContextFillRect(cgContext, CGRectMake(x + 1, y + h - 1, w - 2, 1)); } break; case NS_THEME_MAC_SOURCE_LIST: { if (VibrancyManager::SystemSupportsVibrancy()) { ThemeGeometryType type = ThemeGeometryTypeForWidget(aFrame, aWidgetType); DrawVibrancyBackground(cgContext, macRect, aFrame, type); } else { CGGradientRef backgroundGradient; CGColorSpaceRef rgb = CGColorSpaceCreateDeviceRGB(); CGFloat activeGradientColors[8] = { 0.9137, 0.9294, 0.9490, 1.0, 0.8196, 0.8471, 0.8784, 1.0 }; CGFloat inactiveGradientColors[8] = { 0.9686, 0.9686, 0.9686, 1.0, 0.9216, 0.9216, 0.9216, 1.0 }; CGPoint start = macRect.origin; CGPoint end = CGPointMake(macRect.origin.x, macRect.origin.y + macRect.size.height); BOOL isActive = FrameIsInActiveWindow(aFrame); backgroundGradient = CGGradientCreateWithColorComponents(rgb, isActive ? activeGradientColors : inactiveGradientColors, NULL, 2); CGContextDrawLinearGradient(cgContext, backgroundGradient, start, end, 0); CGGradientRelease(backgroundGradient); CGColorSpaceRelease(rgb); } } break; case NS_THEME_MAC_SOURCE_LIST_SELECTION: case NS_THEME_MAC_ACTIVE_SOURCE_LIST_SELECTION: { // If we're in XUL tree, we need to rely on the source list's clear // background display item. If we cleared the background behind the // selections, the source list would not pick up the right font // smoothing background. So, to simplify a bit, we only support vibrancy // if we're in a source list. if (VibrancyManager::SystemSupportsVibrancy() && IsInSourceList(aFrame)) { ThemeGeometryType type = ThemeGeometryTypeForWidget(aFrame, aWidgetType); DrawVibrancyBackground(cgContext, macRect, aFrame, type); } else { BOOL isActiveSelection = aWidgetType == NS_THEME_MAC_ACTIVE_SOURCE_LIST_SELECTION; RenderWithCoreUI(macRect, cgContext, [NSDictionary dictionaryWithObjectsAndKeys: [NSNumber numberWithBool:isActiveSelection], @"focus", [NSNumber numberWithBool:YES], @"is.flipped", @"kCUIVariantGradientSideBarSelection", @"kCUIVariantKey", (FrameIsInActiveWindow(aFrame) ? @"normal" : @"inactive"), @"state", @"gradient", @"widget", nil]); } } break; case NS_THEME_TAB: DrawSegment(cgContext, macRect, eventState, aFrame, tabRenderSettings); break; case NS_THEME_TABPANELS: DrawTabPanel(cgContext, macRect, aFrame); break; case NS_THEME_RESIZER: DrawResizer(cgContext, macRect, aFrame); break; case NS_THEME_MAC_VIBRANCY_LIGHT: case NS_THEME_MAC_VIBRANCY_DARK: { ThemeGeometryType type = ThemeGeometryTypeForWidget(aFrame, aWidgetType); DrawVibrancyBackground(cgContext, macRect, aFrame, type); break; } } if (hidpi) { // Reset the base CTM. CGContextSetBaseCTM(cgContext, CGAffineTransformIdentity); } nativeDrawing.EndNativeDrawing(); return NS_OK; NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT; } nsIntMargin nsNativeThemeCocoa::DirectionAwareMargin(const nsIntMargin& aMargin, nsIFrame* aFrame) { // Assuming aMargin was originally specified for a horizontal LTR context, // reinterpret the values as logical, and then map to physical coords // according to aFrame's actual writing mode. WritingMode wm = aFrame->GetWritingMode(); nsMargin m = LogicalMargin(wm, aMargin.top, aMargin.right, aMargin.bottom, aMargin.left).GetPhysicalMargin(wm); return nsIntMargin(m.top, m.right, m.bottom, m.left); } static const nsIntMargin kAquaDropdownBorder(1, 22, 2, 5); static const nsIntMargin kAquaComboboxBorder(3, 20, 3, 4); static const nsIntMargin kAquaSearchfieldBorder(3, 5, 2, 19); NS_IMETHODIMP nsNativeThemeCocoa::GetWidgetBorder(nsDeviceContext* aContext, nsIFrame* aFrame, uint8_t aWidgetType, nsIntMargin* aResult) { NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT; aResult->SizeTo(0, 0, 0, 0); switch (aWidgetType) { case NS_THEME_BUTTON: { if (IsButtonTypeMenu(aFrame)) { *aResult = DirectionAwareMargin(kAquaDropdownBorder, aFrame); } else { *aResult = DirectionAwareMargin(nsIntMargin(1, 7, 3, 7), aFrame); } break; } case NS_THEME_TOOLBARBUTTON: { *aResult = DirectionAwareMargin(nsIntMargin(1, 4, 1, 4), aFrame); break; } case NS_THEME_CHECKBOX: case NS_THEME_RADIO: { // nsFormControlFrame::GetIntrinsicWidth and nsFormControlFrame::GetIntrinsicHeight // assume a border width of 2px. aResult->SizeTo(2, 2, 2, 2); break; } case NS_THEME_MENULIST: case NS_THEME_MENULIST_BUTTON: *aResult = DirectionAwareMargin(kAquaDropdownBorder, aFrame); break; case NS_THEME_MENULIST_TEXTFIELD: *aResult = DirectionAwareMargin(kAquaComboboxBorder, aFrame); break; case NS_THEME_NUMBER_INPUT: case NS_THEME_TEXTFIELD: { SInt32 frameOutset = 0; ::GetThemeMetric(kThemeMetricEditTextFrameOutset, &frameOutset); SInt32 textPadding = 0; ::GetThemeMetric(kThemeMetricEditTextWhitespace, &textPadding); frameOutset += textPadding; aResult->SizeTo(frameOutset, frameOutset, frameOutset, frameOutset); break; } case NS_THEME_TEXTFIELD_MULTILINE: aResult->SizeTo(1, 1, 1, 1); break; case NS_THEME_SEARCHFIELD: *aResult = DirectionAwareMargin(kAquaSearchfieldBorder, aFrame); break; case NS_THEME_LISTBOX: { SInt32 frameOutset = 0; ::GetThemeMetric(kThemeMetricListBoxFrameOutset, &frameOutset); aResult->SizeTo(frameOutset, frameOutset, frameOutset, frameOutset); break; } case NS_THEME_SCROLLBARTRACK_HORIZONTAL: case NS_THEME_SCROLLBARTRACK_VERTICAL: { bool isHorizontal = (aWidgetType == NS_THEME_SCROLLBARTRACK_HORIZONTAL); if (nsLookAndFeel::UseOverlayScrollbars()) { if (!nsCocoaFeatures::OnYosemiteOrLater()) { // Pre-10.10, we have to center the thumb rect in the middle of the // scrollbar. Starting with 10.10, the expected rect for thumb // rendering is the full width of the scrollbar. if (isHorizontal) { aResult->top = 2; aResult->bottom = 1; } else { aResult->left = 2; aResult->right = 1; } } // Leave a bit of space at the start and the end on all OS X versions. if (isHorizontal) { aResult->left = 1; aResult->right = 1; } else { aResult->top = 1; aResult->bottom = 1; } } break; } case NS_THEME_STATUSBAR: aResult->SizeTo(1, 0, 0, 0); break; } if (IsHiDPIContext(aFrame->PresContext())) { *aResult = *aResult + *aResult; // doubled } return NS_OK; NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT; } // Return false here to indicate that CSS padding values should be used. There is // no reason to make a distinction between padding and border values, just specify // whatever values you want in GetWidgetBorder and only use this to return true // if you want to override CSS padding values. bool nsNativeThemeCocoa::GetWidgetPadding(nsDeviceContext* aContext, nsIFrame* aFrame, uint8_t aWidgetType, nsIntMargin* aResult) { // We don't want CSS padding being used for certain widgets. // See bug 381639 for an example of why. switch (aWidgetType) { // Radios and checkboxes return a fixed size in GetMinimumWidgetSize // and have a meaningful baseline, so they can't have // author-specified padding. case NS_THEME_CHECKBOX: case NS_THEME_RADIO: aResult->SizeTo(0, 0, 0, 0); return true; } return false; } bool nsNativeThemeCocoa::GetWidgetOverflow(nsDeviceContext* aContext, nsIFrame* aFrame, uint8_t aWidgetType, nsRect* aOverflowRect) { int32_t p2a = aFrame->PresContext()->AppUnitsPerDevPixel(); switch (aWidgetType) { case NS_THEME_BUTTON: case NS_THEME_MAC_DISCLOSURE_BUTTON_OPEN: case NS_THEME_MAC_DISCLOSURE_BUTTON_CLOSED: case NS_THEME_MAC_HELP_BUTTON: case NS_THEME_TOOLBARBUTTON: case NS_THEME_NUMBER_INPUT: case NS_THEME_TEXTFIELD: case NS_THEME_TEXTFIELD_MULTILINE: case NS_THEME_SEARCHFIELD: case NS_THEME_LISTBOX: case NS_THEME_MENULIST: case NS_THEME_MENULIST_BUTTON: case NS_THEME_MENULIST_TEXTFIELD: case NS_THEME_CHECKBOX: case NS_THEME_RADIO: case NS_THEME_TAB: { // We assume that the above widgets can draw a focus ring that will be less than // or equal to 4 pixels thick. nsIntMargin extraSize = nsIntMargin(kMaxFocusRingWidth, kMaxFocusRingWidth, kMaxFocusRingWidth, kMaxFocusRingWidth); nsMargin m(NSIntPixelsToAppUnits(extraSize.top, p2a), NSIntPixelsToAppUnits(extraSize.right, p2a), NSIntPixelsToAppUnits(extraSize.bottom, p2a), NSIntPixelsToAppUnits(extraSize.left, p2a)); aOverflowRect->Inflate(m); return true; } case NS_THEME_PROGRESSBAR: { // Progress bars draw a 2 pixel white shadow under their progress indicators nsMargin m(0, 0, NSIntPixelsToAppUnits(2, p2a), 0); aOverflowRect->Inflate(m); return true; } case NS_THEME_FOCUS_OUTLINE: { aOverflowRect->Inflate(NSIntPixelsToAppUnits(2, p2a)); return true; } } return false; } static const int32_t kRegularScrollbarThumbMinSize = 26; static const int32_t kSmallScrollbarThumbMinSize = 26; NS_IMETHODIMP nsNativeThemeCocoa::GetMinimumWidgetSize(nsPresContext* aPresContext, nsIFrame* aFrame, uint8_t aWidgetType, LayoutDeviceIntSize* aResult, bool* aIsOverridable) { NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT; aResult->SizeTo(0,0); *aIsOverridable = true; switch (aWidgetType) { case NS_THEME_BUTTON: { aResult->SizeTo(pushButtonSettings.minimumSizes[miniControlSize].width, pushButtonSettings.naturalSizes[miniControlSize].height); break; } case NS_THEME_BUTTON_ARROW_UP: case NS_THEME_BUTTON_ARROW_DOWN: { aResult->SizeTo(kMenuScrollArrowSize.width, kMenuScrollArrowSize.height); *aIsOverridable = false; break; } case NS_THEME_MENUARROW: { aResult->SizeTo(kMenuarrowSize.width, kMenuarrowSize.height); *aIsOverridable = false; break; } case NS_THEME_MAC_DISCLOSURE_BUTTON_OPEN: case NS_THEME_MAC_DISCLOSURE_BUTTON_CLOSED: { aResult->SizeTo(kDisclosureButtonSize.width, kDisclosureButtonSize.height); *aIsOverridable = false; break; } case NS_THEME_MAC_HELP_BUTTON: { aResult->SizeTo(kHelpButtonSize.width, kHelpButtonSize.height); *aIsOverridable = false; break; } case NS_THEME_TOOLBARBUTTON: { aResult->SizeTo(0, toolbarButtonHeights[miniControlSize]); break; } case NS_THEME_SPINNER: case NS_THEME_SPINNER_UPBUTTON: case NS_THEME_SPINNER_DOWNBUTTON: { SInt32 buttonHeight = 0, buttonWidth = 0; if (aFrame->GetContent()->IsXULElement()) { ::GetThemeMetric(kThemeMetricLittleArrowsWidth, &buttonWidth); ::GetThemeMetric(kThemeMetricLittleArrowsHeight, &buttonHeight); } else { NSSize size = spinnerSettings.minimumSizes[EnumSizeForCocoaSize(NSMiniControlSize)]; buttonWidth = size.width; buttonHeight = size.height; if (aWidgetType != NS_THEME_SPINNER) { // the buttons are half the height of the spinner buttonHeight /= 2; } } aResult->SizeTo(buttonWidth, buttonHeight); *aIsOverridable = true; break; } case NS_THEME_MENULIST: case NS_THEME_MENULIST_BUTTON: { SInt32 popupHeight = 0; ::GetThemeMetric(kThemeMetricPopupButtonHeight, &popupHeight); aResult->SizeTo(0, popupHeight); break; } case NS_THEME_NUMBER_INPUT: case NS_THEME_TEXTFIELD: case NS_THEME_TEXTFIELD_MULTILINE: case NS_THEME_SEARCHFIELD: { // at minimum, we should be tall enough for 9pt text. // I'm using hardcoded values here because the appearance manager // values for the frame size are incorrect. aResult->SizeTo(0, (2 + 2) /* top */ + 9 + (1 + 1) /* bottom */); break; } case NS_THEME_WINDOW_BUTTON_BOX: { NSSize size = WindowButtonsSize(aFrame); aResult->SizeTo(size.width, size.height); *aIsOverridable = false; break; } case NS_THEME_MAC_FULLSCREEN_BUTTON: { if ([NativeWindowForFrame(aFrame) respondsToSelector:@selector(toggleFullScreen:)] && !nsCocoaFeatures::OnYosemiteOrLater()) { // This value is hardcoded because it's needed before we can measure the // position and size of the fullscreen button. aResult->SizeTo(16, 17); } *aIsOverridable = false; break; } case NS_THEME_PROGRESSBAR: { SInt32 barHeight = 0; ::GetThemeMetric(kThemeMetricNormalProgressBarThickness, &barHeight); aResult->SizeTo(0, barHeight); break; } case NS_THEME_TREETWISTY: case NS_THEME_TREETWISTYOPEN: { SInt32 twistyHeight = 0, twistyWidth = 0; ::GetThemeMetric(kThemeMetricDisclosureButtonWidth, &twistyWidth); ::GetThemeMetric(kThemeMetricDisclosureButtonHeight, &twistyHeight); aResult->SizeTo(twistyWidth, twistyHeight); *aIsOverridable = false; break; } case NS_THEME_TREEHEADER: case NS_THEME_TREEHEADERCELL: { SInt32 headerHeight = 0; ::GetThemeMetric(kThemeMetricListHeaderHeight, &headerHeight); aResult->SizeTo(0, headerHeight - 1); // We don't need the top border. break; } case NS_THEME_TAB: { aResult->SizeTo(0, tabHeights[miniControlSize]); break; } case NS_THEME_RANGE: { // The Mac Appearance Manager API (the old API we're currently using) // doesn't define constants to obtain a minimum size for sliders. We use // the "thickness" of a slider that has default dimensions for both the // minimum width and height to get something sane and so that paint // invalidation works. SInt32 size = 0; if (IsRangeHorizontal(aFrame)) { ::GetThemeMetric(kThemeMetricHSliderHeight, &size); } else { ::GetThemeMetric(kThemeMetricVSliderWidth, &size); } aResult->SizeTo(size, size); *aIsOverridable = true; break; } case NS_THEME_RANGE_THUMB: { SInt32 width = 0; SInt32 height = 0; ::GetThemeMetric(kThemeMetricSliderMinThumbWidth, &width); ::GetThemeMetric(kThemeMetricSliderMinThumbHeight, &height); aResult->SizeTo(width, height); *aIsOverridable = false; break; } case NS_THEME_SCALE_HORIZONTAL: { SInt32 scaleHeight = 0; ::GetThemeMetric(kThemeMetricHSliderHeight, &scaleHeight); aResult->SizeTo(scaleHeight, scaleHeight); *aIsOverridable = false; break; } case NS_THEME_SCALE_VERTICAL: { SInt32 scaleWidth = 0; ::GetThemeMetric(kThemeMetricVSliderWidth, &scaleWidth); aResult->SizeTo(scaleWidth, scaleWidth); *aIsOverridable = false; break; } case NS_THEME_SCROLLBARTHUMB_HORIZONTAL: case NS_THEME_SCROLLBARTHUMB_VERTICAL: { // Find our parent scrollbar frame in order to find out whether we're in // a small or a large scrollbar. nsIFrame *scrollbarFrame = GetParentScrollbarFrame(aFrame); if (!scrollbarFrame) { return NS_ERROR_FAILURE; } bool isSmall = (scrollbarFrame->StyleDisplay()->mAppearance == NS_THEME_SCROLLBAR_SMALL); bool isHorizontal = (aWidgetType == NS_THEME_SCROLLBARTHUMB_HORIZONTAL); int32_t& minSize = isHorizontal ? aResult->width : aResult->height; minSize = isSmall ? kSmallScrollbarThumbMinSize : kRegularScrollbarThumbMinSize; break; } case NS_THEME_SCROLLBAR: case NS_THEME_SCROLLBAR_SMALL: case NS_THEME_SCROLLBARTRACK_VERTICAL: case NS_THEME_SCROLLBARTRACK_HORIZONTAL: { *aIsOverridable = false; if (nsLookAndFeel::UseOverlayScrollbars()) { nsIFrame* scrollbarFrame = GetParentScrollbarFrame(aFrame); if (scrollbarFrame && scrollbarFrame->StyleDisplay()->mAppearance == NS_THEME_SCROLLBAR_SMALL) { aResult->SizeTo(14, 14); } else { aResult->SizeTo(16, 16); } if (IsScrollbarWidthThin(aFrame)) { aResult->SizeTo(8, 8); } break; } // yeah, i know i'm cheating a little here, but i figure that it // really doesn't matter if the scrollbar is vertical or horizontal // and the width metric is a really good metric for every piece // of the scrollbar. nsIFrame *scrollbarFrame = GetParentScrollbarFrame(aFrame); if (!scrollbarFrame) return NS_ERROR_FAILURE; int32_t themeMetric = (scrollbarFrame->StyleDisplay()->mAppearance == NS_THEME_SCROLLBAR_SMALL) ? kThemeMetricSmallScrollBarWidth : kThemeMetricScrollBarWidth; SInt32 scrollbarWidth = 0; ::GetThemeMetric(themeMetric, &scrollbarWidth); if (IsScrollbarWidthThin(aFrame)) { scrollbarWidth /= 2; } aResult->SizeTo(scrollbarWidth, scrollbarWidth); break; } case NS_THEME_SCROLLBAR_NON_DISAPPEARING: { int32_t themeMetric = kThemeMetricScrollBarWidth; if (aFrame) { nsIFrame* scrollbarFrame = GetParentScrollbarFrame(aFrame); if (scrollbarFrame && scrollbarFrame->StyleDisplay()->mAppearance == NS_THEME_SCROLLBAR_SMALL) { // XXX We're interested in the width of non-disappearing scrollbars // to leave enough space for a dropmarker in non-native styled // comboboxes (bug 869314). It isn't clear to me if comboboxes can // ever have small scrollbars. themeMetric = kThemeMetricSmallScrollBarWidth; } } SInt32 scrollbarWidth = 0; ::GetThemeMetric(themeMetric, &scrollbarWidth); aResult->SizeTo(scrollbarWidth, scrollbarWidth); break; } case NS_THEME_SCROLLBARBUTTON_UP: case NS_THEME_SCROLLBARBUTTON_DOWN: case NS_THEME_SCROLLBARBUTTON_LEFT: case NS_THEME_SCROLLBARBUTTON_RIGHT: { if (!IsScrollbarWidthThin(aFrame)) { // Get scrollbar button metrics from the system, except in the case of // thin scrollbars, where we leave them at 0 (collapse) nsIFrame *scrollbarFrame = GetParentScrollbarFrame(aFrame); if (!scrollbarFrame) return NS_ERROR_FAILURE; // Since there is no NS_THEME_SCROLLBARBUTTON_UP_SMALL we need to ask the parent what appearance style it has. int32_t themeMetric = (scrollbarFrame->StyleDisplay()->mAppearance == NS_THEME_SCROLLBAR_SMALL) ? kThemeMetricSmallScrollBarWidth : kThemeMetricScrollBarWidth; SInt32 scrollbarWidth = 0; ::GetThemeMetric(themeMetric, &scrollbarWidth); // It seems that for both sizes of scrollbar, the buttons are one pixel "longer". if (aWidgetType == NS_THEME_SCROLLBARBUTTON_LEFT || aWidgetType == NS_THEME_SCROLLBARBUTTON_RIGHT) aResult->SizeTo(scrollbarWidth+1, scrollbarWidth); else aResult->SizeTo(scrollbarWidth, scrollbarWidth+1); } *aIsOverridable = false; break; } case NS_THEME_RESIZER: { HIThemeGrowBoxDrawInfo drawInfo; drawInfo.version = 0; drawInfo.state = kThemeStateActive; drawInfo.kind = kHIThemeGrowBoxKindNormal; drawInfo.direction = kThemeGrowRight | kThemeGrowDown; drawInfo.size = kHIThemeGrowBoxSizeNormal; HIPoint pnt = { 0, 0 }; HIRect bounds; HIThemeGetGrowBoxBounds(&pnt, &drawInfo, &bounds); aResult->SizeTo(bounds.size.width, bounds.size.height); *aIsOverridable = false; } } if (IsHiDPIContext(aPresContext)) { *aResult = *aResult * 2; } return NS_OK; NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT; } NS_IMETHODIMP nsNativeThemeCocoa::WidgetStateChanged(nsIFrame* aFrame, uint8_t aWidgetType, nsIAtom* aAttribute, bool* aShouldRepaint, const nsAttrValue* aOldValue) { // Some widget types just never change state. switch (aWidgetType) { case NS_THEME_WINDOW_TITLEBAR: case NS_THEME_TOOLBOX: case NS_THEME_TOOLBAR: case NS_THEME_STATUSBAR: case NS_THEME_STATUSBARPANEL: case NS_THEME_RESIZERPANEL: case NS_THEME_TOOLTIP: case NS_THEME_TABPANELS: case NS_THEME_TABPANEL: case NS_THEME_DIALOG: case NS_THEME_MENUPOPUP: case NS_THEME_GROUPBOX: case NS_THEME_PROGRESSCHUNK: case NS_THEME_PROGRESSCHUNK_VERTICAL: case NS_THEME_PROGRESSBAR: case NS_THEME_PROGRESSBAR_VERTICAL: case NS_THEME_METERBAR: case NS_THEME_METERCHUNK: case NS_THEME_MAC_VIBRANCY_LIGHT: case NS_THEME_MAC_VIBRANCY_DARK: *aShouldRepaint = false; return NS_OK; } // XXXdwh Not sure what can really be done here. Can at least guess for // specific widgets that they're highly unlikely to have certain states. // For example, a toolbar doesn't care about any states. if (!aAttribute) { // Hover/focus/active changed. Always repaint. *aShouldRepaint = true; } else { // Check the attribute to see if it's relevant. // disabled, checked, dlgtype, default, etc. *aShouldRepaint = false; if (aAttribute == nsGkAtoms::disabled || aAttribute == nsGkAtoms::checked || aAttribute == nsGkAtoms::selected || aAttribute == nsGkAtoms::visuallyselected || aAttribute == nsGkAtoms::menuactive || aAttribute == nsGkAtoms::sortDirection || aAttribute == nsGkAtoms::focused || aAttribute == nsGkAtoms::_default || aAttribute == nsGkAtoms::open || aAttribute == nsGkAtoms::hover) *aShouldRepaint = true; } return NS_OK; } NS_IMETHODIMP nsNativeThemeCocoa::ThemeChanged() { // This is unimplemented because we don't care if gecko changes its theme // and Mac OS X doesn't have themes. return NS_OK; } bool nsNativeThemeCocoa::ThemeSupportsWidget(nsPresContext* aPresContext, nsIFrame* aFrame, uint8_t aWidgetType) { // We don't have CSS set up to render non-native scrollbars on Mac OS X so we // render natively even if native theme support is disabled. if (aWidgetType != NS_THEME_SCROLLBAR && aPresContext && !aPresContext->PresShell()->IsThemeSupportEnabled()) return false; // if this is a dropdown button in a combobox the answer is always no if (aWidgetType == NS_THEME_MENULIST_BUTTON) { nsIFrame* parentFrame = aFrame->GetParent(); if (parentFrame && (parentFrame->GetType() == nsGkAtoms::comboboxControlFrame)) return false; } switch (aWidgetType) { // Combobox dropdowns don't support native theming in vertical mode. case NS_THEME_MENULIST: case NS_THEME_MENULIST_BUTTON: case NS_THEME_MENULIST_TEXT: case NS_THEME_MENULIST_TEXTFIELD: if (aFrame && aFrame->GetWritingMode().IsVertical()) { return false; } MOZ_FALLTHROUGH; case NS_THEME_LISTBOX: case NS_THEME_DIALOG: case NS_THEME_WINDOW: case NS_THEME_WINDOW_BUTTON_BOX: case NS_THEME_WINDOW_TITLEBAR: case NS_THEME_CHECKMENUITEM: case NS_THEME_MENUPOPUP: case NS_THEME_MENUARROW: case NS_THEME_MENUITEM: case NS_THEME_MENUSEPARATOR: case NS_THEME_MAC_FULLSCREEN_BUTTON: case NS_THEME_TOOLTIP: case NS_THEME_CHECKBOX: case NS_THEME_CHECKBOX_CONTAINER: case NS_THEME_RADIO: case NS_THEME_RADIO_CONTAINER: case NS_THEME_GROUPBOX: case NS_THEME_MAC_HELP_BUTTON: case NS_THEME_MAC_DISCLOSURE_BUTTON_OPEN: case NS_THEME_MAC_DISCLOSURE_BUTTON_CLOSED: case NS_THEME_BUTTON: case NS_THEME_BUTTON_ARROW_UP: case NS_THEME_BUTTON_ARROW_DOWN: case NS_THEME_BUTTON_BEVEL: case NS_THEME_TOOLBARBUTTON: case NS_THEME_SPINNER: case NS_THEME_SPINNER_UPBUTTON: case NS_THEME_SPINNER_DOWNBUTTON: case NS_THEME_TOOLBAR: case NS_THEME_STATUSBAR: case NS_THEME_NUMBER_INPUT: case NS_THEME_TEXTFIELD: case NS_THEME_TEXTFIELD_MULTILINE: case NS_THEME_SEARCHFIELD: case NS_THEME_TOOLBOX: //case NS_THEME_TOOLBARBUTTON: case NS_THEME_PROGRESSBAR: case NS_THEME_PROGRESSBAR_VERTICAL: case NS_THEME_PROGRESSCHUNK: case NS_THEME_PROGRESSCHUNK_VERTICAL: case NS_THEME_METERBAR: case NS_THEME_METERCHUNK: case NS_THEME_SEPARATOR: case NS_THEME_TABPANELS: case NS_THEME_TAB: case NS_THEME_TREETWISTY: case NS_THEME_TREETWISTYOPEN: case NS_THEME_TREEVIEW: case NS_THEME_TREEHEADER: case NS_THEME_TREEHEADERCELL: case NS_THEME_TREEHEADERSORTARROW: case NS_THEME_TREEITEM: case NS_THEME_TREELINE: case NS_THEME_MAC_SOURCE_LIST: case NS_THEME_MAC_SOURCE_LIST_SELECTION: case NS_THEME_MAC_ACTIVE_SOURCE_LIST_SELECTION: case NS_THEME_RANGE: case NS_THEME_SCALE_HORIZONTAL: case NS_THEME_SCALETHUMB_HORIZONTAL: case NS_THEME_SCALE_VERTICAL: case NS_THEME_SCALETHUMB_VERTICAL: case NS_THEME_SCROLLBAR: case NS_THEME_SCROLLBAR_SMALL: case NS_THEME_SCROLLBARBUTTON_UP: case NS_THEME_SCROLLBARBUTTON_DOWN: case NS_THEME_SCROLLBARBUTTON_LEFT: case NS_THEME_SCROLLBARBUTTON_RIGHT: case NS_THEME_SCROLLBARTHUMB_HORIZONTAL: case NS_THEME_SCROLLBARTHUMB_VERTICAL: case NS_THEME_SCROLLBARTRACK_VERTICAL: case NS_THEME_SCROLLBARTRACK_HORIZONTAL: case NS_THEME_SCROLLBAR_NON_DISAPPEARING: return !IsWidgetStyled(aPresContext, aFrame, aWidgetType); case NS_THEME_RESIZER: { nsIFrame* parentFrame = aFrame->GetParent(); if (!parentFrame || parentFrame->GetType() != nsGkAtoms::scrollFrame) return true; // Note that IsWidgetStyled is not called for resizers on Mac. This is // because for scrollable containers, the native resizer looks better // when (non-overlay) scrollbars are present even when the style is // overriden, and the custom transparent resizer looks better when // scrollbars are not present. nsIScrollableFrame* scrollFrame = do_QueryFrame(parentFrame); return (!nsLookAndFeel::UseOverlayScrollbars() && scrollFrame && scrollFrame->GetScrollbarVisibility()); } case NS_THEME_FOCUS_OUTLINE: return true; case NS_THEME_MAC_VIBRANCY_LIGHT: case NS_THEME_MAC_VIBRANCY_DARK: return VibrancyManager::SystemSupportsVibrancy(); } return false; } bool nsNativeThemeCocoa::WidgetIsContainer(uint8_t aWidgetType) { // flesh this out at some point switch (aWidgetType) { case NS_THEME_MENULIST_BUTTON: case NS_THEME_RADIO: case NS_THEME_CHECKBOX: case NS_THEME_PROGRESSBAR: case NS_THEME_METERBAR: case NS_THEME_RANGE: case NS_THEME_MAC_HELP_BUTTON: case NS_THEME_MAC_DISCLOSURE_BUTTON_OPEN: case NS_THEME_MAC_DISCLOSURE_BUTTON_CLOSED: return false; } return true; } bool nsNativeThemeCocoa::ThemeDrawsFocusForWidget(uint8_t aWidgetType) { if (aWidgetType == NS_THEME_MENULIST || aWidgetType == NS_THEME_MENULIST_TEXTFIELD || aWidgetType == NS_THEME_BUTTON || aWidgetType == NS_THEME_MAC_HELP_BUTTON || aWidgetType == NS_THEME_MAC_DISCLOSURE_BUTTON_OPEN || aWidgetType == NS_THEME_MAC_DISCLOSURE_BUTTON_CLOSED || aWidgetType == NS_THEME_RADIO || aWidgetType == NS_THEME_RANGE || aWidgetType == NS_THEME_CHECKBOX) return true; return false; } bool nsNativeThemeCocoa::ThemeNeedsComboboxDropmarker() { return false; } bool nsNativeThemeCocoa::WidgetAppearanceDependsOnWindowFocus(uint8_t aWidgetType) { switch (aWidgetType) { case NS_THEME_DIALOG: case NS_THEME_GROUPBOX: case NS_THEME_TABPANELS: case NS_THEME_BUTTON_ARROW_UP: case NS_THEME_BUTTON_ARROW_DOWN: case NS_THEME_CHECKMENUITEM: case NS_THEME_MENUPOPUP: case NS_THEME_MENUARROW: case NS_THEME_MENUITEM: case NS_THEME_MENUSEPARATOR: case NS_THEME_TOOLTIP: case NS_THEME_SPINNER: case NS_THEME_SPINNER_UPBUTTON: case NS_THEME_SPINNER_DOWNBUTTON: case NS_THEME_SEPARATOR: case NS_THEME_TOOLBOX: case NS_THEME_NUMBER_INPUT: case NS_THEME_TEXTFIELD: case NS_THEME_TREEVIEW: case NS_THEME_TREELINE: case NS_THEME_TEXTFIELD_MULTILINE: case NS_THEME_LISTBOX: case NS_THEME_RESIZER: return false; default: return true; } } bool nsNativeThemeCocoa::IsWindowSheet(nsIFrame* aFrame) { NSWindow* win = NativeWindowForFrame(aFrame); id winDelegate = [win delegate]; nsIWidget* widget = [(WindowDelegate *)winDelegate geckoWidget]; if (!widget) { return false; } return (widget->WindowType() == eWindowType_sheet); } bool nsNativeThemeCocoa::NeedToClearBackgroundBehindWidget(nsIFrame* aFrame, uint8_t aWidgetType) { switch (aWidgetType) { case NS_THEME_MAC_SOURCE_LIST: // If we're in a XUL tree, we don't want to clear the background behind the // selections below, since that would make our source list to not pick up // the right font smoothing background. But since we don't call this method // in nsTreeBodyFrame::BuildDisplayList, we never get here. case NS_THEME_MAC_SOURCE_LIST_SELECTION: case NS_THEME_MAC_ACTIVE_SOURCE_LIST_SELECTION: case NS_THEME_MAC_VIBRANCY_LIGHT: case NS_THEME_MAC_VIBRANCY_DARK: case NS_THEME_TOOLTIP: case NS_THEME_MENUPOPUP: case NS_THEME_MENUITEM: case NS_THEME_CHECKMENUITEM: return true; case NS_THEME_DIALOG: return IsWindowSheet(aFrame); default: return false; } } static nscolor ConvertNSColor(NSColor* aColor) { NSColor* deviceColor = [aColor colorUsingColorSpaceName:NSDeviceRGBColorSpace]; return NS_RGBA((unsigned int)([deviceColor redComponent] * 255.0), (unsigned int)([deviceColor greenComponent] * 255.0), (unsigned int)([deviceColor blueComponent] * 255.0), (unsigned int)([deviceColor alphaComponent] * 255.0)); } bool nsNativeThemeCocoa::WidgetProvidesFontSmoothingBackgroundColor(nsIFrame* aFrame, uint8_t aWidgetType, nscolor* aColor) { switch (aWidgetType) { case NS_THEME_MAC_SOURCE_LIST: case NS_THEME_MAC_SOURCE_LIST_SELECTION: case NS_THEME_MAC_ACTIVE_SOURCE_LIST_SELECTION: case NS_THEME_MAC_VIBRANCY_LIGHT: case NS_THEME_MAC_VIBRANCY_DARK: case NS_THEME_TOOLTIP: case NS_THEME_MENUPOPUP: case NS_THEME_MENUITEM: case NS_THEME_CHECKMENUITEM: case NS_THEME_DIALOG: { if ((aWidgetType == NS_THEME_DIALOG && !IsWindowSheet(aFrame)) || ((aWidgetType == NS_THEME_MAC_SOURCE_LIST_SELECTION || aWidgetType == NS_THEME_MAC_ACTIVE_SOURCE_LIST_SELECTION) && !IsInSourceList(aFrame))) { return false; } ChildView* childView = ChildViewForFrame(aFrame); if (childView) { ThemeGeometryType type = ThemeGeometryTypeForWidget(aFrame, aWidgetType); NSColor* color = [childView vibrancyFontSmoothingBackgroundColorForThemeGeometryType:type]; *aColor = ConvertNSColor(color); return true; } return false; } default: return false; } } nsITheme::ThemeGeometryType nsNativeThemeCocoa::ThemeGeometryTypeForWidget(nsIFrame* aFrame, uint8_t aWidgetType) { switch (aWidgetType) { case NS_THEME_WINDOW_TITLEBAR: return eThemeGeometryTypeTitlebar; case NS_THEME_TOOLBAR: return eThemeGeometryTypeToolbar; case NS_THEME_TOOLBOX: return eThemeGeometryTypeToolbox; case NS_THEME_WINDOW_BUTTON_BOX: return eThemeGeometryTypeWindowButtons; case NS_THEME_MAC_FULLSCREEN_BUTTON: return eThemeGeometryTypeFullscreenButton; case NS_THEME_MAC_VIBRANCY_LIGHT: return eThemeGeometryTypeVibrancyLight; case NS_THEME_MAC_VIBRANCY_DARK: return eThemeGeometryTypeVibrancyDark; case NS_THEME_TOOLTIP: return eThemeGeometryTypeTooltip; case NS_THEME_MENUPOPUP: return eThemeGeometryTypeMenu; case NS_THEME_MENUITEM: case NS_THEME_CHECKMENUITEM: { EventStates eventState = GetContentState(aFrame, aWidgetType); bool isDisabled = IsDisabled(aFrame, eventState); bool isSelected = !isDisabled && CheckBooleanAttr(aFrame, nsGkAtoms::menuactive); return isSelected ? eThemeGeometryTypeHighlightedMenuItem : eThemeGeometryTypeMenu; } case NS_THEME_DIALOG: return IsWindowSheet(aFrame) ? eThemeGeometryTypeSheet : eThemeGeometryTypeUnknown; case NS_THEME_MAC_SOURCE_LIST: return eThemeGeometryTypeSourceList; case NS_THEME_MAC_SOURCE_LIST_SELECTION: return IsInSourceList(aFrame) ? eThemeGeometryTypeSourceListSelection : eThemeGeometryTypeUnknown; case NS_THEME_MAC_ACTIVE_SOURCE_LIST_SELECTION: return IsInSourceList(aFrame) ? eThemeGeometryTypeActiveSourceListSelection : eThemeGeometryTypeUnknown; default: return eThemeGeometryTypeUnknown; } } nsITheme::Transparency nsNativeThemeCocoa::GetWidgetTransparency(nsIFrame* aFrame, uint8_t aWidgetType) { switch (aWidgetType) { case NS_THEME_MENUPOPUP: case NS_THEME_TOOLTIP: return eTransparent; case NS_THEME_DIALOG: return IsWindowSheet(aFrame) ? eTransparent : eOpaque; case NS_THEME_SCROLLBAR_SMALL: case NS_THEME_SCROLLBAR: return nsLookAndFeel::UseOverlayScrollbars() ? eTransparent : eOpaque; case NS_THEME_STATUSBAR: // Knowing that scrollbars and statusbars are opaque improves // performance, because we create layers for them. return eOpaque; case NS_THEME_TOOLBAR: return eOpaque; default: return eUnknownTransparency; } }