/* * ttkMacOSXTheme.c -- * * Tk theme engine for Mac OSX, using the Appearance Manager API. * * Copyright © 2004 Joe English * Copyright © 2005 Neil Madden * Copyright © 2006-2009 Daniel A. Steffen * 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: * * * * 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" /* * Macros for handling drawing contexts. */ #define BEGIN_DRAWING(d) { \ TkMacOSXDrawingContext dc; \ if (!TkMacOSXSetupDrawingContext((d), NULL, &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 #define TTK_TREEVIEW_STATE_SORTARROW TTK_STATE_USER1 /* * Colors and gradients used in Dark Mode. */ static CGFloat darkButtonFace[4] = { 90.0 / 255, 86.0 / 255, 95.0 / 255, 1.0 }; static CGFloat darkPressedButtonFace[4] = { 114.0 / 255, 110.0 / 255, 118.0 / 255, 1.0 }; static CGFloat darkPressedBevelFace[4] = { 135.0 / 255, 136.0 / 255, 138.0 / 255, 1.0 }; static CGFloat darkSelectedBevelFace[4] = { 162.0 / 255, 163.0 / 255, 165.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 darkSelectedTab[4] = { 97.0 / 255, 94.0 / 255, 102.0 / 255, 1.0 }; static CGFloat darkTab[4] = { 44.0 / 255, 41.0 / 255, 50.0 / 255, 1.0 }; static CGFloat darkFocusRing[4] = { 38.0 / 255, 113.0 / 255, 159.0 / 255, 1.0 }; static CGFloat darkFocusRingTop[4] = { 50.0 / 255, 124.0 / 255, 171.0 / 255, 1.0 }; static CGFloat darkFocusRingBottom[4] = { 57.0 / 255, 130.0 / 255, 176.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 darkFrameTop[4] = {1.0, 1.0, 1.0, 0.0625}; static CGFloat darkFrameBottom[4] = {1.0, 1.0, 1.0, 0.125}; static CGFloat darkFrameAccent[4] = {0.0, 0.0, 0.0, 0.0625}; 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 }; static CGFloat pressedPushButtonGradient[8] = { 35.0 / 255, 123.0 / 255, 244.0 / 255, 1.0, 30.0 / 255, 114.0 / 255, 235.0 / 255, 1.0 }; /* * When building on systems earlier than 10.8 there is no reasonable way to * convert an NSColor to a CGColor. We do run-time checking of the OS version, * and never need the CGColor property on older systems, so we can use this * CGCOLOR macro, which evaluates to NULL without raising compiler warnings. * Similarly, we never draw rounded rectangles on older systems which did not * have CGPathCreateWithRoundedRect, so we just redefine it to return NULL. */ #if MAC_OS_X_VERSION_MAX_ALLOWED >= 1080 #define CGCOLOR(nscolor) (nscolor).CGColor #else #define CGCOLOR(nscolor) (0 ? (CGColorRef) (nscolor) : NULL) #define CGPathCreateWithRoundedRect(w, x, y, z) NULL #endif /* * If we try to draw a rounded rectangle with too large of a radius * CoreGraphics will raise a fatal exception. This macro returns if * the width or height is less than twice the radius. Presumably this * only happens when a widget has not yet been configured and has size * 1x1. */ #define CHECK_RADIUS(radius, bounds) \ if ((radius) > (bounds).size.width / 2 || (radius) > (bounds).size.height / 2) { \ return; \ } /*---------------------------------------------------------------------- * +++ 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[] = { {kThemeStateActive, TTK_STATE_ALTERNATE | TTK_STATE_BACKGROUND, 0}, {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} */ }; /*---------------------------------------------------------------------- * NormalizeButtonBounds -- * * Apple's Human Interface Guidelines only allow three specific heights * for most 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. * * The BevelButton is allowed to have arbitrary size, and also has * external padding. This is handled separately here. */ static CGRect NormalizeButtonBounds( SInt32 heightMetric, CGRect bounds) { SInt32 height; if (heightMetric != (SInt32) NoThemeMetric) { ChkErr(GetThemeMetric, heightMetric, &height); bounds.origin.y += (bounds.size.height - height) / 2; bounds.size.height = height; } return bounds; } /*---------------------------------------------------------------------- * +++ Backgrounds * * Support for contrasting background colors when GroupBoxes or Tabbed * panes are nested inside each other. Early versions of macOS used ridged * borders, so do not need contrasting backgrounds. */ /* * 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 const CGFloat WINDOWBACKGROUND[4] = { 235.0 / 255, 235.0 / 255, 235.0 / 255, 1.0 }; static const CGFloat WHITERGBA[4] = {1.0, 1.0, 1.0, 1.0}; static const CGFloat BLACKRGBA[4] = {0.0, 0.0, 0.0, 1.0}; /*---------------------------------------------------------------------- * GetBackgroundColor -- * * Fills the array rgba with the color coordinates for a background color. * Start with the background color of a window's geometry container, or * the standard ttk window background if there is no container. If the * contrast parameter is nonzero, modify this color to be darker, for the * aqua appearance, or lighter for the DarkAqua appearance. This is * primarily used by the Fill and Background elements. */ static void GetBackgroundColor( TCL_UNUSED(CGContextRef), Tk_Window tkwin, int contrast, CGFloat *rgba) { TkWindow *winPtr = (TkWindow *)tkwin; TkWindow *containerPtr = (TkWindow *)TkGetContainer(tkwin); while (containerPtr && containerPtr->privatePtr) { if (containerPtr->privatePtr->flags & TTK_HAS_CONTRASTING_BG) { break; } containerPtr = (TkWindow *)TkGetContainer(containerPtr); } if (containerPtr && containerPtr->privatePtr) { for (int i = 0; i < 4; i++) { rgba[i] = containerPtr->privatePtr->fillRGBA[i]; } } else { if ([NSApp macOSVersion] > 101300) { NSColorSpace *deviceRGB = [NSColorSpace deviceRGBColorSpace]; NSColor *windowColor = [[NSColor windowBackgroundColor] colorUsingColorSpace: deviceRGB]; [windowColor getComponents: rgba]; } else { for (int i = 0; i < 4; i++) { rgba[i] = WINDOWBACKGROUND[i]; } } } if (contrast) { int isDark = (rgba[0] + rgba[1] + rgba[2] < 1.5); if (isDark) { for (int i = 0; i < 3; i++) { rgba[i] += 8.0 / 255.0; } } else { for (int i = 0; i < 3; i++) { rgba[i] -= 8.0 / 255.0; } } if (winPtr->privatePtr) { winPtr->privatePtr->flags |= TTK_HAS_CONTRASTING_BG; for (int i = 0; i < 4; i++) { winPtr->privatePtr->fillRGBA[i] = rgba[i]; } } } } /*---------------------------------------------------------------------- * +++ Single Arrow Images -- * * Used in ListHeaders and Comboboxes as well as disclosure triangles in * macOS 11. */ static void DrawDownArrow( CGContextRef context, CGRect bounds, CGFloat inset, CGFloat size, const CGFloat *rgba) { CGFloat x, y; CGContextSetRGBStrokeColor(context, rgba[0], rgba[1], rgba[2], rgba[3]); CGContextSetLineWidth(context, 1.5); x = bounds.origin.x + inset; y = bounds.origin.y + trunc(bounds.size.height / 2) + 1; CGContextBeginPath(context); CGPoint arrow[3] = { {x, y - size / 4}, {x + size / 2, y + size / 4}, {x + size, y - size / 4} }; CGContextAddLines(context, arrow, 3); CGContextStrokePath(context); } static void DrawUpArrow( CGContextRef context, CGRect bounds, CGFloat inset, CGFloat size, const CGFloat *rgba) { CGFloat x, y; CGContextSetRGBStrokeColor(context, rgba[0], rgba[1], rgba[2], rgba[3]); CGContextSetLineWidth(context, 1.5); x = bounds.origin.x + inset; y = bounds.origin.y + trunc(bounds.size.height / 2); CGContextBeginPath(context); CGPoint arrow[3] = { {x, y + size / 4}, {x + size / 2, y - size / 4}, {x + size, y + size / 4} }; CGContextAddLines(context, arrow, 3); CGContextStrokePath(context); } static void DrawClosedDisclosure( CGContextRef context, CGRect bounds, CGFloat inset, CGFloat size, CGFloat *rgba) { CGFloat x, y; CGContextSetRGBStrokeColor(context, rgba[0], rgba[1], rgba[2], rgba[3]); CGContextSetLineWidth(context, 1.5); x = bounds.origin.x + inset; y = bounds.origin.y + trunc(bounds.size.height / 2); CGContextBeginPath(context); CGPoint arrow[3] = { {x, y - size / 4 - 1}, {x + size / 2, y}, {x, y + size / 4 + 1} }; CGContextAddLines(context, arrow, 3); CGContextStrokePath(context); } static void DrawOpenDisclosure( CGContextRef context, CGRect bounds, CGFloat inset, CGFloat size, CGFloat *rgba) { CGFloat x, y; CGContextSetRGBStrokeColor(context, rgba[0], rgba[1], rgba[2], rgba[3]); CGContextSetLineWidth(context, 1.5); x = bounds.origin.x + inset; y = bounds.origin.y + trunc(bounds.size.height / 2); CGContextBeginPath(context); CGPoint arrow[3] = { {x, y - size / 4}, {x + size / 2, y + size / 2}, {x + size, y - size / 4} }; CGContextAddLines(context, arrow, 3); CGContextStrokePath(context); } /*---------------------------------------------------------------------- * +++ Double Arrow Buttons -- * * Used in MenuButtons and SpinButtons. */ static void DrawUpDownArrows( CGContextRef context, CGRect bounds, CGFloat inset, CGFloat size, const CGFloat *rgba) { CGFloat x, y; CGContextSetRGBStrokeColor(context, rgba[0], rgba[1], rgba[2], rgba[3]); CGContextSetLineWidth(context, 1.5); x = bounds.origin.x + inset; y = bounds.origin.y + trunc(bounds.size.height / 2); CGContextBeginPath(context); CGPoint bottomArrow[3] = {{x, y + 2}, {x + size / 2, y + 2 + size / 2}, {x + size, y + 2}}; CGContextAddLines(context, bottomArrow, 3); CGPoint topArrow[3] = {{x, y - 2}, {x + size / 2, y - 2 - size / 2}, {x + size, y - 2}}; CGContextAddLines(context, topArrow, 3); CGContextStrokePath(context); } /*---------------------------------------------------------------------- * +++ FillButtonBackground -- * * Fills a rounded rectangle with a transparent black gradient. * This is a no-op if building on 10.8 or older. */ static void FillButtonBackground( CGContextRef context, CGRect bounds, CGFloat radius) { CHECK_RADIUS(radius, bounds) 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); } /*---------------------------------------------------------------------- * DrawGroupBox -- * * This is a standalone drawing procedure which draws the contrasting * rounded rectangular box for LabelFrames and Notebook panes used in * more recent versions of macOS. */ static void DrawGroupBox( CGRect bounds, CGContextRef context, Tk_Window tkwin) { CHECK_RADIUS(4, bounds) CGPathRef path; NSColorSpace *deviceRGB = [NSColorSpace deviceRGBColorSpace]; NSColor *borderColor, *bgColor; static CGFloat border[4] = {1.0, 1.0, 1.0, 0.25}; CGFloat fill[4]; GetBackgroundColor(context, tkwin, 1, fill); bgColor = [NSColor colorWithColorSpace: deviceRGB components: fill count: 4]; CGContextSetFillColorSpace(context, deviceRGB.CGColorSpace); CGContextSetFillColorWithColor(context, CGCOLOR(bgColor)); 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, CGCOLOR(borderColor)); [borderColor getComponents: fill]; CGContextSetRGBFillColor(context, fill[0], fill[1], fill[2], fill[3]); CGContextBeginPath(context); CGContextAddPath(context, path); CGContextReplacePathWithStrokedPath(context); CGContextFillPath(context); CFRelease(path); } /*---------------------------------------------------------------------- * SolidFillRoundedRectangle -- * * Fill a rounded rectangle with a specified solid color. */ static void SolidFillRoundedRectangle( CGContextRef context, CGRect bounds, CGFloat radius, NSColor *color) { CGPathRef path; CHECK_RADIUS(radius, bounds) path = CGPathCreateWithRoundedRect(bounds, radius, radius, NULL); if (!path) { return; } CGContextSetFillColorWithColor(context, CGCOLOR(color)); CGContextBeginPath(context); CGContextAddPath(context, path); CGContextFillPath(context); CFRelease(path); } /*---------------------------------------------------------------------- * +++ DrawListHeader -- * * This is a standalone drawing procedure which draws column headers for * a Treeview in the Aqua appearance. The HITheme headers have not * matched the native ones since OSX 10.8. Note that the header image is * ignored, but we draw arrows according to the state. */ static void DrawListHeader( CGRect bounds, CGContextRef context, Tk_Window tkwin, int state) { NSColorSpace *deviceRGB = [NSColorSpace deviceRGBColorSpace]; NSColor *strokeColor, *bgColor; static CGFloat borderRGBA[4] = { 200.0 / 255, 200.0 / 255, 200.0 / 255, 1.0 }; static CGFloat separatorRGBA[4] = { 220.0 / 255, 220.0 / 255, 220.0 / 255, 1.0 }; static CGFloat activeBgRGBA[4] = { 238.0 / 255, 238.0 / 255, 238.0 / 255, 1.0 }; static CGFloat inactiveBgRGBA[4] = { 246.0 / 255, 246.0 / 255, 246.0 / 255, 1.0 }; /* * Apple changes the background of a list header when the window is not * active. But Ttk does not indicate that in the state of a TreeHeader. * So we have to query the Apple window manager. */ NSWindow *win = TkMacOSXGetNSWindowForDrawable(Tk_WindowId(tkwin)); CGFloat *bgRGBA = [win isKeyWindow] ? activeBgRGBA : inactiveBgRGBA; CGFloat x = bounds.origin.x, y = bounds.origin.y; CGFloat w = bounds.size.width, h = bounds.size.height; CGPoint top[2] = {{x, y + 1}, {x + w, y + 1}}; CGPoint bottom[2] = {{x, y + h}, {x + w, y + h}}; CGPoint separator[2] = {{x + w - 1, y + 3}, {x + w - 1, y + h - 3}}; bgColor = [NSColor colorWithColorSpace: deviceRGB components: bgRGBA count: 4]; CGContextSaveGState(context); CGContextSetShouldAntialias(context, false); CGContextSetFillColorSpace(context, deviceRGB.CGColorSpace); CGContextSetStrokeColorSpace(context, deviceRGB.CGColorSpace); CGContextBeginPath(context); CGContextSetFillColorWithColor(context, CGCOLOR(bgColor)); CGContextAddRect(context, bounds); CGContextFillPath(context); strokeColor = [NSColor colorWithColorSpace: deviceRGB components: separatorRGBA count: 4]; CGContextSetStrokeColorWithColor(context, CGCOLOR(strokeColor)); CGContextAddLines(context, separator, 2); CGContextStrokePath(context); strokeColor = [NSColor colorWithColorSpace: deviceRGB components: borderRGBA count: 4]; CGContextSetStrokeColorWithColor(context, CGCOLOR(strokeColor)); CGContextAddLines(context, top, 2); CGContextStrokePath(context); CGContextAddLines(context, bottom, 2); CGContextStrokePath(context); CGContextRestoreGState(context); if (state & TTK_TREEVIEW_STATE_SORTARROW) { CGRect arrowBounds = bounds; arrowBounds.origin.x = bounds.origin.x + bounds.size.width - 16; arrowBounds.size.width = 16; if (state & TTK_STATE_ALTERNATE) { DrawUpArrow(context, arrowBounds, 3, 8, BLACKRGBA); } else if (state & TTK_STATE_SELECTED) { DrawDownArrow(context, arrowBounds, 3, 8, BLACKRGBA); } } } /*---------------------------------------------------------------------- * +++ 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. * * Drawing the dark widgets requires NSColors that were introduced in OSX * 10.14, so we make some of these functions be no-ops when building on * systems older than 10.14. */ /*---------------------------------------------------------------------- * GradientFillRoundedRectangle -- * * Fill a rounded rectangle with a specified gradient. */ static void GradientFillRoundedRectangle( CGContextRef context, CGRect bounds, CGFloat radius, CGFloat *colors, int numColors) { NSColorSpace *deviceRGB = [NSColorSpace deviceRGBColorSpace]; CGPathRef path; CHECK_RADIUS(radius, bounds) 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)) { if ([NSApp macOSVersion] < 120000) { GradientFillRoundedRectangle(context, bounds, 4, pressedPushButtonGradient, 2); } else { faceColor = [NSColor colorWithColorSpace: deviceRGB components: darkPressedButtonFace count: 4]; SolidFillRoundedRectangle(context, bounds, 4, faceColor); } } else if (kind == kThemePushButton && (state & TTK_STATE_ALTERNATE) && !(state & TTK_STATE_BACKGROUND)) { GradientFillRoundedRectangle(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]; } SolidFillRoundedRectangle(context, bounds, 4, faceColor); } /* * If this is a popup, draw the arrow button. */ if ((kind == kThemePopupButton) | (kind == kThemeComboBox)) { CGRect arrowBounds = bounds; arrowBounds.size.width = 16; arrowBounds.origin.x += bounds.size.width - 16; /* * If the toplevel is front, paint the button blue. */ if (!(state & TTK_STATE_BACKGROUND) && !(state & TTK_STATE_DISABLED)) { GradientFillRoundedRectangle(context, arrowBounds, 4, darkSelectedGradient, 2); } if (kind == kThemePopupButton) { DrawUpDownArrows(context, arrowBounds, 3, 7, WHITERGBA); } else { DrawDownArrow(context, arrowBounds, 4, 8, WHITERGBA); } } HighlightButtonBorder(context, bounds); } /*---------------------------------------------------------------------- * +++ DrawDarkIncDecButton -- * * This is a standalone drawing procedure which draws an IncDecButton * (as used in a Spinbox) in the Dark Mode style. */ static void DrawDarkIncDecButton( CGRect bounds, ThemeDrawState drawState, Ttk_State state, CGContextRef context) { NSColorSpace *deviceRGB = [NSColorSpace deviceRGBColorSpace]; NSColor *faceColor; bounds = CGRectInset(bounds, 0, -1); CGContextClipToRect(context, bounds); FillButtonBackground(context, bounds, 6); /* * Fill the button face with the appropriate color. */ bounds = CGRectInset(bounds, 1, 1); if (state & TTK_STATE_DISABLED) { faceColor = [NSColor colorWithColorSpace: deviceRGB components: darkDisabledButtonFace count: 4]; } else { faceColor = [NSColor colorWithColorSpace: deviceRGB components: darkButtonFace count: 4]; } SolidFillRoundedRectangle(context, bounds, 4, faceColor); /* * If pressed, paint the appropriate half blue. */ if (state & TTK_STATE_PRESSED) { CGRect clip = bounds; clip.size.height /= 2; CGContextSaveGState(context); if (drawState == kThemeStatePressedDown) { clip.origin.y += clip.size.height; } CGContextClipToRect(context, clip); GradientFillRoundedRectangle(context, bounds, 5, darkSelectedGradient, 2); CGContextRestoreGState(context); } DrawUpDownArrows(context, bounds, 3, 5, WHITERGBA); HighlightButtonBorder(context, bounds); } /*---------------------------------------------------------------------- * +++ DrawDarkBevelButton -- * * This is a standalone drawing procedure which draws RoundedBevelButtons * in the Dark Mode style. */ static void DrawDarkBevelButton( CGRect bounds, Ttk_State state, CGContextRef context) { NSColorSpace *deviceRGB = [NSColorSpace deviceRGBColorSpace]; NSColor *faceColor; CGContextClipToRect(context, bounds); FillButtonBackground(context, bounds, 5); /* * Fill the button face with the appropriate color. */ bounds = CGRectInset(bounds, 1, 1); if (state & TTK_STATE_PRESSED) { faceColor = [NSColor colorWithColorSpace: deviceRGB components: darkPressedBevelFace count: 4]; } else if ((state & TTK_STATE_DISABLED) || (state & TTK_STATE_ALTERNATE)) { faceColor = [NSColor colorWithColorSpace: deviceRGB components: darkDisabledButtonFace count: 4]; } else if (state & TTK_STATE_SELECTED) { faceColor = [NSColor colorWithColorSpace: deviceRGB components: darkSelectedBevelFace count: 4]; } else { faceColor = [NSColor colorWithColorSpace: deviceRGB components: darkButtonFace count: 4]; } SolidFillRoundedRectangle(context, bounds, 4, faceColor); 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))) { GradientFillRoundedRectangle(context, bounds, 3, darkSelectedGradient, 2); } else { GradientFillRoundedRectangle(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, CGCOLOR(stroke)); } 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))) { GradientFillRoundedRectangle(context, bounds, 8, darkSelectedGradient, 2); } else { GradientFillRoundedRectangle(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, CGCOLOR(fill)); } 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; int OSVersion = [NSApp macOSVersion]; CGContextSetLineWidth(context, 1.0); CGContextClipToRect(context, bounds); /* * Extend the bounds to one or both sides so the rounded part will be * clipped off. */ if (OSVersion < 110000 || !(state & TTK_STATE_SELECTED)) { 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) { if (OSVersion < 110000) { faceColor = [NSColor colorWithColorSpace: deviceRGB components: darkDisabledButtonFace count: 4]; } else { faceColor = [NSColor colorWithColorSpace: deviceRGB components: darkTab count: 4]; } } else { if (OSVersion < 110000) { faceColor = [NSColor colorWithColorSpace: deviceRGB components: darkButtonFace count: 4]; } else { faceColor = [NSColor colorWithColorSpace: deviceRGB components: darkTab count: 4]; } } SolidFillRoundedRectangle(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, CGCOLOR(stroke)); 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 with the current accent color. * 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)) { if (OSVersion < 110000) { GradientFillRoundedRectangle(context, bounds, 4, darkSelectedGradient, 2); } else { faceColor = [NSColor colorWithColorSpace: deviceRGB components: darkSelectedTab count: 4]; SolidFillRoundedRectangle(context, bounds, 4, faceColor); } } else { if (OSVersion < 110000) { faceColor = [NSColor colorWithColorSpace: deviceRGB components: darkInactiveSelectedTab count: 4]; } else { faceColor = [NSColor colorWithColorSpace: deviceRGB components: darkSelectedTab count: 4]; } SolidFillRoundedRectangle(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, TCL_UNUSED(Tk_Window)) { 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, CGCOLOR(fillColor)); CGContextFillRect(context, bounds); } /*---------------------------------------------------------------------- * +++ DrawDarkFocusRing -- * * This is a standalone drawing procedure which draws a focus ring around * an Entry widget in Dark Mode. */ static void DrawDarkFocusRing( CGRect bounds, CGContextRef context) { CGRect insetBounds = CGRectInset(bounds, -3, -3); CHECK_RADIUS(4, insetBounds) NSColorSpace *deviceRGB = [NSColorSpace deviceRGBColorSpace]; NSColor *strokeColor; NSColor *fillColor = [NSColor colorWithColorSpace:deviceRGB components:darkFocusRing count:4]; CGFloat x = bounds.origin.x, y = bounds.origin.y; CGFloat w = bounds.size.width, h = bounds.size.height; CGPoint topPart[4] = { {x, y + h}, {x, y + 1}, {x + w - 1, y + 1}, {x + w - 1, y + h} }; CGPoint bottom[2] = {{x, y + h}, {x + w, y + h}}; CGContextSaveGState(context); CGContextSetShouldAntialias(context, false); CGContextBeginPath(context); strokeColor = [NSColor colorWithColorSpace: deviceRGB components: darkFocusRingTop count: 4]; CGContextSetStrokeColorWithColor(context, CGCOLOR(strokeColor)); CGContextAddLines(context, topPart, 4); CGContextStrokePath(context); strokeColor = [NSColor colorWithColorSpace: deviceRGB components: darkFocusRingBottom count: 4]; CGContextSetStrokeColorWithColor(context, CGCOLOR(strokeColor)); CGContextAddLines(context, bottom, 2); CGContextStrokePath(context); CGContextSetShouldAntialias(context, true); CGContextSetFillColorWithColor(context, CGCOLOR(fillColor)); CGPathRef path = CGPathCreateWithRoundedRect(insetBounds, 4, 4, NULL); CGContextBeginPath(context); CGContextAddPath(context, path); CGPathRelease(path); CGContextAddRect(context, bounds); CGContextEOFillPath(context); CGContextRestoreGState(context); } /*---------------------------------------------------------------------- * +++ DrawDarkFrame -- * * This is a standalone drawing procedure which draws various * types of borders in Dark Mode. */ static void DrawDarkFrame( CGRect bounds, CGContextRef context, HIThemeFrameKind kind) { NSColorSpace *deviceRGB = [NSColorSpace deviceRGBColorSpace]; NSColor *stroke; CGContextSetStrokeColorSpace(context, deviceRGB.CGColorSpace); CGFloat x = bounds.origin.x, y = bounds.origin.y; CGFloat w = bounds.size.width, h = bounds.size.height; CGPoint topPart[4] = { {x, y + h - 1}, {x, y + 1}, {x + w, y + 1}, {x + w, y + h - 1} }; CGPoint bottom[2] = {{x, y + h}, {x + w, y + h}}; CGPoint accent[2] = {{x, y + 1}, {x + w, y + 1}}; switch (kind) { case kHIThemeFrameTextFieldSquare: CGContextSaveGState(context); CGContextSetShouldAntialias(context, false); CGContextBeginPath(context); stroke = [NSColor colorWithColorSpace: deviceRGB components: darkFrameTop count: 4]; CGContextSetStrokeColorWithColor(context, CGCOLOR(stroke)); CGContextAddLines(context, topPart, 4); CGContextStrokePath(context); stroke = [NSColor colorWithColorSpace: deviceRGB components: darkFrameBottom count: 4]; CGContextSetStrokeColorWithColor(context, CGCOLOR(stroke)); CGContextAddLines(context, bottom, 2); CGContextStrokePath(context); stroke = [NSColor colorWithColorSpace: deviceRGB components: darkFrameAccent count: 4]; CGContextSetStrokeColorWithColor(context, CGCOLOR(stroke)); CGContextAddLines(context, accent, 2); CGContextStrokePath(context); CGContextRestoreGState(context); break; default: break; } } /*---------------------------------------------------------------------- * +++ DrawListHeader -- * * This is a standalone drawing procedure which draws column * headers for a Treeview in the Dark Mode. */ static void DrawDarkListHeader( CGRect bounds, CGContextRef context, TCL_UNUSED(Tk_Window), int state) { NSColorSpace *deviceRGB = [NSColorSpace deviceRGBColorSpace]; NSColor *stroke; CGContextSetStrokeColorSpace(context, deviceRGB.CGColorSpace); CGFloat x = bounds.origin.x, y = bounds.origin.y; CGFloat w = bounds.size.width, h = bounds.size.height; CGPoint top[2] = {{x, y + 1}, {x + w, y + 1}}; CGPoint bottom[2] = {{x, y + h}, {x + w, y + h}}; CGPoint separator[2] = {{x + w - 1, y + 3}, {x + w - 1, y + h - 3}}; CGContextSaveGState(context); CGContextSetShouldAntialias(context, false); stroke = [NSColor colorWithColorSpace: deviceRGB components: darkFrameBottom count: 4]; CGContextSetStrokeColorWithColor(context, CGCOLOR(stroke)); CGContextBeginPath(context); CGContextAddLines(context, top, 2); CGContextStrokePath(context); CGContextAddLines(context, bottom, 2); CGContextStrokePath(context); CGContextAddLines(context, separator, 2); CGContextStrokePath(context); CGContextRestoreGState(context); if (state & TTK_TREEVIEW_STATE_SORTARROW) { CGRect arrowBounds = bounds; arrowBounds.origin.x = bounds.origin.x + bounds.size.width - 16; arrowBounds.size.width = 16; if (state & TTK_STATE_ALTERNATE) { DrawUpArrow(context, arrowBounds, 3, 8, WHITERGBA); } else if (state & TTK_STATE_SELECTED) { DrawDownArrow(context, arrowBounds, 3, 8, WHITERGBA); } } } /*---------------------------------------------------------------------- * +++ Button element: Used for elements drawn with DrawThemeButton. */ /* * When Ttk draws the various types of buttons, a pointer to one of these * is passed as the clientData. */ typedef struct { ThemeButtonKind kind; ThemeMetric heightMetric; } ThemeButtonParams; static ThemeButtonParams PushButtonParams = {kThemePushButton, kThemeMetricPushButtonHeight}, CheckBoxParams = {kThemeCheckBox, kThemeMetricCheckBoxHeight}, RadioButtonParams = {kThemeRadioButton, kThemeMetricRadioButtonHeight}, BevelButtonParams = {kThemeRoundedBevelButton, NoThemeMetric}, PopupButtonParams = {kThemePopupButton, kThemeMetricPopupButtonHeight}, DisclosureParams = { kThemeDisclosureButton, kThemeMetricDisclosureTriangleHeight }, ListHeaderParams = {kThemeListHeaderButton, kThemeMetricListHeaderHeight}; static Ttk_StateTable ButtonValueTable[] = { {kThemeButtonOff, TTK_STATE_ALTERNATE | TTK_STATE_BACKGROUND, 0}, {kThemeButtonMixed, TTK_STATE_ALTERNATE, 0}, {kThemeButtonOn, TTK_STATE_SELECTED, 0}, {kThemeButtonOff, 0, 0} /* * Others: kThemeDisclosureRight, kThemeDisclosureDown, * kThemeDisclosureLeft */ }; static Ttk_StateTable ButtonAdornmentTable[] = { {kThemeAdornmentNone, TTK_STATE_ALTERNATE | TTK_STATE_BACKGROUND, 0}, {kThemeAdornmentDefault | kThemeAdornmentFocus, TTK_STATE_ALTERNATE | TTK_STATE_FOCUS, 0}, {kThemeAdornmentFocus, TTK_STATE_FOCUS, 0}, {kThemeAdornmentDefault, TTK_STATE_ALTERNATE, 0}, {kThemeAdornmentNone, 0, 0} }; /*---------------------------------------------------------------------- * +++ computeButtonDrawInfo -- * * Fill in an appearance manager HIThemeButtonDrawInfo record. */ static inline HIThemeButtonDrawInfo computeButtonDrawInfo( ThemeButtonParams *params, Ttk_State state, TCL_UNUSED(Tk_Window)) { /* * See ButtonElementDraw for the explanation of why we always draw * PushButtons in the active state. */ SInt32 HIThemeState; HIThemeState = Ttk_StateTableLookup(ThemeStateTable, state); switch (params->kind) { case kThemePushButton: HIThemeState &= ~kThemeStateInactive; HIThemeState |= kThemeStateActive; break; default: 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; } /*---------------------------------------------------------------------- * +++ Button elements. */ static void ButtonElementMinSize( void *clientData, int *minWidth, int *minHeight) { ThemeButtonParams *params = (ThemeButtonParams *)clientData; if (params->heightMetric != NoThemeMetric) { ChkErr(GetThemeMetric, params->heightMetric, (SInt *) minHeight); /* * The theme height does not include the 1-pixel border around * the button, although it does include the 1-pixel shadow at * the bottom. */ *minHeight += 2; /* * For buttons with labels the minwidth must be 0 to force the * correct text layout. For example, a non-zero value will cause the * text to be left justified, no matter what -anchor setting is used in * the style. */ *minWidth = 0; } } static void ButtonElementSize( void *clientData, TCL_UNUSED(void *), /* elementRecord */ Tk_Window tkwin, int *minWidth, int *minHeight, Ttk_Padding *paddingPtr) { ThemeButtonParams *params = (ThemeButtonParams *)clientData; const HIThemeButtonDrawInfo info = computeButtonDrawInfo(params, 0, tkwin); static const CGRect scratchBounds = {{0, 0}, {100, 100}}; CGRect contentBounds, backgroundBounds; int verticalPad; ButtonElementMinSize(clientData, minWidth, minHeight); /* * Given a hypothetical bounding rectangle for a button, HIToolbox will * compute a bounding rectangle for the button contents and a bounding * rectangle for the button background. The background bounds are large * enough to contain the image of the button in any state, which might * include highlight borders, shadows, etc. The content rectangle is not * centered vertically within the background rectangle, presumably because * shadows only appear on the bottom. Nonetheless, when HITools is asked * to draw a button with a certain bounding rectangle it draws the button * centered within the rectangle. * * To compute the effective padding around a button we request the * content and bounding rectangles for a 100x100 button and use the * padding between those. However, we symmetrize the padding on the * top and bottom, because that is how the button will be drawn. */ ChkErr(HIThemeGetButtonContentBounds, &scratchBounds, &info, &contentBounds); ChkErr(HIThemeGetButtonBackgroundBounds, &scratchBounds, &info, &backgroundBounds); paddingPtr->left = contentBounds.origin.x - backgroundBounds.origin.x; paddingPtr->right = CGRectGetMaxX(backgroundBounds) - CGRectGetMaxX(contentBounds); verticalPad = backgroundBounds.size.height - contentBounds.size.height; paddingPtr->top = paddingPtr->bottom = verticalPad / 2; } static void ButtonElementDraw( void *clientData, TCL_UNUSED(void *), /* elementRecord */ Tk_Window tkwin, Drawable d, Ttk_Box b, Ttk_State state) { ThemeButtonParams *params = (ThemeButtonParams *)clientData; CGRect bounds = BoxToRect(d, b); HIThemeButtonDrawInfo info = computeButtonDrawInfo(params, state, tkwin); bounds = NormalizeButtonBounds(params->heightMetric, bounds); BEGIN_DRAWING(d) if (TkMacOSXInDarkMode(tkwin)) { 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; case kThemeRoundedBevelButton: DrawDarkBevelButton(bounds, state, dc.context); break; default: ChkErr(HIThemeDrawButton, &bounds, &info, dc.context, HIOrientation, NULL); } } else if (info.kind == kThemePushButton && (state & TTK_STATE_PRESSED)) { bounds.size.height += 2; if ([NSApp macOSVersion] > 100800) { GradientFillRoundedRectangle(dc.context, bounds, 4, pressedPushButtonGradient, 2); } } else { /* * Apple's PushButton and PopupButton do not change their fill color * when the window is inactive. However, 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 text background color before asking * HIToolbox to draw it. For PushButtons, we simply draw them in the * active state. */ if (info.kind == kThemePopupButton && (state & TTK_STATE_BACKGROUND)) { CGRect innerBounds = CGRectInset(bounds, 1, 1); NSColor *whiteRGBA = [NSColor whiteColor]; SolidFillRoundedRectangle(dc.context, innerBounds, 4, whiteRGBA); } /* * A BevelButton with mixed value is drawn borderless, which does make * much sense for us. */ if (info.kind == kThemeRoundedBevelButton && info.value == kThemeButtonMixed) { info.value = kThemeButtonOff; info.state = kThemeStateInactive; } 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, 0}, {kThemeTabNonFrontInactive, TTK_STATE_BACKGROUND, 0}, {kThemeTabFrontUnavailable, TTK_STATE_DISABLED | TTK_STATE_SELECTED, 0}, {kThemeTabNonFrontUnavailable, TTK_STATE_DISABLED, 0}, {kThemeTabFront, TTK_STATE_SELECTED, 0}, {kThemeTabNonFrontPressed, TTK_STATE_PRESSED, 0}, {kThemeTabNonFront, 0, 0} }; static Ttk_StateTable TabAdornmentTable[] = { {kHIThemeTabAdornmentNone, TTK_STATE_FIRST_TAB | TTK_STATE_LAST_TAB, 0}, {kHIThemeTabAdornmentTrailingSeparator, TTK_STATE_FIRST_TAB, 0}, {kHIThemeTabAdornmentNone, TTK_STATE_LAST_TAB, 0}, {kHIThemeTabAdornmentTrailingSeparator, 0, 0}, }; static Ttk_StateTable TabPositionTable[] = { {kHIThemeTabPositionOnly, TTK_STATE_FIRST_TAB | TTK_STATE_LAST_TAB, 0}, {kHIThemeTabPositionFirst, TTK_STATE_FIRST_TAB, 0}, {kHIThemeTabPositionLast, TTK_STATE_LAST_TAB, 0}, {kHIThemeTabPositionMiddle, 0, 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. * * */ static void TabElementSize( TCL_UNUSED(void *), /* clientData */ TCL_UNUSED(void *), /* elementRecord */ TCL_UNUSED(Tk_Window), /* tkwin */ TCL_UNUSED(int *), /* minWidth */ int *minHeight, Ttk_Padding *paddingPtr) { GetThemeMetric(kThemeMetricLargeTabHeight, (SInt32 *) minHeight); *paddingPtr = Ttk_MakePadding(0, 0, 0, 2); } static void TabElementDraw( TCL_UNUSED(void *), /* clientData */ TCL_UNUSED(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 (TkMacOSXInDarkMode(tkwin)) { DrawDarkTab(bounds, state, dc.context); } else { ChkErr(HIThemeDrawTab, &bounds, &info, dc.context, HIOrientation, NULL); } END_DRAWING } static Ttk_ElementSpec TabElementSpec = { TK_STYLE_VERSION_2, sizeof(NullElement), TtkNullElementOptions, TabElementSize, TabElementDraw }; /* * Notebook panes: */ static void PaneElementSize( TCL_UNUSED(void *), /* clientData */ TCL_UNUSED(void *), /* elementRecord */ TCL_UNUSED(Tk_Window), /* tkwin */ TCL_UNUSED(int *), /* minWidth */ TCL_UNUSED(int *), /* minHeight */ Ttk_Padding *paddingPtr) { *paddingPtr = Ttk_MakePadding(9, 5, 9, 9); } static void PaneElementDraw( TCL_UNUSED(void *), /* clientData */ TCL_UNUSED(void *), /* elementRecord */ Tk_Window tkwin, Drawable d, Ttk_Box b, Ttk_State state) { CGRect bounds = BoxToRect(d, b); bounds.origin.y -= kThemeMetricTabFrameOverlap; bounds.size.height += kThemeMetricTabFrameOverlap; BEGIN_DRAWING(d) if ([NSApp macOSVersion] > 100800) { 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); } END_DRAWING } static Ttk_ElementSpec PaneElementSpec = { TK_STYLE_VERSION_2, sizeof(NullElement), TtkNullElementOptions, PaneElementSize, PaneElementDraw }; /*---------------------------------------------------------------------- * +++ Labelframe elements -- * * 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( TCL_UNUSED(void *), /* clientData */ TCL_UNUSED(void *), /* elementRecord */ TCL_UNUSED(Tk_Window), /* tkwin */ TCL_UNUSED(int *), /* minWidth */ TCL_UNUSED(int *), /* minHeight */ Ttk_Padding *paddingPtr) { *paddingPtr = Ttk_UniformPadding(4); } static void GroupElementDraw( TCL_UNUSED(void *), /* clientData */ TCL_UNUSED(void *), /* elementRecord */ Tk_Window tkwin, Drawable d, Ttk_Box b, Ttk_State state) { CGRect bounds = BoxToRect(d, b); BEGIN_DRAWING(d) if ([NSApp macOSVersion] > 100800) { 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); } END_DRAWING } static Ttk_ElementSpec GroupElementSpec = { TK_STYLE_VERSION_2, sizeof(NullElement), TtkNullElementOptions, GroupElementSize, GroupElementDraw }; /*---------------------------------------------------------------------- * +++ Entry elements -- * * 3 pixels padding for focus rectangle * 2 pixels padding for EditTextFrame */ typedef struct { Tcl_Obj *backgroundObj; Tcl_Obj *fieldbackgroundObj; } EntryElement; #define ENTRY_DEFAULT_BACKGROUND "systemTextBackgroundColor" static Ttk_ElementOptionSpec EntryElementOptions[] = { {"-background", TK_OPTION_BORDER, Tk_Offset(EntryElement, backgroundObj), ENTRY_DEFAULT_BACKGROUND}, {"-fieldbackground", TK_OPTION_BORDER, Tk_Offset(EntryElement, fieldbackgroundObj), ENTRY_DEFAULT_BACKGROUND}, {NULL, TK_OPTION_BOOLEAN, 0, NULL} }; static void EntryElementSize( TCL_UNUSED(void *), /* clientData */ TCL_UNUSED(void *), /* elementRecord */ TCL_UNUSED(Tk_Window), /* tkwin */ TCL_UNUSED(int *), /* minWidth */ TCL_UNUSED(int *), /* minHeight */ Ttk_Padding *paddingPtr) { *paddingPtr = Ttk_MakePadding(7, 5, 7, 6); } static void EntryElementDraw( TCL_UNUSED(void *), void *elementRecord, Tk_Window tkwin, Drawable d, Ttk_Box b, Ttk_State state) { EntryElement *e = (EntryElement *)elementRecord; Ttk_Box inner = Ttk_PadBox(b, Ttk_UniformPadding(3)); CGRect bounds = BoxToRect(d, inner); NSColor *background; Tk_3DBorder backgroundPtr = NULL; static const char *defaultBG = ENTRY_DEFAULT_BACKGROUND; if (TkMacOSXInDarkMode(tkwin)) { BEGIN_DRAWING(d) NSColorSpace *deviceRGB = [NSColorSpace deviceRGBColorSpace]; CGFloat fill[4]; GetBackgroundColor(dc.context, tkwin, 1, fill); /* * Lighten the background to provide contrast. */ for (int i = 0; i < 3; i++) { fill[i] += 9.0 / 255.0; } background = [NSColor colorWithColorSpace: deviceRGB components: fill count: 4]; CGContextSetFillColorWithColor(dc.context, CGCOLOR(background)); CGContextFillRect(dc.context, bounds); if (state & TTK_STATE_FOCUS) { DrawDarkFocusRing(bounds, dc.context); } else { DrawDarkFrame(bounds, dc.context, kHIThemeFrameTextFieldSquare); } END_DRAWING } else { const HIThemeFrameDrawInfo info = { .version = 0, .kind = kHIThemeFrameTextFieldSquare, .state = Ttk_StateTableLookup(ThemeStateTable, state), .isFocused = state & TTK_STATE_FOCUS, }; /* * Earlier versions of the Aqua theme ignored the -fieldbackground * option and used the -background as if it were -fieldbackground. * Here we are enabling -fieldbackground. For backwards * compatibility, if -fieldbackground is set to the default color and * -background is set to a different color then we use -background as * -fieldbackground. */ if (0 != strcmp(Tcl_GetString(e->fieldbackgroundObj), defaultBG)) { backgroundPtr = Tk_Get3DBorderFromObj(tkwin, e->fieldbackgroundObj); } else if (0 != strcmp(Tcl_GetString(e->backgroundObj), defaultBG)) { backgroundPtr = Tk_Get3DBorderFromObj(tkwin, e->backgroundObj); } if (backgroundPtr != NULL) { XFillRectangle(Tk_Display(tkwin), d, Tk_3DBorderGC(tkwin, backgroundPtr, TK_3D_FLAT_GC), inner.x, inner.y, inner.width, inner.height); } BEGIN_DRAWING(d) if (backgroundPtr == NULL) { if ([NSApp macOSVersion] > 100800) { background = [NSColor textBackgroundColor]; CGContextSetFillColorWithColor(dc.context, CGCOLOR(background)); } else { CGContextSetRGBFillColor(dc.context, 1.0, 1.0, 1.0, 1.0); } CGContextFillRect(dc.context, bounds); } ChkErr(HIThemeDrawFrame, &bounds, &info, dc.context, HIOrientation); END_DRAWING } } static Ttk_ElementSpec EntryElementSpec = { TK_STYLE_VERSION_2, sizeof(EntryElement), EntryElementOptions, EntryElementSize, EntryElementDraw }; /*---------------------------------------------------------------------- * +++ Combobox elements -- * * NOTES: * The HIToolbox has incomplete and inconsistent support for ComboBoxes. * There is no constant available to get the height of a ComboBox with * GetThemeMetric. In fact, ComboBoxes are the same (fixed) height as * PopupButtons and PushButtons, but they have no shadow at the bottom. * As a result, they are drawn 1 pixel above the center of the bounds * rectangle rather than being centered like the other buttons. One can * request background bounds for a ComboBox, and it is reported with * height 23, while the actual button face, including its 1-pixel border * has height 21. Attempting to request the content bounds returns a 0x0 * rectangle. Measurement indicates that the arrow button has width 18. * * With no help available from HIToolbox, we have to use hard-wired * constants for the padding. We shift the bounding rectangle downward by * 1 pixel to account for the fact that the button is not centered. */ static Ttk_Padding ComboboxPadding = {4, 4, 20, 4}; static Ttk_Padding DarkComboboxPadding = {6, 6, 22, 6}; static void ComboboxElementSize( TCL_UNUSED(void *), /* clientData */ TCL_UNUSED(void *), /* elementRecord */ Tk_Window tkwin, int *minWidth, int *minHeight, Ttk_Padding *paddingPtr) { *minWidth = 24; *minHeight = 23; if (TkMacOSXInDarkMode(tkwin)) { *paddingPtr = DarkComboboxPadding; } else { *paddingPtr = ComboboxPadding; } } static void ComboboxElementDraw( TCL_UNUSED(void *), /* clientData */ TCL_UNUSED(void *), /* elementRecord */ Tk_Window tkwin, Drawable d, Ttk_Box b, Ttk_State state) { CGRect bounds = BoxToRect(d, b); 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) if (TkMacOSXInDarkMode(tkwin)) { bounds = CGRectInset(bounds, 3, 3); if (state & TTK_STATE_FOCUS) { DrawDarkFocusRing(bounds, dc.context); } DrawDarkButton(bounds, info.kind, state, dc.context); } else { if ([NSApp macOSVersion] > 100800) { if ((state & TTK_STATE_BACKGROUND) && !(state & TTK_STATE_DISABLED)) { NSColor *background = [NSColor textBackgroundColor]; CGRect innerBounds = CGRectInset(bounds, 1, 4); bounds.origin.y += 1; SolidFillRoundedRectangle(dc.context, innerBounds, 4, background); } } ChkErr(HIThemeDrawButton, &bounds, &info, dc.context, HIOrientation, NULL); } END_DRAWING } static Ttk_ElementSpec ComboboxElementSpec = { TK_STYLE_VERSION_2, sizeof(NullElement), TtkNullElementOptions, ComboboxElementSize, ComboboxElementDraw }; /*---------------------------------------------------------------------- * +++ Spinbutton elements -- * * 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. * * Ttk expects the up and down arrows to be distinct elements but * HIToolbox draws them as one widget with two different pressed states. * We work around this by defining them as separate elements in the * layout, but making each one have a drawing method which also draws the * other one. The down button does no drawing when not pressed, and when * pressed draws the entire IncDecButton in its "pressed down" state. * The up button draws the entire IncDecButton when not pressed and when * pressed draws the IncDecButton in its "pressed up" state. NOTE: This * means that when the down button is pressed the IncDecButton will be * drawn twice, first in unpressed state by the up arrow and then in * "pressed down" state by the down button. The drawing must be done in * that order. So the up button must be listed first in the layout. */ static Ttk_Padding SpinbuttonMargins = {0, 0, 2, 0}; static void SpinButtonElementSize( TCL_UNUSED(void *), /* clientdata */ TCL_UNUSED(void *), /* elementRecord */ TCL_UNUSED(Tk_Window), /* tkwin */ int *minWidth, int *minHeight, TCL_UNUSED(Ttk_Padding *)) /* PaddingPtr */ { SInt32 s; ChkErr(GetThemeMetric, kThemeMetricLittleArrowsWidth, &s); *minWidth = s + Ttk_PaddingWidth(SpinbuttonMargins); ChkErr(GetThemeMetric, kThemeMetricLittleArrowsHeight, &s); *minHeight = (s + Ttk_PaddingHeight(SpinbuttonMargins)) / 2; } static void SpinButtonUpElementDraw( TCL_UNUSED(void *), /* clientData */ TCL_UNUSED(void *), /* elementRecord */ Tk_Window tkwin, Drawable d, Ttk_Box b, Ttk_State state) { CGRect bounds = BoxToRect(d, Ttk_PadBox(b, SpinbuttonMargins)); int infoState; bounds.size.height *= 2; if (state & TTK_STATE_PRESSED) { infoState = kThemeStatePressedUp; } else { infoState = Ttk_StateTableLookup(ThemeStateTable, state); } const HIThemeButtonDrawInfo info = { .version = 0, .state = infoState, .kind = kThemeIncDecButton, .value = Ttk_StateTableLookup(ButtonValueTable, state), .adornment = kThemeAdornmentNone, }; BEGIN_DRAWING(d) if (TkMacOSXInDarkMode(tkwin)) { DrawDarkIncDecButton(bounds, infoState, state, dc.context); } else { ChkErr(HIThemeDrawButton, &bounds, &info, dc.context, HIOrientation, NULL); } END_DRAWING } static Ttk_ElementSpec SpinButtonUpElementSpec = { TK_STYLE_VERSION_2, sizeof(NullElement), TtkNullElementOptions, SpinButtonElementSize, SpinButtonUpElementDraw }; static void SpinButtonDownElementDraw( TCL_UNUSED(void *), /* clientData */ TCL_UNUSED(void *), /* elementRecord */ Tk_Window tkwin, Drawable d, Ttk_Box b, Ttk_State state) { CGRect bounds = BoxToRect(d, Ttk_PadBox(b, SpinbuttonMargins)); int infoState = 0; bounds.origin.y -= bounds.size.height; bounds.size.height += bounds.size.height; if (state & TTK_STATE_PRESSED) { infoState = kThemeStatePressedDown; } else { return; } const HIThemeButtonDrawInfo info = { .version = 0, .state = infoState, .kind = kThemeIncDecButton, .value = Ttk_StateTableLookup(ButtonValueTable, state), .adornment = kThemeAdornmentNone, }; BEGIN_DRAWING(d) if (TkMacOSXInDarkMode(tkwin)) { DrawDarkIncDecButton(bounds, infoState, state, dc.context); } else { ChkErr(HIThemeDrawButton, &bounds, &info, dc.context, HIOrientation, NULL); } END_DRAWING } static Ttk_ElementSpec SpinButtonDownElementSpec = { TK_STYLE_VERSION_2, sizeof(NullElement), TtkNullElementOptions, SpinButtonElementSize, SpinButtonDownElementDraw }; /*---------------------------------------------------------------------- * +++ DrawThemeTrack-based elements -- * * Progress bars and scales. (See also: <>) */ /* * 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), NULL}, {"-to", TK_OPTION_DOUBLE, Tk_Offset(TrackElement, toObj), NULL}, {"-value", TK_OPTION_DOUBLE, Tk_Offset(TrackElement, valueObj), NULL}, {"-orient", TK_OPTION_STRING, Tk_Offset(TrackElement, orientObj), NULL}, {NULL, TK_OPTION_BOOLEAN, 0, NULL} }; static void TrackElementSize( void *clientData, TCL_UNUSED(void *), /* elementRecord */ TCL_UNUSED(Tk_Window), /* tkwin */ int *minWidth, int *minHeight, TCL_UNUSED(Ttk_Padding *)) /* paddingPtr */ { TrackElementData *data = (TrackElementData *)clientData; SInt32 size = 24; /* reasonable default ... */ ChkErr(GetThemeMetric, data->thicknessMetric, &size); *minWidth = *minHeight = size; } static void TrackElementDraw( void *clientData, void *elementRecord, Tk_Window tkwin, Drawable d, Ttk_Box b, Ttk_State state) { TrackElementData *data = (TrackElementData *)clientData; TrackElement *elem = (TrackElement *)elementRecord; int orientation = TTK_ORIENT_HORIZONTAL; double from = 0, to = 100, value = 0, factor; CGRect bounds; 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); /* * HIThemeTrackDrawInfo uses 2-byte alignment; assigning to a separate * bounds variable avoids UBSan (-fsanitize=alignment) complaints. */ bounds = BoxToRect(d, b); HIThemeTrackDrawInfo info = { .version = 0, .kind = data->kind, .bounds = bounds, .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 (TkMacOSXInDarkMode(tkwin)) { 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); } SolidFillRoundedRectangle(dc.context, bounds, 2, trackColor); } ChkErr(HIThemeDrawTrack, &info, NULL, dc.context, HIOrientation); END_DRAWING } static Ttk_ElementSpec TrackElementSpec = { TK_STYLE_VERSION_2, sizeof(TrackElement), TrackElementOptions, TrackElementSize, TrackElementDraw }; /*---------------------------------------------------------------------- * Slider elements -- <> * * 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. * */ static void SliderElementSize( TCL_UNUSED(void *), /* clientData */ TCL_UNUSED(void *), /* elementRecord */ TCL_UNUSED(Tk_Window), /* tkwin */ int *minWidth, int *minHeight, TCL_UNUSED(Ttk_Padding *)) /* paddingPtr */ { *minWidth = *minHeight = 24; } static Ttk_ElementSpec SliderElementSpec = { TK_STYLE_VERSION_2, sizeof(NullElement), TtkNullElementOptions, SliderElementSize, TtkNullElementDraw }; /*---------------------------------------------------------------------- * +++ Progress bar elements -- * * @@@ 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"}, {NULL, TK_OPTION_BOOLEAN, 0, NULL} }; static void PbarElementSize( TCL_UNUSED(void *), /* clientData */ TCL_UNUSED(void *), /* elementRecord */ TCL_UNUSED(Tk_Window), /* tkwin */ int *minWidth, int *minHeight, TCL_UNUSED(Ttk_Padding *)) /* paddingPtr */ { SInt32 size = 24; /* @@@ Check HIG for correct default */ ChkErr(GetThemeMetric, kThemeMetricLargeProgressBarThickness, &size); *minWidth = *minHeight = size; } static void PbarElementDraw( TCL_UNUSED(void *), void *elementRecord, Tk_Window tkwin, Drawable d, Ttk_Box b, Ttk_State state) { PbarElement *pbar = (PbarElement *)elementRecord; int orientation = TTK_ORIENT_HORIZONTAL, phase = 0, kind; /* * Using 1000 as the maximum should give better than 1 pixel * resolution for most progress bars. */ int ivalue, imaximum = 1000; CGRect bounds; Ttk_GetOrientFromObj(NULL, pbar->orientObj, &orientation); kind = !strcmp("indeterminate", Tcl_GetString(pbar->modeObj)) ? kThemeIndeterminateBar : kThemeProgressBar; if (kind == kThemeIndeterminateBar) { Tcl_GetIntFromObj(NULL, pbar->phaseObj, &phase); /* * On macOS 11 the fraction of an indeterminate progress bar which is * traversed by the oscillating thumb is value / maximum. The phase * determines the position of the moving thumb in that range and is * apparently expected to vary between 0 and 120. On earlier systems * it is unclear how the phase is used in generating the animation. */ ivalue = imaximum; } else { double value, maximum; Tcl_GetDoubleFromObj(NULL, pbar->valueObj, &value); Tcl_GetDoubleFromObj(NULL, pbar->maximumObj, &maximum); ivalue = (value / maximum)*1000; } /* * HIThemeTrackDrawInfo uses 2-byte alignment; assigning to a separate * bounds variable avoids UBSan (-fsanitize=alignment) complaints. */ bounds = BoxToRect(d, b); HIThemeTrackDrawInfo info = { .version = 0, .kind = kind, .bounds = bounds, .min = 0, .max = imaximum, .value = ivalue, .attributes = kThemeTrackShowThumb | (orientation == TTK_ORIENT_HORIZONTAL ? kThemeTrackHorizontal : 0), .enableState = Ttk_StateTableLookup(ThemeTrackEnableTable, state), .trackInfo.progress.phase = phase }; BEGIN_DRAWING(d) if (TkMacOSXInDarkMode(tkwin)) { 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); } SolidFillRoundedRectangle(dc.context, bounds, 3, trackColor); } ChkErr(HIThemeDrawTrack, &info, NULL, dc.context, HIOrientation); END_DRAWING } static Ttk_ElementSpec PbarElementSpec = { TK_STYLE_VERSION_2, sizeof(PbarElement), PbarElementOptions, PbarElementSize, PbarElementDraw }; /*---------------------------------------------------------------------- * +++ Scrollbar elements */ typedef struct { Tcl_Obj *orientObj; } ScrollbarElement; static Ttk_ElementOptionSpec ScrollbarElementOptions[] = { {"-orient", TK_OPTION_STRING, Tk_Offset(ScrollbarElement, orientObj), "horizontal"}, {NULL, TK_OPTION_BOOLEAN, 0, NULL} }; static void TroughElementSize( TCL_UNUSED(void *), /* clientData */ void *elementRecord, TCL_UNUSED(Tk_Window), /* tkwin */ int *minWidth, int *minHeight, Ttk_Padding *paddingPtr) { ScrollbarElement *scrollbar = (ScrollbarElement *)elementRecord; int orientation = TTK_ORIENT_HORIZONTAL; SInt32 thickness = 15; Ttk_GetOrientFromObj(NULL, scrollbar->orientObj, &orientation); ChkErr(GetThemeMetric, kThemeMetricScrollBarWidth, &thickness); if (orientation == TTK_ORIENT_HORIZONTAL) { *minHeight = thickness; if ([NSApp macOSVersion] > 100700) { *paddingPtr = Ttk_MakePadding(4, 4, 4, 3); } } else { *minWidth = thickness; if ([NSApp macOSVersion] > 100700) { *paddingPtr = Ttk_MakePadding(4, 4, 3, 4); } } } static CGFloat lightTrough[4] = {250.0 / 255, 250.0 / 255, 250.0 / 255, 1.0}; static CGFloat darkTrough[4] = {45.0 / 255, 46.0 / 255, 49.0 / 255, 1.0}; static CGFloat lightInactiveThumb[4] = { 200.0 / 255, 200.0 / 255, 200.0 / 255, 1.0 }; static CGFloat lightActiveThumb[4] = { 133.0 / 255, 133.0 / 255, 133.0 / 255, 1.0 }; static CGFloat darkInactiveThumb[4] = { 116.0 / 255, 117.0 / 255, 118.0 / 255, 1.0 }; static CGFloat darkActiveThumb[4] = { 158.0 / 255, 158.0 / 255, 159.0 / 255, 1.0 }; static void TroughElementDraw( TCL_UNUSED(void *), /* clientData */ void *elementRecord, Tk_Window tkwin, Drawable d, Ttk_Box b, TCL_UNUSED(Ttk_State)) /* state */ { ScrollbarElement *scrollbar = (ScrollbarElement *)elementRecord; int orientation = TTK_ORIENT_HORIZONTAL; CGRect bounds = BoxToRect(d, b); NSColorSpace *deviceRGB = [NSColorSpace deviceRGBColorSpace]; NSColor *troughColor; CGFloat *rgba = TkMacOSXInDarkMode(tkwin) ? darkTrough : lightTrough; Ttk_GetOrientFromObj(NULL, scrollbar->orientObj, &orientation); if (orientation == TTK_ORIENT_HORIZONTAL) { bounds = CGRectInset(bounds, 0, 1); } else { bounds = CGRectInset(bounds, 1, 0); } troughColor = [NSColor colorWithColorSpace: deviceRGB components: rgba count: 4]; BEGIN_DRAWING(d) if ([NSApp macOSVersion] > 100800) { CGContextSetFillColorWithColor(dc.context, CGCOLOR(troughColor)); } else { ChkErr(HIThemeSetFill, kThemeBrushDocumentWindowBackground, NULL, dc.context, HIOrientation); } CGContextFillRect(dc.context, bounds); END_DRAWING } static Ttk_ElementSpec TroughElementSpec = { TK_STYLE_VERSION_2, sizeof(ScrollbarElement), ScrollbarElementOptions, TroughElementSize, TroughElementDraw }; static void ThumbElementSize( TCL_UNUSED(void *), /* clientData */ void *elementRecord, TCL_UNUSED(Tk_Window), /* tkwin */ int *minWidth, int *minHeight, TCL_UNUSED(Ttk_Padding *)) /* paddingPtr */ { ScrollbarElement *scrollbar = (ScrollbarElement *)elementRecord; int orientation = TTK_ORIENT_HORIZONTAL; Ttk_GetOrientFromObj(NULL, scrollbar->orientObj, &orientation); if (orientation == TTK_ORIENT_VERTICAL) { *minHeight = 18; *minWidth = 8; } else { *minHeight = 8; *minWidth = 18; } } static void ThumbElementDraw( TCL_UNUSED(void *), /* clientData */ void *elementRecord, Tk_Window tkwin, Drawable d, Ttk_Box b, Ttk_State state) { ScrollbarElement *scrollbar = (ScrollbarElement *)elementRecord; int orientation = TTK_ORIENT_HORIZONTAL; Ttk_GetOrientFromObj(NULL, scrollbar->orientObj, &orientation); /* * In order to make ttk scrollbars work correctly it is necessary to be * able to display the thumb element at the size and location which the ttk * scrollbar widget requests. The algorithm that HIToolbox uses to * determine the thumb geometry from the input values of min, max, value * and viewSize is undocumented. A seemingly natural algorithm is * implemented below. This code uses that algorithm for older OS versions, * because using HITools also handles drawing the buttons and 3D thumb used * on those systems. For newer systems the cleanest approach is to just * draw the thumb directly. */ if ([NSApp macOSVersion] > 100800) { CGRect thumbBounds = BoxToRect(d, b); NSColorSpace *deviceRGB = [NSColorSpace deviceRGBColorSpace]; NSColor *thumbColor; CGFloat *rgba; if ((orientation == TTK_ORIENT_HORIZONTAL && thumbBounds.size.width >= Tk_Width(tkwin) - 8) || (orientation == TTK_ORIENT_VERTICAL && thumbBounds.size.height >= Tk_Height(tkwin) - 8)) { return; } int isDark = TkMacOSXInDarkMode(tkwin); if ((state & TTK_STATE_PRESSED) || (state & TTK_STATE_HOVER)) { rgba = isDark ? darkActiveThumb : lightActiveThumb; } else { rgba = isDark ? darkInactiveThumb : lightInactiveThumb; } thumbColor = [NSColor colorWithColorSpace: deviceRGB components: rgba count: 4]; BEGIN_DRAWING(d) SolidFillRoundedRectangle(dc.context, thumbBounds, 4, thumbColor); END_DRAWING } else { double thumbSize, trackSize, visibleSize, factor, fraction; MacDrawable *macWin = (MacDrawable *)Tk_WindowId(tkwin); CGRect troughBounds = {{macWin->xOff, macWin->yOff}, {Tk_Width(tkwin), Tk_Height(tkwin)}}; /* * The info struct has integer fields, which will be converted to * floats in the drawing routine. All of values provided in the info * struct, namely min, max, value, and viewSize are only defined up to * an arbitrary scale factor. To avoid roundoff error we scale so * that the viewSize is a large float which is smaller than the * largest int. */ HIThemeTrackDrawInfo info = { .version = 0, .bounds = troughBounds, .min = 0, .attributes = kThemeTrackShowThumb | kThemeTrackThumbRgnIsNotGhost, .enableState = kThemeTrackActive }; factor = RangeToFactor(100.0); if (orientation == TTK_ORIENT_HORIZONTAL) { trackSize = troughBounds.size.width; thumbSize = b.width; fraction = b.x / trackSize; } else { trackSize = troughBounds.size.height; thumbSize = b.height; fraction = b.y / trackSize; } visibleSize = (thumbSize / trackSize) * factor; info.max = factor - visibleSize; info.trackInfo.scrollbar.viewsize = visibleSize; if ([NSApp macOSVersion] < 100800 || orientation == TTK_ORIENT_HORIZONTAL) { info.value = factor * fraction; } else { info.value = info.max - factor * fraction; } if ((state & TTK_STATE_PRESSED) || (state & TTK_STATE_HOVER)) { info.trackInfo.scrollbar.pressState = kThemeThumbPressed; } else { info.trackInfo.scrollbar.pressState = 0; } if (orientation == TTK_ORIENT_HORIZONTAL) { info.attributes |= kThemeTrackHorizontal; } else { info.attributes &= ~kThemeTrackHorizontal; } BEGIN_DRAWING(d) HIThemeDrawTrack(&info, 0, dc.context, kHIThemeOrientationNormal); END_DRAWING } } static Ttk_ElementSpec ThumbElementSpec = { TK_STYLE_VERSION_2, sizeof(ScrollbarElement), ScrollbarElementOptions, ThumbElementSize, ThumbElementDraw }; static void ArrowElementSize( TCL_UNUSED(void *), /* clientData */ TCL_UNUSED(void *), /* elementRecord */ TCL_UNUSED(Tk_Window), /* tkwin */ int *minWidth, int *minHeight, TCL_UNUSED(Ttk_Padding *)) /* paddingPtr */ { if ([NSApp macOSVersion] < 100800) { *minHeight = *minWidth = 14; } else { *minHeight = *minWidth = -1; } } static Ttk_ElementSpec ArrowElementSpec = { TK_STYLE_VERSION_2, sizeof(ScrollbarElement), ScrollbarElementOptions, ArrowElementSize, TtkNullElementDraw }; /*---------------------------------------------------------------------- * +++ 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( TCL_UNUSED(void *), /* clientData */ TCL_UNUSED(void *), /* elementRecord */ TCL_UNUSED(Tk_Window), /* tkwin */ int *minWidth, int *minHeight, TCL_UNUSED(Ttk_Padding *)) /* paddingPtr */ { *minWidth = *minHeight = 1; } static void SeparatorElementDraw( TCL_UNUSED(void *), /* clientData */ TCL_UNUSED(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 (TkMacOSXInDarkMode(tkwin)) { DrawDarkSeparator(bounds, dc.context, tkwin); } else { ChkErr(HIThemeDrawSeparator, &bounds, &info, dc.context, HIOrientation); } END_DRAWING } static Ttk_ElementSpec SeparatorElementSpec = { TK_STYLE_VERSION_2, sizeof(NullElement), TtkNullElementOptions, SeparatorElementSize, SeparatorElementDraw }; /*---------------------------------------------------------------------- * +++ Size grip elements -- (obsolete) */ static const ThemeGrowDirection sizegripGrowDirection = kThemeGrowRight | kThemeGrowDown; static void SizegripElementSize( TCL_UNUSED(void *), /* clientData */ TCL_UNUSED(void *), /* elementRecord */ TCL_UNUSED(Tk_Window), /* tkwin */ int *minWidth, int *minHeight, TCL_UNUSED(Ttk_Padding *)) /* paddingPtr */ { HIThemeGrowBoxDrawInfo info = { .version = 0, .state = kThemeStateActive, .kind = kHIThemeGrowBoxKindNormal, .direction = sizegripGrowDirection, .size = kHIThemeGrowBoxSizeNormal, }; CGRect bounds = CGRectZero; ChkErr(HIThemeGetGrowBoxBounds, &bounds.origin, &info, &bounds); *minWidth = bounds.size.width; *minHeight = bounds.size.height; } static void SizegripElementDraw( TCL_UNUSED(void *), /* clientData */ TCL_UNUSED(void *), /* elementRecord */ TCL_UNUSED(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 placing 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( TCL_UNUSED(void *), /* clientData */ TCL_UNUSED(void *), /* elementRecord */ Tk_Window tkwin, Drawable d, Ttk_Box b, Ttk_State state) { CGRect bounds = BoxToRect(d, b); if ([NSApp macOSVersion] > 100800) { NSColorSpace *deviceRGB = [NSColorSpace deviceRGBColorSpace]; NSColor *bgColor; CGFloat fill[4]; BEGIN_DRAWING(d) GetBackgroundColor(dc.context, tkwin, 0, fill); bgColor = [NSColor colorWithColorSpace: deviceRGB components: fill count: 4]; CGContextSetFillColorSpace(dc.context, deviceRGB.CGColorSpace); CGContextSetFillColorWithColor(dc.context, CGCOLOR(bgColor)); CGContextFillRect(dc.context, bounds); END_DRAWING } else { ThemeBrush brush = (state & TTK_STATE_BACKGROUND) ? kThemeBrushModelessDialogBackgroundInactive : kThemeBrushModelessDialogBackgroundActive; BEGIN_DRAWING(d) ChkErr(HIThemeSetFill, brush, NULL, dc.context, HIOrientation); CGContextFillRect(dc.context, bounds); END_DRAWING } } static void BackgroundElementDraw( void *clientData, void *elementRecord, Tk_Window tkwin, Drawable d, TCL_UNUSED(Ttk_Box), 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. * * * */ static void ToolbarBackgroundElementDraw( TCL_UNUSED(void *), /* clientData */ TCL_UNUSED(void *), /* elementRecord */ Tk_Window tkwin, Drawable d, TCL_UNUSED(Ttk_Box), TCL_UNUSED(Ttk_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 }; /*---------------------------------------------------------------------- * +++ Field elements -- * * Used for the Treeview widget. This is like the BackgroundElement * except that the fieldbackground color is configurable. */ typedef struct { Tcl_Obj *backgroundObj; } FieldElement; static Ttk_ElementOptionSpec FieldElementOptions[] = { {"-fieldbackground", TK_OPTION_BORDER, Tk_Offset(FieldElement, backgroundObj), "white"}, {NULL, TK_OPTION_BOOLEAN, 0, NULL} }; static void FieldElementDraw( TCL_UNUSED(void *), /* clientData */ void *elementRecord, Tk_Window tkwin, Drawable d, Ttk_Box b, TCL_UNUSED(Ttk_State)) { FieldElement *e = (FieldElement *)elementRecord; Tk_3DBorder backgroundPtr = Tk_Get3DBorderFromObj(tkwin, e->backgroundObj); XFillRectangle(Tk_Display(tkwin), d, Tk_3DBorderGC(tkwin, backgroundPtr, TK_3D_FLAT_GC), b.x, b.y, b.width, b.height); } static Ttk_ElementSpec FieldElementSpec = { TK_STYLE_VERSION_2, sizeof(FieldElement), FieldElementOptions, TtkNullElementSize, FieldElementDraw }; /*---------------------------------------------------------------------- * +++ Treeview headers -- * * On systems older than 10.9 the header is a kThemeListHeaderButton drawn * by HIToolbox. On newer systems those buttons do not match the Apple * buttons, so we draw them from scratch. */ static Ttk_StateTable TreeHeaderValueTable[] = { {kThemeButtonOn, TTK_STATE_ALTERNATE, 0}, {kThemeButtonOn, TTK_STATE_SELECTED, 0}, {kThemeButtonOff, 0, 0} }; static Ttk_StateTable TreeHeaderAdornmentTable[] = { {kThemeAdornmentHeaderButtonSortUp, TTK_STATE_ALTERNATE | TTK_TREEVIEW_STATE_SORTARROW, 0}, {kThemeAdornmentDefault, TTK_STATE_SELECTED | TTK_TREEVIEW_STATE_SORTARROW, 0}, {kThemeAdornmentHeaderButtonNoSortArrow, TTK_STATE_ALTERNATE, 0}, {kThemeAdornmentHeaderButtonNoSortArrow, TTK_STATE_SELECTED, 0}, {kThemeAdornmentFocus, TTK_STATE_FOCUS, 0}, {kThemeAdornmentNone, 0, 0} }; static void TreeAreaElementSize ( TCL_UNUSED(void *), /* clientData */ TCL_UNUSED(void *), /* elementRecord */ TCL_UNUSED(Tk_Window), /* tkwin */ TCL_UNUSED(int *), /* minWidth */ TCL_UNUSED(int *), /* minHeight */ Ttk_Padding *paddingPtr) { /* * Padding is needed to get the heading text to align correctly, since the * widget expects the heading to be the same height as a row. */ if ([NSApp macOSVersion] > 100800) { paddingPtr->top = 4; } } static Ttk_ElementSpec TreeAreaElementSpec = { TK_STYLE_VERSION_2, sizeof(NullElement), TtkNullElementOptions, TreeAreaElementSize, TtkNullElementDraw }; static void TreeHeaderElementSize( void *clientData, void *elementRecord, Tk_Window tkwin, int *minWidth, int *minHeight, Ttk_Padding *paddingPtr) { if ([NSApp macOSVersion] > 100800) { *minHeight = 24; } else { ButtonElementSize(clientData, elementRecord, tkwin, minWidth, minHeight, paddingPtr); } } static void TreeHeaderElementDraw( void *clientData, TCL_UNUSED(void *), /* elementRecord */ Tk_Window tkwin, Drawable d, Ttk_Box b, Ttk_State state) { ThemeButtonParams *params = (ThemeButtonParams *)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) if ([NSApp macOSVersion] > 100800) { /* * Compensate for the padding added in TreeHeaderElementSize, so * the larger heading will be drawn at the top of the widget. */ bounds.origin.y -= 4; if (TkMacOSXInDarkMode(tkwin)) { DrawDarkListHeader(bounds, dc.context, tkwin, state); } else { DrawListHeader(bounds, dc.context, tkwin, state); } } else { ChkErr(HIThemeDrawButton, &bounds, &info, dc.context, HIOrientation, NULL); } END_DRAWING } static Ttk_ElementSpec TreeHeaderElementSpec = { TK_STYLE_VERSION_2, sizeof(NullElement), TtkNullElementOptions, TreeHeaderElementSize, TreeHeaderElementDraw }; /*---------------------------------------------------------------------- * +++ Disclosure triangles -- */ #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( TCL_UNUSED(void *), /* clientData */ TCL_UNUSED(void *), /* elementRecord */ TCL_UNUSED(Tk_Window), /* tkwin */ int *minWidth, int *minHeight, TCL_UNUSED(Ttk_Padding *)) /* paddingPtr */ { SInt32 s; ChkErr(GetThemeMetric, kThemeMetricDisclosureTriangleWidth, &s); *minWidth = s; ChkErr(GetThemeMetric, kThemeMetricDisclosureTriangleHeight, &s); *minHeight = s; } static void DisclosureElementDraw( TCL_UNUSED(void *), /* clientData */ TCL_UNUSED(void *), /* elementRecord */ Tk_Window tkwin, Drawable d, Ttk_Box b, Ttk_State state) { if (!(state & TTK_TREEVIEW_STATE_LEAF)) { int triangleState = TkMacOSXInDarkMode(tkwin) ? kThemeStateInactive : kThemeStateActive; CGRect bounds = BoxToRect(d, b); const HIThemeButtonDrawInfo info = { .version = 0, .state = triangleState, .kind = kThemeDisclosureTriangle, .value = Ttk_StateTableLookup(DisclosureValueTable, state), .adornment = kThemeAdornmentDrawIndicatorOnly, }; BEGIN_DRAWING(d) if ([NSApp macOSVersion] >= 110000) { CGFloat rgba[4]; NSColorSpace *deviceRGB = [NSColorSpace deviceRGBColorSpace]; NSColor *stroke = [[NSColor textColor] colorUsingColorSpace: deviceRGB]; [stroke getComponents: rgba]; if (state & TTK_TREEVIEW_STATE_OPEN) { DrawOpenDisclosure(dc.context, bounds, 2, 8, rgba); } else { DrawClosedDisclosure(dc.context, bounds, 2, 12, rgba); } } else { 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_FILL_BOTH, TTK_GROUP("Combobox.padding", TTK_FILL_BOTH, TTK_NODE("Combobox.textarea", TTK_FILL_BOTH)))) /* Notebook tabs -- no focus ring */ TTK_LAYOUT("Tab", TTK_GROUP("Notebook.tab", TTK_FILL_BOTH, TTK_GROUP("Notebook.padding", TTK_FILL_BOTH, TTK_NODE("Notebook.label", TTK_FILL_BOTH)))) /* Spinbox -- buttons 2px to the right of the field. */ TTK_LAYOUT("TSpinbox", TTK_GROUP("Spinbox.buttons", TTK_PACK_RIGHT, TTK_NODE("Spinbox.uparrow", TTK_PACK_TOP | TTK_STICK_E) TTK_NODE("Spinbox.downarrow", TTK_PACK_BOTTOM | TTK_STICK_E)) TTK_GROUP("Spinbox.field", TTK_FILL_X, TTK_NODE("Spinbox.textarea", TTK_FILL_X))) /* Progress bars -- track only */ TTK_LAYOUT("TProgressbar", TTK_NODE("Progressbar.track", TTK_FILL_BOTH)) /* Treeview -- no border. */ TTK_LAYOUT("Treeview", TTK_GROUP("Treeview.field", TTK_FILL_BOTH, TTK_GROUP("Treeview.padding", TTK_FILL_BOTH, TTK_NODE("Treeview.treearea", TTK_FILL_BOTH)))) /* Tree heading -- no border, fixed height */ TTK_LAYOUT("Heading", TTK_NODE("Treeheading.cell", TTK_FILL_BOTH) TTK_NODE("Treeheading.image", TTK_PACK_RIGHT) TTK_NODE("Treeheading.text", TTK_PACK_TOP)) /* 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))) /* Scrollbar Layout -- Buttons at the bottom (Snow Leopard and Lion only) */ TTK_LAYOUT("Vertical.TScrollbar", TTK_GROUP("Vertical.Scrollbar.trough", TTK_FILL_Y, TTK_NODE("Vertical.Scrollbar.thumb", TTK_FILL_BOTH) TTK_NODE("Vertical.Scrollbar.downarrow", TTK_PACK_BOTTOM) TTK_NODE("Vertical.Scrollbar.uparrow", TTK_PACK_BOTTOM))) TTK_LAYOUT("Horizontal.TScrollbar", TTK_GROUP("Horizontal.Scrollbar.trough", TTK_FILL_X, TTK_NODE("Horizontal.Scrollbar.thumb", TTK_FILL_BOTH) TTK_NODE("Horizontal.Scrollbar.rightarrow", TTK_PACK_RIGHT) TTK_NODE("Horizontal.Scrollbar.leftarrow", TTK_PACK_RIGHT))) 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, "field", &FieldElementSpec, 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.uparrow", &SpinButtonUpElementSpec, 0); Ttk_RegisterElementSpec(themePtr, "Spinbox.downarrow", &SpinButtonDownElementSpec, 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, "Treeview.treearea", &TreeAreaElementSpec, 0); 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); /* * <> * In some themes the Layouts for a progress bar has a trough element and a * pbar element. But in our case the appearance manager draws both parts * of the progress bar, so we just have a single element called ".track". */ Ttk_RegisterElementSpec(themePtr, "Progressbar.track", &PbarElementSpec, 0); Ttk_RegisterElementSpec(themePtr, "Scale.trough", &TrackElementSpec, &ScaleData); Ttk_RegisterElementSpec(themePtr, "Scale.slider", &SliderElementSpec, 0); Ttk_RegisterElementSpec(themePtr, "Vertical.Scrollbar.trough", &TroughElementSpec, 0); Ttk_RegisterElementSpec(themePtr, "Vertical.Scrollbar.thumb", &ThumbElementSpec, 0); Ttk_RegisterElementSpec(themePtr, "Horizontal.Scrollbar.trough", &TroughElementSpec, 0); Ttk_RegisterElementSpec(themePtr, "Horizontal.Scrollbar.thumb", &ThumbElementSpec, 0); /* * If we are not in Snow Leopard or Lion the arrows won't actually be * displayed. */ Ttk_RegisterElementSpec(themePtr, "Vertical.Scrollbar.uparrow", &ArrowElementSpec, 0); Ttk_RegisterElementSpec(themePtr, "Vertical.Scrollbar.downarrow", &ArrowElementSpec, 0); Ttk_RegisterElementSpec(themePtr, "Horizontal.Scrollbar.leftarrow", &ArrowElementSpec, 0); Ttk_RegisterElementSpec(themePtr, "Horizontal.Scrollbar.rightarrow", &ArrowElementSpec, 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: */