365Cocoa

A snippet of Objective-C code per day, 365 days long.
Written by Pieter Omvlee, developer and owner of Bohemian Coding.

May 20

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…


  1. 365cocoa posted this