I'm implementing a custom NSMenuItem view that shows a highlight as the user mouses over it. To do this, the code calls NSRectFill
after setting [NSColor selectedMenuItemColor]
as the active color. However, I noticed that the result is not simply a solid color — it actually draws a gradient instead. Very nice, but wondering how this "magic" works — i.e. if I wanted to define my own color that didn't just draw solid, how would I?
2 Answers
I don't know how this actually works, but I found a way to replicate the behavior with custom gradients (or any other drawing operations). The "trick" is to use a CGPatternRef
, which allows you to specify a callback function for drawing the pattern. Normally, this callback function draws one "cell" of the pattern, but you can just specify a very large pattern size (e.g. CGFLOAT_MAX
) to be able to fill the entire area in one invocation of the callback.
To demonstrate the technique, here's a category on NSColor
that allows you to create a color from an NSGradient
. When you set
that color and then use it to fill an area, the gradient is drawn (linear, from bottom to top, but you can easily change that). This even works for stroking paths or filling non-rectangular paths, like [[NSBezierPath bezierPathWithOvalInRect:NSMakeRect(0, 0, 100, 100)] fill]
because NSBezierPath
automatically clips the drawing.
//NSColor+Gradient.h
#import <Cocoa/Cocoa.h>
@interface NSColor (Gradient)
+ (NSColor *)my_gradientColorWithGradient:(NSGradient *)gradient;
@end
//NSColor+Gradient.m
#import "NSColor+Gradient.h"
#import <objc/runtime.h>
static void DrawGradientPattern(void * info, CGContextRef context)
{
NSGraphicsContext *currentContext = [NSGraphicsContext currentContext];
CGRect clipRect = CGContextGetClipBoundingBox(context);
[NSGraphicsContext setCurrentContext:[NSGraphicsContext graphicsContextWithGraphicsPort:context flipped:NO]];
NSGradient *gradient = (__bridge NSGradient *)info;
[gradient drawInRect:NSRectFromCGRect(clipRect) angle:90.0];
[NSGraphicsContext setCurrentContext:currentContext];
}
@implementation NSColor (Gradient)
+ (NSColor *)my_gradientColorWithGradient:(NSGradient *)gradient
{
CGColorSpaceRef colorSpace = CGColorSpaceCreatePattern(NULL);
CGPatternCallbacks callbacks;
callbacks.drawPattern = &DrawGradientPattern;
callbacks.releaseInfo = NULL;
CGPatternRef pattern = CGPatternCreate((__bridge void *)(gradient), CGRectMake(0, 0, CGFLOAT_MAX, CGFLOAT_MAX), CGAffineTransformIdentity, CGFLOAT_MAX, CGFLOAT_MAX, kCGPatternTilingConstantSpacing, true, &callbacks);
const CGFloat components[4] = {1.0, 1.0, 1.0, 1.0};
CGColorRef cgColor = CGColorCreateWithPattern(colorSpace, pattern, components);
CGColorSpaceRelease(colorSpace);
NSColor *color = [NSColor colorWithCGColor:cgColor];
objc_setAssociatedObject(color, "gradient", gradient, OBJC_ASSOCIATION_RETAIN);
return color;
}
@end
Usage example:
NSArray *colors = @[ [NSColor redColor], [NSColor blueColor] ];
NSGradient *gradient = [[NSGradient alloc] initWithColors:colors];
NSColor *gradientColor = [NSColor my_gradientColorWithGradient:gradient];
[gradientColor set];
NSRectFill(NSMakeRect(0, 0, 100, 100));
[[NSBezierPath bezierPathWithOvalInRect:NSMakeRect(100, 0, 100, 100)] fill];
Result:

- 53,243
- 5
- 129
- 141
-
Awesome…yes, bridging over to the CGPattern functionality definitely gets closer! The only missing piece then is how it handles partial redrawing of the view. I.e. you are simply drawing the gradient within the current clip, but when "touching up" part of a view this may not line up with the previously drawn portion? Seems like either it would have to use internal knowledge of which view the thread is currently drawing, or maybe some pattern phase magic? – natevw Mar 26 '13 at 21:37
-
Hmm, true. Not sure how I'd approach that or if it's even possible (using public APIs)... – omz Mar 26 '13 at 22:13
-
Unfortunately can't test your code directly on 10.7, but I've confirmed two things: 1) at least when wrapping your code to draw the cgColor into the current Cocoa context's graphicsPort, it fits the gradient to the dirtyRect as feared, while 2) the selectedMenuItemColor does "magically" draw no matter the clip rect or drawn rect. Upvoting for best guess in the meanwhile though! – natevw Mar 26 '13 at 22:36
My best guess is that it is defined as some sort of pattern image, however this does not fully answer my question because it looks as though these patterns would normally be drawn tiled rather than stretched.
This is corroborated by an Apple engineer's post on cocoa-dev which states:
[[NSColor selectedMenuItemColor] set]; NSRectFill(someRect);
This works because the selectedMenuItemColor is a pattern that happens to draw a gradient. You could just as easily draw nearly anything with a pattern […]
He does not elaborate how these patterns can be drawn stretched instead of tiled, though, as the highlighted menu item background is. Another post in that thread claims it is a "special-case within the drawing code" but he may simply be speculating.

- 16,807
- 8
- 66
- 90