Search code examples
objective-ccocoadrawnsimage

How to draw a NSImage like images in NSButtons (with a deepness)?


Is there any way to draw an NSImage like images in NSButtons or other cocoa interface elements?

Here are examples: enter image description here enter image description here

Apple uses pdf's with black icons: enter image description here


Solution

  • If you simply want this effect to be applied when you use your own images in a button, use [myImage setTemplate:YES]. There is no built-in way to draw images with this effect outside of a button that has the style shown in your screenshots.

    You can however replicate the effect using Core Graphics. If you look closely, the effect consists of a horizontal gradient, a white drop shadow and a dark inner shadow (the latter is the most difficult).

    You could implement this as a category on NSImage:

    //NSImage+EtchedDrawing.h:
    @interface NSImage (EtchedImageDrawing)    
    - (void)drawEtchedInRect:(NSRect)rect;
    @end
    
    //NSImage+EtchedDrawing.m:
    @implementation NSImage (EtchedImageDrawing)
    
    - (void)drawEtchedInRect:(NSRect)rect
    {
        NSSize size = rect.size;
        CGFloat dropShadowOffsetY = size.width <= 64.0 ? -1.0 : -2.0;
        CGFloat innerShadowBlurRadius = size.width <= 32.0 ? 1.0 : 4.0;
    
        CGContextRef c = [[NSGraphicsContext currentContext] graphicsPort];
    
        //save the current graphics state
        CGContextSaveGState(c);
    
        //Create mask image:
        NSRect maskRect = rect;
        CGImageRef maskImage = [self CGImageForProposedRect:&maskRect context:[NSGraphicsContext currentContext] hints:nil];
    
        //Draw image and white drop shadow:
        CGContextSetShadowWithColor(c, CGSizeMake(0, dropShadowOffsetY), 0, CGColorGetConstantColor(kCGColorWhite));
        [self drawInRect:maskRect fromRect:NSMakeRect(0, 0, self.size.width, self.size.height) operation:NSCompositeSourceOver fraction:1.0];
    
        //Clip drawing to mask:
        CGContextClipToMask(c, NSRectToCGRect(maskRect), maskImage);
    
        //Draw gradient:
        NSGradient *gradient = [[[NSGradient alloc] initWithStartingColor:[NSColor colorWithDeviceWhite:0.5 alpha:1.0]
                                                              endingColor:[NSColor colorWithDeviceWhite:0.25 alpha:1.0]] autorelease];
        [gradient drawInRect:maskRect angle:90.0];
        CGContextSetShadowWithColor(c, CGSizeMake(0, -1), innerShadowBlurRadius, CGColorGetConstantColor(kCGColorBlack));
    
        //Draw inner shadow with inverted mask:
        CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
        CGContextRef maskContext = CGBitmapContextCreate(NULL, CGImageGetWidth(maskImage), CGImageGetHeight(maskImage), 8, CGImageGetWidth(maskImage) * 4, colorSpace, kCGImageAlphaPremultipliedLast);
        CGColorSpaceRelease(colorSpace);
        CGContextSetBlendMode(maskContext, kCGBlendModeXOR);
        CGContextDrawImage(maskContext, maskRect, maskImage);
        CGContextSetRGBFillColor(maskContext, 1.0, 1.0, 1.0, 1.0);
        CGContextFillRect(maskContext, maskRect);
        CGImageRef invertedMaskImage = CGBitmapContextCreateImage(maskContext);
        CGContextDrawImage(c, maskRect, invertedMaskImage);
        CGImageRelease(invertedMaskImage);
        CGContextRelease(maskContext);
    
        //restore the graphics state
        CGContextRestoreGState(c);
    }
    
    @end
    

    Example usage in a view:

    - (void)drawRect:(NSRect)dirtyRect
    {
        [[NSColor colorWithDeviceWhite:0.8 alpha:1.0] set];
        NSRectFill(self.bounds);
    
        NSImage *image = [NSImage imageNamed:@"MyIcon.pdf"];
        [image drawEtchedInRect:self.bounds];
    }
    

    This would give you the following result (shown in different sizes): Screenshot

    You may need to experiment a bit with the gradient colors and offset/blur radius of the two shadows to get closer to the original effect.