andymatuschak.org: Square Signals

This article was published on Wednesday, January 11th, 2006 at 2:05 pm.

Trey Songz ringtonesPeter Gabriel ringtones

Making the HUD, Item 1: A Frame-Themed Party

The History of the HUD

An important note: You really shouldn’t use private methods. Especially since it’s not necessary for this: check out HMBlkAppKit.

The first time I saw the HUD, I was really annoyed. It first appeared in iPhoto ‘05 as an Adjustments window. It was most primitive in this app, having few gradients or highlights about. I didn’t really understand what it was for or why Apple had added it.

Then I saw the HUD in Motion. It had been refined dramatically—it was clearly using a new implementation—and seemed suddenly much more useful. I wondered why.

The HUD appeared again in Aperture, Apple’s newest app. Mysteriously, Aperture has once again reimplemented the HUD interface, throwing out iPhoto’s and Motion’s implementations. What a waste of money. Anyway, it was in this app that I finally realized what the HUD was really for:

The HUD Isn’t That Bad, Really.

The HUD is useful as an inspector style in full-screen or large-window apps where one can’t afford to lose any real estate to an opaque inspector. Motion and Aperture have these huge canvases full of imagery; the designers evidently didn’t want anything obscuring them or getting in the way of the artist as he worked. The HUD was born. I think it’s a fantastic idea.

The only problem is that Apple hasn’t opened the HUD up to developers. In fact, thus far, they’ve reimplemented the style in every app they’ve published that uses it. This is a problem: the changes are minor, but they create confusing inconsistencies in the GUI and mean a lot for work for everyone down the line.

Now, a developer here or there is inevitably going to implement some small subset of the HUD for his app. Then another, then another, all with different implementations and slightly different looks that would be confusing as hell. I’ve decided that it’s my duty as an interface hacker to intervene and make a really good, complete implementation of my own that anyone can use. This is going to be tedious as hell, but I think it’ll be kind of fun. With any luck, I’ll get it to a releasable state.

The Plan of Attack

Most of these kind of hacks are made using custom content views and a bunch of images. This one’s made using a custom NSThemeFrame (bet you’ve never heard of that class before!), vectors, and CGShaders.

So I didn’t want to use custom NSWindow content view:—that’s a hack, and it would keep the guy using the framework from providing a content view of his own. Lame. So how do normal NSWindows draw their widgets and elements? There’s nothing in the docs about it, so I used a Cocoa hacker’s best friend: class-dump. I dumped AppKit, started looking through the classes, and it wasn’t long before I found a whole family of classes responsible for this kind of thing.

The grandfather’s NSFrameView; his son (the father) is NSTitledFrame; the prodigal son is NSThemeFrame, which is used in all standard Cocoa windows for what is called “frame view”, “border view”, and “theme view” in various methods.

Using a Custom NSThemeFrame

But how the hell do we use it? There’s no setFrameView: method on NSWindow. Searches on CocoaDev and the cocoa-dev mailing list turn up nothing—has no one done this before? The theme frame must be created on initialization, and the closest thing to “theme” we’ve got in NSWindow’s init method is the style mask. After searching AppKit’s private headers for “styleMask”, I found this selector in NSWindow:

+ (Class)frameViewClassForStyleMask:(unsigned int)fp8;

Bingo! So! Here’s what we’ve got to do:

  1. Subclass NSWindow (I call my subclass OHWindow; OH stands for OpenHUD).
  2. Make up a style mask constant for our custom appearance. Keep in mind that this is really unsafe, so try to pick a bitshift value that’s high (but still well under INT_MAX), and just pray that Apple doesn’t use it for something in the future. I picked 2 << 23, so don’t use that one!
  3. Create our OHWindow programmatically, OR’ing in the style mask constant we just made up. My code:
    [[OHWindow alloc] initWithContentRect:NSMakeRect(200, 200, 200, 200)
                                styleMask:NSTitledWindowMask | NSClosableWindowMask | NSResizableWindowMask | OHHUDWindowMask
                                  backing:NSBackingStoreBuffered defer:NO]
  4. Create a NSThemeFrame subclass (mine’s OHThemeFrame).
    • You’ll have to add the dumped NSThemeFrame, NSTitledFrame, and NSFrameView headers to your project.
    • In the first two, change the import line from #import <AppKit/something.h> to #import "something.h".
    • In NSFrameView, remove the NSView import.
  5. Override + frameViewClassForStyleMask: in our custom NSWindow to check (using AND) for the existence of this tag, returning our custom NSThemeFrame class if it does. Here’s my method:
    + (Class)frameViewClassForStyleMask:(unsigned int)styleMask
    {
        if (styleMask & OHHUDWindowMask)
            return [OHThemeFrame class];
        return [super frameViewClassForStyleMask:styleMask];
    }

The Background Color

First up for me was to get that nice dark gray translucence. I began by overriding - contentFill. This worked just fine, but we’ll need to implement our own background drawing method later to get the rounded corners and borders.

To get translucence, make sure you call setOpaque:NO on the window. I used (.12549, .12549, .12549, 1) for the background and then .95 alpha on the window itself. The color values in this article are acquired through screencaps, F-Script Anywhere, and trial-and-error, so bear with me.

The Window Body

So we want to draw some curved corners. This means overriding drawRect: and doing the window’s fill and borders ourselves. That’s okay, we’re up to it. Here’s the basic code I used:

- (int)titlebarHeight
{
     return 19;
}
 
