How does +[NSColor selectedMenuItemColor] magicall

2020-07-17 15:00发布

问题:

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?

回答1:

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:



回答2:

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.