diff options
Diffstat (limited to 'tk8.6/macosx/ttkMacOSXTheme.c')
-rw-r--r-- | tk8.6/macosx/ttkMacOSXTheme.c | 3186 |
1 files changed, 3186 insertions, 0 deletions
diff --git a/tk8.6/macosx/ttkMacOSXTheme.c b/tk8.6/macosx/ttkMacOSXTheme.c new file mode 100644 index 0000000..50c79bb --- /dev/null +++ b/tk8.6/macosx/ttkMacOSXTheme.c @@ -0,0 +1,3186 @@ +/* + * ttkMacOSXTheme.c -- + * + * Tk theme engine for Mac OSX, using the Appearance Manager API. + * + * Copyright (c) 2004 Joe English + * Copyright (c) 2005 Neil Madden + * Copyright (c) 2006-2009 Daniel A. Steffen <das@users.sourceforge.net> + * Copyright 2008-2009, Apple Inc. + * Copyright 2009 Kevin Walzer/WordTech Communications LLC. + * Copyright 2019 Marc Culler + * + * See the file "license.terms" for information on usage and redistribution + * of this file, and for a DISCLAIMER OF ALL WARRANTIES. + * + * See also: + * + * <URL: http://developer.apple.com/documentation/Carbon/Reference/ + * Appearance_Manager/appearance_manager/APIIndex.html > + * + * Notes: + * "Active" means different things in Mac and Tk terminology -- + * On Aqua, widgets are "Active" if they belong to the foreground window, + * "Inactive" if they are in a background window. Tk uses the term + * "active" to mean that the mouse cursor is over a widget; aka "hover", + * "prelight", or "hot-tracked". Aqua doesn't use this kind of feedback. + * + * The QuickDraw/Carbon coordinate system is relative to the top-level + * window, not to the Tk_Window. BoxToRect() accounts for this. + */ + +#include "tkMacOSXPrivate.h" +#include "ttk/ttkTheme.h" +#include <math.h> + +/* + * Macros for handling drawing contexts. + */ + +#define BEGIN_DRAWING(d) { \ + TkMacOSXDrawingContext dc; \ + if (!TkMacOSXSetupDrawingContext((d), NULL, 1, &dc)) {return;} +#define END_DRAWING \ + TkMacOSXRestoreDrawingContext(&dc);} + +#define HIOrientation kHIThemeOrientationNormal +#define NoThemeMetric 0xFFFFFFFF + +#ifdef __LP64__ +#define RangeToFactor(maximum) (((double) (INT_MAX >> 1)) / (maximum)) +#else +#define RangeToFactor(maximum) (((double) (LONG_MAX >> 1)) / (maximum)) +#endif /* __LP64__ */ + +#define TTK_STATE_FIRST_TAB TTK_STATE_USER1 +#define TTK_STATE_LAST_TAB TTK_STATE_USER2 +#define TTK_TREEVIEW_STATE_SORTARROW TTK_STATE_USER1 + +/* + * Colors and gradients used in Dark Mode. + */ + +static CGFloat darkButtonFace[4] = { + 112.0 / 255, 113.0 / 255, 115.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 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 +}; + +/* + * 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 + +/*---------------------------------------------------------------------- + * +++ Utilities. + */ + +/* + * BoxToRect -- + * Convert a Ttk_Box in Tk coordinates relative to the given Drawable + * to a native Rect relative to the containing port. + */ + +static inline CGRect BoxToRect( + Drawable d, + Ttk_Box b) +{ + MacDrawable *md = (MacDrawable *) d; + CGRect rect; + + rect.origin.y = b.y + md->yOff; + rect.origin.x = b.x + md->xOff; + rect.size.height = b.height; + rect.size.width = b.width; + + return rect; +} + +/* + * Table mapping Tk states to Appearance manager ThemeStates + */ + +static Ttk_StateTable ThemeStateTable[] = { + {kThemeStateUnavailable, TTK_STATE_DISABLED, 0}, + {kThemeStatePressed, TTK_STATE_PRESSED, 0}, + {kThemeStateInactive, TTK_STATE_BACKGROUND, 0}, + {kThemeStateActive, 0, 0} + + /* Others: Not sure what these are supposed to mean. Up/Down have + * something to do with "little arrow" increment controls... Dunno what + * a "Rollover" is. + * NEM: Rollover is TTK_STATE_ACTIVE... but we don't handle that yet, by + * the looks of things + * + * {kThemeStateRollover, 0, 0}, + * {kThemeStateUnavailableInactive, 0, 0} + * {kThemeStatePressedUp, 0, 0}, + * {kThemeStatePressedDown, 0, 0} + */ +}; + +/*---------------------------------------------------------------------- + * 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 CGFloat windowBackground[4] = { + 235.0 / 255, 235.0 / 255, 235.0 / 255, 1.0 +}; +static CGFloat whiteRGBA[4] = {1.0, 1.0, 1.0, 1.0}; +static 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 master, or the + * standard ttk window background if there is no master. 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( + CGContextRef context, + Tk_Window tkwin, + int contrast, + CGFloat *rgba) +{ + TkWindow *winPtr = (TkWindow *) tkwin; + TkWindow *masterPtr = (TkWindow *) TkGetGeomMaster(tkwin); + + while (masterPtr != NULL) { + if (masterPtr->privatePtr->flags & TTK_HAS_CONTRASTING_BG) { + break; + } + masterPtr = (TkWindow *) TkGetGeomMaster(masterPtr); + } + if (masterPtr) { + for (int i = 0; i < 4; i++) { + rgba[i] = masterPtr->privatePtr->fillRGBA[i]; + } + } else { + if ([NSApp macMinorVersion] > 13) { + 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; + } + } + winPtr->privatePtr->flags |= TTK_HAS_CONTRASTING_BG; + for (int i = 0; i < 4; i++) { + winPtr->privatePtr->fillRGBA[i] = rgba[i]; + } + } +} + + +/*---------------------------------------------------------------------- + * +++ Single Arrow Buttons -- + * + * Used in ListHeaders and Comboboxes. + */ + +static void DrawDownArrow( + 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 / 4}, + {x + size, y - size / 4} + }; + CGContextAddLines(context, arrow, 3); + CGContextStrokePath(context); +} + +static void DrawUpArrow( + 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 / 4}, + {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, + 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) +{ + 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) +{ + 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; + + CGContextSetFillColorWithColor(context, CGCOLOR(color)); + path = CGPathCreateWithRoundedRect(bounds, radius, radius, NULL); + 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 = TkMacOSXDrawableWindow(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; + 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)) { + 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; + + CGContextSetLineWidth(context, 1.0); + CGContextClipToRect(context, bounds); + + /* + * Extend the bounds to one or both sides so the rounded part will be + * clipped off. + */ + + if (!(state & TTK_STATE_FIRST_TAB)) { + bounds.origin.x -= 10; + bounds.size.width += 10; + } + + if (!(state & TTK_STATE_LAST_TAB)) { + bounds.size.width += 10; + } + + /* + * Fill the tab face with the appropriate color or gradient. Use a solid + * color if the tab is not selected, otherwise use a blue or gray + * gradient. + */ + + bounds = CGRectInset(bounds, 1, 1); + if (!(state & TTK_STATE_SELECTED)) { + if (state & TTK_STATE_DISABLED) { + faceColor = [NSColor colorWithColorSpace: deviceRGB + components: darkDisabledButtonFace + count: 4]; + } else { + faceColor = [NSColor colorWithColorSpace: deviceRGB + components: darkButtonFace + count: 4]; + } + 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 blue. If it is first, cover up + * the separator line drawn by the second one. (The selected tab is + * always drawn last.) + */ + + if ((state & TTK_STATE_FIRST_TAB) && !(state & TTK_STATE_LAST_TAB)) { + bounds.size.width += 1; + } + if (!(state & TTK_STATE_BACKGROUND)) { + GradientFillRoundedRectangle(context, bounds, 4, + darkSelectedGradient, 2); + } else { + faceColor = [NSColor colorWithColorSpace: deviceRGB + components: darkInactiveSelectedTab + 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, + Tk_Window tkwin) +{ + static CGFloat fill[4] = {1.0, 1.0, 1.0, 0.3}; + NSColorSpace *deviceRGB = [NSColorSpace deviceRGBColorSpace]; + NSColor *fillColor = [NSColor colorWithColorSpace: deviceRGB + components: fill + count:4]; + + CGContextSetFillColorWithColor(context, 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) +{ + 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(CGRectInset(bounds, -3, -3), + 4, 4, NULL); + CGContextBeginPath(context); + CGContextAddPath(context, 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, + Tk_Window tkwin, + 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}, {x + w, y}}; + CGPoint bottom[2] = {{x, y + h}, {x + w, y + h}}; + CGPoint separator[2] = {{x + w, y + 3}, {x + w, 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[] = { + {kThemeButtonMixed, TTK_STATE_ALTERNATE, 0}, + {kThemeButtonOn, TTK_STATE_SELECTED, 0}, + {kThemeButtonOff, 0, 0} + + /* + * Others: kThemeDisclosureRight, kThemeDisclosureDown, + * kThemeDisclosureLeft + */ + +}; +static Ttk_StateTable ButtonAdornmentTable[] = { + {kThemeAdornmentDefault | kThemeAdornmentFocus, + TTK_STATE_ALTERNATE | TTK_STATE_FOCUS, 0}, + {kThemeAdornmentDefault, TTK_STATE_ALTERNATE, 0}, + {kThemeAdornmentNone, TTK_STATE_ALTERNATE, 0}, + {kThemeAdornmentFocus, TTK_STATE_FOCUS, 0}, + {kThemeAdornmentNone, 0, 0} +}; + +/*---------------------------------------------------------------------- + * +++ computeButtonDrawInfo -- + * + * Fill in an appearance manager HIThemeButtonDrawInfo record. + */ + +static inline HIThemeButtonDrawInfo computeButtonDrawInfo( + ThemeButtonParams *params, + Ttk_State state, + Tk_Window tkwin) +{ + + /* + * See ButtonElementDraw for the explanation of why we always draw + * PushButtons in the active state. + */ + + SInt32 HIThemeState; + + 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, + void *elementRecord, + Tk_Window tkwin, + int *minWidth, + int *minHeight, + Ttk_Padding *paddingPtr) +{ + ThemeButtonParams *params = clientData; + + if (params->heightMetric != NoThemeMetric) { + ChkErr(GetThemeMetric, params->heightMetric, 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; + + /* + * The minwidth must be 0 to force the generic ttk code to compute 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, + void *elementRecord, + Tk_Window tkwin, + int *minWidth, + int *minHeight, + Ttk_Padding *paddingPtr) +{ + ThemeButtonParams *params = clientData; + const HIThemeButtonDrawInfo info = + computeButtonDrawInfo(params, 0, tkwin); + static const CGRect scratchBounds = {{0, 0}, {100, 100}}; + CGRect contentBounds, backgroundBounds; + int verticalPad; + + ButtonElementMinSize(clientData, elementRecord, tkwin, + minWidth, minHeight, paddingPtr); + + /* + * 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, + void *elementRecord, + Tk_Window tkwin, + Drawable d, + Ttk_Box b, + Ttk_State state) +{ + ThemeButtonParams *params = 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 { + + /* + * 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}, + {kThemeTabNonFrontInactive, TTK_STATE_BACKGROUND}, + {kThemeTabFrontUnavailable, TTK_STATE_DISABLED | TTK_STATE_SELECTED}, + {kThemeTabNonFrontUnavailable, TTK_STATE_DISABLED}, + {kThemeTabFront, TTK_STATE_SELECTED}, + {kThemeTabNonFrontPressed, TTK_STATE_PRESSED}, + {kThemeTabNonFront, 0} +}; +static Ttk_StateTable TabAdornmentTable[] = { + {kHIThemeTabAdornmentNone, TTK_STATE_FIRST_TAB | TTK_STATE_LAST_TAB}, + {kHIThemeTabAdornmentTrailingSeparator, TTK_STATE_FIRST_TAB}, + {kHIThemeTabAdornmentNone, TTK_STATE_LAST_TAB}, + {kHIThemeTabAdornmentTrailingSeparator, 0}, +}; +static Ttk_StateTable TabPositionTable[] = { + {kHIThemeTabPositionOnly, TTK_STATE_FIRST_TAB | TTK_STATE_LAST_TAB}, + {kHIThemeTabPositionFirst, TTK_STATE_FIRST_TAB}, + {kHIThemeTabPositionLast, TTK_STATE_LAST_TAB}, + {kHIThemeTabPositionMiddle, 0}, +}; + +/* + * Apple XHIG Tab View Specifications: + * + * Control sizes: Tab views are available in regular, small, and mini sizes. + * The tab height is fixed for each size, but you control the size of the pane + * area. The tab heights for each size are listed below: + * - Regular size: 20 pixels. + * - Small: 17 pixels. + * - Mini: 15 pixels. + * + * Label spacing and fonts: The tab labels should be in a font that’s + * proportional to the size of the tab view control. In addition, the label + * should be placed so that there are equal margins of space before and after + * it. The guidelines below provide the specifications you should use for tab + * labels: + * - Regular size: System font. Center in tab, leaving 12 pixels on each + *side. + * - Small: Small system font. Center in tab, leaving 10 pixels on each side. + * - Mini: Mini system font. Center in tab, leaving 8 pixels on each side. + * + * Control spacing: Whether you decide to inset a tab view in a window or + * extend its edges to the window sides and bottom, you should place the top + * edge of the tab view 12 or 14 pixels below the bottom edge of the title bar + * (or toolbar, if there is one). If you choose to inset a tab view in a + * window, you should leave a margin of 20 pixels between the sides and bottom + * of the tab view and the sides and bottom of the window (although 16 pixels + * is also an acceptable margin-width). If you need to provide controls below + * the tab view, leave enough space below the tab view so the controls are 20 + * pixels above the bottom edge of the window and 12 pixels between the tab + * view and the controls. + * + * If you choose to extend the tab view sides and bottom so that they meet the + * window sides and bottom, you should leave a margin of at least 20 pixels + * between the content in the tab view and the tab-view edges. + * + * <URL: http://developer.apple.com/documentation/userexperience/Conceptual/ + * AppleHIGuidelines/XHIGControls/XHIGControls.html#//apple_ref/doc/uid/ + * TP30000359-TPXREF116> + */ + +static void TabElementSize( + void *clientData, + void *elementRecord, + Tk_Window tkwin, + int *minWidth, + int *minHeight, + Ttk_Padding *paddingPtr) +{ + GetThemeMetric(kThemeMetricLargeTabHeight, (SInt32 *) minHeight); + *paddingPtr = Ttk_MakePadding(0, 0, 0, 2); + +} + +static void TabElementDraw( + void *clientData, + void *elementRecord, + Tk_Window tkwin, + Drawable d, + Ttk_Box b, + Ttk_State state) +{ + CGRect bounds = BoxToRect(d, b); + HIThemeTabDrawInfo info = { + .version = 1, + .style = Ttk_StateTableLookup(TabStyleTable, state), + .direction = kThemeTabNorth, + .size = kHIThemeTabSizeNormal, + .adornment = Ttk_StateTableLookup(TabAdornmentTable, state), + .kind = kHIThemeTabKindNormal, + .position = Ttk_StateTableLookup(TabPositionTable, state), + }; + + BEGIN_DRAWING(d) + if (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( + void *clientData, + void *elementRecord, + Tk_Window tkwin, + int *minWidth, + int *minHeight, + Ttk_Padding *paddingPtr) +{ + *paddingPtr = Ttk_MakePadding(9, 5, 9, 9); +} + +static void PaneElementDraw( + void *clientData, + void *elementRecord, + Tk_Window tkwin, + Drawable d, + Ttk_Box b, + Ttk_State state) +{ + CGRect bounds = BoxToRect(d, b); + + bounds.origin.y -= kThemeMetricTabFrameOverlap; + bounds.size.height += kThemeMetricTabFrameOverlap; + BEGIN_DRAWING(d) + if ([NSApp macMinorVersion] > 8) { + 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( + void *clientData, + void *elementRecord, + Tk_Window tkwin, + int *minWidth, + int *minHeight, + Ttk_Padding *paddingPtr) +{ + *paddingPtr = Ttk_UniformPadding(4); +} + +static void GroupElementDraw( + void *clientData, + void *elementRecord, + Tk_Window tkwin, + Drawable d, + Ttk_Box b, + Ttk_State state) +{ + CGRect bounds = BoxToRect(d, b); + + BEGIN_DRAWING(d) + if ([NSApp macMinorVersion] > 8) { + 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}, + {0} +}; + +static void EntryElementSize( + void *clientData, + void *elementRecord, + Tk_Window tkwin, + int *minWidth, + int *minHeight, + Ttk_Padding *paddingPtr) +{ + *paddingPtr = Ttk_MakePadding(7, 5, 7, 6); +} + +static void EntryElementDraw( + void *clientData, + void *elementRecord, + Tk_Window tkwin, + Drawable d, + Ttk_Box b, + Ttk_State state) +{ + EntryElement *e = 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 macMinorVersion] > 8) { + 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, 2, 20, 2}; + +static void ComboboxElementSize( + void *clientData, + void *elementRecord, + Tk_Window tkwin, + int *minWidth, + int *minHeight, + Ttk_Padding *paddingPtr) +{ + *minWidth = 24; + *minHeight = 23; + *paddingPtr = ComboboxPadding; +} + +static void ComboboxElementDraw( + void *clientData, + 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) + bounds.origin.y += 1; + if (TkMacOSXInDarkMode(tkwin)) { + bounds.size.height += 1; + DrawDarkButton(bounds, info.kind, state, dc.context); + } else if ([NSApp macMinorVersion] > 8) { + if ((state & TTK_STATE_BACKGROUND) && + !(state & TTK_STATE_DISABLED)) { + NSColor *background = [NSColor textBackgroundColor]; + CGRect innerBounds = CGRectInset(bounds, 1, 2); + 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 SpinButtonUpElementSize( + void *clientData, + void *elementRecord, + Tk_Window tkwin, + int *minWidth, + int *minHeight, + 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( + void *clientData, + 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, + SpinButtonUpElementSize, + SpinButtonUpElementDraw +}; +static void SpinButtonDownElementSize( + void *clientData, + void *elementRecord, + Tk_Window tkwin, + int *minWidth, + int *minHeight, + 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 SpinButtonDownElementDraw( + void *clientData, + 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 *= 2; + 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, + SpinButtonDownElementSize, + SpinButtonDownElementDraw +}; + +/*---------------------------------------------------------------------- + * +++ DrawThemeTrack-based elements -- + * + * Progress bars and scales. (See also: <<NOTE-TRACKS>>) + */ + +/* + * Apple does not change the appearance of a slider when the window becomes + * inactive. So we shouldn't either. + */ + +static Ttk_StateTable ThemeTrackEnableTable[] = { + {kThemeTrackDisabled, TTK_STATE_DISABLED, 0}, + {kThemeTrackActive, TTK_STATE_BACKGROUND, 0}, + {kThemeTrackActive, 0, 0} + /* { kThemeTrackNothingToScroll, ?, ? }, */ +}; + +typedef struct { /* TrackElement client data */ + ThemeTrackKind kind; + SInt32 thicknessMetric; +} TrackElementData; + +static TrackElementData ScaleData = { + kThemeSlider, kThemeMetricHSliderHeight +}; + +typedef struct { + Tcl_Obj *fromObj; /* minimum value */ + Tcl_Obj *toObj; /* maximum value */ + Tcl_Obj *valueObj; /* current value */ + Tcl_Obj *orientObj; /* horizontal / vertical */ +} TrackElement; + +static Ttk_ElementOptionSpec TrackElementOptions[] = { + {"-from", TK_OPTION_DOUBLE, Tk_Offset(TrackElement, fromObj)}, + {"-to", TK_OPTION_DOUBLE, Tk_Offset(TrackElement, toObj)}, + {"-value", TK_OPTION_DOUBLE, Tk_Offset(TrackElement, valueObj)}, + {"-orient", TK_OPTION_STRING, Tk_Offset(TrackElement, orientObj)}, + {0, 0, 0} +}; +static void TrackElementSize( + void *clientData, + void *elementRecord, + Tk_Window tkwin, + int *minWidth, + int *minHeight, + Ttk_Padding *paddingPtr) +{ + TrackElementData *data = 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 = clientData; + TrackElement *elem = elementRecord; + int orientation = TTK_ORIENT_HORIZONTAL; + double from = 0, to = 100, value = 0, factor; + + Ttk_GetOrientFromObj(NULL, elem->orientObj, &orientation); + Tcl_GetDoubleFromObj(NULL, elem->fromObj, &from); + Tcl_GetDoubleFromObj(NULL, elem->toObj, &to); + Tcl_GetDoubleFromObj(NULL, elem->valueObj, &value); + factor = RangeToFactor(to - from); + + HIThemeTrackDrawInfo info = { + .version = 0, + .kind = data->kind, + .bounds = BoxToRect(d, b), + .min = from * factor, + .max = to * factor, + .value = value * factor, + .attributes = kThemeTrackShowThumb | + (orientation == TTK_ORIENT_HORIZONTAL ? + kThemeTrackHorizontal : 0), + .enableState = Ttk_StateTableLookup(ThemeTrackEnableTable, state), + .trackInfo.progress.phase = 0, + }; + + if (info.kind == kThemeSlider) { + info.trackInfo.slider.pressState = state & TTK_STATE_PRESSED ? + kThemeThumbPressed : 0; + if (state & TTK_STATE_ALTERNATE) { + info.trackInfo.slider.thumbDir = kThemeThumbDownward; + } else { + info.trackInfo.slider.thumbDir = kThemeThumbPlain; + } + } + BEGIN_DRAWING(d) + if (TkMacOSXInDarkMode(tkwin)) { + CGRect bounds = BoxToRect(d, b); + NSColorSpace *deviceRGB = [NSColorSpace deviceRGBColorSpace]; + NSColor *trackColor = [NSColor colorWithColorSpace: deviceRGB + components: darkTrack + count: 4]; + if (orientation == TTK_ORIENT_HORIZONTAL) { + bounds = CGRectInset(bounds, 1, bounds.size.height / 2 - 2); + } else { + bounds = CGRectInset(bounds, bounds.size.width / 2 - 3, 2); + } + 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 -- <<NOTE-TRACKS>> + * + * Has geometry only. The Scale widget adjusts the position of this element, + * and uses it for hit detection. In the Aqua theme, the slider is actually + * drawn as part of the trough element. + * + */ + +static void SliderElementSize( + void *clientData, + void *elementRecord, + Tk_Window tkwin, + int *minWidth, + int *minHeight, + 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"}, + {0, 0, 0, 0} +}; +static void PbarElementSize( + void *clientData, + void *elementRecord, + Tk_Window tkwin, + int *minWidth, + int *minHeight, + Ttk_Padding *paddingPtr) +{ + SInt32 size = 24; /* @@@ Check HIG for correct default */ + + ChkErr(GetThemeMetric, kThemeMetricLargeProgressBarThickness, &size); + *minWidth = *minHeight = size; +} + +static void PbarElementDraw( + void *clientData, + void *elementRecord, + Tk_Window tkwin, + Drawable d, + Ttk_Box b, + Ttk_State state) +{ + PbarElement *pbar = elementRecord; + int orientation = TTK_ORIENT_HORIZONTAL, phase = 0; + double value = 0, maximum = 100, factor; + + Ttk_GetOrientFromObj(NULL, pbar->orientObj, &orientation); + Tcl_GetDoubleFromObj(NULL, pbar->valueObj, &value); + Tcl_GetDoubleFromObj(NULL, pbar->maximumObj, &maximum); + Tcl_GetIntFromObj(NULL, pbar->phaseObj, &phase); + factor = RangeToFactor(maximum); + + HIThemeTrackDrawInfo info = { + .version = 0, + .kind = + (!strcmp("indeterminate", + Tcl_GetString(pbar->modeObj)) && value) ? + kThemeIndeterminateBar : kThemeProgressBar, + .bounds = BoxToRect(d, b), + .min = 0, + .max = maximum * factor, + .value = value * factor, + .attributes = kThemeTrackShowThumb | + (orientation == TTK_ORIENT_HORIZONTAL ? + kThemeTrackHorizontal : 0), + .enableState = Ttk_StateTableLookup(ThemeTrackEnableTable, state), + .trackInfo.progress.phase = phase, + }; + + BEGIN_DRAWING(d) + if (TkMacOSXInDarkMode(tkwin)) { + CGRect bounds = BoxToRect(d, b); + NSColorSpace *deviceRGB = [NSColorSpace deviceRGBColorSpace]; + NSColor *trackColor = [NSColor colorWithColorSpace: deviceRGB + components: darkTrack + count: 4]; + if (orientation == TTK_ORIENT_HORIZONTAL) { + bounds = CGRectInset(bounds, 1, bounds.size.height / 2 - 3); + } else { + bounds = CGRectInset(bounds, bounds.size.width / 2 - 3, 1); + } + 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"}, + {0, 0, 0, 0} +}; +static void TroughElementSize( + void *clientData, + void *elementRecord, + Tk_Window tkwin, + int *minWidth, + int *minHeight, + Ttk_Padding *paddingPtr) +{ + ScrollbarElement *scrollbar = 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 macMinorVersion] > 7) { + *paddingPtr = Ttk_MakePadding(4, 4, 4, 3); + } + } else { + *minWidth = thickness; + if ([NSApp macMinorVersion] > 7) { + *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( + void *clientData, + void *elementRecord, + Tk_Window tkwin, + Drawable d, + Ttk_Box b, + Ttk_State state) +{ + ScrollbarElement *scrollbar = 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 macMinorVersion] > 8) { + 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( + void *clientData, + void *elementRecord, + Tk_Window tkwin, + int *minWidth, + int *minHeight, + Ttk_Padding *paddingPtr) +{ + ScrollbarElement *scrollbar = 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( + void *clientData, + void *elementRecord, + Tk_Window tkwin, + Drawable d, + Ttk_Box b, + Ttk_State state) +{ + ScrollbarElement *scrollbar = 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 macMinorVersion] > 8) { + 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 macMinorVersion] < 8 || + 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( + void *clientData, + void *elementRecord, + Tk_Window tkwin, + int *minWidth, + int *minHeight, + Ttk_Padding *paddingPtr) +{ + if ([NSApp macMinorVersion] < 8) { + *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( + void *clientData, + void *elementRecord, + Tk_Window tkwin, + int *minWidth, + int *minHeight, + Ttk_Padding *paddingPtr) +{ + *minWidth = *minHeight = 1; +} + +static void SeparatorElementDraw( + void *clientData, + void *elementRecord, + Tk_Window tkwin, + Drawable d, + Ttk_Box b, + unsigned int state) +{ + CGRect bounds = BoxToRect(d, b); + const HIThemeSeparatorDrawInfo info = { + .version = 0, + /* Separator only supports kThemeStateActive, kThemeStateInactive */ + .state = Ttk_StateTableLookup(ThemeStateTable, + state & TTK_STATE_BACKGROUND), + }; + + BEGIN_DRAWING(d) + if (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( + void *clientData, + void *elementRecord, + Tk_Window tkwin, + int *minWidth, + int *minHeight, + 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( + void *clientData, + void *elementRecord, + Tk_Window tkwin, + Drawable d, + Ttk_Box b, + unsigned int state) +{ + CGRect bounds = BoxToRect(d, b); + HIThemeGrowBoxDrawInfo info = { + .version = 0, + /* Grow box only supports kThemeStateActive, kThemeStateInactive */ + .state = Ttk_StateTableLookup(ThemeStateTable, + state & TTK_STATE_BACKGROUND), + .kind = kHIThemeGrowBoxKindNormal, + .direction = sizegripGrowDirection, + .size = kHIThemeGrowBoxSizeNormal, + }; + + BEGIN_DRAWING(d) + ChkErr(HIThemeDrawGrowBox, &bounds.origin, &info, dc.context, + HIOrientation); + END_DRAWING +} + +static Ttk_ElementSpec SizegripElementSpec = { + TK_STYLE_VERSION_2, + sizeof(NullElement), + TtkNullElementOptions, + SizegripElementSize, + SizegripElementDraw +}; + +/*---------------------------------------------------------------------- + * +++ Background and fill elements -- + * + * Before drawing any ttk widget, its bounding rectangle is filled with a + * background color. This color must match the background color of the + * containing widget to avoid looking ugly. The need for care when doing + * this is exacerbated by the fact that ttk enforces its "native look" by + * not allowing user control of the background or highlight colors of ttk + * widgets. + * + * This job is made more complicated in recent versions of macOS by the + * fact that the Appkit GroupBox (used for ttk LabelFrames) and + * TabbedPane (used for the Notebook widget) both place their content + * inside a rectangle with rounded corners that has a color which + * contrasts with the dialog background color. Moreover, although the + * Apple human interface guidelines recommend against doing so, there are + * times when one wants to nest these widgets, for example 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( + void *clientData, + void *elementRecord, + Tk_Window tkwin, + Drawable d, + Ttk_Box b, + Ttk_State state) +{ + CGRect bounds = BoxToRect(d, b); + + if ([NSApp macMinorVersion] > 8) { + 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); + //QDSetPatternOrigin(PatternOrigin(tkwin, d)); + CGContextFillRect(dc.context, bounds); + END_DRAWING + } +} + +static void BackgroundElementDraw( + void *clientData, + void *elementRecord, + Tk_Window tkwin, + Drawable d, + Ttk_Box b, + unsigned int state) +{ + FillElementDraw(clientData, elementRecord, tkwin, d, Ttk_WinBox(tkwin), + state); +} + +static Ttk_ElementSpec FillElementSpec = { + TK_STYLE_VERSION_2, + sizeof(NullElement), + TtkNullElementOptions, + TtkNullElementSize, + FillElementDraw +}; +static Ttk_ElementSpec BackgroundElementSpec = { + TK_STYLE_VERSION_2, + sizeof(NullElement), + TtkNullElementOptions, + TtkNullElementSize, + BackgroundElementDraw +}; + +/*---------------------------------------------------------------------- + * +++ ToolbarBackground element -- toolbar style for frames. + * + * This is very similar to the normal background element, but uses a + * different ThemeBrush in order to get the lighter pinstripe effect + * used in toolbars. We use SetThemeBackground() rather than + * ApplyThemeBackground() in order to get the right style. + * + * <URL: http://developer.apple.com/documentation/Carbon/Reference/ + * Appearance_Manager/appearance_manager/constant_7.html#/ + * /apple_ref/doc/uid/TP30000243/C005321> + * + */ + +static void ToolbarBackgroundElementDraw( + void *clientData, + void *elementRecord, + Tk_Window tkwin, + Drawable d, + Ttk_Box b, + Ttk_State state) +{ + ThemeBrush brush = kThemeBrushToolbarBackground; + CGRect bounds = BoxToRect(d, Ttk_WinBox(tkwin)); + + BEGIN_DRAWING(d) + ChkErr(HIThemeSetFill, brush, NULL, dc.context, HIOrientation); + //QDSetPatternOrigin(PatternOrigin(tkwin, d)); + CGContextFillRect(dc.context, bounds); + END_DRAWING +} + +static Ttk_ElementSpec ToolbarBackgroundElementSpec = { + TK_STYLE_VERSION_2, + sizeof(NullElement), + TtkNullElementOptions, + TtkNullElementSize, + ToolbarBackgroundElementDraw +}; + +/*---------------------------------------------------------------------- + * +++ Field elements -- + * + * Used for the Treeview widget. This is like the BackgroundElement + * except that the fieldbackground color is configureable. + */ + +typedef struct { + Tcl_Obj *backgroundObj; +} FieldElement; + +static Ttk_ElementOptionSpec FieldElementOptions[] = { + {"-fieldbackground", TK_OPTION_BORDER, + Tk_Offset(FieldElement, backgroundObj), "white"}, + {NULL, 0, 0, NULL} +}; + +static void FieldElementDraw( + void *clientData, + void *elementRecord, + Tk_Window tkwin, + Drawable d, + Ttk_Box b, + Ttk_State state) +{ + FieldElement *e = 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}, + {kThemeButtonOn, TTK_STATE_SELECTED}, + {kThemeButtonOff, 0} +}; + +static Ttk_StateTable TreeHeaderAdornmentTable[] = { + {kThemeAdornmentHeaderButtonSortUp, + TTK_STATE_ALTERNATE | TTK_TREEVIEW_STATE_SORTARROW}, + {kThemeAdornmentDefault, + TTK_STATE_SELECTED | TTK_TREEVIEW_STATE_SORTARROW}, + {kThemeAdornmentHeaderButtonNoSortArrow, TTK_STATE_ALTERNATE}, + {kThemeAdornmentHeaderButtonNoSortArrow, TTK_STATE_SELECTED}, + {kThemeAdornmentFocus, TTK_STATE_FOCUS}, + {kThemeAdornmentNone, 0} +}; + +static void TreeAreaElementSize ( + void *clientData, + void *elementRecord, + Tk_Window tkwin, + int *minWidth, + 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 macMinorVersion] > 8) { + 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 macMinorVersion] > 8) { + *minHeight = 24; + } else { + ButtonElementSize(clientData, elementRecord, tkwin, minWidth, + minHeight, paddingPtr); + } +} + +static void TreeHeaderElementDraw( + void *clientData, + void *elementRecord, + Tk_Window tkwin, + Drawable d, + Ttk_Box b, + Ttk_State state) +{ + ThemeButtonParams *params = clientData; + CGRect bounds = BoxToRect(d, b); + const HIThemeButtonDrawInfo info = { + .version = 0, + .state = Ttk_StateTableLookup(ThemeStateTable, state), + .kind = params->kind, + .value = Ttk_StateTableLookup(TreeHeaderValueTable, state), + .adornment = Ttk_StateTableLookup(TreeHeaderAdornmentTable, state), + }; + + BEGIN_DRAWING(d) + if ([NSApp macMinorVersion] > 8) { + + /* + * 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( + void *clientData, + void *elementRecord, + Tk_Window tkwin, + int *minWidth, + int *minHeight, + Ttk_Padding *paddingPtr) +{ + SInt32 s; + + ChkErr(GetThemeMetric, kThemeMetricDisclosureTriangleWidth, &s); + *minWidth = s; + ChkErr(GetThemeMetric, kThemeMetricDisclosureTriangleHeight, &s); + *minHeight = s; +} + +static void DisclosureElementDraw( + void *clientData, + void *elementRecord, + Tk_Window tkwin, + Drawable d, + Ttk_Box b, + Ttk_State state) +{ + if (!(state & TTK_TREEVIEW_STATE_LEAF)) { + 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) + 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_EXPAND | TTK_FILL_BOTH, + TTK_NODE("Notebook.label", TTK_EXPAND | 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_EXPAND | TTK_FILL_X, + TTK_NODE("Spinbox.textarea", TTK_EXPAND | TTK_FILL_X))) + +/* Progress bars -- track only */ +TTK_LAYOUT("TProgressbar", + TTK_NODE("Progressbar.track", TTK_EXPAND | 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_PACK_TOP | TTK_EXPAND | 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_PACK_LEFT | TTK_EXPAND | 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); + + /* + * <<NOTE-TRACKS>> + * 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: + */ |