/* * ttkMacOSXTheme.c -- * * Tk theme engine for Mac OSX, using the Appearance Manager API. * * Copyright (c) 2004 Joe English * Copyright (c) 2005 Neil Madden * Copyright (c) 2006-2009 Daniel A. Steffen <[email protected]> * Copyright 2008-2009, Apple Inc. * Copyright 2009 Kevin Walzer/WordTech Communications LLC. * Copyright 2019 Marc Culler * * See the file "license.terms" for information on usage and redistribution * of this file, and for a DISCLAIMER OF ALL WARRANTIES. * * See also: * * <URL: http://developer.apple.com/documentation/Carbon/Reference/ * Appearance_Manager/appearance_manager/APIIndex.html > * * Notes: * "Active" means different things in Mac and Tk terminology -- * On Aqua, widgets are "Active" if they belong to the foreground window, * "Inactive" if they are in a background window. * Tk uses the term "active" to mean that the mouse cursor * is over a widget; aka "hover", "prelight", or "hot-tracked". * Aqua doesn't use this kind of feedback. * * The QuickDraw/Carbon coordinate system is relative to the * top-level window, not to the Tk_Window. BoxToRect() * accounts for this. */ #include "tkMacOSXPrivate.h" #include "ttk/ttkTheme.h" #include <math.h> /* * Macros for handling drawing contexts. */ #define BEGIN_DRAWING(d) { \ TkMacOSXDrawingContext dc; \ if (!TkMacOSXSetupDrawingContext((d), NULL, 1, &dc)) {return;} #define END_DRAWING \ TkMacOSXRestoreDrawingContext(&dc); } #define HIOrientation kHIThemeOrientationNormal #define NoThemeMetric 0xFFFFFFFF #ifdef __LP64__ #define RangeToFactor(maximum) (((double) (INT_MAX >> 1)) / (maximum)) #else #define RangeToFactor(maximum) (((double) (LONG_MAX >> 1)) / (maximum)) #endif /* __LP64__ */ #define TTK_STATE_FIRST_TAB TTK_STATE_USER1 #define TTK_STATE_LAST_TAB TTK_STATE_USER2 /*---------------------------------------------------------------------- * +++ Utilities. */ /* * BoxToRect -- * Convert a Ttk_Box in Tk coordinates relative to the given Drawable * to a native Rect relative to the containing port. */ static inline CGRect BoxToRect(Drawable d, Ttk_Box b) { MacDrawable *md = (MacDrawable*)d; CGRect rect; rect.origin.y = b.y + md->yOff; rect.origin.x = b.x + md->xOff; rect.size.height = b.height; rect.size.width = b.width; return rect; } /* * Table mapping Tk states to Appearance manager ThemeStates */ static Ttk_StateTable ThemeStateTable[] = { {kThemeStateUnavailable, TTK_STATE_DISABLED, 0}, {kThemeStatePressed, TTK_STATE_PRESSED, 0}, {kThemeStateInactive, TTK_STATE_BACKGROUND, 0}, {kThemeStateActive, 0, 0} /* Others: Not sure what these are supposed to mean. Up/Down have something to do with "little arrow" increment controls... Dunno what a "Rollover" is. NEM: Rollover is TTK_STATE_ACTIVE... but we don't handle that yet, by the looks of things {kThemeStateRollover, 0, 0}, {kThemeStateUnavailableInactive, 0, 0} {kThemeStatePressedUp, 0, 0}, {kThemeStatePressedDown, 0, 0} */ }; #if MAC_OS_X_VERSION_MIN_REQUIRED > 1080 /*---------------------------------------------------------------------- * +++ Support for contrasting background colors when GroupBoxes * or Tabbed panes are nested inside each other. */ /* * For systems older than 10.14, [NSColor windowBackGroundColor] generates * garbage when called from this function. In 10.14 it works correctly, * and must be used in order to have a background color which responds * to Dark Mode. So we use this hard-wired RGBA color on the older systems * which don't support Dark Mode anyway. */ static CGFloat windowBackground[4] = {235.0/255, 235.0/255, 235.0/255, 1.0}; static int GetBoxColor( CGContextRef context, Tk_Window tkwin, int depth, CGFloat *fill) { TkWindow *winPtr = (TkWindow *)tkwin; NSColorSpace *deviceRGB = [NSColorSpace deviceRGBColorSpace]; if ([NSApp macMinorVersion] > 13) { NSColor *windowColor = [[NSColor windowBackgroundColor] colorUsingColorSpace: deviceRGB]; [windowColor getComponents: fill]; } else { for (int i = 0; i < 4; i++) { fill[i] = windowBackground[i]; } } int isDark = (fill[0] + fill[1] + fill[2] < 1.5); /* * Compute the nesting depth of the widget. */ for (TkWindow *topPtr = winPtr->parentPtr; topPtr != NULL; topPtr = topPtr->parentPtr) { if (topPtr->privatePtr && (topPtr->privatePtr->flags & TTK_HAS_DARKER_BG)) { depth++; } if (depth > 7) { break; } } /* * Increase the contrast by one step for each level of nesting. */ if (isDark) { for (int i=0; i<3; i++) { fill[i] += (1.0 + depth*7.0)/255.0; } } else { for (int i=0; i<3; i++) { fill[i] -= (depth*8.0)/255.0; } } return depth; } /* * DrawGroupBox -- * * This is a standalone drawing procedure which draws the contrasting * rounded rectangular box for LabelFrames and Notebook panes. */ static void DrawGroupBox( CGRect bounds, CGContextRef context, Tk_Window tkwin) { CGPathRef path; NSColorSpace *deviceRGB = [NSColorSpace deviceRGBColorSpace]; NSColor *borderColor, *bgColor; static CGFloat border[4] = {1.0, 1.0, 1.0, 0.25}; CGFloat fill[4]; GetBoxColor(context, tkwin, 1, fill); bgColor = [NSColor colorWithColorSpace: deviceRGB components: fill count: 4]; CGContextSetFillColorSpace(context, deviceRGB.CGColorSpace); CGContextSetFillColorWithColor(context, bgColor.CGColor); path = CGPathCreateWithRoundedRect(bounds, 4, 4, NULL); CGContextClipToRect(context, bounds); CGContextBeginPath(context); CGContextAddPath(context, path); CGContextFillPath(context); borderColor = [NSColor colorWithColorSpace: deviceRGB components: border count: 4]; CGContextSetFillColorWithColor(context, borderColor.CGColor); [borderColor getComponents: fill]; CGContextSetRGBFillColor(context, fill[0], fill[1], fill[2], fill[3]); CGContextBeginPath(context); CGContextAddPath(context, path); CGContextReplacePathWithStrokedPath(context); CGContextFillPath(context); CFRelease(path); } /* * NormalizeButtonBounds -- * * Apple's Human Interface Guidelines only allow three specific heights for buttons: * Regular, small and mini. We always use the regular size. However, Ttk may * provide an arbitrary bounding rectangle. We always draw the button centered * vertically on the rectangle, and having the same width as the rectangle. * This function returns the actual bounding rectangle that will be used in * drawing the button. */ static CGRect NormalizeButtonBounds( SInt32 heightMetric, CGRect bounds) { SInt32 height; ChkErr(GetThemeMetric, heightMetric, &height); bounds.origin.y += (bounds.size.height - height)/2; bounds.size.height = height; return bounds; } /* SolidFillButtonFace -- * * Fill a rounded rectangle with a specified solid color. */ static void SolidFillButtonFace( CGContextRef context, CGRect bounds, CGFloat radius, NSColor *color) { CGPathRef path; CGContextSetFillColorWithColor(context, color.CGColor); path = CGPathCreateWithRoundedRect(bounds, radius, radius, NULL); CGContextBeginPath(context); CGContextAddPath(context, path); CGContextFillPath(context); CFRelease(path); } #endif /* MAC_OS_X_VERSION_MIN_REQUIRED > 1080 */ #if MAC_OS_X_VERSION_MIN_REQUIRED > 101300 /*---------------------------------------------------------------------- * +++ Drawing procedures for widgets in Apple's "Dark Mode" (10.14 and up). * * The HIToolbox does not support Dark Mode, and apparently never will, * so to make widgets look "native" we have to provide analogues of the * HITheme drawing functions to be used in DarkAqua. We continue to use * HITheme in Aqua, since it understands earlier versions of the OS. */ /* * Colors and gradients used in Dark Mode. */ static CGFloat darkButtonFace[4] = {112.0/255, 113.0/255, 115.0/255, 1.0}; static CGFloat darkDisabledButtonFace[4] = {86.0/255, 87.0/255, 89.0/255, 1.0}; static CGFloat darkInactiveSelectedTab[4] = {159.0/255, 160.0/255, 161.0/255, 1.0}; static CGFloat darkTabSeparator[4] = {0.0, 0.0, 0.0, 0.25}; static CGFloat darkTrack[4] = {1.0, 1.0, 1.0, 0.25}; static CGFloat darkTopGradient[8] = {1.0, 1.0, 1.0, 0.3, 1.0, 1.0, 1.0, 0.0}; static CGFloat darkBackgroundGradient[8] = {0.0, 0.0, 0.0, 0.1, 0.0, 0.0, 0.0, 0.25}; static CGFloat darkInactiveGradient[8] = {89.0/255, 90.0/255, 93.0/255, 1.0, 119.0/255, 120.0/255, 122.0/255, 1.0}; static CGFloat darkSelectedGradient[8] = {23.0/255, 111.0/255, 232.0/255, 1.0, 20.0/255, 94.0/255, 206.0/255, 1.0}; /* FillButtonBackground -- * * Fills a rounded rectangle with a transparent black gradient. */ static void FillButtonBackground( CGContextRef context, CGRect bounds, CGFloat radius) { CGPathRef path; NSColorSpace *deviceRGB = [NSColorSpace deviceRGBColorSpace]; CGGradientRef backgroundGradient = CGGradientCreateWithColorComponents( deviceRGB.CGColorSpace, darkBackgroundGradient, NULL, 2); CGPoint backgroundEnd = {bounds.origin.x, bounds.origin.y + bounds.size.height}; CGContextBeginPath(context); path = CGPathCreateWithRoundedRect(bounds, radius, radius, NULL); CGContextAddPath(context, path); CGContextClip(context); CGContextDrawLinearGradient(context, backgroundGradient, bounds.origin, backgroundEnd, 0); CFRelease(path); CFRelease(backgroundGradient); } /* HighlightButtonBorder -- * * Accent the top border of a rounded rectangle with a transparent * white gradient. */ static void HighlightButtonBorder( CGContextRef context, CGRect bounds) { NSColorSpace *deviceRGB = [NSColorSpace deviceRGBColorSpace]; CGPoint topEnd = {bounds.origin.x, bounds.origin.y + 3}; CGGradientRef topGradient = CGGradientCreateWithColorComponents( deviceRGB.CGColorSpace, darkTopGradient, NULL, 2); CGContextSaveGState(context); CGContextBeginPath(context); CGContextAddArc(context, bounds.origin.x + 4, bounds.origin.y + 4, 4, PI, 3*PI/2, 0); CGContextAddArc(context, bounds.origin.x + bounds.size.width - 4, bounds.origin.y + 4, 4, 3*PI/2, 0, 0); CGContextReplacePathWithStrokedPath(context); CGContextClip(context); CGContextDrawLinearGradient(context, topGradient, bounds.origin, topEnd, 0.0); CGContextRestoreGState(context); CFRelease(topGradient); } /* GradientFillButtonFace -- * * Fill a rounded rectangle with a specified gradient. */ static void GradientFillButtonFace( CGContextRef context, CGRect bounds, CGFloat radius, CGFloat* colors, int numColors) { NSColorSpace *deviceRGB = [NSColorSpace deviceRGBColorSpace]; CGPathRef path; CGPoint end = {bounds.origin.x, bounds.origin.y + bounds.size.height}; CGGradientRef gradient = CGGradientCreateWithColorComponents( deviceRGB.CGColorSpace, colors, NULL, numColors); path = CGPathCreateWithRoundedRect(bounds, radius, radius, NULL); CGContextBeginPath(context); CGContextAddPath(context, path); CGContextClip(context); CGContextDrawLinearGradient(context, gradient, bounds.origin, end, 0); CFRelease(path); CFRelease(gradient); } /* * DrawDarkButton -- * * This is a standalone drawing procedure which draws PushButtons and * PopupButtons in the Dark Mode style. */ static void DrawDarkButton( CGRect bounds, ThemeButtonKind kind, Ttk_State state, CGContextRef context) { NSColorSpace *deviceRGB = [NSColorSpace deviceRGBColorSpace]; NSColor *faceColor; /* * To match the appearance of Apple's buttons we need to increase the * height by 1 pixel. */ bounds.size.height += 1; CGContextClipToRect(context, bounds); FillButtonBackground(context, bounds, 5); /* * Fill the button face with the appropriate color. */ bounds = CGRectInset(bounds, 1, 1); if (kind == kThemePushButton && (state & TTK_STATE_PRESSED)) { GradientFillButtonFace(context, bounds, 4, darkSelectedGradient, 2); } else { if (state & TTK_STATE_DISABLED) { faceColor = [NSColor colorWithColorSpace: deviceRGB components: darkDisabledButtonFace count: 4]; } else { faceColor = [NSColor colorWithColorSpace: deviceRGB components: darkButtonFace count: 4]; } SolidFillButtonFace(context, bounds, 4, faceColor); } /* * If this is a popup, draw the arrow button. */ if (kind == kThemePopupButton) { CGFloat x, y; CGRect arrowBounds = bounds; arrowBounds.size.width = 16; arrowBounds.origin.x += bounds.size.width - 16; CGContextSaveGState(context); /* * If the toplevel is front, paint the button blue. */ if (!(state & TTK_STATE_BACKGROUND)) { GradientFillButtonFace(context, arrowBounds, 4, darkSelectedGradient, 2); } /* * Stroke the arrows. */ CGContextSetRGBStrokeColor(context, 1.0, 1.0, 1.0, 1.0); CGContextSetLineWidth(context, 1.5); x = arrowBounds.origin.x + 5; y = arrowBounds.origin.y + trunc(arrowBounds.size.height/2); CGContextBeginPath(context); CGPoint bottomArrow[3] = {{x, y+2}, {x+3.5, y+5.5}, {x+7, y+2}}; CGContextAddLines(context, bottomArrow, 3); CGPoint topArrow[3] = {{x, y-2}, {x+3.5, y-5.5}, {x+7, y-2}}; CGContextAddLines(context, topArrow, 3); CGContextStrokePath(context); CGContextRestoreGState(context); } HighlightButtonBorder(context, bounds); } /* * DrawDarkCheckBox -- * * This is a standalone drawing procedure which draws Checkboxes * in the Dark Mode style. */ static void DrawDarkCheckBox( CGRect bounds, Ttk_State state, CGContextRef context) { CGRect checkbounds = {{0, bounds.size.height/2 - 8},{16, 16}}; NSColorSpace *deviceRGB = [NSColorSpace deviceRGBColorSpace]; NSColor *stroke; CGFloat x, y; bounds = CGRectOffset(checkbounds, bounds.origin.x, bounds.origin.y); x = bounds.origin.x; y = bounds.origin.y; CGContextClipToRect(context, bounds); FillButtonBackground(context, bounds, 4); bounds = CGRectInset(bounds, 1, 1); if (!(state & TTK_STATE_BACKGROUND) && !(state & TTK_STATE_DISABLED) && ((state & TTK_STATE_SELECTED) || (state & TTK_STATE_ALTERNATE))) { GradientFillButtonFace(context, bounds, 3, darkSelectedGradient, 2); } else { GradientFillButtonFace(context, bounds, 3, darkInactiveGradient, 2); } HighlightButtonBorder(context, bounds); if ((state & TTK_STATE_SELECTED) || (state & TTK_STATE_ALTERNATE)) { CGContextSetStrokeColorSpace(context, deviceRGB.CGColorSpace); if (state & TTK_STATE_DISABLED) { stroke = [NSColor disabledControlTextColor]; } else { stroke = [NSColor controlTextColor]; } CGContextSetStrokeColorWithColor(context, stroke.CGColor); } if (state & TTK_STATE_SELECTED) { CGContextSetLineWidth(context, 1.5); CGContextBeginPath(context); CGPoint check[3] = {{x+4, y+8}, {x+7, y+11}, {x+11, y+4}}; CGContextAddLines(context, check, 3); CGContextStrokePath(context); } else if (state & TTK_STATE_ALTERNATE) { CGContextSetLineWidth(context, 2.0); CGContextBeginPath(context); CGPoint bar[2] = {{x+4, y+8}, {x+12, y+8}}; CGContextAddLines(context, bar, 2); CGContextStrokePath(context); } } /* * DrawDarkRadioButton -- * * This is a standalone drawing procedure which draws RadioButtons * in the Dark Mode style. */ static void DrawDarkRadioButton( CGRect bounds, Ttk_State state, CGContextRef context) { CGRect checkbounds = {{0, bounds.size.height/2 - 9},{18, 18}}; NSColorSpace *deviceRGB = [NSColorSpace deviceRGBColorSpace]; NSColor *fill; CGFloat x, y; bounds = CGRectOffset(checkbounds, bounds.origin.x, bounds.origin.y); x = bounds.origin.x; y = bounds.origin.y; CGContextClipToRect(context, bounds); FillButtonBackground(context, bounds, 9); bounds = CGRectInset(bounds, 1, 1); if (!(state & TTK_STATE_BACKGROUND) && !(state & TTK_STATE_DISABLED) && ((state & TTK_STATE_SELECTED) || (state & TTK_STATE_ALTERNATE))) { GradientFillButtonFace(context, bounds, 8, darkSelectedGradient, 2); } else { GradientFillButtonFace(context, bounds, 8, darkInactiveGradient, 2); } HighlightButtonBorder(context, bounds); if ((state & TTK_STATE_SELECTED) || (state & TTK_STATE_ALTERNATE)) { CGContextSetStrokeColorSpace(context, deviceRGB.CGColorSpace); if (state & TTK_STATE_DISABLED) { fill = [NSColor disabledControlTextColor]; } else { fill = [NSColor controlTextColor]; } CGContextSetFillColorWithColor(context, fill.CGColor); } if (state & TTK_STATE_SELECTED) { CGContextBeginPath(context); CGRect dot = {{x + 6, y + 6}, {6, 6}}; CGContextAddEllipseInRect(context, dot); CGContextFillPath(context); } else if (state & TTK_STATE_ALTERNATE) { CGRect bar = {{x + 5, y + 8}, {8, 2}}; CGContextFillRect(context, bar); } } /* * DrawDarkTab -- * * This is a standalone drawing procedure which draws Tabbed Pane * Tabs in the Dark Mode style. */ static void DrawDarkTab( CGRect bounds, Ttk_State state, CGContextRef context) { NSColorSpace *deviceRGB = [NSColorSpace deviceRGBColorSpace]; NSColor *faceColor, *stroke; CGRect originalBounds= bounds; CGContextSetLineWidth(context, 1.0); CGContextClipToRect(context, bounds); /* * Extend the bounds to one or both sides so the rounded part will be * clipped off. */ if (!(state & TTK_STATE_FIRST_TAB)) { bounds.origin.x -= 10; bounds.size.width += 10; } if (!(state & TTK_STATE_LAST_TAB)) { bounds.size.width += 10; } /* * Fill the tab face with the appropriate color or gradient. Use a * solid color if the tab is not selected, otherwise use a blue or * gray gradient. */ bounds = CGRectInset(bounds, 1, 1); if (!(state & TTK_STATE_SELECTED)) { if (state & TTK_STATE_DISABLED) { faceColor = [NSColor colorWithColorSpace: deviceRGB components: darkDisabledButtonFace count: 4]; } else { faceColor = [NSColor colorWithColorSpace: deviceRGB components: darkButtonFace count: 4]; } SolidFillButtonFace(context, bounds, 4, faceColor); /* * Draw a separator line on the left side of the tab if it * not first. */ if (!(state & TTK_STATE_FIRST_TAB)) { CGContextSaveGState(context); CGContextSetShouldAntialias(context, false); stroke = [NSColor colorWithColorSpace: deviceRGB components: darkTabSeparator count: 4]; CGContextSetStrokeColorWithColor(context, stroke.CGColor); CGContextBeginPath(context); CGContextMoveToPoint(context, originalBounds.origin.x, originalBounds.origin.y + 1); CGContextAddLineToPoint(context, originalBounds.origin.x, originalBounds.origin.y + originalBounds.size.height - 1); CGContextStrokePath(context); CGContextRestoreGState(context); } } else { /* * This is the selected tab; paint it blue. If it is first, cover up * the separator line drawn by the second one. (The selected tab is * always drawn last.) */ if ((state & TTK_STATE_FIRST_TAB) && !(state & TTK_STATE_LAST_TAB)) { bounds.size.width += 1; } if (!(state & TTK_STATE_BACKGROUND)) { GradientFillButtonFace(context, bounds, 4, darkSelectedGradient, 2); } else { faceColor = [NSColor colorWithColorSpace: deviceRGB components: darkInactiveSelectedTab count: 4]; SolidFillButtonFace(context, bounds, 4, faceColor); } HighlightButtonBorder(context, bounds); } } /* * DrawDarkSeparator -- * * This is a standalone drawing procedure which draws a separator widget * in Dark Mode. */ static void DrawDarkSeparator( CGRect bounds, CGContextRef context, Tk_Window tkwin) { static CGFloat fill[4] = {1.0, 1.0, 1.0, 0.3}; NSColorSpace *deviceRGB = [NSColorSpace deviceRGBColorSpace]; NSColor *fillColor = [NSColor colorWithColorSpace: deviceRGB components: fill count:4]; CGContextSetFillColorWithColor(context, fillColor.CGColor); CGContextFillRect(context, bounds); } #endif /* MAC_OS_X_VERSION_MIN_REQUIRED >101300 */ /*---------------------------------------------------------------------- * +++ Button element: Used for elements drawn with DrawThemeButton. */ typedef struct { ThemeButtonKind kind; ThemeMetric heightMetric; } ThemeButtonParams; static ThemeButtonParams PushButtonParams = { kThemePushButton, kThemeMetricPushButtonHeight }, CheckBoxParams = { kThemeCheckBox, kThemeMetricCheckBoxHeight }, RadioButtonParams = { kThemeRadioButton, kThemeMetricRadioButtonHeight }, BevelButtonParams = { kThemeBevelButton, NoThemeMetric }, PopupButtonParams = { kThemePopupButton, kThemeMetricPopupButtonHeight }, DisclosureParams = { kThemeDisclosureButton, kThemeMetricDisclosureTriangleHeight }, ListHeaderParams = { kThemeListHeaderButton, kThemeMetricListHeaderHeight }; static Ttk_StateTable ButtonValueTable[] = { { kThemeButtonMixed, TTK_STATE_ALTERNATE, 0 }, { kThemeButtonOn, TTK_STATE_SELECTED, 0 }, { kThemeButtonOff, 0, 0 } /* Others: kThemeDisclosureRight, kThemeDisclosureDown, kThemeDisclosureLeft */ }; static Ttk_StateTable ButtonAdornmentTable[] = { { kThemeAdornmentDefault | kThemeAdornmentFocus, TTK_STATE_ALTERNATE | TTK_STATE_FOCUS, 0 }, { kThemeAdornmentDefault, TTK_STATE_ALTERNATE, 0 }, { kThemeAdornmentFocus, TTK_STATE_FOCUS, 0 }, { kThemeAdornmentNone, 0, 0 }}; /* * computeButtonDrawInfo -- * Fill in an appearance manager HIThemeButtonDrawInfo record. */ static inline HIThemeButtonDrawInfo computeButtonDrawInfo( ThemeButtonParams *params, Ttk_State state, Tk_Window tkwin) { /* * See ButtonElementDraw for the explanation of why we always draw * PushButtons in the active state. */ SInt32 HIThemeState; switch (params->kind) { case kThemePushButton: HIThemeState = kThemeStateActive; break; default: HIThemeState = Ttk_StateTableLookup(ThemeStateTable, state); break; } const HIThemeButtonDrawInfo info = { .version = 0, .state = HIThemeState, .kind = params ? params->kind : 0, .value = Ttk_StateTableLookup(ButtonValueTable, state), .adornment = Ttk_StateTableLookup(ButtonAdornmentTable, state), }; return info; } static void ButtonElementSizeNoPadding( void *clientData, void *elementRecord, Tk_Window tkwin, int *widthPtr, int *heightPtr, Ttk_Padding *paddingPtr) { ThemeButtonParams *params = clientData; if (params->heightMetric != NoThemeMetric) { SInt32 height; ChkErr(GetThemeMetric, params->heightMetric, &height); *heightPtr = height; } } static void ButtonElementSize( void *clientData, void *elementRecord, Tk_Window tkwin, int *widthPtr, int *heightPtr, Ttk_Padding *paddingPtr) { ThemeButtonParams *params = clientData; const HIThemeButtonDrawInfo info = computeButtonDrawInfo(params, 0, tkwin); static const CGRect scratchBounds = {{0, 0}, {100, 100}}; CGRect contentBounds; ButtonElementSizeNoPadding( clientData, elementRecord, tkwin, widthPtr, heightPtr, paddingPtr); /* * To compute internal padding, query the appearance manager * for the content bounds of a dummy rectangle, then use * the difference as the padding. */ ChkErr(HIThemeGetButtonContentBounds, &scratchBounds, &info, &contentBounds); paddingPtr->left = CGRectGetMinX(contentBounds); paddingPtr->right = CGRectGetMaxX(scratchBounds) - CGRectGetMaxX(contentBounds); } static void ButtonElementDraw( void *clientData, void *elementRecord, Tk_Window tkwin, Drawable d, Ttk_Box b, Ttk_State state) { BEGIN_DRAWING(d) ThemeButtonParams *params = clientData; CGRect bounds = BoxToRect(d, b); HIThemeButtonDrawInfo info = computeButtonDrawInfo(params, state, tkwin); bounds = NormalizeButtonBounds(params->heightMetric, bounds); if (TkMacOSXInDarkMode(tkwin)) { #if MAC_OS_X_VERSION_MIN_REQUIRED > 101300 switch (info.kind) { case kThemePushButton: case kThemePopupButton: DrawDarkButton(bounds, info.kind, state, dc.context); break; case kThemeCheckBox: DrawDarkCheckBox(bounds, state, dc.context); break; case kThemeRadioButton: DrawDarkRadioButton(bounds, state, dc.context); break; default: ChkErr(HIThemeDrawButton, &bounds, &info, dc.context, HIOrientation, NULL); } #endif } else { /* * Apple's PushButton and PopupButton do not change their (white) fill * color when the window is inactive although, except in 10.7 (Lion), * the color of the arrow button on a PopupButton does change. For * some reason HITheme fills inactive buttons with a transparent color * that allows the window background to show through, leading to * inconsistent behavior. We work around this by filling behind an * inactive PopupButton with a white color before asking HIToolbox to * draw it. PopupButton. For PushButtons, we simply draw them in the * active state. */ if (info.kind == kThemePopupButton && (state & TTK_STATE_BACKGROUND)) { CGRect innerBounds = CGRectInset(bounds, 1, 1); SolidFillButtonFace(dc.context, innerBounds, 4, [NSColor whiteColor]); } ChkErr(HIThemeDrawButton, &bounds, &info, dc.context, HIOrientation, NULL); } END_DRAWING } static Ttk_ElementSpec ButtonElementSpec = { TK_STYLE_VERSION_2, sizeof(NullElement), TtkNullElementOptions, ButtonElementSize, ButtonElementDraw }; /*---------------------------------------------------------------------- * +++ Notebook elements. */ /* Tab position logic, c.f. ttkNotebook.c TabState() */ static Ttk_StateTable TabStyleTable[] = { { kThemeTabFrontInactive, TTK_STATE_SELECTED | TTK_STATE_BACKGROUND}, { kThemeTabNonFrontInactive, TTK_STATE_BACKGROUND}, { kThemeTabFrontUnavailable, TTK_STATE_DISABLED | TTK_STATE_SELECTED}, { kThemeTabNonFrontUnavailable, TTK_STATE_DISABLED}, { kThemeTabFront, TTK_STATE_SELECTED}, { kThemeTabNonFrontPressed, TTK_STATE_PRESSED}, { kThemeTabNonFront, 0} }; static Ttk_StateTable TabAdornmentTable[] = { { kHIThemeTabAdornmentNone, TTK_STATE_FIRST_TAB | TTK_STATE_LAST_TAB}, { kHIThemeTabAdornmentTrailingSeparator, TTK_STATE_FIRST_TAB}, { kHIThemeTabAdornmentNone, TTK_STATE_LAST_TAB}, { kHIThemeTabAdornmentTrailingSeparator, 0 }, }; static Ttk_StateTable TabPositionTable[] = { { kHIThemeTabPositionOnly, TTK_STATE_FIRST_TAB | TTK_STATE_LAST_TAB}, { kHIThemeTabPositionFirst, TTK_STATE_FIRST_TAB}, { kHIThemeTabPositionLast, TTK_STATE_LAST_TAB}, { kHIThemeTabPositionMiddle, 0 }, }; /* * Apple XHIG Tab View Specifications: * * Control sizes: Tab views are available in regular, small, and mini sizes. * The tab height is fixed for each size, but you control the size of the pane * area. The tab heights for each size are listed below: * - Regular size: 20 pixels. * - Small: 17 pixels. * - Mini: 15 pixels. * * Label spacing and fonts: The tab labels should be in a font that’s * proportional to the size of the tab view control. In addition, the label * should be placed so that there are equal margins of space before and after * it. The guidelines below provide the specifications you should use for tab * labels: * - Regular size: System font. Center in tab, leaving 12 pixels on each side. * - Small: Small system font. Center in tab, leaving 10 pixels on each side. * - Mini: Mini system font. Center in tab, leaving 8 pixels on each side. * * Control spacing: Whether you decide to inset a tab view in a window or * extend its edges to the window sides and bottom, you should place the top * edge of the tab view 12 or 14 pixels below the bottom edge of the title bar * (or toolbar, if there is one). If you choose to inset a tab view in a * window, you should leave a margin of 20 pixels between the sides and bottom * of the tab view and the sides and bottom of the window (although 16 pixels * is also an acceptable margin-width). If you need to provide controls below * the tab view, leave enough space below the tab view so the controls are 20 * pixels above the bottom edge of the window and 12 pixels between the tab * view and the controls. * If you choose to extend the tab view sides and bottom so that they meet the * window sides and bottom, you should leave a margin of at least 20 pixels * between the content in the tab view and the tab-view edges. * * <URL: http://developer.apple.com/documentation/userexperience/Conceptual/ * AppleHIGuidelines/XHIGControls/XHIGControls.html#//apple_ref/doc/uid/ * TP30000359-TPXREF116> */ static void TabElementSize( void *clientData, void *elementRecord, Tk_Window tkwin, int *widthPtr, int *heightPtr, Ttk_Padding *paddingPtr) { GetThemeMetric(kThemeMetricLargeTabHeight, (SInt32 *)heightPtr); *paddingPtr = Ttk_MakePadding(0, 0, 0, 2); } static void TabElementDraw( void *clientData, void *elementRecord, Tk_Window tkwin, Drawable d, Ttk_Box b, Ttk_State state) { CGRect bounds = BoxToRect(d, b); HIThemeTabDrawInfo info = { .version = 1, .style = Ttk_StateTableLookup(TabStyleTable, state), .direction = kThemeTabNorth, .size = kHIThemeTabSizeNormal, .adornment = Ttk_StateTableLookup(TabAdornmentTable, state), .kind = kHIThemeTabKindNormal, .position = Ttk_StateTableLookup(TabPositionTable, state), }; BEGIN_DRAWING(d) #if MAC_OS_X_VERSION_MIN_REQUIRED > 101300 if (TkMacOSXInDarkMode(tkwin)) { DrawDarkTab(bounds, state, dc.context); } else { ChkErr(HIThemeDrawTab, &bounds, &info, dc.context, HIOrientation, NULL); } #else ChkErr(HIThemeDrawTab, &bounds, &info, dc.context, HIOrientation, NULL); #endif END_DRAWING } static Ttk_ElementSpec TabElementSpec = { TK_STYLE_VERSION_2, sizeof(NullElement), TtkNullElementOptions, TabElementSize, TabElementDraw }; /* * Notebook panes: */ static void PaneElementSize( void *clientData, void *elementRecord, Tk_Window tkwin, int *widthPtr, int *heightPtr, Ttk_Padding *paddingPtr) { *paddingPtr = Ttk_MakePadding(9, 5, 9, 9); } static void PaneElementDraw( void *clientData, void *elementRecord, Tk_Window tkwin, Drawable d, Ttk_Box b, Ttk_State state) { TkWindow *winPtr = (TkWindow *)tkwin; MacDrawable *macWin = winPtr->privatePtr; CGRect bounds = BoxToRect(d, b); bounds.origin.y -= kThemeMetricTabFrameOverlap; bounds.size.height += kThemeMetricTabFrameOverlap; BEGIN_DRAWING(d) #if MAC_OS_X_VERSION_MIN_REQUIRED > 10800 DrawGroupBox(bounds, dc.context, tkwin); #else HIThemeTabPaneDrawInfo info = { .version = 1, .state = Ttk_StateTableLookup(ThemeStateTable, state), .direction = kThemeTabNorth, .size = kHIThemeTabSizeNormal, .kind = kHIThemeTabKindNormal, .adornment = kHIThemeTabPaneAdornmentNormal, }; bounds.origin.y -= kThemeMetricTabFrameOverlap; bounds.size.height += kThemeMetricTabFrameOverlap; ChkErr(HIThemeDrawTabPane, &bounds, &info, dc.context, HIOrientation); #endif END_DRAWING [TkMacOSXDrawableView(macWin) setNeedsDisplay:YES]; if (macWin != NULL) { macWin->flags |= TTK_HAS_DARKER_BG; } } static Ttk_ElementSpec PaneElementSpec = { TK_STYLE_VERSION_2, sizeof(NullElement), TtkNullElementOptions, PaneElementSize, PaneElementDraw }; /* * Labelframe borders: * Use "primary group box ..." * Quoth DrawThemePrimaryGroup reference: * "The primary group box frame is drawn inside the specified * rectangle and is a maximum of 2 pixels thick." * * "Maximum of 2 pixels thick" is apparently a lie; * looks more like 4 to me with shading. */ static void GroupElementSize( void *clientData, void *elementRecord, Tk_Window tkwin, int *widthPtr, int *heightPtr, Ttk_Padding *paddingPtr) { *paddingPtr = Ttk_UniformPadding(4); } static void GroupElementDraw( void *clientData, void *elementRecord, Tk_Window tkwin, Drawable d, Ttk_Box b, Ttk_State state) { TkWindow *winPtr = (TkWindow *)tkwin; CGRect bounds = BoxToRect(d, b); BEGIN_DRAWING(d) #if MAC_OS_X_VERSION_MIN_REQUIRED > 10800 DrawGroupBox(bounds, dc.context, tkwin); #else const HIThemeGroupBoxDrawInfo info = { .version = 0, .state = Ttk_StateTableLookup(ThemeStateTable, state), .kind = kHIThemeGroupBoxKindPrimaryOpaque, }; ChkErr(HIThemeDrawGroupBox, &bounds, &info, dc.context, HIOrientation); #endif END_DRAWING if (winPtr->privatePtr != NULL) { winPtr->privatePtr->flags |= TTK_HAS_DARKER_BG; } } static Ttk_ElementSpec GroupElementSpec = { TK_STYLE_VERSION_2, sizeof(NullElement), TtkNullElementOptions, GroupElementSize, GroupElementDraw }; /*---------------------------------------------------------------------- * +++ Entry element -- * 3 pixels padding for focus rectangle * 2 pixels padding for EditTextFrame */ typedef struct { Tcl_Obj *backgroundObj; } EntryElement; static Ttk_ElementOptionSpec EntryElementOptions[] = { { "-background", TK_OPTION_BORDER, Tk_Offset(EntryElement,backgroundObj), "white" }, {0} }; static void EntryElementSize( void *clientData, void *elementRecord, Tk_Window tkwin, int *widthPtr, int *heightPtr, Ttk_Padding *paddingPtr) { *paddingPtr = Ttk_UniformPadding(5); } static void EntryElementDraw( void *clientData, void *elementRecord, Tk_Window tkwin, Drawable d, Ttk_Box b, Ttk_State state) { EntryElement *e = elementRecord; Tk_3DBorder backgroundPtr = Tk_Get3DBorderFromObj(tkwin,e->backgroundObj); Ttk_Box inner = Ttk_PadBox(b, Ttk_UniformPadding(3)); CGRect bounds = BoxToRect(d, inner); const HIThemeFrameDrawInfo info = { .version = 0, .kind = kHIThemeFrameTextFieldSquare, .state = Ttk_StateTableLookup(ThemeStateTable, state), .isFocused = state & TTK_STATE_FOCUS, }; /* * Erase w/background color: */ XFillRectangle(Tk_Display(tkwin), d, Tk_3DBorderGC(tkwin, backgroundPtr, TK_3D_FLAT_GC), inner.x,inner.y, inner.width, inner.height); BEGIN_DRAWING(d) ChkErr(HIThemeDrawFrame, &bounds, &info, dc.context, HIOrientation); /*if (state & TTK_STATE_FOCUS) { ChkErr(DrawThemeFocusRect, &bounds, 1); }*/ END_DRAWING } static Ttk_ElementSpec EntryElementSpec = { TK_STYLE_VERSION_2, sizeof(EntryElement), EntryElementOptions, EntryElementSize, EntryElementDraw }; /*---------------------------------------------------------------------- * +++ Combobox: * * NOTES: * kThemeMetricComboBoxLargeDisclosureWidth -> 17 * Padding and margins guesstimated by trial-and-error. */ static Ttk_Padding ComboboxPadding = { 2, 3, 17, 1 }; static Ttk_Padding ComboboxMargins = { 3, 4, 4, 3 }; static void ComboboxElementSize( void *clientData, void *elementRecord, Tk_Window tkwin, int *widthPtr, int *heightPtr, Ttk_Padding *paddingPtr) { *widthPtr = 0; *heightPtr = 0; *paddingPtr = Ttk_AddPadding(ComboboxMargins, ComboboxPadding); } static void ComboboxElementDraw( void *clientData, void *elementRecord, Tk_Window tkwin, Drawable d, Ttk_Box b, Ttk_State state) { CGRect bounds = BoxToRect(d, Ttk_PadBox(b, ComboboxMargins)); const HIThemeButtonDrawInfo info = { .version = 0, .state = Ttk_StateTableLookup(ThemeStateTable, state), .kind = kThemeComboBox, .value = Ttk_StateTableLookup(ButtonValueTable, state), .adornment = Ttk_StateTableLookup(ButtonAdornmentTable, state), }; BEGIN_DRAWING(d) ChkErr(HIThemeDrawButton, &bounds, &info, dc.context, HIOrientation, NULL); END_DRAWING } static Ttk_ElementSpec ComboboxElementSpec = { TK_STYLE_VERSION_2, sizeof(NullElement), TtkNullElementOptions, ComboboxElementSize, ComboboxElementDraw }; /*---------------------------------------------------------------------- * +++ Spinbuttons. * * From Apple HIG, part III, section "Controls", "The Stepper Control": * there should be 2 pixels of space between the stepper control (AKA * IncDecButton, AKA "little arrows") and the text field it modifies. */ static Ttk_Padding SpinbuttonMargins = {2,0,2,0}; static void SpinButtonElementSize( void *clientData, void *elementRecord, Tk_Window tkwin, int *widthPtr, int *heightPtr, Ttk_Padding *paddingPtr) { SInt32 s; ChkErr(GetThemeMetric, kThemeMetricLittleArrowsWidth, &s); *widthPtr = s + Ttk_PaddingWidth(SpinbuttonMargins); ChkErr(GetThemeMetric, kThemeMetricLittleArrowsHeight, &s); *heightPtr = s + Ttk_PaddingHeight(SpinbuttonMargins); } static void SpinButtonElementDraw( void *clientData, void *elementRecord, Tk_Window tkwin, Drawable d, Ttk_Box b, Ttk_State state) { CGRect bounds = BoxToRect(d, Ttk_PadBox(b, SpinbuttonMargins)); /* @@@ can't currently distinguish PressedUp (== Pressed) from PressedDown; * ignore this bit for now [see #2219588] */ const HIThemeButtonDrawInfo info = { .version = 0, .state = Ttk_StateTableLookup(ThemeStateTable, state & ~TTK_STATE_PRESSED), .kind = kThemeIncDecButton, .value = Ttk_StateTableLookup(ButtonValueTable, state), .adornment = kThemeAdornmentNone, }; BEGIN_DRAWING(d) ChkErr(HIThemeDrawButton, &bounds, &info, dc.context, HIOrientation, NULL); END_DRAWING } static Ttk_ElementSpec SpinButtonElementSpec = { TK_STYLE_VERSION_2, sizeof(NullElement), TtkNullElementOptions, SpinButtonElementSize, SpinButtonElementDraw }; /*---------------------------------------------------------------------- * +++ DrawThemeTrack-based elements -- * Progress bars and scales. (See also: <<NOTE-TRACKS>>) */ /* * Apple does not change the appearance of a slider when the window * becomes inactive. So we shouldn't either. */ static Ttk_StateTable ThemeTrackEnableTable[] = { { kThemeTrackDisabled, TTK_STATE_DISABLED, 0 }, { kThemeTrackActive, TTK_STATE_BACKGROUND, 0 }, { kThemeTrackActive, 0, 0 } /* { kThemeTrackNothingToScroll, ?, ? }, */ }; typedef struct { /* TrackElement client data */ ThemeTrackKind kind; SInt32 thicknessMetric; } TrackElementData; static TrackElementData ScaleData = { kThemeSlider, kThemeMetricHSliderHeight }; typedef struct { Tcl_Obj *fromObj; /* minimum value */ Tcl_Obj *toObj; /* maximum value */ Tcl_Obj *valueObj; /* current value */ Tcl_Obj *orientObj; /* horizontal / vertical */ } TrackElement; static Ttk_ElementOptionSpec TrackElementOptions[] = { { "-from", TK_OPTION_DOUBLE, Tk_Offset(TrackElement,fromObj) }, { "-to", TK_OPTION_DOUBLE, Tk_Offset(TrackElement,toObj) }, { "-value", TK_OPTION_DOUBLE, Tk_Offset(TrackElement,valueObj) }, { "-orient", TK_OPTION_STRING, Tk_Offset(TrackElement,orientObj) }, {0,0,0} }; static void TrackElementSize( void *clientData, void *elementRecord, Tk_Window tkwin, int *widthPtr, int *heightPtr, Ttk_Padding *paddingPtr) { TrackElementData *data = clientData; SInt32 size = 24; /* reasonable default ... */ ChkErr(GetThemeMetric, data->thicknessMetric, &size); *widthPtr = *heightPtr = size; } static void TrackElementDraw( void *clientData, void *elementRecord, Tk_Window tkwin, Drawable d, Ttk_Box b, Ttk_State state) { TrackElementData *data = clientData; TrackElement *elem = elementRecord; int orientation = TTK_ORIENT_HORIZONTAL; double from = 0, to = 100, value = 0, factor; Ttk_GetOrientFromObj(NULL, elem->orientObj, &orientation); Tcl_GetDoubleFromObj(NULL, elem->fromObj, &from); Tcl_GetDoubleFromObj(NULL, elem->toObj, &to); Tcl_GetDoubleFromObj(NULL, elem->valueObj, &value); factor = RangeToFactor(to - from); HIThemeTrackDrawInfo info = { .version = 0, .kind = data->kind, .bounds = BoxToRect(d, b), .min = from * factor, .max = to * factor, .value = value * factor, .attributes = kThemeTrackShowThumb | (orientation == TTK_ORIENT_HORIZONTAL ? kThemeTrackHorizontal : 0), .enableState = Ttk_StateTableLookup(ThemeTrackEnableTable, state), .trackInfo.progress.phase = 0, }; if (info.kind == kThemeSlider) { info.trackInfo.slider.pressState = state & TTK_STATE_PRESSED ? kThemeThumbPressed : 0; if (state & TTK_STATE_ALTERNATE) { info.trackInfo.slider.thumbDir = kThemeThumbDownward; } else { info.trackInfo.slider.thumbDir = kThemeThumbPlain; } } BEGIN_DRAWING(d) #if MAC_OS_X_VERSION_MIN_REQUIRED > 101300 if (TkMacOSXInDarkMode(tkwin)) { CGRect bounds = BoxToRect(d, b); NSColorSpace *deviceRGB = [NSColorSpace deviceRGBColorSpace]; NSColor *trackColor = [NSColor colorWithColorSpace: deviceRGB components: darkTrack count: 4]; if (orientation == TTK_ORIENT_HORIZONTAL) { bounds = CGRectInset(bounds, 1, bounds.size.height/2 - 2); } else { bounds = CGRectInset(bounds, bounds.size.width/2 - 3, 2); } SolidFillButtonFace(dc.context, bounds, 2, trackColor); } #endif ChkErr(HIThemeDrawTrack, &info, NULL, dc.context, HIOrientation); END_DRAWING } static Ttk_ElementSpec TrackElementSpec = { TK_STYLE_VERSION_2, sizeof(TrackElement), TrackElementOptions, TrackElementSize, TrackElementDraw }; /* * Slider element -- <<NOTE-TRACKS>> * Has geometry only. The Scale widget adjusts the position of this element, * and uses it for hit detection. In the Aqua theme, the slider is actually * drawn as part of the trough element. * * Also buggy: The geometry here is a Wild-Assed-Guess; I can't * figure out how to get the Appearance Manager to tell me the * slider size. */ static void SliderElementSize( void *clientData, void *elementRecord, Tk_Window tkwin, int *widthPtr, int *heightPtr, Ttk_Padding *paddingPtr) { *widthPtr = *heightPtr = 24; } static Ttk_ElementSpec SliderElementSpec = { TK_STYLE_VERSION_2, sizeof(NullElement), TtkNullElementOptions, SliderElementSize, TtkNullElementDraw }; /*---------------------------------------------------------------------- * +++ Progress bar element (new): * * @@@ NOTE: According to an older revision of the Aqua reference docs, * @@@ the 'phase' field is between 0 and 4. Newer revisions say * @@@ that it can be any UInt8 value. */ typedef struct { Tcl_Obj *orientObj; /* horizontal / vertical */ Tcl_Obj *valueObj; /* current value */ Tcl_Obj *maximumObj; /* maximum value */ Tcl_Obj *phaseObj; /* animation phase */ Tcl_Obj *modeObj; /* progress bar mode */ } PbarElement; static Ttk_ElementOptionSpec PbarElementOptions[] = { { "-orient", TK_OPTION_STRING, Tk_Offset(PbarElement,orientObj), "horizontal" }, { "-value", TK_OPTION_DOUBLE, Tk_Offset(PbarElement,valueObj), "0" }, { "-maximum", TK_OPTION_DOUBLE, Tk_Offset(PbarElement,maximumObj), "100" }, { "-phase", TK_OPTION_INT, Tk_Offset(PbarElement,phaseObj), "0" }, { "-mode", TK_OPTION_STRING, Tk_Offset(PbarElement,modeObj), "determinate" }, {0,0,0,0} }; static void PbarElementSize( void *clientData, void *elementRecord, Tk_Window tkwin, int *widthPtr, int *heightPtr, Ttk_Padding *paddingPtr) { SInt32 size = 24; /* @@@ Check HIG for correct default */ ChkErr(GetThemeMetric, kThemeMetricLargeProgressBarThickness, &size); *widthPtr = *heightPtr = size; } static void PbarElementDraw( void *clientData, void *elementRecord, Tk_Window tkwin, Drawable d, Ttk_Box b, Ttk_State state) { PbarElement *pbar = elementRecord; int orientation = TTK_ORIENT_HORIZONTAL, phase = 0; double value = 0, maximum = 100, factor; Ttk_GetOrientFromObj(NULL, pbar->orientObj, &orientation); Tcl_GetDoubleFromObj(NULL, pbar->valueObj, &value); Tcl_GetDoubleFromObj(NULL, pbar->maximumObj, &maximum); Tcl_GetIntFromObj(NULL, pbar->phaseObj, &phase); factor = RangeToFactor(maximum); HIThemeTrackDrawInfo info = { .version = 0, .kind = (!strcmp("indeterminate", Tcl_GetString(pbar->modeObj)) && value) ? kThemeIndeterminateBar : kThemeProgressBar, .bounds = BoxToRect(d, b), .min = 0, .max = maximum * factor, .value = value * factor, .attributes = kThemeTrackShowThumb | (orientation == TTK_ORIENT_HORIZONTAL ? kThemeTrackHorizontal : 0), .enableState = Ttk_StateTableLookup(ThemeTrackEnableTable, state), .trackInfo.progress.phase = phase, }; BEGIN_DRAWING(d) #if MAC_OS_X_VERSION_MIN_REQUIRED > 101300 if (TkMacOSXInDarkMode(tkwin)) { CGRect bounds = BoxToRect(d, b); NSColorSpace *deviceRGB = [NSColorSpace deviceRGBColorSpace]; NSColor *trackColor = [NSColor colorWithColorSpace: deviceRGB components: darkTrack count: 4]; if (orientation == TTK_ORIENT_HORIZONTAL) { bounds = CGRectInset(bounds, 1, bounds.size.height/2 - 3); } else { bounds = CGRectInset(bounds, bounds.size.width/2 - 3, 1); } SolidFillButtonFace(dc.context, bounds, 3, trackColor); } #endif ChkErr(HIThemeDrawTrack, &info, NULL, dc.context, HIOrientation); END_DRAWING } static Ttk_ElementSpec PbarElementSpec = { TK_STYLE_VERSION_2, sizeof(PbarElement), PbarElementOptions, PbarElementSize, PbarElementDraw }; /*---------------------------------------------------------------------- * +++ Separator element. * * DrawThemeSeparator() guesses the orientation of the line from the width * and height of the rectangle, so the same element can can be used for * horizontal, vertical, and general separators. */ static void SeparatorElementSize( void *clientData, void *elementRecord, Tk_Window tkwin, int *widthPtr, int *heightPtr, Ttk_Padding *paddingPtr) { *widthPtr = *heightPtr = 1; } static void SeparatorElementDraw( void *clientData, void *elementRecord, Tk_Window tkwin, Drawable d, Ttk_Box b, unsigned int state) { CGRect bounds = BoxToRect(d, b); const HIThemeSeparatorDrawInfo info = { .version = 0, /* Separator only supports kThemeStateActive, kThemeStateInactive */ .state = Ttk_StateTableLookup(ThemeStateTable, state & TTK_STATE_BACKGROUND), }; BEGIN_DRAWING(d) #if MAC_OS_X_VERSION_MIN_REQUIRED > 101300 if (TkMacOSXInDarkMode(tkwin)) { DrawDarkSeparator(bounds, dc.context, tkwin); } else { ChkErr(HIThemeDrawSeparator, &bounds, &info, dc.context, HIOrientation); } #else ChkErr(HIThemeDrawSeparator, &bounds, &info, dc.context, HIOrientation); #endif END_DRAWING } static Ttk_ElementSpec SeparatorElementSpec = { TK_STYLE_VERSION_2, sizeof(NullElement), TtkNullElementOptions, SeparatorElementSize, SeparatorElementDraw }; /*---------------------------------------------------------------------- * +++ Size grip element. */ static const ThemeGrowDirection sizegripGrowDirection = kThemeGrowRight|kThemeGrowDown; static void SizegripElementSize( void *clientData, void *elementRecord, Tk_Window tkwin, int *widthPtr, int *heightPtr, Ttk_Padding *paddingPtr) { HIThemeGrowBoxDrawInfo info = { .version = 0, .state = kThemeStateActive, .kind = kHIThemeGrowBoxKindNormal, .direction = sizegripGrowDirection, .size = kHIThemeGrowBoxSizeNormal, }; CGRect bounds = CGRectZero; ChkErr(HIThemeGetGrowBoxBounds, &bounds.origin, &info, &bounds); *widthPtr = bounds.size.width; *heightPtr = bounds.size.height; } static void SizegripElementDraw( void *clientData, void *elementRecord, Tk_Window tkwin, Drawable d, Ttk_Box b, unsigned int state) { CGRect bounds = BoxToRect(d, b); HIThemeGrowBoxDrawInfo info = { .version = 0, /* Grow box only supports kThemeStateActive, kThemeStateInactive */ .state = Ttk_StateTableLookup(ThemeStateTable, state & TTK_STATE_BACKGROUND), .kind = kHIThemeGrowBoxKindNormal, .direction = sizegripGrowDirection, .size = kHIThemeGrowBoxSizeNormal, }; BEGIN_DRAWING(d) ChkErr(HIThemeDrawGrowBox, &bounds.origin, &info, dc.context, HIOrientation); END_DRAWING } static Ttk_ElementSpec SizegripElementSpec = { TK_STYLE_VERSION_2, sizeof(NullElement), TtkNullElementOptions, SizegripElementSize, SizegripElementDraw }; /*---------------------------------------------------------------------- * +++ Background and fill elements. * * Before drawing any ttk widget, its bounding rectangle is filled with a * background color. This color must match the background color of the * containing widget to avoid looking ugly. The need for care when doing * this is exacerbated by the fact that ttk enforces its "native look" by * not allowing user control of the background or highlight colors of ttk * widgets. * * This job is made more complicated in recent versions of macOS by the fact * that the Appkit GroupBox (used for ttk LabelFrames) and TabbedPane (used * for the Notebook widget) both place their content inside a rectangle with * rounded corners that has a color which contrasts with the dialog * background color. Moreover, although the Apple human interface * guidelines recommend against doing so, there are times when one wants to * nest these widgets, for example having a GroupBox inside of a TabbedPane. * To have the right contrast, each level of nesting requires a different * color. * * Previous Tk releases used the HIThemeDrawGroupBox routine to draw * GroupBoxes and TabbedPanes. This meant that the best that could be done * was to set the GroupBox to be of kind kHIThemeGroupBoxKindPrimaryOpaque, * and set its fill color to be the system background color. If widgets * inside the box were drawn with the system background color the * backgrounds would match. But this produces a GroupBox with no contrast, * the only visual clue being a faint highlighting around the top of the * GroupBox. Moreover, the TabbedPane does not have an Opaque version, so * while it is drawn inside a contrasting rounded rectangle, the widgets * inside the pane needed to be enclosed in a frame with the system * background color. This added a visual artifact since the frame's * background color does not match the Pane's background color. That code * has now been replaced with the standalone drawing procedure * macOSXDrawGroupBox, which draws a rounded rectangle with an appropriate * contrasting background color. * * Patterned backgrounds, which are now obsolete, should be aligned with the * coordinate system of the top-level window. Apparently failing to do this * used to cause graphics anomalies when drawing into an off-screen graphics * port. The code for handling this is currently commented out. */ static void FillElementDraw( void *clientData, void *elementRecord, Tk_Window tkwin, Drawable d, Ttk_Box b, Ttk_State state) { CGRect bounds = BoxToRect(d, b); BEGIN_DRAWING(d) #if MAC_OS_X_VERSION_MIN_REQUIRED > 10800 NSColorSpace *deviceRGB = [NSColorSpace deviceRGBColorSpace]; NSColor *bgColor; CGFloat fill[4]; GetBoxColor(dc.context, tkwin, 0, fill); bgColor = [NSColor colorWithColorSpace: deviceRGB components: fill count: 4]; CGContextSetFillColorSpace(dc.context, deviceRGB.CGColorSpace); CGContextSetFillColorWithColor(dc.context, bgColor.CGColor); CGContextFillRect(dc.context, bounds); #else ThemeBrush brush = (state & TTK_STATE_BACKGROUND) ? kThemeBrushModelessDialogBackgroundInactive : kThemeBrushModelessDialogBackgroundActive; ChkErr(HIThemeSetFill, brush, NULL, dc.context, HIOrientation); //QDSetPatternOrigin(PatternOrigin(tkwin, d)); CGContextFillRect(dc.context, bounds); #endif END_DRAWING } static void BackgroundElementDraw( void *clientData, void *elementRecord, Tk_Window tkwin, Drawable d, Ttk_Box b, unsigned int state) { FillElementDraw(clientData, elementRecord, tkwin, d, Ttk_WinBox(tkwin), state); } static Ttk_ElementSpec FillElementSpec = { TK_STYLE_VERSION_2, sizeof(NullElement), TtkNullElementOptions, TtkNullElementSize, FillElementDraw }; static Ttk_ElementSpec BackgroundElementSpec = { TK_STYLE_VERSION_2, sizeof(NullElement), TtkNullElementOptions, TtkNullElementSize, BackgroundElementDraw }; /*---------------------------------------------------------------------- * +++ ToolbarBackground element -- toolbar style for frames. * * This is very similar to the normal background element, but uses a * different ThemeBrush in order to get the lighter pinstripe effect * used in toolbars. We use SetThemeBackground() rather than * ApplyThemeBackground() in order to get the right style. * * <URL: http://developer.apple.com/documentation/Carbon/Reference/ * Appearance_Manager/appearance_manager/constant_7.html#/ * /apple_ref/doc/uid/TP30000243/C005321> * */ static void ToolbarBackgroundElementDraw( void *clientData, void *elementRecord, Tk_Window tkwin, Drawable d, Ttk_Box b, Ttk_State state) { ThemeBrush brush = kThemeBrushToolbarBackground; CGRect bounds = BoxToRect(d, Ttk_WinBox(tkwin)); BEGIN_DRAWING(d) ChkErr(HIThemeSetFill, brush, NULL, dc.context, HIOrientation); //QDSetPatternOrigin(PatternOrigin(tkwin, d)); CGContextFillRect(dc.context, bounds); END_DRAWING } static Ttk_ElementSpec ToolbarBackgroundElementSpec = { TK_STYLE_VERSION_2, sizeof(NullElement), TtkNullElementOptions, TtkNullElementSize, ToolbarBackgroundElementDraw }; /*---------------------------------------------------------------------- * +++ Treeview header * Redefine the header to use a kThemeListHeaderButton. */ #define TTK_TREEVIEW_STATE_SORTARROW TTK_STATE_USER1 static Ttk_StateTable TreeHeaderValueTable[] = { { kThemeButtonOn, TTK_STATE_ALTERNATE}, { kThemeButtonOn, TTK_STATE_SELECTED}, { kThemeButtonOff, 0} }; static Ttk_StateTable TreeHeaderAdornmentTable[] = { { kThemeAdornmentHeaderButtonSortUp, TTK_STATE_ALTERNATE|TTK_TREEVIEW_STATE_SORTARROW}, { kThemeAdornmentDefault, TTK_STATE_SELECTED|TTK_TREEVIEW_STATE_SORTARROW}, { kThemeAdornmentHeaderButtonNoSortArrow, TTK_STATE_ALTERNATE}, { kThemeAdornmentHeaderButtonNoSortArrow, TTK_STATE_SELECTED}, { kThemeAdornmentFocus, TTK_STATE_FOCUS}, { kThemeAdornmentNone, 0} }; static void TreeHeaderElementDraw( void *clientData, void *elementRecord, Tk_Window tkwin, Drawable d, Ttk_Box b, Ttk_State state) { ThemeButtonParams *params = clientData; CGRect bounds = BoxToRect(d, b); const HIThemeButtonDrawInfo info = { .version = 0, .state = Ttk_StateTableLookup(ThemeStateTable, state), .kind = params->kind, .value = Ttk_StateTableLookup(TreeHeaderValueTable, state), .adornment = Ttk_StateTableLookup(TreeHeaderAdornmentTable, state), }; BEGIN_DRAWING(d) ChkErr(HIThemeDrawButton, &bounds, &info, dc.context, HIOrientation, NULL); END_DRAWING } static Ttk_ElementSpec TreeHeaderElementSpec = { TK_STYLE_VERSION_2, sizeof(NullElement), TtkNullElementOptions, ButtonElementSizeNoPadding, TreeHeaderElementDraw }; /* * Disclosure triangle: */ #define TTK_TREEVIEW_STATE_OPEN TTK_STATE_USER1 #define TTK_TREEVIEW_STATE_LEAF TTK_STATE_USER2 static Ttk_StateTable DisclosureValueTable[] = { { kThemeDisclosureDown, TTK_TREEVIEW_STATE_OPEN, 0 }, { kThemeDisclosureRight, 0, 0 }, }; static void DisclosureElementSize( void *clientData, void *elementRecord, Tk_Window tkwin, int *widthPtr, int *heightPtr, Ttk_Padding *paddingPtr) { SInt32 s; ChkErr(GetThemeMetric, kThemeMetricDisclosureTriangleWidth, &s); *widthPtr = s; ChkErr(GetThemeMetric, kThemeMetricDisclosureTriangleHeight, &s); *heightPtr = s; } static void DisclosureElementDraw( void *clientData, void *elementRecord, Tk_Window tkwin, Drawable d, Ttk_Box b, Ttk_State state) { if (!(state & TTK_TREEVIEW_STATE_LEAF)) { CGRect bounds = BoxToRect(d, b); const HIThemeButtonDrawInfo info = { .version = 0, .state = Ttk_StateTableLookup(ThemeStateTable, state), .kind = kThemeDisclosureTriangle, .value = Ttk_StateTableLookup(DisclosureValueTable, state), .adornment = kThemeAdornmentDrawIndicatorOnly, }; BEGIN_DRAWING(d) ChkErr(HIThemeDrawButton, &bounds, &info, dc.context, HIOrientation, NULL); END_DRAWING } } static Ttk_ElementSpec DisclosureElementSpec = { TK_STYLE_VERSION_2, sizeof(NullElement), TtkNullElementOptions, DisclosureElementSize, DisclosureElementDraw }; /*---------------------------------------------------------------------- * +++ Widget layouts. */ TTK_BEGIN_LAYOUT_TABLE(LayoutTable) TTK_LAYOUT("Toolbar", TTK_NODE("Toolbar.background", TTK_FILL_BOTH)) TTK_LAYOUT("TButton", TTK_GROUP("Button.button", TTK_FILL_BOTH, TTK_GROUP("Button.padding", TTK_FILL_BOTH, TTK_NODE("Button.label", TTK_FILL_BOTH)))) TTK_LAYOUT("TRadiobutton", TTK_GROUP("Radiobutton.button", TTK_FILL_BOTH, TTK_GROUP("Radiobutton.padding", TTK_FILL_BOTH, TTK_NODE("Radiobutton.label", TTK_PACK_LEFT)))) TTK_LAYOUT("TCheckbutton", TTK_GROUP("Checkbutton.button", TTK_FILL_BOTH, TTK_GROUP("Checkbutton.padding", TTK_FILL_BOTH, TTK_NODE("Checkbutton.label", TTK_PACK_LEFT)))) TTK_LAYOUT("TMenubutton", TTK_GROUP("Menubutton.button", TTK_FILL_BOTH, TTK_GROUP("Menubutton.padding", TTK_FILL_BOTH, TTK_NODE("Menubutton.label", TTK_PACK_LEFT)))) TTK_LAYOUT("TCombobox", TTK_GROUP("Combobox.button", TTK_PACK_TOP|TTK_FILL_X, TTK_GROUP("Combobox.padding", TTK_FILL_BOTH, TTK_NODE("Combobox.textarea", TTK_FILL_X)))) /* Notebook tabs -- no focus ring */ TTK_LAYOUT("Tab", TTK_GROUP("Notebook.tab", TTK_FILL_BOTH, TTK_GROUP("Notebook.padding", TTK_EXPAND|TTK_FILL_BOTH, TTK_NODE("Notebook.label", TTK_EXPAND|TTK_FILL_BOTH)))) /* Progress bars -- track only */ TTK_LAYOUT("TSpinbox", TTK_NODE("Spinbox.spinbutton", TTK_PACK_RIGHT|TTK_STICK_E) TTK_GROUP("Spinbox.field", TTK_EXPAND|TTK_FILL_X, TTK_NODE("Spinbox.textarea", TTK_EXPAND|TTK_FILL_X))) TTK_LAYOUT("TProgressbar", TTK_NODE("Progressbar.track", TTK_EXPAND|TTK_FILL_BOTH)) /* Tree heading -- no border, fixed height */ TTK_LAYOUT("Heading", TTK_NODE("Treeheading.cell", TTK_FILL_X) TTK_NODE("Treeheading.image", TTK_PACK_RIGHT) TTK_NODE("Treeheading.text", 0)) /* Tree items -- omit focus ring */ TTK_LAYOUT("Item", TTK_GROUP("Treeitem.padding", TTK_FILL_BOTH, TTK_NODE("Treeitem.indicator", TTK_PACK_LEFT) TTK_NODE("Treeitem.image", TTK_PACK_LEFT) TTK_NODE("Treeitem.text", TTK_PACK_LEFT))) TTK_END_LAYOUT_TABLE /*---------------------------------------------------------------------- * +++ Initialization. */ static int AquaTheme_Init(Tcl_Interp *interp) { Ttk_Theme themePtr = Ttk_CreateTheme(interp, "aqua", NULL); if (!themePtr) { return TCL_ERROR; } /* * Elements: */ Ttk_RegisterElementSpec(themePtr, "background", &BackgroundElementSpec, 0); Ttk_RegisterElementSpec(themePtr, "fill", &FillElementSpec, 0); Ttk_RegisterElementSpec(themePtr, "Toolbar.background", &ToolbarBackgroundElementSpec, 0); Ttk_RegisterElementSpec(themePtr, "Button.button", &ButtonElementSpec, &PushButtonParams); Ttk_RegisterElementSpec(themePtr, "Checkbutton.button", &ButtonElementSpec, &CheckBoxParams); Ttk_RegisterElementSpec(themePtr, "Radiobutton.button", &ButtonElementSpec, &RadioButtonParams); Ttk_RegisterElementSpec(themePtr, "Toolbutton.border", &ButtonElementSpec, &BevelButtonParams); Ttk_RegisterElementSpec(themePtr, "Menubutton.button", &ButtonElementSpec, &PopupButtonParams); Ttk_RegisterElementSpec(themePtr, "Spinbox.spinbutton", &SpinButtonElementSpec, 0); Ttk_RegisterElementSpec(themePtr, "Combobox.button", &ComboboxElementSpec, 0); Ttk_RegisterElementSpec(themePtr, "Treeitem.indicator", &DisclosureElementSpec, &DisclosureParams); Ttk_RegisterElementSpec(themePtr, "Treeheading.cell", &TreeHeaderElementSpec, &ListHeaderParams); Ttk_RegisterElementSpec(themePtr, "Notebook.tab", &TabElementSpec, 0); Ttk_RegisterElementSpec(themePtr, "Notebook.client", &PaneElementSpec, 0); Ttk_RegisterElementSpec(themePtr, "Labelframe.border", &GroupElementSpec,0); Ttk_RegisterElementSpec(themePtr, "Entry.field", &EntryElementSpec,0); Ttk_RegisterElementSpec(themePtr, "Spinbox.field", &EntryElementSpec,0); Ttk_RegisterElementSpec(themePtr, "separator", &SeparatorElementSpec,0); Ttk_RegisterElementSpec(themePtr, "hseparator", &SeparatorElementSpec,0); Ttk_RegisterElementSpec(themePtr, "vseparator", &SeparatorElementSpec,0); Ttk_RegisterElementSpec(themePtr, "sizegrip", &SizegripElementSpec,0); /* * <<NOTE-TRACKS>> * The Progressbar widget adjusts the size of the pbar element. * In the Aqua theme, the appearance manager computes the bar geometry; * we do all the drawing in the ".track" element and leave the .pbar out. */ Ttk_RegisterElementSpec(themePtr,"Scale.trough", &TrackElementSpec, &ScaleData); Ttk_RegisterElementSpec(themePtr,"Scale.slider", &SliderElementSpec,0); Ttk_RegisterElementSpec(themePtr,"Progressbar.track", &PbarElementSpec, 0); /* * Layouts: */ Ttk_RegisterLayouts(themePtr, LayoutTable); Tcl_PkgProvide(interp, "ttk::theme::aqua", TTK_VERSION); return TCL_OK; } MODULE_SCOPE int Ttk_MacOSXPlatformInit(Tcl_Interp *interp) { return AquaTheme_Init(interp); } /* * Local Variables: * mode: objc * c-basic-offset: 4 * fill-column: 79 * coding: utf-8 * End: */
