diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
commit | 36d22d82aa202bb199967e9512281e9a53db42c9 (patch) | |
tree | 105e8c98ddea1c1e4784a60a5a6410fa416be2de /widget/cocoa/nsNativeThemeCocoa.mm | |
parent | Initial commit. (diff) | |
download | firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.tar.xz firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.zip |
Adding upstream version 115.7.0esr.upstream/115.7.0esr
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'widget/cocoa/nsNativeThemeCocoa.mm')
-rw-r--r-- | widget/cocoa/nsNativeThemeCocoa.mm | 3444 |
1 files changed, 3444 insertions, 0 deletions
diff --git a/widget/cocoa/nsNativeThemeCocoa.mm b/widget/cocoa/nsNativeThemeCocoa.mm new file mode 100644 index 0000000000..e14f161663 --- /dev/null +++ b/widget/cocoa/nsNativeThemeCocoa.mm @@ -0,0 +1,3444 @@ +/* -*- 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 <objc/NSObjCRuntime.h> + +#include "mozilla/gfx/2D.h" +#include "mozilla/gfx/Helpers.h" +#include "mozilla/gfx/PathHelpers.h" +#include "nsChildView.h" +#include "nsDeviceContext.h" +#include "nsLayoutUtils.h" +#include "nsObjCExceptions.h" +#include "nsNumberControlFrame.h" +#include "nsRangeFrame.h" +#include "nsRect.h" +#include "nsSize.h" +#include "nsStyleConsts.h" +#include "nsPresContext.h" +#include "nsIContent.h" +#include "mozilla/dom/Document.h" +#include "nsIFrame.h" +#include "nsAtom.h" +#include "nsNameSpaceManager.h" +#include "nsPresContext.h" +#include "nsGkAtoms.h" +#include "nsCocoaFeatures.h" +#include "nsCocoaWindow.h" +#include "nsNativeThemeColors.h" +#include "nsIScrollableFrame.h" +#include "mozilla/ClearOnShutdown.h" +#include "mozilla/Range.h" +#include "mozilla/dom/Element.h" +#include "mozilla/dom/HTMLMeterElement.h" +#include "mozilla/layers/StackingContextHelper.h" +#include "mozilla/StaticPrefs_layout.h" +#include "mozilla/StaticPrefs_widget.h" +#include "nsLookAndFeel.h" +#include "MacThemeGeometryType.h" +#include "SDKDeclarations.h" +#include "VibrancyManager.h" + +#include "gfxContext.h" +#include "gfxQuartzSurface.h" +#include "gfxQuartzNativeDrawing.h" +#include "gfxUtils.h" // for ToDeviceColor +#include <algorithm> + +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); +} + +static bool IsDarkAppearance(NSAppearance* appearance) { + if (@available(macOS 10.14, *)) { + return [appearance.name isEqualToString:NSAppearanceNameDarkAqua]; + } + return false; +} + +// 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 + +// This is the window for our MOZCellDrawView. When an NSCell is drawn, some NSCell implementations +// look at the draw view's window to determine whether the cell should draw with the active look. +@interface MOZCellDrawWindow : NSWindow +@property BOOL cellsShouldLookActive; +@end + +@implementation MOZCellDrawWindow + +// Override three different methods, for good measure. The NSCell implementation could call any one +// of them. +- (BOOL)_hasActiveAppearance { + return self.cellsShouldLookActive; +} +- (BOOL)hasKeyAppearance { + return self.cellsShouldLookActive; +} +- (BOOL)_hasKeyAppearance { + return self.cellsShouldLookActive; +} + +@end + +// The purpose of this class is to provide objects that can be used when drawing +// NSCells using drawWithFrame:inView: without causing any harm. Only a small +// number of methods are called on the draw view, among those "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! +// This class needs to be an NSControl so that NSTextFieldCell (and +// NSSearchFieldCell, which is a subclass of NSTextFieldCell) draws a focus ring. +@interface MOZCellDrawView : NSControl +// Called by NSTreeHeaderCell during drawing. +@property BOOL _drawingEndSeparator; +@end + +@implementation MOZCellDrawView + +- (BOOL)isFlipped { + return YES; +} + +- (NSText*)currentEditor { + return nil; +} + +@end + +static void DrawFocusRingForCellIfNeeded(NSCell* aCell, NSRect aWithFrame, NSView* aInView) { + if ([aCell showsFirstResponder]) { + CGContextRef cgContext = [[NSGraphicsContext currentContext] CGContext]; + 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 void DrawCellIncludingFocusRing(NSCell* aCell, NSRect aWithFrame, NSView* aInView) { + [aCell drawWithFrame:aWithFrame inView:aInView]; + 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 = [[NSGraphicsContext currentContext] CGContext]; + + 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 == NSControlSizeRegular) { + tdi.kind = mIsIndeterminate ? kThemeLargeIndeterminateBar : kThemeLargeProgressBar; + } else { + NS_ASSERTION(size == NSControlSizeSmall, + "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 MOZSearchFieldCell : NSSearchFieldCell +@property BOOL shouldUseToolbarStyle; +@end + +@implementation MOZSearchFieldCell + +- (instancetype)init { + // We would like to render a search field which has the magnifying glass icon at the start of the + // search field, and no cancel button. + // On 10.12 and 10.13, empty search fields render the magnifying glass icon in the middle of the + // field. So in order to get the icon to show at the start of the field, we need to give the field + // some content. We achieve this with a single space character. + self = [super initTextCell:@" "]; + + // However, because the field is now non-empty, by default it shows a cancel button. To hide the + // cancel button, override it with a custom NSButtonCell which renders nothing. + NSButtonCell* invisibleCell = [[NSButtonCell alloc] initImageCell:nil]; + invisibleCell.bezeled = NO; + invisibleCell.bordered = NO; + self.cancelButtonCell = invisibleCell; + [invisibleCell release]; + + return self; +} + +- (BOOL)_isToolbarMode { + return self.shouldUseToolbarStyle; +} + +@end + +#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 == NSControlSizeMini) + return miniControlSize; + else if (cocoaControlSize == NSControlSizeSmall) + return smallControlSize; + else + return regularControlSize; +} + +static NSControlSize CocoaSizeForEnum(int32_t enumControlSize) { + if (enumControlSize == miniControlSize) + return NSControlSizeMini; + else if (enumControlSize == smallControlSize) + return NSControlSizeSmall; + else + return NSControlSizeRegular; +} + +static NSString* CUIControlSizeForCocoaSize(NSControlSize aControlSize) { + if (aControlSize == NSControlSizeRegular) + return @"regular"; + else if (aControlSize == NSControlSizeSmall) + return @"small"; + else + return @"mini"; +} + +static void InflateControlRect(NSRect* rect, NSControlSize cocoaControlSize, + const float marginSet[][3][4]) { + if (!marginSet) return; + + static int osIndex = yosemiteOSorlater; + 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 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->GetWindowType() == widget::WindowType::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 = nsLayoutUtils::GetCrossDocParentFrameInProcess(frame)) { + if (frame->StyleDisplay()->EffectiveAppearance() == StyleAppearance::MozMacSourceList) { + return true; + } + } + return false; +} + +NS_IMPL_ISUPPORTS_INHERITED(nsNativeThemeCocoa, nsNativeTheme, nsITheme) + +nsNativeThemeCocoa::nsNativeThemeCocoa() : ThemeCocoa(ScrollbarStyle()) { + NS_OBJC_BEGIN_TRY_IGNORE_BLOCK; + + kMaxFocusRingWidth = 7; + + // 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 = [[NSButtonCell alloc] initTextCell:@""]; + [mRadioButtonCell setButtonType:NSRadioButton]; + + mCheckboxCell = [[NSButtonCell alloc] initTextCell:@""]; + [mCheckboxCell setButtonType:NSSwitchButton]; + [mCheckboxCell setAllowsMixedState:YES]; + + mTextFieldCell = [[NSTextFieldCell alloc] initTextCell:@""]; + [mTextFieldCell setBezeled:YES]; + [mTextFieldCell setEditable:YES]; + [mTextFieldCell setFocusRingType:NSFocusRingTypeExterior]; + + mSearchFieldCell = [[MOZSearchFieldCell alloc] init]; + [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]; + + mTreeHeaderCell = [[NSTableHeaderCell alloc] init]; + + mCellDrawView = [[MOZCellDrawView alloc] init]; + + if (XRE_IsParentProcess()) { + // Put the cell draw view into a window that is never shown. + // This allows us to convince some NSCell implementations (such as NSButtonCell for default + // buttons) to draw with the active appearance. Another benefit of putting the draw view in a + // window is the fact that it lets NSTextFieldCell (and its subclass NSSearchFieldCell) inherit + // the current NSApplication effectiveAppearance automatically, so the field adapts to Dark Mode + // correctly. + // We don't create this window when the native theme is used in the content process because + // NSWindow creation runs into the sandbox and because we never run default buttons in content + // processes anyway. + mCellDrawWindow = [[MOZCellDrawWindow alloc] initWithContentRect:NSZeroRect + styleMask:NSWindowStyleMaskBorderless + backing:NSBackingStoreBuffered + defer:NO]; + [mCellDrawWindow.contentView addSubview:mCellDrawView]; + } + + NS_OBJC_END_TRY_IGNORE_BLOCK; +} + +nsNativeThemeCocoa::~nsNativeThemeCocoa() { + NS_OBJC_BEGIN_TRY_IGNORE_BLOCK; + + [mMeterBarCell release]; + [mProgressBarCell release]; + [mDisclosureButtonCell release]; + [mHelpButtonCell release]; + [mPushButtonCell release]; + [mRadioButtonCell release]; + [mCheckboxCell release]; + [mTextFieldCell release]; + [mSearchFieldCell release]; + [mDropdownCell release]; + [mComboBoxCell release]; + [mTreeHeaderCell release]; + [mCellDrawWindow release]; + [mCellDrawView release]; + + NS_OBJC_END_TRY_IGNORE_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 graphicsContextWithCGContext: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_IGNORE_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 graphicsContextWithCGContext: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 graphicsContextWithCGContext: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_IGNORE_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 NSControlSizeRegular 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_IGNORE_BLOCK; + + const float rectWidth = destRect.size.width, rectHeight = destRect.size.height; + const NSSize* sizes = settings.naturalSizes; + const NSSize miniSize = sizes[EnumSizeForCocoaSize(NSControlSizeMini)]; + const NSSize smallSize = sizes[EnumSizeForCocoaSize(NSControlSizeSmall)]; + const NSSize regularSize = sizes[EnumSizeForCocoaSize(NSControlSizeRegular)]; + + 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 = NSControlSizeRegular; + 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_IGNORE_BLOCK; +} + +@interface NSWindow (CoreUIRendererPrivate) ++ (CUIRendererRef)coreUIRenderer; +@end + +@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) { + if (!aSkipAreaCheck && aRect.size.width * aRect.size.height > BITMAP_MAX_AREA) { + return; + } + + NSAppearance* appearance = NSAppearance.currentAppearance; + 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 auto& va = aFrame->StyleDisplay()->mVerticalAlign; + auto kw = va.IsKeyword() ? va.AsKeyword() : StyleVerticalAlignKeyword::Middle; + switch (kw) { + case StyleVerticalAlignKeyword::Top: + case StyleVerticalAlignKeyword::TextTop: + return 0.0f; + + case StyleVerticalAlignKeyword::Sub: + case StyleVerticalAlignKeyword::Super: + case StyleVerticalAlignKeyword::Middle: + case StyleVerticalAlignKeyword::MozMiddleWithBaseline: + return 0.5f; + + case StyleVerticalAlignKeyword::Baseline: + case StyleVerticalAlignKeyword::Bottom: + case StyleVerticalAlignKeyword::TextBottom: + return 1.0f; + + default: + MOZ_ASSERT_UNREACHABLE("invalid vertical-align"); + return 0.5f; + } +} + +static void ApplyControlParamsToNSCell(nsNativeThemeCocoa::ControlParams aControlParams, + NSCell* aCell) { + [aCell setEnabled:!aControlParams.disabled]; + [aCell setShowsFirstResponder:(aControlParams.focused && !aControlParams.disabled && + aControlParams.insideActiveWindow)]; + [aCell setHighlighted:aControlParams.pressed]; +} + +// 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 + }}}; + +static NSControlStateValue CellStateForCheckboxOrRadioState( + nsNativeThemeCocoa::CheckboxOrRadioState aState) { + switch (aState) { + case nsNativeThemeCocoa::CheckboxOrRadioState::eOff: + return NSOffState; + case nsNativeThemeCocoa::CheckboxOrRadioState::eOn: + return NSOnState; + case nsNativeThemeCocoa::CheckboxOrRadioState::eIndeterminate: + return NSMixedState; + } +} + +void nsNativeThemeCocoa::DrawCheckboxOrRadio(CGContextRef cgContext, bool inCheckbox, + const HIRect& inBoxRect, + const CheckboxOrRadioParams& aParams) { + NS_OBJC_BEGIN_TRY_IGNORE_BLOCK; + + NSButtonCell* cell = inCheckbox ? mCheckboxCell : mRadioButtonCell; + ApplyControlParamsToNSCell(aParams.controlParams, cell); + + [cell setState:CellStateForCheckboxOrRadioState(aParams.state)]; + [cell setControlTint:(aParams.controlParams.insideActiveWindow ? [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); + + if (mCellDrawWindow) { + mCellDrawWindow.cellsShouldLookActive = aParams.controlParams.insideActiveWindow; + } + DrawCellWithSnapping(cell, cgContext, drawRect, inCheckbox ? checkboxSettings : radioSettings, + aParams.verticalAlignFactor, mCellDrawView, NO); + + NS_OBJC_END_TRY_IGNORE_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 + }}}; + +static bool IsToolbarStyleContainer(nsIFrame* aFrame) { + nsIContent* content = aFrame->GetContent(); + if (!content) { + return false; + } + + if (content->IsAnyOfXULElements(nsGkAtoms::toolbar, nsGkAtoms::toolbox, nsGkAtoms::statusbar)) { + return true; + } + + switch (aFrame->StyleDisplay()->EffectiveAppearance()) { + case StyleAppearance::Toolbar: + case StyleAppearance::Statusbar: + return true; + default: + return false; + } +} + +static bool IsInsideToolbar(nsIFrame* aFrame) { + for (nsIFrame* frame = aFrame; frame; frame = frame->GetParent()) { + if (IsToolbarStyleContainer(frame)) { + return true; + } + } + return false; +} + +nsNativeThemeCocoa::TextFieldParams nsNativeThemeCocoa::ComputeTextFieldParams( + nsIFrame* aFrame, ElementState aEventState) { + TextFieldParams params; + params.insideToolbar = IsInsideToolbar(aFrame); + params.disabled = aEventState.HasState(ElementState::DISABLED); + + // See ShouldUnconditionallyDrawFocusRingIfFocused. + params.focused = aEventState.HasState(ElementState::FOCUS); + + params.rtl = IsFrameRTL(aFrame); + params.verticalAlignFactor = VerticalAlignFactor(aFrame); + return params; +} + +void nsNativeThemeCocoa::DrawTextField(CGContextRef cgContext, const HIRect& inBoxRect, + const TextFieldParams& aParams) { + NS_OBJC_BEGIN_TRY_IGNORE_BLOCK; + + NSTextFieldCell* cell = mTextFieldCell; + [cell setEnabled:!aParams.disabled]; + [cell setShowsFirstResponder:aParams.focused]; + + if (mCellDrawWindow) { + mCellDrawWindow.cellsShouldLookActive = YES; // TODO: propagate correct activeness state + } + DrawCellWithSnapping(cell, cgContext, inBoxRect, searchFieldSettings, aParams.verticalAlignFactor, + mCellDrawView, aParams.rtl); + + NS_OBJC_END_TRY_IGNORE_BLOCK; +} + +void nsNativeThemeCocoa::DrawSearchField(CGContextRef cgContext, const HIRect& inBoxRect, + const TextFieldParams& aParams) { + NS_OBJC_BEGIN_TRY_IGNORE_BLOCK; + + mSearchFieldCell.enabled = !aParams.disabled; + mSearchFieldCell.showsFirstResponder = aParams.focused; + mSearchFieldCell.placeholderString = @""; + mSearchFieldCell.shouldUseToolbarStyle = aParams.insideToolbar; + + if (mCellDrawWindow) { + mCellDrawWindow.cellsShouldLookActive = YES; // TODO: propagate correct activeness state + } + DrawCellWithSnapping(mSearchFieldCell, cgContext, inBoxRect, searchFieldSettings, + aParams.verticalAlignFactor, mCellDrawView, aParams.rtl); + + NS_OBJC_END_TRY_IGNORE_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; + +NSString* nsNativeThemeCocoa::GetMenuIconName(const MenuIconParams& aParams) { + switch (aParams.icon) { + case MenuIcon::eCheckmark: + return kCheckmarkImage; + case MenuIcon::eMenuArrow: + return aParams.rtl ? kMenuarrowLeftImage : kMenuarrowRightImage; + case MenuIcon::eMenuDownScrollArrow: + return kMenuDownScrollArrowImage; + case MenuIcon::eMenuUpScrollArrow: + return kMenuUpScrollArrowImage; + } +} + +NSSize nsNativeThemeCocoa::GetMenuIconSize(MenuIcon aIcon) { + switch (aIcon) { + case MenuIcon::eCheckmark: + return kCheckmarkSize; + case MenuIcon::eMenuArrow: + return kMenuarrowSize; + case MenuIcon::eMenuDownScrollArrow: + case MenuIcon::eMenuUpScrollArrow: + return kMenuScrollArrowSize; + } +} + +nsNativeThemeCocoa::MenuIconParams nsNativeThemeCocoa::ComputeMenuIconParams( + nsIFrame* aFrame, ElementState aEventState, MenuIcon aIcon) { + bool isDisabled = aEventState.HasState(ElementState::DISABLED); + + MenuIconParams params; + params.icon = aIcon; + params.disabled = isDisabled; + params.insideActiveMenuItem = !isDisabled && CheckBooleanAttr(aFrame, nsGkAtoms::menuactive); + params.centerHorizontally = true; + params.rtl = IsFrameRTL(aFrame); + return params; +} + +void nsNativeThemeCocoa::DrawMenuIcon(CGContextRef cgContext, const CGRect& aRect, + const MenuIconParams& aParams) { + NS_OBJC_BEGIN_TRY_IGNORE_BLOCK; + + NSSize size = GetMenuIconSize(aParams.icon); + + // Adjust size and position of our drawRect. + CGFloat paddingX = std::max(CGFloat(0.0), aRect.size.width - size.width); + CGFloat paddingY = std::max(CGFloat(0.0), aRect.size.height - size.height); + CGFloat paddingStartX = std::min(paddingX, kMenuIconIndent); + CGFloat paddingEndX = std::max(CGFloat(0.0), paddingX - kMenuIconIndent); + CGRect drawRect = CGRectMake(aRect.origin.x + (aParams.centerHorizontally ? ceil(paddingX / 2) + : aParams.rtl ? paddingEndX + : paddingStartX), + aRect.origin.y + ceil(paddingY / 2), size.width, size.height); + + NSString* state; + if (aParams.disabled) { + state = @"disabled"; + } else if (aParams.insideActiveMenuItem) { + state = @"pressed"; + } else if (IsDarkAppearance(NSAppearance.currentAppearance)) { + // CUIDraw draws the image with a color that's too faint for the dark + // appearance. The "pressed" state happens to use white, which looks better + // and matches the white text color, so use it instead of "normal". + state = @"pressed"; + } else { + state = @"normal"; + } + + NSString* imageName = GetMenuIconName(aParams); + + 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_IGNORE_BLOCK; +} + +nsNativeThemeCocoa::MenuItemParams nsNativeThemeCocoa::ComputeMenuItemParams( + nsIFrame* aFrame, ElementState aEventState, bool aIsChecked) { + bool isDisabled = aEventState.HasState(ElementState::DISABLED); + + MenuItemParams params; + params.checked = aIsChecked; + params.disabled = isDisabled; + params.selected = !isDisabled && CheckBooleanAttr(aFrame, nsGkAtoms::menuactive); + params.rtl = IsFrameRTL(aFrame); + return params; +} + +void nsNativeThemeCocoa::DrawMenuItem(CGContextRef cgContext, const CGRect& inBoxRect, + const MenuItemParams& aParams) { + if (aParams.checked) { + MenuIconParams params; + params.disabled = aParams.disabled; + params.insideActiveMenuItem = aParams.selected; + params.rtl = aParams.rtl; + params.icon = MenuIcon::eCheckmark; + DrawMenuIcon(cgContext, inBoxRect, params); + } +} + +void nsNativeThemeCocoa::DrawMenuSeparator(CGContextRef cgContext, const CGRect& inBoxRect, + const MenuItemParams& aParams) { + // Workaround for visual artifacts issues with + // HIThemeDrawMenuSeparator on macOS Big Sur. + if (nsCocoaFeatures::OnBigSurOrLater()) { + CGRect separatorRect = inBoxRect; + separatorRect.size.height = 1; + separatorRect.size.width -= 42; + separatorRect.origin.x += 21; + if (!IsDarkAppearance(NSAppearance.currentAppearance)) { + // Use transparent black with an alpha similar to the native separator. + // The values 231 (menu background) and 205 (separator color) have been + // sampled from a window screenshot of a native context menu. + CGContextSetRGBFillColor(cgContext, 0.0, 0.0, 0.0, (231 - 205) / 231.0); + } else { + // Similar to above, use white with an alpha. The values 45 (menu + // background) and 81 (separator color) were sampled on macOS 12 with the + // "Reduce transparency" system setting turned on. + CGContextSetRGBFillColor(cgContext, 1.0, 1.0, 1.0, 1.0 + ((45 - 81) / 45.0)); + } + CGContextFillRect(cgContext, separatorRect); + return; + } + + ThemeMenuState menuState; + if (aParams.disabled) { + menuState = kThemeMenuDisabled; + } else { + menuState = aParams.selected ? kThemeMenuSelected : kThemeMenuActive; + } + + HIThemeMenuItemDrawInfo midi = {0, kThemeMenuItemPlain, menuState}; + HIThemeDrawMenuSeparator(&inBoxRect, &inBoxRect, &midi, cgContext, HITHEME_ORIENTATION); +} + +static bool ShouldUnconditionallyDrawFocusRingIfFocused(nsIFrame* aFrame) { + // Mac always draws focus rings for textboxes and lists. + switch (aFrame->StyleDisplay()->EffectiveAppearance()) { + case StyleAppearance::NumberInput: + case StyleAppearance::Textfield: + case StyleAppearance::Textarea: + case StyleAppearance::Searchfield: + case StyleAppearance::Listbox: + return true; + default: + return false; + } +} + +nsNativeThemeCocoa::ControlParams nsNativeThemeCocoa::ComputeControlParams( + nsIFrame* aFrame, ElementState aEventState) { + ControlParams params; + params.disabled = aEventState.HasState(ElementState::DISABLED); + params.insideActiveWindow = FrameIsInActiveWindow(aFrame); + params.pressed = aEventState.HasAllStates(ElementState::ACTIVE | ElementState::HOVER); + params.focused = aEventState.HasState(ElementState::FOCUS) && + (aEventState.HasState(ElementState::FOCUSRING) || + ShouldUnconditionallyDrawFocusRingIfFocused(aFrame)); + params.rtl = IsFrameRTL(aFrame); + return params; +} + +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, + ButtonType aButtonType, ControlParams aControlParams) { + NS_OBJC_BEGIN_TRY_IGNORE_BLOCK; + + ApplyControlParamsToNSCell(aControlParams, mPushButtonCell); + [mPushButtonCell setBezelStyle:NSRoundedBezelStyle]; + mPushButtonCell.keyEquivalent = aButtonType == ButtonType::eDefaultPushButton ? @"\r" : @""; + + if (mCellDrawWindow) { + mCellDrawWindow.cellsShouldLookActive = aControlParams.insideActiveWindow; + } + DrawCellWithSnapping(mPushButtonCell, cgContext, inBoxRect, pushButtonSettings, 0.5f, + mCellDrawView, aControlParams.rtl, 1.0f); + + NS_OBJC_END_TRY_IGNORE_BLOCK; +} + +void nsNativeThemeCocoa::DrawSquareBezelPushButton(CGContextRef cgContext, const HIRect& inBoxRect, + ControlParams aControlParams) { + NS_OBJC_BEGIN_TRY_IGNORE_BLOCK; + + ApplyControlParamsToNSCell(aControlParams, mPushButtonCell); + [mPushButtonCell setBezelStyle:NSShadowlessSquareBezelStyle]; + + if (mCellDrawWindow) { + mCellDrawWindow.cellsShouldLookActive = aControlParams.insideActiveWindow; + } + DrawCellWithScaling(mPushButtonCell, cgContext, inBoxRect, NSControlSizeRegular, NSZeroSize, + NSMakeSize(14, 0), NULL, mCellDrawView, aControlParams.rtl); + + NS_OBJC_END_TRY_IGNORE_BLOCK; +} + +void nsNativeThemeCocoa::DrawHelpButton(CGContextRef cgContext, const HIRect& inBoxRect, + ControlParams aControlParams) { + NS_OBJC_BEGIN_TRY_IGNORE_BLOCK; + + ApplyControlParamsToNSCell(aControlParams, mHelpButtonCell); + + if (mCellDrawWindow) { + mCellDrawWindow.cellsShouldLookActive = aControlParams.insideActiveWindow; + } + DrawCellWithScaling(mHelpButtonCell, cgContext, inBoxRect, NSControlSizeRegular, NSZeroSize, + kHelpButtonSize, NULL, mCellDrawView, + false); // Don't mirror icon in RTL. + + NS_OBJC_END_TRY_IGNORE_BLOCK; +} + +void nsNativeThemeCocoa::DrawDisclosureButton(CGContextRef cgContext, const HIRect& inBoxRect, + ControlParams aControlParams, + NSControlStateValue aCellState) { + NS_OBJC_BEGIN_TRY_IGNORE_BLOCK; + + ApplyControlParamsToNSCell(aControlParams, mDisclosureButtonCell); + [mDisclosureButtonCell setState:aCellState]; + + if (mCellDrawWindow) { + mCellDrawWindow.cellsShouldLookActive = aControlParams.insideActiveWindow; + } + DrawCellWithScaling(mDisclosureButtonCell, cgContext, inBoxRect, NSControlSizeRegular, NSZeroSize, + kDisclosureButtonSize, NULL, mCellDrawView, + false); // Don't mirror icon in RTL. + + NS_OBJC_END_TRY_IGNORE_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); +} + +static ThemeDrawState ToThemeDrawState(const nsNativeThemeCocoa::ControlParams& aParams) { + if (aParams.disabled) { + return kThemeStateUnavailable; + } + if (aParams.pressed) { + return kThemeStatePressed; + } + return kThemeStateActive; +} + +void nsNativeThemeCocoa::DrawHIThemeButton(CGContextRef cgContext, const HIRect& aRect, + ThemeButtonKind aKind, ThemeButtonValue aValue, + ThemeDrawState aState, ThemeButtonAdornment aAdornment, + const ControlParams& aParams) { + NS_OBJC_BEGIN_TRY_IGNORE_BLOCK; + + HIThemeButtonDrawInfo bdi; + bdi.version = 0; + bdi.kind = aKind; + bdi.value = aValue; + bdi.state = aState; + bdi.adornment = aAdornment; + + if (aParams.focused && aParams.insideActiveWindow) { + bdi.adornment |= kThemeAdornmentFocus; + } + + RenderTransformedHIThemeControl(cgContext, aRect, RenderButton, &bdi, aParams.rtl); + +#if DRAW_IN_FRAME_DEBUG + CGContextSetRGBFillColor(cgContext, 0.0, 0.0, 0.5, 0.25); + CGContextFillRect(cgContext, inBoxRect); +#endif + + NS_OBJC_END_TRY_IGNORE_BLOCK; +} + +void nsNativeThemeCocoa::DrawButton(CGContextRef cgContext, const HIRect& inBoxRect, + const ButtonParams& aParams) { + ControlParams controlParams = aParams.controlParams; + + switch (aParams.button) { + case ButtonType::eRegularPushButton: + case ButtonType::eDefaultPushButton: + DrawPushButton(cgContext, inBoxRect, aParams.button, controlParams); + return; + case ButtonType::eSquareBezelPushButton: + DrawSquareBezelPushButton(cgContext, inBoxRect, controlParams); + return; + case ButtonType::eArrowButton: + DrawHIThemeButton(cgContext, inBoxRect, kThemeArrowButton, kThemeButtonOn, + kThemeStateUnavailable, kThemeAdornmentArrowDownArrow, controlParams); + return; + case ButtonType::eHelpButton: + DrawHelpButton(cgContext, inBoxRect, controlParams); + return; + case ButtonType::eTreeTwistyPointingRight: + DrawHIThemeButton(cgContext, inBoxRect, kThemeDisclosureButton, kThemeDisclosureRight, + ToThemeDrawState(controlParams), kThemeAdornmentNone, controlParams); + return; + case ButtonType::eTreeTwistyPointingDown: + DrawHIThemeButton(cgContext, inBoxRect, kThemeDisclosureButton, kThemeDisclosureDown, + ToThemeDrawState(controlParams), kThemeAdornmentNone, controlParams); + return; + case ButtonType::eDisclosureButtonClosed: + DrawDisclosureButton(cgContext, inBoxRect, controlParams, NSOffState); + return; + case ButtonType::eDisclosureButtonOpen: + DrawDisclosureButton(cgContext, inBoxRect, controlParams, NSOnState); + return; + } +} + +nsNativeThemeCocoa::TreeHeaderCellParams nsNativeThemeCocoa::ComputeTreeHeaderCellParams( + nsIFrame* aFrame, ElementState aEventState) { + TreeHeaderCellParams params; + params.controlParams = ComputeControlParams(aFrame, aEventState); + params.sortDirection = GetTreeSortDirection(aFrame); + params.lastTreeHeaderCell = IsLastTreeHeaderCell(aFrame); + return params; +} + +@interface NSTableHeaderCell (NSTableHeaderCell_setSortable) +// This method has been present in the same form since at least macOS 10.4. +- (void)_setSortable:(BOOL)arg1 + showSortIndicator:(BOOL)arg2 + ascending:(BOOL)arg3 + priority:(NSInteger)arg4 + highlightForSort:(BOOL)arg5; +@end + +void nsNativeThemeCocoa::DrawTreeHeaderCell(CGContextRef cgContext, const HIRect& inBoxRect, + const TreeHeaderCellParams& aParams) { + NS_OBJC_BEGIN_TRY_IGNORE_BLOCK; + + // Without clearing the cell's title, it takes on a default value of "Field", + // which is displayed underneath the title set in the front-end. + NSCell* cell = (NSCell*)mTreeHeaderCell; + cell.title = @""; + + if ([mTreeHeaderCell respondsToSelector:@selector + (_setSortable:showSortIndicator:ascending:priority:highlightForSort:)]) { + switch (aParams.sortDirection) { + case eTreeSortDirection_Ascending: + [mTreeHeaderCell _setSortable:YES + showSortIndicator:YES + ascending:YES + priority:0 + highlightForSort:YES]; + break; + case eTreeSortDirection_Descending: + [mTreeHeaderCell _setSortable:YES + showSortIndicator:YES + ascending:NO + priority:0 + highlightForSort:YES]; + break; + default: + // eTreeSortDirection_Natural + [mTreeHeaderCell _setSortable:YES + showSortIndicator:NO + ascending:YES + priority:0 + highlightForSort:NO]; + break; + } + } + + mTreeHeaderCell.enabled = !aParams.controlParams.disabled; + mTreeHeaderCell.state = + (mTreeHeaderCell.enabled && aParams.controlParams.pressed) ? NSOnState : NSOffState; + + mCellDrawView._drawingEndSeparator = !aParams.lastTreeHeaderCell; + + NSGraphicsContext* savedContext = NSGraphicsContext.currentContext; + NSGraphicsContext.currentContext = [NSGraphicsContext graphicsContextWithCGContext:cgContext + flipped:YES]; + DrawCellIncludingFocusRing(mTreeHeaderCell, inBoxRect, mCellDrawView); + NSGraphicsContext.currentContext = savedContext; + +#if DRAW_IN_FRAME_DEBUG + CGContextSetRGBFillColor(cgContext, 0.0, 0.0, 0.5, 0.25); + CGContextFillRect(cgContext, inBoxRect); +#endif + + NS_OBJC_END_TRY_IGNORE_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, + const DropdownParams& aParams) { + NS_OBJC_BEGIN_TRY_IGNORE_BLOCK; + + [mDropdownCell setPullsDown:aParams.pullsDown]; + NSCell* cell = aParams.editable ? (NSCell*)mComboBoxCell : (NSCell*)mDropdownCell; + + ApplyControlParamsToNSCell(aParams.controlParams, cell); + + if (aParams.controlParams.insideActiveWindow) { + [cell setControlTint:[NSColor currentControlTint]]; + } else { + [cell setControlTint:NSClearControlTint]; + } + + const CellRenderSettings& settings = + aParams.editable ? editableMenulistSettings : dropdownSettings; + + if (mCellDrawWindow) { + mCellDrawWindow.cellsShouldLookActive = aParams.controlParams.insideActiveWindow; + } + DrawCellWithSnapping(cell, cgContext, inBoxRect, settings, 0.5f, mCellDrawView, + aParams.controlParams.rtl); + + NS_OBJC_END_TRY_IGNORE_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 + }}}; + +HIThemeButtonDrawInfo nsNativeThemeCocoa::SpinButtonDrawInfo(ThemeButtonKind aKind, + const SpinButtonParams& aParams) { + HIThemeButtonDrawInfo bdi; + bdi.version = 0; + bdi.kind = aKind; + bdi.value = kThemeButtonOff; + bdi.adornment = kThemeAdornmentNone; + + if (aParams.disabled) { + bdi.state = kThemeStateUnavailable; + } else if (aParams.insideActiveWindow && aParams.pressedButton) { + if (*aParams.pressedButton == SpinButton::eUp) { + bdi.state = kThemeStatePressedUp; + } else { + bdi.state = kThemeStatePressedDown; + } + } else { + bdi.state = kThemeStateActive; + } + + return bdi; +} + +void nsNativeThemeCocoa::DrawSpinButtons(CGContextRef cgContext, const HIRect& inBoxRect, + const SpinButtonParams& aParams) { + NS_OBJC_BEGIN_TRY_IGNORE_BLOCK; + + HIThemeButtonDrawInfo bdi = SpinButtonDrawInfo(kThemeIncDecButton, aParams); + HIThemeDrawButton(&inBoxRect, &bdi, cgContext, HITHEME_ORIENTATION, NULL); + + NS_OBJC_END_TRY_IGNORE_BLOCK; +} + +void nsNativeThemeCocoa::DrawSpinButton(CGContextRef cgContext, const HIRect& inBoxRect, + SpinButton aDrawnButton, const SpinButtonParams& aParams) { + NS_OBJC_BEGIN_TRY_IGNORE_BLOCK; + + HIThemeButtonDrawInfo bdi = SpinButtonDrawInfo(kThemeIncDecButtonMini, aParams); + + // 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 (aDrawnButton == SpinButton::eDown) { + 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_IGNORE_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 + }}}}}; + +nsNativeThemeCocoa::ProgressParams nsNativeThemeCocoa::ComputeProgressParams( + nsIFrame* aFrame, ElementState aEventState, bool aIsHorizontal) { + ProgressParams params; + params.value = GetProgressValue(aFrame); + params.max = GetProgressMaxValue(aFrame); + params.verticalAlignFactor = VerticalAlignFactor(aFrame); + params.insideActiveWindow = FrameIsInActiveWindow(aFrame); + params.indeterminate = aEventState.HasState(ElementState::INDETERMINATE); + params.horizontal = aIsHorizontal; + params.rtl = IsFrameRTL(aFrame); + return params; +} + +void nsNativeThemeCocoa::DrawProgress(CGContextRef cgContext, const HIRect& inBoxRect, + const ProgressParams& aParams) { + NS_OBJC_BEGIN_TRY_IGNORE_BLOCK; + + NSProgressBarCell* cell = mProgressBarCell; + + [cell setValue:aParams.value]; + [cell setMax:aParams.max]; + [cell setIndeterminate:aParams.indeterminate]; + [cell setHorizontal:aParams.horizontal]; + [cell setControlTint:(aParams.insideActiveWindow ? [NSColor currentControlTint] + : NSClearControlTint)]; + + if (mCellDrawWindow) { + mCellDrawWindow.cellsShouldLookActive = aParams.insideActiveWindow; + } + DrawCellWithSnapping(cell, cgContext, inBoxRect, + progressSettings[aParams.horizontal][aParams.indeterminate], + aParams.verticalAlignFactor, mCellDrawView, aParams.rtl); + + NS_OBJC_END_TRY_IGNORE_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 + }}}; + +nsNativeThemeCocoa::MeterParams nsNativeThemeCocoa::ComputeMeterParams(nsIFrame* aFrame) { + nsIContent* content = aFrame->GetContent(); + if (!(content && content->IsHTMLElement(nsGkAtoms::meter))) { + return MeterParams(); + } + + HTMLMeterElement* meterElement = static_cast<HTMLMeterElement*>(content); + MeterParams params; + params.value = meterElement->Value(); + params.min = meterElement->Min(); + params.max = meterElement->Max(); + ElementState states = meterElement->State(); + if (states.HasState(ElementState::SUB_OPTIMUM)) { + params.optimumState = OptimumState::eSubOptimum; + } else if (states.HasState(ElementState::SUB_SUB_OPTIMUM)) { + params.optimumState = OptimumState::eSubSubOptimum; + } + params.horizontal = !IsVerticalMeter(aFrame); + params.verticalAlignFactor = VerticalAlignFactor(aFrame); + params.rtl = IsFrameRTL(aFrame); + + return params; +} + +void nsNativeThemeCocoa::DrawMeter(CGContextRef cgContext, const HIRect& inBoxRect, + const MeterParams& aParams) { + NS_OBJC_BEGIN_TRY_IGNORE_BLOCK + + NSLevelIndicatorCell* cell = mMeterBarCell; + + [cell setMinValue:aParams.min]; + [cell setMaxValue:aParams.max]; + [cell setDoubleValue:aParams.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. + */ + switch (aParams.optimumState) { + case OptimumState::eOptimum: + [cell setWarningValue:aParams.max + 1]; + [cell setCriticalValue:aParams.max + 1]; + break; + case OptimumState::eSubOptimum: + [cell setWarningValue:aParams.value]; + [cell setCriticalValue:aParams.max + 1]; + break; + case OptimumState::eSubSubOptimum: + [cell setWarningValue:aParams.max + 1]; + [cell setCriticalValue:aParams.value]; + break; + } + + HIRect rect = CGRectStandardize(inBoxRect); + BOOL vertical = !aParams.horizontal; + + 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)); + } + + if (mCellDrawWindow) { + mCellDrawWindow.cellsShouldLookActive = YES; // TODO: propagate correct activeness state + } + DrawCellWithSnapping(cell, cgContext, rect, meterSetting, aParams.verticalAlignFactor, + mCellDrawView, !vertical && aParams.rtl); + + CGContextRestoreGState(cgContext); + + NS_OBJC_END_TRY_IGNORE_BLOCK +} + +void nsNativeThemeCocoa::DrawTabPanel(CGContextRef cgContext, const HIRect& inBoxRect, + bool aIsInsideActiveWindow) { + NS_OBJC_BEGIN_TRY_IGNORE_BLOCK; + + HIThemeTabPaneDrawInfo tpdi; + + tpdi.version = 1; + tpdi.state = aIsInsideActiveWindow ? kThemeStateActive : kThemeStateInactive; + tpdi.direction = kThemeTabNorth; + tpdi.size = kHIThemeTabSizeNormal; + tpdi.kind = kHIThemeTabKindNormal; + + HIThemeDrawTabPane(&inBoxRect, &tpdi, cgContext, HITHEME_ORIENTATION); + + NS_OBJC_END_TRY_IGNORE_BLOCK; +} + +Maybe<nsNativeThemeCocoa::ScaleParams> nsNativeThemeCocoa::ComputeHTMLScaleParams( + nsIFrame* aFrame, ElementState aEventState) { + nsRangeFrame* rangeFrame = do_QueryFrame(aFrame); + if (!rangeFrame) { + return Nothing(); + } + + bool isHorizontal = IsRangeHorizontal(aFrame); + + // ScaleParams requires integer min, max and value. This is purely for + // drawing, so we normalize to a range 0-1000 here. + ScaleParams params; + params.value = int32_t(rangeFrame->GetValueAsFractionOfRange() * 1000); + params.min = 0; + params.max = 1000; + params.reverse = !isHorizontal || rangeFrame->IsRightToLeft(); + params.insideActiveWindow = FrameIsInActiveWindow(aFrame); + params.focused = aEventState.HasState(ElementState::FOCUSRING); + params.disabled = aEventState.HasState(ElementState::DISABLED); + params.horizontal = isHorizontal; + return Some(params); +} + +void nsNativeThemeCocoa::DrawScale(CGContextRef cgContext, const HIRect& inBoxRect, + const ScaleParams& aParams) { + NS_OBJC_BEGIN_TRY_IGNORE_BLOCK; + + HIThemeTrackDrawInfo tdi; + + tdi.version = 0; + tdi.kind = kThemeMediumSlider; + tdi.bounds = inBoxRect; + tdi.min = aParams.min; + tdi.max = aParams.max; + tdi.value = aParams.value; + tdi.attributes = kThemeTrackShowThumb; + if (aParams.horizontal) { + tdi.attributes |= kThemeTrackHorizontal; + } + if (aParams.reverse) { + tdi.attributes |= kThemeTrackRightToLeft; + } + if (aParams.focused) { + tdi.attributes |= kThemeTrackHasFocus; + } + if (aParams.disabled) { + tdi.enableState = kThemeTrackDisabled; + } else { + tdi.enableState = aParams.insideActiveWindow ? kThemeTrackActive : kThemeTrackInactive; + } + tdi.trackInfo.slider.thumbDir = kThemeThumbPlain; + tdi.trackInfo.slider.pressState = 0; + + HIThemeDrawTrack(&tdi, NULL, cgContext, HITHEME_ORIENTATION); + + NS_OBJC_END_TRY_IGNORE_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; +} + +static CGRect SeparatorAdjustedRect(CGRect aRect, nsNativeThemeCocoa::SegmentParams aParams) { + // 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 (!aParams.atLeftEnd && !aParams.drawsLeftSeparator) { + // The segment to the left of us draws the separator, so we need to make + // room for it. + aRect.origin.x += 1; + aRect.size.width -= 1; + } + if (aParams.drawsRightSeparator) { + // 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; +}; + +static const CGFloat tabHeights[3] = {17, 20, 23}; + +static const SegmentedControlRenderSettings tabRenderSettings = {tabHeights, @"tab"}; + +static const CGFloat toolbarButtonHeights[3] = {15, 18, 22}; + +static const SegmentedControlRenderSettings toolbarButtonRenderSettings = { + toolbarButtonHeights, @"kCUIWidgetButtonSegmentedSCurve"}; + +nsNativeThemeCocoa::SegmentParams nsNativeThemeCocoa::ComputeSegmentParams( + nsIFrame* aFrame, ElementState aEventState, SegmentType aSegmentType) { + SegmentParams params; + params.segmentType = aSegmentType; + params.insideActiveWindow = FrameIsInActiveWindow(aFrame); + params.pressed = IsPressedButton(aFrame); + params.selected = IsSelectedButton(aFrame); + params.focused = aEventState.HasState(ElementState::FOCUSRING); + bool isRTL = IsFrameRTL(aFrame); + nsIFrame* left = GetAdjacentSiblingFrameWithSameAppearance(aFrame, isRTL); + nsIFrame* right = GetAdjacentSiblingFrameWithSameAppearance(aFrame, !isRTL); + params.atLeftEnd = !left; + params.atRightEnd = !right; + params.drawsLeftSeparator = SeparatorResponsibility(left, aFrame) == aFrame; + params.drawsRightSeparator = SeparatorResponsibility(aFrame, right) == aFrame; + params.rtl = isRTL; + return params; +} + +static SegmentedControlRenderSettings RenderSettingsForSegmentType( + nsNativeThemeCocoa::SegmentType aSegmentType) { + switch (aSegmentType) { + case nsNativeThemeCocoa::SegmentType::eToolbarButton: + return toolbarButtonRenderSettings; + case nsNativeThemeCocoa::SegmentType::eTab: + return tabRenderSettings; + } +} + +void nsNativeThemeCocoa::DrawSegment(CGContextRef cgContext, const HIRect& inBoxRect, + const SegmentParams& aParams) { + SegmentedControlRenderSettings renderSettings = RenderSettingsForSegmentType(aParams.segmentType); + NSControlSize controlSize = FindControlSize(inBoxRect.size.height, renderSettings.heights, 4.0f); + CGRect drawRect = SeparatorAdjustedRect(inBoxRect, aParams); + + NSDictionary* dict = @{ + @"widget" : renderSettings.widgetName, + @"kCUIPresentationStateKey" : (aParams.insideActiveWindow ? @"kCUIPresentationStateActiveKey" + : @"kCUIPresentationStateInactive"), + @"kCUIPositionKey" : ToolbarButtonPosition(aParams.atLeftEnd, aParams.atRightEnd), + @"kCUISegmentLeadingSeparatorKey" : [NSNumber numberWithBool:aParams.drawsLeftSeparator], + @"kCUISegmentTrailingSeparatorKey" : [NSNumber numberWithBool:aParams.drawsRightSeparator], + @"value" : [NSNumber numberWithBool:aParams.selected], + @"state" : + (aParams.pressed ? @"pressed" : (aParams.insideActiveWindow ? @"normal" : @"inactive")), + @"focus" : [NSNumber numberWithBool:aParams.focused], + @"size" : CUIControlSizeForCocoaSize(controlSize), + @"is.flipped" : [NSNumber numberWithBool:YES], + @"direction" : @"up" + }; + + RenderWithCoreUI(drawRect, cgContext, dict); +} + +void nsNativeThemeCocoa::DrawToolbar(CGContextRef cgContext, const CGRect& inBoxRect, + bool aIsMain) { + CGRect drawRect = inBoxRect; + + // top border + drawRect.size.height = 1.0f; + DrawNativeGreyColorInRect(cgContext, toolbarTopBorderGrey, drawRect, aIsMain); + + // background + drawRect.origin.y += drawRect.size.height; + drawRect.size.height = inBoxRect.size.height - 2.0f; + DrawNativeGreyColorInRect(cgContext, toolbarFillGrey, drawRect, aIsMain); + + // bottom border + drawRect.origin.y += drawRect.size.height; + drawRect.size.height = 1.0f; + DrawNativeGreyColorInRect(cgContext, toolbarBottomBorderGrey, drawRect, aIsMain); +} + +static bool ToolbarCanBeUnified(const gfx::Rect& aRect, NSWindow* aWindow) { + if (![aWindow isKindOfClass:[ToolbarWindow class]]) return false; + + ToolbarWindow* win = (ToolbarWindow*)aWindow; + float unifiedToolbarHeight = [win unifiedToolbarHeight]; + return aRect.X() == 0 && aRect.Width() >= [win frame].size.width && + aRect.YMost() <= unifiedToolbarHeight; +} + +void nsNativeThemeCocoa::DrawStatusBar(CGContextRef cgContext, const HIRect& inBoxRect, + bool aIsMain) { + NS_OBJC_BEGIN_TRY_IGNORE_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", (aIsMain ? @"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_IGNORE_BLOCK; +} + +void nsNativeThemeCocoa::DrawMultilineTextField(CGContextRef cgContext, const CGRect& inBoxRect, + bool aIsFocused) { + mTextFieldCell.enabled = YES; + mTextFieldCell.showsFirstResponder = aIsFocused; + + if (mCellDrawWindow) { + mCellDrawWindow.cellsShouldLookActive = YES; + } + + // DrawCellIncludingFocusRing draws into the current NSGraphicsContext, so do the usual + // save+restore dance. + NSGraphicsContext* savedContext = NSGraphicsContext.currentContext; + NSGraphicsContext.currentContext = [NSGraphicsContext graphicsContextWithCGContext:cgContext + flipped:YES]; + DrawCellIncludingFocusRing(mTextFieldCell, inBoxRect, mCellDrawView); + NSGraphicsContext.currentContext = savedContext; +} + +void nsNativeThemeCocoa::DrawSourceListSelection(CGContextRef aContext, const CGRect& aRect, + bool aWindowIsActive, bool aSelectionIsActive) { + NSColor* fillColor; + if (aSelectionIsActive) { + // Active selection, blue or graphite. + fillColor = ControlAccentColor(); + } else { + // Inactive selection, gray. + if (aWindowIsActive) { + fillColor = [NSColor colorWithWhite:0.871 alpha:1.0]; + } else { + fillColor = [NSColor colorWithWhite:0.808 alpha:1.0]; + } + } + CGContextSetFillColorWithColor(aContext, [fillColor CGColor]); + CGContextFillRect(aContext, aRect); +} + +static bool IsHiDPIContext(nsDeviceContext* aContext) { + return AppUnitsPerCSSPixel() >= 2 * aContext->AppUnitsPerDevPixelAtUnitFullZoom(); +} + +Maybe<nsNativeThemeCocoa::WidgetInfo> nsNativeThemeCocoa::ComputeWidgetInfo( + nsIFrame* aFrame, StyleAppearance aAppearance, const nsRect& aRect) { + NS_OBJC_BEGIN_TRY_BLOCK_RETURN; + + // setup to draw into the correct port + int32_t p2a = aFrame->PresContext()->AppUnitsPerDevPixel(); + + gfx::Rect nativeWidgetRect(aRect.x, aRect.y, aRect.width, aRect.height); + nativeWidgetRect.Scale(1.0 / gfxFloat(p2a)); + float originalHeight = nativeWidgetRect.Height(); + nativeWidgetRect.Round(); + if (nativeWidgetRect.IsEmpty()) { + return Nothing(); // Don't attempt to draw invisible widgets. + } + + bool hidpi = IsHiDPIContext(aFrame->PresContext()->DeviceContext()); + if (hidpi) { + // Use high-resolution drawing. + nativeWidgetRect.Scale(0.5f); + originalHeight *= 0.5f; + } + + ElementState elementState = GetContentState(aFrame, aAppearance); + + switch (aAppearance) { + case StyleAppearance::Menupopup: + return Nothing(); + + case StyleAppearance::Menuarrow: + return Some( + WidgetInfo::MenuIcon(ComputeMenuIconParams(aFrame, elementState, MenuIcon::eMenuArrow))); + + case StyleAppearance::Menuitem: + case StyleAppearance::Checkmenuitem: + return Some(WidgetInfo::MenuItem(ComputeMenuItemParams( + aFrame, elementState, aAppearance == StyleAppearance::Checkmenuitem))); + + case StyleAppearance::Menuseparator: + return Some(WidgetInfo::MenuSeparator(ComputeMenuItemParams(aFrame, elementState, false))); + + case StyleAppearance::ButtonArrowUp: + case StyleAppearance::ButtonArrowDown: { + MenuIcon icon = aAppearance == StyleAppearance::ButtonArrowUp + ? MenuIcon::eMenuUpScrollArrow + : MenuIcon::eMenuDownScrollArrow; + return Some(WidgetInfo::MenuIcon(ComputeMenuIconParams(aFrame, elementState, icon))); + } + + case StyleAppearance::Tooltip: + return Nothing(); + + case StyleAppearance::Checkbox: + case StyleAppearance::Radio: { + bool isCheckbox = (aAppearance == StyleAppearance::Checkbox); + + CheckboxOrRadioParams params; + params.state = CheckboxOrRadioState::eOff; + if (elementState.HasState(ElementState::INDETERMINATE)) { + params.state = CheckboxOrRadioState::eIndeterminate; + } else if (elementState.HasState(ElementState::CHECKED)) { + params.state = CheckboxOrRadioState::eOn; + } + params.controlParams = ComputeControlParams(aFrame, elementState); + params.verticalAlignFactor = VerticalAlignFactor(aFrame); + if (isCheckbox) { + return Some(WidgetInfo::Checkbox(params)); + } + return Some(WidgetInfo::Radio(params)); + } + + case StyleAppearance::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.) + DocumentState docState = aFrame->GetContent()->OwnerDoc()->GetDocumentState(); + ControlParams params = ComputeControlParams(aFrame, elementState); + params.insideActiveWindow = !docState.HasState(DocumentState::WINDOW_INACTIVE); + return Some(WidgetInfo::Button(ButtonParams{params, ButtonType::eDefaultPushButton})); + } + if (IsButtonTypeMenu(aFrame)) { + ControlParams controlParams = ComputeControlParams(aFrame, elementState); + controlParams.pressed = IsOpenButton(aFrame); + DropdownParams params; + params.controlParams = controlParams; + params.pullsDown = true; + params.editable = false; + return Some(WidgetInfo::Dropdown(params)); + } + if (originalHeight > DO_SQUARE_BUTTON_HEIGHT) { + // 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. + return Some(WidgetInfo::Button(ButtonParams{ComputeControlParams(aFrame, elementState), + ButtonType::eSquareBezelPushButton})); + } + return Some(WidgetInfo::Button(ButtonParams{ComputeControlParams(aFrame, elementState), + ButtonType::eRegularPushButton})); + + case StyleAppearance::MozMacHelpButton: + return Some(WidgetInfo::Button( + ButtonParams{ComputeControlParams(aFrame, elementState), ButtonType::eHelpButton})); + + case StyleAppearance::MozMacDisclosureButtonOpen: + case StyleAppearance::MozMacDisclosureButtonClosed: { + ButtonType buttonType = (aAppearance == StyleAppearance::MozMacDisclosureButtonClosed) + ? ButtonType::eDisclosureButtonClosed + : ButtonType::eDisclosureButtonOpen; + return Some( + WidgetInfo::Button(ButtonParams{ComputeControlParams(aFrame, elementState), buttonType})); + } + + case StyleAppearance::Spinner: { + bool isSpinner = (aAppearance == StyleAppearance::Spinner); + nsIContent* content = aFrame->GetContent(); + if (isSpinner && 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; + } + SpinButtonParams params; + if (content->IsElement()) { + if (content->AsElement()->AttrValueIs(kNameSpaceID_None, nsGkAtoms::state, u"up"_ns, + eCaseMatters)) { + params.pressedButton = Some(SpinButton::eUp); + } else if (content->AsElement()->AttrValueIs(kNameSpaceID_None, nsGkAtoms::state, + u"down"_ns, eCaseMatters)) { + params.pressedButton = Some(SpinButton::eDown); + } + } + params.disabled = elementState.HasState(ElementState::DISABLED); + params.insideActiveWindow = FrameIsInActiveWindow(aFrame); + + return Some(WidgetInfo::SpinButtons(params)); + } + + case StyleAppearance::SpinnerUpbutton: + case StyleAppearance::SpinnerDownbutton: { + nsNumberControlFrame* numberControlFrame = + nsNumberControlFrame::GetNumberControlFrameForSpinButton(aFrame); + if (numberControlFrame) { + SpinButtonParams params; + if (numberControlFrame->SpinnerUpButtonIsDepressed()) { + params.pressedButton = Some(SpinButton::eUp); + } else if (numberControlFrame->SpinnerDownButtonIsDepressed()) { + params.pressedButton = Some(SpinButton::eDown); + } + params.disabled = elementState.HasState(ElementState::DISABLED); + params.insideActiveWindow = FrameIsInActiveWindow(aFrame); + if (aAppearance == StyleAppearance::SpinnerUpbutton) { + return Some(WidgetInfo::SpinButtonUp(params)); + } + return Some(WidgetInfo::SpinButtonDown(params)); + } + } break; + + case StyleAppearance::Toolbarbutton: { + SegmentParams params = + ComputeSegmentParams(aFrame, elementState, SegmentType::eToolbarButton); + params.insideActiveWindow = [NativeWindowForFrame(aFrame) isMainWindow]; + return Some(WidgetInfo::Segment(params)); + } + + case StyleAppearance::Separator: + return Some(WidgetInfo::Separator()); + + case StyleAppearance::Toolbar: { + NSWindow* win = NativeWindowForFrame(aFrame); + bool isMain = [win isMainWindow]; + if (ToolbarCanBeUnified(nativeWidgetRect, win)) { + // Unified toolbars are drawn similar to vibrancy; we communicate their extents via the + // theme geometry mechanism and then place native views under Gecko's rendering. So Gecko + // just needs to be transparent in the place where the toolbar should be visible. + return Nothing(); + } + return Some(WidgetInfo::Toolbar(isMain)); + } + + case StyleAppearance::MozWindowTitlebar: { + return Nothing(); + } + + case StyleAppearance::Statusbar: + return Some(WidgetInfo::StatusBar(IsActive(aFrame, YES))); + + case StyleAppearance::MenulistButton: + case StyleAppearance::Menulist: { + ControlParams controlParams = ComputeControlParams(aFrame, elementState); + controlParams.pressed = IsOpenButton(aFrame); + DropdownParams params; + params.controlParams = controlParams; + params.pullsDown = false; + params.editable = false; + return Some(WidgetInfo::Dropdown(params)); + } + + case StyleAppearance::MozMenulistArrowButton: + return Some(WidgetInfo::Button( + ButtonParams{ComputeControlParams(aFrame, elementState), ButtonType::eArrowButton})); + + case StyleAppearance::Groupbox: + return Some(WidgetInfo::GroupBox()); + + case StyleAppearance::Textfield: + case StyleAppearance::NumberInput: + return Some(WidgetInfo::TextField(ComputeTextFieldParams(aFrame, elementState))); + + case StyleAppearance::Searchfield: + return Some(WidgetInfo::SearchField(ComputeTextFieldParams(aFrame, elementState))); + + case StyleAppearance::ProgressBar: { + if (elementState.HasState(ElementState::INDETERMINATE)) { + if (!QueueAnimatedContentForRefresh(aFrame->GetContent(), 30)) { + NS_WARNING("Unable to animate progressbar!"); + } + } + return Some(WidgetInfo::ProgressBar( + ComputeProgressParams(aFrame, elementState, !IsVerticalProgress(aFrame)))); + } + + case StyleAppearance::Meter: + return Some(WidgetInfo::Meter(ComputeMeterParams(aFrame))); + + case StyleAppearance::Progresschunk: + case StyleAppearance::Meterchunk: + // Do nothing: progress and meter bars cases will draw chunks. + break; + + case StyleAppearance::Treetwisty: + return Some(WidgetInfo::Button(ButtonParams{ComputeControlParams(aFrame, elementState), + ButtonType::eTreeTwistyPointingRight})); + + case StyleAppearance::Treetwistyopen: + return Some(WidgetInfo::Button(ButtonParams{ComputeControlParams(aFrame, elementState), + ButtonType::eTreeTwistyPointingDown})); + + case StyleAppearance::Treeheadercell: + return Some(WidgetInfo::TreeHeaderCell(ComputeTreeHeaderCellParams(aFrame, elementState))); + + case StyleAppearance::Treeitem: + case StyleAppearance::Treeview: + return Some(WidgetInfo::ColorFill(sRGBColor(1.0, 1.0, 1.0, 1.0))); + + case StyleAppearance::Treeheader: + // do nothing, taken care of by individual header cells + case StyleAppearance::Treeheadersortarrow: + // do nothing, taken care of by treeview header + case StyleAppearance::Treeline: + // do nothing, these lines don't exist on macos + break; + + case StyleAppearance::Range: { + Maybe<ScaleParams> params = ComputeHTMLScaleParams(aFrame, elementState); + if (params) { + return Some(WidgetInfo::Scale(*params)); + } + break; + } + + case StyleAppearance::Textarea: + return Some(WidgetInfo::MultilineTextField(elementState.HasState(ElementState::FOCUS))); + + case StyleAppearance::Listbox: + return Some(WidgetInfo::ListBox()); + + case StyleAppearance::MozMacSourceList: { + return Nothing(); + } + + case StyleAppearance::MozMacSourceListSelection: + case StyleAppearance::MozMacActiveSourceListSelection: { + // We only support vibrancy for source list selections if we're inside + // a source list, because we need the background to be transparent. + if (IsInSourceList(aFrame)) { + return Nothing(); + } + bool isInActiveWindow = FrameIsInActiveWindow(aFrame); + if (aAppearance == StyleAppearance::MozMacActiveSourceListSelection) { + return Some(WidgetInfo::ActiveSourceListSelection(isInActiveWindow)); + } + return Some(WidgetInfo::InactiveSourceListSelection(isInActiveWindow)); + } + + case StyleAppearance::Tab: { + SegmentParams params = ComputeSegmentParams(aFrame, elementState, SegmentType::eTab); + params.pressed = params.pressed && !params.selected; + return Some(WidgetInfo::Segment(params)); + } + + case StyleAppearance::Tabpanels: + return Some(WidgetInfo::TabPanel(FrameIsInActiveWindow(aFrame))); + + default: + break; + } + + return Nothing(); + + NS_OBJC_END_TRY_BLOCK_RETURN(Nothing()); +} + +static bool IsWidgetNonNative(StyleAppearance aAppearance) { + return nsNativeTheme::IsWidgetScrollbarPart(aAppearance) || + aAppearance == StyleAppearance::FocusOutline; +} + +NS_IMETHODIMP +nsNativeThemeCocoa::DrawWidgetBackground(gfxContext* aContext, nsIFrame* aFrame, + StyleAppearance aAppearance, const nsRect& aRect, + const nsRect& aDirtyRect, DrawOverflow aDrawOverflow) { + NS_OBJC_BEGIN_TRY_BLOCK_RETURN; + + if (IsWidgetNonNative(aAppearance)) { + return ThemeCocoa::DrawWidgetBackground(aContext, aFrame, aAppearance, aRect, aDirtyRect, + aDrawOverflow); + } + + Maybe<WidgetInfo> widgetInfo = ComputeWidgetInfo(aFrame, aAppearance, aRect); + + if (!widgetInfo) { + return NS_OK; + } + + int32_t p2a = aFrame->PresContext()->AppUnitsPerDevPixel(); + + gfx::Rect nativeWidgetRect = NSRectToRect(aRect, p2a); + nativeWidgetRect.Round(); + + bool hidpi = IsHiDPIContext(aFrame->PresContext()->DeviceContext()); + + auto colorScheme = LookAndFeel::ColorSchemeForFrame(aFrame); + + RenderWidget(*widgetInfo, colorScheme, *aContext->GetDrawTarget(), nativeWidgetRect, + NSRectToRect(aDirtyRect, p2a), hidpi ? 2.0f : 1.0f); + + return NS_OK; + + NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE); +} + +void nsNativeThemeCocoa::RenderWidget(const WidgetInfo& aWidgetInfo, + LookAndFeel::ColorScheme aScheme, DrawTarget& aDrawTarget, + const gfx::Rect& aWidgetRect, const gfx::Rect& aDirtyRect, + float aScale) { + // Some of the drawing below uses NSAppearance.currentAppearance behind the scenes. + // Set it to the appearance we want, the same way as nsLookAndFeel::NativeGetColor. + NSAppearance.currentAppearance = NSAppearanceForColorScheme(aScheme); + + // Also set the cell draw window's appearance; this is respected by NSTextFieldCell (and its + // subclass NSSearchFieldCell). + if (mCellDrawWindow) { + mCellDrawWindow.appearance = NSAppearance.currentAppearance; + } + + const Widget widget = aWidgetInfo.Widget(); + + // Some widgets render using DrawTarget, and some using CGContext. + switch (widget) { + case Widget::eColorFill: { + sRGBColor color = aWidgetInfo.Params<sRGBColor>(); + aDrawTarget.FillRect(aWidgetRect, ColorPattern(ToDeviceColor(color))); + break; + } + default: { + AutoRestoreTransform autoRestoreTransform(&aDrawTarget); + gfx::Rect widgetRect = aWidgetRect; + gfx::Rect dirtyRect = aDirtyRect; + + dirtyRect.Scale(1.0f / aScale); + widgetRect.Scale(1.0f / aScale); + aDrawTarget.SetTransform(aDrawTarget.GetTransform().PreScale(aScale, aScale)); + + // The remaining widgets require a CGContext. + CGRect macRect = + CGRectMake(widgetRect.X(), widgetRect.Y(), widgetRect.Width(), widgetRect.Height()); + + gfxQuartzNativeDrawing nativeDrawing(aDrawTarget, dirtyRect); + + 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; + } + + // Set the context's "base transform" to in order to get correctly-sized focus rings. + CGContextSetBaseCTM(cgContext, CGAffineTransformMakeScale(aScale, aScale)); + + switch (widget) { + case Widget::eColorFill: + MOZ_CRASH("already handled in outer switch"); + break; + case Widget::eMenuIcon: { + MenuIconParams params = aWidgetInfo.Params<MenuIconParams>(); + DrawMenuIcon(cgContext, macRect, params); + break; + } + case Widget::eMenuItem: { + MenuItemParams params = aWidgetInfo.Params<MenuItemParams>(); + DrawMenuItem(cgContext, macRect, params); + break; + } + case Widget::eMenuSeparator: { + MenuItemParams params = aWidgetInfo.Params<MenuItemParams>(); + DrawMenuSeparator(cgContext, macRect, params); + break; + } + case Widget::eCheckbox: { + CheckboxOrRadioParams params = aWidgetInfo.Params<CheckboxOrRadioParams>(); + DrawCheckboxOrRadio(cgContext, true, macRect, params); + break; + } + case Widget::eRadio: { + CheckboxOrRadioParams params = aWidgetInfo.Params<CheckboxOrRadioParams>(); + DrawCheckboxOrRadio(cgContext, false, macRect, params); + break; + } + case Widget::eButton: { + ButtonParams params = aWidgetInfo.Params<ButtonParams>(); + DrawButton(cgContext, macRect, params); + break; + } + case Widget::eDropdown: { + DropdownParams params = aWidgetInfo.Params<DropdownParams>(); + DrawDropdown(cgContext, macRect, params); + break; + } + case Widget::eSpinButtons: { + SpinButtonParams params = aWidgetInfo.Params<SpinButtonParams>(); + DrawSpinButtons(cgContext, macRect, params); + break; + } + case Widget::eSpinButtonUp: { + SpinButtonParams params = aWidgetInfo.Params<SpinButtonParams>(); + DrawSpinButton(cgContext, macRect, SpinButton::eUp, params); + break; + } + case Widget::eSpinButtonDown: { + SpinButtonParams params = aWidgetInfo.Params<SpinButtonParams>(); + DrawSpinButton(cgContext, macRect, SpinButton::eDown, params); + break; + } + case Widget::eSegment: { + SegmentParams params = aWidgetInfo.Params<SegmentParams>(); + DrawSegment(cgContext, macRect, params); + break; + } + case Widget::eSeparator: { + HIThemeSeparatorDrawInfo sdi = {0, kThemeStateActive}; + HIThemeDrawSeparator(&macRect, &sdi, cgContext, HITHEME_ORIENTATION); + break; + } + case Widget::eToolbar: { + bool isMain = aWidgetInfo.Params<bool>(); + DrawToolbar(cgContext, macRect, isMain); + break; + } + case Widget::eStatusBar: { + bool isMain = aWidgetInfo.Params<bool>(); + DrawStatusBar(cgContext, macRect, isMain); + break; + } + case Widget::eGroupBox: { + HIThemeGroupBoxDrawInfo gdi = {0, kThemeStateActive, kHIThemeGroupBoxKindPrimary}; + HIThemeDrawGroupBox(&macRect, &gdi, cgContext, HITHEME_ORIENTATION); + break; + } + case Widget::eTextField: { + TextFieldParams params = aWidgetInfo.Params<TextFieldParams>(); + DrawTextField(cgContext, macRect, params); + break; + } + case Widget::eSearchField: { + TextFieldParams params = aWidgetInfo.Params<TextFieldParams>(); + DrawSearchField(cgContext, macRect, params); + break; + } + case Widget::eProgressBar: { + ProgressParams params = aWidgetInfo.Params<ProgressParams>(); + DrawProgress(cgContext, macRect, params); + break; + } + case Widget::eMeter: { + MeterParams params = aWidgetInfo.Params<MeterParams>(); + DrawMeter(cgContext, macRect, params); + break; + } + case Widget::eTreeHeaderCell: { + TreeHeaderCellParams params = aWidgetInfo.Params<TreeHeaderCellParams>(); + DrawTreeHeaderCell(cgContext, macRect, params); + break; + } + case Widget::eScale: { + ScaleParams params = aWidgetInfo.Params<ScaleParams>(); + DrawScale(cgContext, macRect, params); + break; + } + case Widget::eMultilineTextField: { + bool isFocused = aWidgetInfo.Params<bool>(); + DrawMultilineTextField(cgContext, macRect, isFocused); + break; + } + case Widget::eListBox: { + // Fill the content with the control background color. + CGContextSetFillColorWithColor(cgContext, [NSColor.controlBackgroundColor CGColor]); + CGContextFillRect(cgContext, macRect); + // Draw the frame using kCUIWidgetScrollViewFrame. This is what NSScrollView uses in + // -[NSScrollView drawRect:] if you give it a borderType of NSBezelBorder. + RenderWithCoreUI( + macRect, cgContext, @{ + @"widget" : @"kCUIWidgetScrollViewFrame", + @"kCUIIsFlippedKey" : @YES, + @"kCUIVariantMetal" : @NO, + }); + break; + } + case Widget::eActiveSourceListSelection: + case Widget::eInactiveSourceListSelection: { + bool isInActiveWindow = aWidgetInfo.Params<bool>(); + bool isActiveSelection = aWidgetInfo.Widget() == Widget::eActiveSourceListSelection; + DrawSourceListSelection(cgContext, macRect, isInActiveWindow, isActiveSelection); + break; + } + case Widget::eTabPanel: { + bool isInsideActiveWindow = aWidgetInfo.Params<bool>(); + DrawTabPanel(cgContext, macRect, isInsideActiveWindow); + break; + } + } + + // Reset the base CTM. + CGContextSetBaseCTM(cgContext, CGAffineTransformIdentity); + + nativeDrawing.EndNativeDrawing(); + } + } +} + +bool nsNativeThemeCocoa::CreateWebRenderCommandsForWidget( + mozilla::wr::DisplayListBuilder& aBuilder, mozilla::wr::IpcResourceUpdateQueue& aResources, + const mozilla::layers::StackingContextHelper& aSc, + mozilla::layers::RenderRootStateManager* aManager, nsIFrame* aFrame, + StyleAppearance aAppearance, const nsRect& aRect) { + if (IsWidgetNonNative(aAppearance)) { + return ThemeCocoa::CreateWebRenderCommandsForWidget(aBuilder, aResources, aSc, aManager, aFrame, + aAppearance, aRect); + } + + // This list needs to stay consistent with the list in DrawWidgetBackground. + // For every switch case in DrawWidgetBackground, there are three choices: + // - If the case in DrawWidgetBackground draws nothing for the given widget + // type, then don't list it here. We will hit the "default: return true;" + // case. + // - If the case in DrawWidgetBackground draws something simple for the given + // widget type, imitate that drawing using WebRender commands. + // - If the case in DrawWidgetBackground draws something complicated for the + // given widget type, return false here. + switch (aAppearance) { + case StyleAppearance::Menuarrow: + case StyleAppearance::Menuitem: + case StyleAppearance::Checkmenuitem: + case StyleAppearance::Menuseparator: + case StyleAppearance::ButtonArrowUp: + case StyleAppearance::ButtonArrowDown: + case StyleAppearance::Checkbox: + case StyleAppearance::Radio: + case StyleAppearance::Button: + case StyleAppearance::MozMacHelpButton: + case StyleAppearance::MozMacDisclosureButtonOpen: + case StyleAppearance::MozMacDisclosureButtonClosed: + case StyleAppearance::Spinner: + case StyleAppearance::SpinnerUpbutton: + case StyleAppearance::SpinnerDownbutton: + case StyleAppearance::Toolbarbutton: + case StyleAppearance::Separator: + case StyleAppearance::Toolbar: + case StyleAppearance::MozWindowTitlebar: + case StyleAppearance::Statusbar: + case StyleAppearance::Menulist: + case StyleAppearance::MenulistButton: + case StyleAppearance::MozMenulistArrowButton: + case StyleAppearance::Groupbox: + case StyleAppearance::Textfield: + case StyleAppearance::NumberInput: + case StyleAppearance::Searchfield: + case StyleAppearance::ProgressBar: + case StyleAppearance::Meter: + case StyleAppearance::Treeheadercell: + case StyleAppearance::Treetwisty: + case StyleAppearance::Treetwistyopen: + case StyleAppearance::Treeitem: + case StyleAppearance::Treeview: + case StyleAppearance::Range: + return false; + + case StyleAppearance::Textarea: + case StyleAppearance::Listbox: + case StyleAppearance::Tab: + case StyleAppearance::Tabpanels: + return false; + + default: + return true; + } +} + +LayoutDeviceIntMargin nsNativeThemeCocoa::DirectionAwareMargin(const LayoutDeviceIntMargin& 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 LayoutDeviceIntMargin(m.top, m.right, m.bottom, m.left); +} + +static const LayoutDeviceIntMargin kAquaDropdownBorder(1, 22, 2, 5); +static const LayoutDeviceIntMargin kAquaComboboxBorder(3, 20, 3, 4); +static const LayoutDeviceIntMargin kAquaSearchfieldBorder(3, 5, 2, 19); +static const LayoutDeviceIntMargin kAquaSearchfieldBorderBigSur(5, 5, 4, 26); + +LayoutDeviceIntMargin nsNativeThemeCocoa::GetWidgetBorder(nsDeviceContext* aContext, + nsIFrame* aFrame, + StyleAppearance aAppearance) { + LayoutDeviceIntMargin result; + + NS_OBJC_BEGIN_TRY_BLOCK_RETURN; + switch (aAppearance) { + case StyleAppearance::Button: { + if (IsButtonTypeMenu(aFrame)) { + result = DirectionAwareMargin(kAquaDropdownBorder, aFrame); + } else { + result = DirectionAwareMargin(LayoutDeviceIntMargin(1, 7, 3, 7), aFrame); + } + break; + } + + case StyleAppearance::Toolbarbutton: { + result = DirectionAwareMargin(LayoutDeviceIntMargin(1, 4, 1, 4), aFrame); + break; + } + + case StyleAppearance::Checkbox: + case StyleAppearance::Radio: { + // nsCheckboxRadioFrame::GetIntrinsicWidth and nsCheckboxRadioFrame::GetIntrinsicHeight + // assume a border width of 2px. + result.SizeTo(2, 2, 2, 2); + break; + } + + case StyleAppearance::Menulist: + case StyleAppearance::MenulistButton: + case StyleAppearance::MozMenulistArrowButton: + result = DirectionAwareMargin(kAquaDropdownBorder, aFrame); + break; + + case StyleAppearance::Menuarrow: + if (nsCocoaFeatures::OnBigSurOrLater()) { + result.SizeTo(0, 0, 0, 28); + } + break; + + case StyleAppearance::NumberInput: + case StyleAppearance::Textfield: { + SInt32 frameOutset = 0; + ::GetThemeMetric(kThemeMetricEditTextFrameOutset, &frameOutset); + + SInt32 textPadding = 0; + ::GetThemeMetric(kThemeMetricEditTextWhitespace, &textPadding); + + frameOutset += textPadding; + + result.SizeTo(frameOutset, frameOutset, frameOutset, frameOutset); + break; + } + + case StyleAppearance::Textarea: + result.SizeTo(1, 1, 1, 1); + break; + + case StyleAppearance::Searchfield: { + auto border = nsCocoaFeatures::OnBigSurOrLater() ? kAquaSearchfieldBorderBigSur + : kAquaSearchfieldBorder; + result = DirectionAwareMargin(border, aFrame); + break; + } + + case StyleAppearance::Listbox: { + SInt32 frameOutset = 0; + ::GetThemeMetric(kThemeMetricListBoxFrameOutset, &frameOutset); + result.SizeTo(frameOutset, frameOutset, frameOutset, frameOutset); + break; + } + + case StyleAppearance::Statusbar: + result.SizeTo(1, 0, 0, 0); + break; + + default: + break; + } + + if (IsHiDPIContext(aContext)) { + result = result + result; // doubled + } + + NS_OBJC_END_TRY_BLOCK_RETURN(result); +} + +// 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, + StyleAppearance aAppearance, + LayoutDeviceIntMargin* aResult) { + // We don't want CSS padding being used for certain widgets. + // See bug 381639 for an example of why. + switch (aAppearance) { + // Radios and checkboxes return a fixed size in GetMinimumWidgetSize + // and have a meaningful baseline, so they can't have + // author-specified padding. + case StyleAppearance::Checkbox: + case StyleAppearance::Radio: + aResult->SizeTo(0, 0, 0, 0); + return true; + + case StyleAppearance::Menuarrow: + case StyleAppearance::Searchfield: + if (nsCocoaFeatures::OnBigSurOrLater()) { + return true; + } + break; + + default: + break; + } + return false; +} + +bool nsNativeThemeCocoa::GetWidgetOverflow(nsDeviceContext* aContext, nsIFrame* aFrame, + StyleAppearance aAppearance, nsRect* aOverflowRect) { + if (IsWidgetNonNative(aAppearance)) { + return ThemeCocoa::GetWidgetOverflow(aContext, aFrame, aAppearance, aOverflowRect); + } + nsIntMargin overflow; + switch (aAppearance) { + case StyleAppearance::Button: + case StyleAppearance::MozMacDisclosureButtonOpen: + case StyleAppearance::MozMacDisclosureButtonClosed: + case StyleAppearance::MozMacHelpButton: + case StyleAppearance::Toolbarbutton: + case StyleAppearance::NumberInput: + case StyleAppearance::Textfield: + case StyleAppearance::Textarea: + case StyleAppearance::Searchfield: + case StyleAppearance::Listbox: + case StyleAppearance::Menulist: + case StyleAppearance::MenulistButton: + case StyleAppearance::MozMenulistArrowButton: + case StyleAppearance::Checkbox: + case StyleAppearance::Radio: + case StyleAppearance::Tab: { + overflow.SizeTo(kMaxFocusRingWidth, kMaxFocusRingWidth, kMaxFocusRingWidth, + kMaxFocusRingWidth); + break; + } + case StyleAppearance::ProgressBar: { + // Progress bars draw a 2 pixel white shadow under their progress indicators. + overflow.bottom = 2; + break; + } + case StyleAppearance::Meter: { + // Meter bars overflow their boxes by about 2 pixels. + overflow.SizeTo(2, 2, 2, 2); + break; + } + default: + break; + } + + if (IsHiDPIContext(aContext)) { + // Double the number of device pixels. + overflow += overflow; + } + + if (overflow != nsIntMargin()) { + int32_t p2a = aFrame->PresContext()->AppUnitsPerDevPixel(); + aOverflowRect->Inflate(nsMargin( + NSIntPixelsToAppUnits(overflow.top, p2a), NSIntPixelsToAppUnits(overflow.right, p2a), + NSIntPixelsToAppUnits(overflow.bottom, p2a), NSIntPixelsToAppUnits(overflow.left, p2a))); + return true; + } + + return false; +} + +LayoutDeviceIntSize nsNativeThemeCocoa::GetMinimumWidgetSize(nsPresContext* aPresContext, + nsIFrame* aFrame, + StyleAppearance aAppearance) { + NS_OBJC_BEGIN_TRY_BLOCK_RETURN; + + if (IsWidgetNonNative(aAppearance)) { + return ThemeCocoa::GetMinimumWidgetSize(aPresContext, aFrame, aAppearance); + } + + LayoutDeviceIntSize result; + switch (aAppearance) { + case StyleAppearance::Button: { + result.SizeTo(pushButtonSettings.minimumSizes[miniControlSize].width, + pushButtonSettings.naturalSizes[miniControlSize].height); + break; + } + + case StyleAppearance::ButtonArrowUp: + case StyleAppearance::ButtonArrowDown: { + result.SizeTo(kMenuScrollArrowSize.width, kMenuScrollArrowSize.height); + break; + } + + case StyleAppearance::Menuarrow: { + result.SizeTo(kMenuarrowSize.width, kMenuarrowSize.height); + break; + } + + case StyleAppearance::MozMacDisclosureButtonOpen: + case StyleAppearance::MozMacDisclosureButtonClosed: { + result.SizeTo(kDisclosureButtonSize.width, kDisclosureButtonSize.height); + break; + } + + case StyleAppearance::MozMacHelpButton: { + result.SizeTo(kHelpButtonSize.width, kHelpButtonSize.height); + break; + } + + case StyleAppearance::Toolbarbutton: { + result.SizeTo(0, toolbarButtonHeights[miniControlSize]); + break; + } + + case StyleAppearance::Spinner: + case StyleAppearance::SpinnerUpbutton: + case StyleAppearance::SpinnerDownbutton: { + SInt32 buttonHeight = 0, buttonWidth = 0; + if (aFrame->GetContent()->IsXULElement()) { + ::GetThemeMetric(kThemeMetricLittleArrowsWidth, &buttonWidth); + ::GetThemeMetric(kThemeMetricLittleArrowsHeight, &buttonHeight); + } else { + NSSize size = spinnerSettings.minimumSizes[EnumSizeForCocoaSize(NSControlSizeMini)]; + buttonWidth = size.width; + buttonHeight = size.height; + if (aAppearance != StyleAppearance::Spinner) { + // the buttons are half the height of the spinner + buttonHeight /= 2; + } + } + result.SizeTo(buttonWidth, buttonHeight); + break; + } + + case StyleAppearance::Menulist: + case StyleAppearance::MenulistButton: { + SInt32 popupHeight = 0; + ::GetThemeMetric(kThemeMetricPopupButtonHeight, &popupHeight); + result.SizeTo(0, popupHeight); + break; + } + + case StyleAppearance::NumberInput: + case StyleAppearance::Textfield: + case StyleAppearance::Textarea: + case StyleAppearance::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. + result.SizeTo(0, (2 + 2) /* top */ + 9 + (1 + 1) /* bottom */); + break; + } + + case StyleAppearance::MozWindowButtonBox: { + NSSize size = WindowButtonsSize(aFrame); + result.SizeTo(size.width, size.height); + break; + } + + case StyleAppearance::ProgressBar: { + SInt32 barHeight = 0; + ::GetThemeMetric(kThemeMetricNormalProgressBarThickness, &barHeight); + result.SizeTo(0, barHeight); + break; + } + + case StyleAppearance::Separator: { + result.SizeTo(1, 1); + break; + } + + case StyleAppearance::Treetwisty: + case StyleAppearance::Treetwistyopen: { + SInt32 twistyHeight = 0, twistyWidth = 0; + ::GetThemeMetric(kThemeMetricDisclosureButtonWidth, &twistyWidth); + ::GetThemeMetric(kThemeMetricDisclosureButtonHeight, &twistyHeight); + result.SizeTo(twistyWidth, twistyHeight); + break; + } + + case StyleAppearance::Treeheader: + case StyleAppearance::Treeheadercell: { + SInt32 headerHeight = 0; + ::GetThemeMetric(kThemeMetricListHeaderHeight, &headerHeight); + result.SizeTo(0, headerHeight); + break; + } + + case StyleAppearance::Tab: { + result.SizeTo(0, tabHeights[miniControlSize]); + break; + } + + case StyleAppearance::RangeThumb: { + SInt32 width = 0; + SInt32 height = 0; + ::GetThemeMetric(kThemeMetricSliderMinThumbWidth, &width); + ::GetThemeMetric(kThemeMetricSliderMinThumbHeight, &height); + result.SizeTo(width, height); + break; + } + + case StyleAppearance::MozMenulistArrowButton: + return ThemeCocoa::GetMinimumWidgetSize(aPresContext, aFrame, aAppearance); + + default: + break; + } + + if (IsHiDPIContext(aPresContext->DeviceContext())) { + result = result * 2; + } + + return result; + + NS_OBJC_END_TRY_BLOCK_RETURN(LayoutDeviceIntSize()); +} + +NS_IMETHODIMP +nsNativeThemeCocoa::WidgetStateChanged(nsIFrame* aFrame, StyleAppearance aAppearance, + nsAtom* aAttribute, bool* aShouldRepaint, + const nsAttrValue* aOldValue) { + // Some widget types just never change state. + switch (aAppearance) { + case StyleAppearance::MozWindowTitlebar: + case StyleAppearance::Toolbox: + case StyleAppearance::Toolbar: + case StyleAppearance::Statusbar: + case StyleAppearance::Tooltip: + case StyleAppearance::Tabpanels: + case StyleAppearance::Tabpanel: + case StyleAppearance::Dialog: + case StyleAppearance::Menupopup: + case StyleAppearance::Groupbox: + case StyleAppearance::Progresschunk: + case StyleAppearance::ProgressBar: + case StyleAppearance::Meter: + case StyleAppearance::Meterchunk: + *aShouldRepaint = false; + return NS_OK; + default: + break; + } + + // 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 macOS system appearance changes are handled by + // nsLookAndFeel::SystemWantsDarkTheme. + return NS_OK; +} + +bool nsNativeThemeCocoa::ThemeSupportsWidget(nsPresContext* aPresContext, nsIFrame* aFrame, + StyleAppearance aAppearance) { + if (IsWidgetNonNative(aAppearance)) { + return ThemeCocoa::ThemeSupportsWidget(aPresContext, aFrame, aAppearance); + } + // if this is a dropdown button in a combobox the answer is always no + if (aAppearance == StyleAppearance::MozMenulistArrowButton) { + nsIFrame* parentFrame = aFrame->GetParent(); + if (parentFrame && parentFrame->IsComboboxControlFrame()) return false; + } + + switch (aAppearance) { + // Combobox dropdowns don't support native theming in vertical mode. + case StyleAppearance::Menulist: + case StyleAppearance::MenulistButton: + case StyleAppearance::MozMenulistArrowButton: + case StyleAppearance::MenulistText: + if (aFrame && aFrame->GetWritingMode().IsVertical()) { + return false; + } + [[fallthrough]]; + + case StyleAppearance::Listbox: + case StyleAppearance::Dialog: + case StyleAppearance::Window: + case StyleAppearance::MozWindowButtonBox: + case StyleAppearance::MozWindowTitlebar: + case StyleAppearance::Checkmenuitem: + case StyleAppearance::Menupopup: + case StyleAppearance::Menuarrow: + case StyleAppearance::Menuitem: + case StyleAppearance::Menuseparator: + case StyleAppearance::Tooltip: + + case StyleAppearance::Checkbox: + case StyleAppearance::CheckboxContainer: + case StyleAppearance::Radio: + case StyleAppearance::RadioContainer: + case StyleAppearance::Groupbox: + case StyleAppearance::MozMacHelpButton: + case StyleAppearance::MozMacDisclosureButtonOpen: + case StyleAppearance::MozMacDisclosureButtonClosed: + case StyleAppearance::Button: + case StyleAppearance::ButtonArrowUp: + case StyleAppearance::ButtonArrowDown: + case StyleAppearance::Toolbarbutton: + case StyleAppearance::Spinner: + case StyleAppearance::SpinnerUpbutton: + case StyleAppearance::SpinnerDownbutton: + case StyleAppearance::Toolbar: + case StyleAppearance::Statusbar: + case StyleAppearance::NumberInput: + case StyleAppearance::Textfield: + case StyleAppearance::Textarea: + case StyleAppearance::Searchfield: + case StyleAppearance::Toolbox: + case StyleAppearance::ProgressBar: + case StyleAppearance::Progresschunk: + case StyleAppearance::Meter: + case StyleAppearance::Meterchunk: + case StyleAppearance::Separator: + + case StyleAppearance::Tabpanels: + case StyleAppearance::Tab: + + case StyleAppearance::Treetwisty: + case StyleAppearance::Treetwistyopen: + case StyleAppearance::Treeview: + case StyleAppearance::Treeheader: + case StyleAppearance::Treeheadercell: + case StyleAppearance::Treeheadersortarrow: + case StyleAppearance::Treeitem: + case StyleAppearance::Treeline: + case StyleAppearance::MozMacSourceList: + case StyleAppearance::MozMacSourceListSelection: + case StyleAppearance::MozMacActiveSourceListSelection: + + case StyleAppearance::Range: + return !IsWidgetStyled(aPresContext, aFrame, aAppearance); + + default: + break; + } + + return false; +} + +bool nsNativeThemeCocoa::WidgetIsContainer(StyleAppearance aAppearance) { + // flesh this out at some point + switch (aAppearance) { + case StyleAppearance::MozMenulistArrowButton: + case StyleAppearance::Radio: + case StyleAppearance::Checkbox: + case StyleAppearance::ProgressBar: + case StyleAppearance::Meter: + case StyleAppearance::Range: + case StyleAppearance::MozMacHelpButton: + case StyleAppearance::MozMacDisclosureButtonOpen: + case StyleAppearance::MozMacDisclosureButtonClosed: + return false; + default: + break; + } + return true; +} + +bool nsNativeThemeCocoa::ThemeDrawsFocusForWidget(nsIFrame*, StyleAppearance aAppearance) { + switch (aAppearance) { + case StyleAppearance::Textarea: + case StyleAppearance::Textfield: + case StyleAppearance::Searchfield: + case StyleAppearance::NumberInput: + case StyleAppearance::Menulist: + case StyleAppearance::MenulistButton: + case StyleAppearance::Button: + case StyleAppearance::MozMacHelpButton: + case StyleAppearance::MozMacDisclosureButtonOpen: + case StyleAppearance::MozMacDisclosureButtonClosed: + case StyleAppearance::Radio: + case StyleAppearance::Range: + case StyleAppearance::Checkbox: + return true; + default: + return false; + } +} + +bool nsNativeThemeCocoa::ThemeNeedsComboboxDropmarker() { return false; } + +bool nsNativeThemeCocoa::WidgetAppearanceDependsOnWindowFocus(StyleAppearance aAppearance) { + switch (aAppearance) { + case StyleAppearance::Dialog: + case StyleAppearance::Groupbox: + case StyleAppearance::Tabpanels: + case StyleAppearance::ButtonArrowUp: + case StyleAppearance::ButtonArrowDown: + case StyleAppearance::Checkmenuitem: + case StyleAppearance::Menupopup: + case StyleAppearance::Menuarrow: + case StyleAppearance::Menuitem: + case StyleAppearance::Menuseparator: + case StyleAppearance::Tooltip: + case StyleAppearance::Spinner: + case StyleAppearance::SpinnerUpbutton: + case StyleAppearance::SpinnerDownbutton: + case StyleAppearance::Separator: + case StyleAppearance::Toolbox: + case StyleAppearance::NumberInput: + case StyleAppearance::Textfield: + case StyleAppearance::Treeview: + case StyleAppearance::Treeline: + case StyleAppearance::Textarea: + case StyleAppearance::Listbox: + return false; + default: + return true; + } +} + +nsITheme::ThemeGeometryType nsNativeThemeCocoa::ThemeGeometryTypeForWidget( + nsIFrame* aFrame, StyleAppearance aAppearance) { + switch (aAppearance) { + case StyleAppearance::MozWindowTitlebar: + return eThemeGeometryTypeTitlebar; + case StyleAppearance::Toolbar: + return eThemeGeometryTypeToolbar; + case StyleAppearance::Toolbox: + return eThemeGeometryTypeToolbox; + case StyleAppearance::MozWindowButtonBox: + return eThemeGeometryTypeWindowButtons; + case StyleAppearance::Tooltip: + return eThemeGeometryTypeTooltip; + case StyleAppearance::Menupopup: + return eThemeGeometryTypeMenu; + case StyleAppearance::Menuitem: + case StyleAppearance::Checkmenuitem: { + ElementState elementState = GetContentState(aFrame, aAppearance); + bool isDisabled = elementState.HasState(ElementState::DISABLED); + bool isSelected = !isDisabled && CheckBooleanAttr(aFrame, nsGkAtoms::menuactive); + return isSelected ? eThemeGeometryTypeHighlightedMenuItem : eThemeGeometryTypeMenu; + } + case StyleAppearance::MozMacSourceList: + return eThemeGeometryTypeSourceList; + case StyleAppearance::MozMacSourceListSelection: + return IsInSourceList(aFrame) ? eThemeGeometryTypeSourceListSelection + : eThemeGeometryTypeUnknown; + case StyleAppearance::MozMacActiveSourceListSelection: + return IsInSourceList(aFrame) ? eThemeGeometryTypeActiveSourceListSelection + : eThemeGeometryTypeUnknown; + default: + return eThemeGeometryTypeUnknown; + } +} + +nsITheme::Transparency nsNativeThemeCocoa::GetWidgetTransparency(nsIFrame* aFrame, + StyleAppearance aAppearance) { + if (IsWidgetScrollbarPart(aAppearance)) { + return ThemeCocoa::GetWidgetTransparency(aFrame, aAppearance); + } + + switch (aAppearance) { + case StyleAppearance::Menupopup: + case StyleAppearance::Tooltip: + case StyleAppearance::Dialog: + case StyleAppearance::Toolbar: + return eTransparent; + + case StyleAppearance::Statusbar: + // Knowing that scrollbars and statusbars are opaque improves + // performance, because we create layers for them. + return eOpaque; + + default: + return eUnknownTransparency; + } +} + +already_AddRefed<widget::Theme> do_CreateNativeThemeDoNotUseDirectly() { + return do_AddRef(new nsNativeThemeCocoa()); +} |