- (void)drawRect:(NSRect)rect
{
    [[NSColor clearColor] set];
    NSRectFill(rect);
    [self _drawTitleBar:rect];
 
    [[self contentFill] set];
    NSRect frame = [self frame];
 
    NSRect pathRect = NSMakeRect(0, 0, NSWidth(frame), NSMaxY(frame) - [self titlebarHeight]);
    id path = [NSBezierPath bezierPathWithRoundedRect:pathRect
                                         cornerRadius:6.5
                                            inCorners:OSBottomLeftCorner | OSBottomRightCorner];
    [path fill];
}

bezierPathWithRoundedRect:cornerRadius:inCorners: is a special utility class I wrote for Pixen a while ago; it basically just makes rectangular beziers that are rounded in certain corners. You can grab the code (NSBezierPath+PXRoundedRectanglesAdditions.[h/m]) from Pixen’s repository or do it yourself easily enough with curveToPoint.

There’s a lot more code in the method for drawing the border with fancy clipping, but I’ll leave that as an exercise for the reader.

The Title Bar

The title bar’s the next big task, and it’s really divided into several parts. First, we’ll do the basic drawing. Looking around in some private headers, I found that the method to override is _drawTitleBar:. So I did that with code that looks something like this:

- (void)_drawTitleBar:(NSRect)rect
{
    NSRect frame = [self frame];
    NSRect titleRect = NSMakeRect(0, NSMaxY(frame) - [self titlebarHeight], NSWidth(frame), [self titlebarHeight]);
    id path = [NSBezierPath bezierPathWithRoundedRect:titleRect
                                         cornerRadius:[self topCornerRadius]
                                            inCorners:OSTopLeftCorner | OSTopRightCorner];
    [[NSColor colorWithDeviceRed:.239215 green:.239215
                            blue:.239215 alpha:.77647] set];
    [path fill];
}

Since our title bar is a little shorter than expected, we’ve gotta let something know what the new hitrect is for dragging. That’s accomplished through titlebarRect as follows:

- (NSRect)titlebarRect
{
    return NSMakeRect(0, NSMaxY([self bounds]) - [self titlebarHeight],
                      NSWidth([self bounds]), [self titlebarHeight]);
}

The Title String

Okay, first off, we’ve gotta use a different font. I figured out from more header rummaging that we’ve gotta override titleFont:

- titleFont
{
    return [NSFont boldSystemFontOfSize:10];
}

Also, the title string’s placed a little lower in HUD windows, so:

- (NSRect)_titlebarTitleRect
{
    return NSOffsetRect([super _titlebarTitleRect], 0, -1);
}

The Close Button

The close button was tricky. I basically started by doing this in the init method:

[[[self window] standardWindowButton:NSWindowCloseButton] setCell:myCustomCloseCell];

But that didn’t work: it turns out that title bar widgets have to respond to this really weird “obscured” accessor. In a fit of pure hackish-ness, I made a custom NSButtonCell with this in its @implementation:

- (void)setObscured:(BOOL)isObscured
{
    _isObscured = isObscured;
}
 
- (BOOL)isObscured
{
    return _isObscured;
}

Yeah. Not exactly elegant, but it worked. Back in init of OHThemeFrame, I used this code to get my own button cell up and running:

OHTitleBarButtonCell *closeCell = [[[OHTitleBarButtonCell alloc] initImageCell:[NSImage imageNamed:@"closeButton"]] autorelease];
[closeCell setButtonType:NSMomentaryChangeButton];
[closeCell setBordered:NO];
[closeCell setTarget:[self window]];
[closeCell setAction:@selector(orderOut:)];
[[[self window] standardWindowButton:NSWindowCloseButton] setCell:closeCell];

I got my hands on the closeButton image using F-Script Anywhere. While I was doing this, I made sure to hide the other buttons:

[[[self window] standardWindowButton:NSWindowMiniaturizeButton] setHidden:YES];
[[[self window] standardWindowButton:NSWindowZoomButton] setHidden:YES];

Now, we’ve got to get the button put in the right place, which involves overriding—you guessed it—another private method.

- (NSPoint)_closeButtonOrigin
{
    return NSMakePoint(2, NSMaxY([self frame]) - 17);
}

The Gradient

I’m not going to go into it too much here—drawing good vector-based gradients is the subject of another article—but the pro HUD windows have a gradient in their title bar when they’re key. I used CGShader and some screengrab-obtained color values to obtain the results you see.

Update: Chad Weider’s CTGradient is quite nice. Check it out.

The Resize Indicator

The last important thing to update is the resize indicator, which looks different in HUD windows, and which I ripped using F-Script Anywhere. The method to override here is _drawResizeIndicators:. Here’s my code:

- (void)_drawResizeIndicators:(NSRect)rect
{
    NSImage *resizeIndicator = [NSImage imageNamed:@"resizeIndicator"];
    NSPoint indicatorPoint = NSMakePoint(NSMaxX([self frame]) - 2 - [resizeIndicator size].width, 2);
    NSRect indicatorRect = (NSRect){NSZeroPoint, [resizeIndicator size]};
    [resizeIndicator drawAtPoint:indicatorPoint fromRect:indicatorRect
                       operation:NSCompositeSourceAtop fraction:1];
}

Our Methodology

Do you start to see a pattern here? When we’re trying to create hacked-up custom views, we prowl through class-dumped AppKit headers, find methods to override, and figure out what they’re supposed to do. This is a theme that will be repeating throughout the HUD kit exercise.

Got Thoughts?

By all means share them, and start the conversation.

Leave a Comment

Currently you have JavaScript disabled. In order to post comments, please make sure JavaScript and Cookies are enabled, and reload the page.

You can follow any responses to this entry via its RSS comments feed.

If you're looking for something specific then give the search form below a try:

RSS Wordpress Grady (theme) Return to the Top ↑