Day 54: Tedious Rounded Views
Last post I wrote about transparent windows. I concluded that you only needed to write your own content view to make the window in any shape you like. Let’s suppose we want to have a rounded view with a pointing arrow on one of the four sides. The code is a bit tedious but might be a nice example of doing graphics in code wherever possible:
First we want to know the area in the view that will be the rounded rectangle, so we subtract on the specific side the width/height of the arrow. The contstants used should be self-explanatory.
- (NSRect)availableContentRect
{
NSRect contentRect = [self bounds];
if (arrowPosition == CHArrowPositionNone)
return NSZeroRect;
if (arrowPosition == CHArrowPositionLeft) {
contentRect.origin.x += kArrowSize;
contentRect.size.width -= kArrowSize;
} else if (arrowPosition == CHArrowPositionTop) {
contentRect.size.height -= kArrowSize;
} else if (arrowPosition == CHArrowPositionRight) {
contentRect.size.width -= kArrowSize;
} else if (arrowPosition == CHArrowPositionBottom) {
contentRect.origin.y += kArrowSize;
contentRect.size.height -= kArrowSize;
}
return contentRect;
}
Second, we construct the path using this rectangle. For those familiar with NSBezierPath it should be easy to understand albeit a bit tedious, as said. Again, the constants used need no explanation.
- (NSBezierPath *)backgroundPath
{
NSRect rect = [self availableContentRect];
if (NSEqualRects(NSZeroRect, rect))
return nil;
CGFloat minX = NSMinX(rect);
CGFloat maxX = NSMaxX(rect);
CGFloat minY = NSMinY(rect);
CGFloat maxY = NSMaxY(rect);
NSBezierPath *path = [NSBezierPath bezierPath];
[path moveToPoint:NSMakePoint(minX, minY+kCornerRadius)];
if ([self hasArrow] && arrowPosition == CHArrowPositionLeft) {
[path lineToPoint:NSMakePoint(minX, NSMidY(rect)-kArrowSize)];
[path relativeLineToPoint:NSMakePoint(-kArrowSize, kArrowSize)];
[path relativeLineToPoint:NSMakePoint(kArrowSize, kArrowSize)];
}
[path lineToPoint:NSMakePoint(minX, maxY-kCornerRadius)];
[path curveToPoint:NSMakePoint(minX+kCornerRadius, maxY)
controlPoint1:NSMakePoint(minX, maxY-kCornerRadius/2)
controlPoint2:NSMakePoint(minX+kCornerRadius/2, maxY)];
if ([self hasArrow] && arrowPosition == CHArrowPositionTop) {
[path lineToPoint:NSMakePoint(NSMidX(rect)-kArrowSize, NSMaxY(rect))];
[path relativeLineToPoint:NSMakePoint(kArrowSize, kArrowSize)];
[path relativeLineToPoint:NSMakePoint(kArrowSize, -kArrowSize)];
}
[path lineToPoint:NSMakePoint(maxX-kCornerRadius, maxY)];
[path curveToPoint:NSMakePoint(maxX, maxY-kCornerRadius)
controlPoint1:NSMakePoint(maxX-kCornerRadius/2,maxY)
controlPoint2:NSMakePoint(maxX, maxY-kCornerRadius/2)];
if ([self hasArrow] && arrowPosition == CHArrowPositionRight) {
[path lineToPoint:NSMakePoint(maxX, NSMidY(rect)+kArrowSize)];
[path relativeLineToPoint:NSMakePoint(kArrowSize, -kArrowSize)];
[path relativeLineToPoint:NSMakePoint(-kArrowSize, -kArrowSize)];
}
[path lineToPoint:NSMakePoint(maxX, minY+kCornerRadius)];
[path curveToPoint:NSMakePoint(maxX-kCornerRadius, minY)
controlPoint1:NSMakePoint(maxX, minY+kCornerRadius/2)
controlPoint2:NSMakePoint(maxX-kCornerRadius/2, minY)];
if ([self hasArrow] && arrowPosition == CHArrowPositionBottom) {
[path lineToPoint:NSMakePoint(NSMidX(rect)+kArrowSize, NSMinY(rect))];
[path relativeLineToPoint:NSMakePoint(-kArrowSize, -kArrowSize)];
[path relativeLineToPoint:NSMakePoint(-kArrowSize, kArrowSize)];
}
[path lineToPoint:NSMakePoint(minX+kCornerRadius, minY)];
[path lineToPoint:NSMakePoint(minX+kCornerRadius, minY)];
[path curveToPoint:NSMakePoint(minX, minY+kCornerRadius)
controlPoint1:NSMakePoint(minX+kCornerRadius/2, minY)
controlPoint2:NSMakePoint(minX,minY+kCornerRadius/2)];
[path closePath];
return path;
}
The last step of course is to draw the path. This’ll depend on how you’d like your window to look. You can go in all directions; mimic the MAAttachedWindow or the look I choose for in for example SlipCover and Sketch:

Again, a nice example of some custom drawing code using gradients and clipping paths:
- (void)drawRect:(NSRect)rect
{
[[NSColor clearColor] set];
NSRectFill([self bounds]);
NSBezierPath *path = [self backgroundPath];
NSColor *startColor = [NSColor colorWithDeviceWhite:0.871 alpha:0.9];
NSColor *endColor = [NSColor colorWithDeviceWhite:1.000 alpha:0.9];
NSGradient *gradient = [[NSGradient alloc] initWithColorsAndLocations:startColor, 0.0, endColor, 1.0, nil];
[[gradient autorelease] drawInBezierPath:path angle:90];
[NSGraphicsContext saveGraphicsState];
[[[NSColor whiteColor] colorWithAlphaComponent:0.5] set];
[path setLineWidth:2.0];
[path addClip];
[path stroke];
[NSGraphicsContext restoreGraphicsState];
}
And with that, I’m taking a few days off…