Text Aliasing in Custom Drawn OSX Status Bar Items

Its fairly straightforward for an OSX app to add a status bar item:

// -1: variable length
var statusBarItem = NSStatusBar.systemStatusBar().statusItemWithLength(-1)

As of OSX 10.10, most drawing modes have been “softly” deprecated, including custom views. To quote the docs:

Custom views should not be set on a status item. The button property with a template image will allow proper styling of the status item in various states and contexts and should be used instead.

This isn’t a major issue for static status items which can set text via the button’s title or an image via the button’s image. Non-static status items will likely miss the ability to set custom views.

One workaround is to install a drawing handler for button’s image. The handler will be called within the appropriate drawing context. Note that this method is preferable over creating a “rendered” NSImage (lockFocus & draw) since lockFocus() will ignore text antialiasing.

let image = NSImage(size: mySize, flipped: false, drawingHandler: drawImage)
image.cacheMode = .Never //don't cache, always draw
statusItem.button.image = image

func drawImage(rect: NSRect) -> Bool {
  var context = NSGraphicsContext.currentContext()?.CGContext
  CGContextSaveGState(context!)

  //draw

  CGContextRestoreGState(context!)
}

Beginning with Yosemite, status items are required to deal with dark modes. If we supply a template image, Yosemite will automatically handle the display mode (a template image comprises only clear or greyscale pixels).

let image = NSImage(size: mySize, flipped: false, drawingHandler: drawImage)
image.setTemplate(true)
...

However, text aliasing isn’t optimal when template mode is turned on. Turns out that for best performance, text anti-aliasing uses a dash of color pixels which isn’t possible in pure-greyscale template mode. Note the differences in the images below:

label-with-template

setTemplate(true)

label-template-zoomed
label-no-template
setTemplate(false)
label-no-template-zoomed

In dark mode:

dark-label-template

setTemplate(true)

dark-label-template-zoomed
dark-label-no-template

setTemplate(false)
dark-label-no-template-zoomed

The solution? Don’t set template mode and handle display mode changes appropriately.

Update May 04

The app which prompted this study is now live on the app store.