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