iCurses

bad alignmentI’ve previously written about getting fixed-width fonts to display properly on the iPhone. Today I’d like to add a little to that discussion, and show how you might create a (very simple) ncurses-like display on the iPhone.

Header

The class declaration is pretty simple. The w and h members track the size of the display (in characters), while screen buffers the contents of the display as a simple byte array. The font and p members track the display’s typeface and current cursor position (in characters), respectively.

@interface CursesView : UIView 
{
	UIFont*			font;
	
	int			w;
	int			h;
	char*			screen;
	CGPoint			p;
}

- (void)clear;
- (void)move:(CGPoint)np;
- (void)printw:(char*)fmt, ...;

@end

Implementation

The implementation is also pretty straightforward. It begins with two initializer functions:

- (id)initWithFrame:(CGRect)frame
{
	if (self = [super initWithFrame:frame])
	{
		if (self.frame.size.width != 320)
		{
			// This class assumes that it's 320 points wide
			[self release];
			return nil;
		}
		font = [[UIFont fontWithName:@"CourierNewPSMT" size:16] retain];

		w = 33;
		h = floor(self.frame.size.height/[font pointSize]);
		screen = (char*) malloc(w*h+1);
	}
	return self;
}


- (id)initWithCoder:(NSCoder*)aDecoder
{
	if (self = [super initWithCoder:aDecoder])
	{
		if (self.frame.size.width != 320)
		{
			// This class assumes that it's 320 points wide
			[self release];
			return nil;
		}
		font = [[UIFont fontWithName:@"CourierNewPSMT" size:16] retain];
		
		w = 33;
		h = floor(self.frame.size.height/[font pointSize]);
		screen = (char*) malloc(w*h+1);
	}
	return self;
}

Both methods are needed: initWithCoder: for initialization from NIB files, and initWithFrame: for programmatic initialization. The methods don’t do anything too fancy — they do enforce a 320-point width assumption, since the font and display width in characters are hard-coded.

The drawRect: method is the real heart of the matter:

- (void)drawRect:(CGRect)rect
{
	// What to render, and how
	screen[w*h] = '\0';
	NSString* s = [NSString stringWithCString:screen encoding:NSASCIIStringEncoding];
	
	// Dimensions
	CGSize size = self.frame.size;
	
	// Get a pointer to context
	CGContextRef context = UIGraphicsGetCurrentContext();
	
	// Flop text rightside-up
	CGContextSetTextMatrix(context, CGAffineTransformMakeScale(1, -1));
	
	// Clear background
	CGContextSetFillColorWithColor(context, [[UIColor blackColor] CGColor]);
	CGContextFillRect(context, CGRectMake(0, 0, size.width, size.height));
	
	// Render text (Setup)
	NSUInteger ch = [font pointSize];
	CGContextSetTextDrawingMode(context, kCGTextFillStroke);
	CGContextSetLineWidth(context, 1.0);
	CGContextSelectFont(context, [[font fontName] cStringUsingEncoding:NSASCIIStringEncoding], ch, kCGEncodingMacRoman);
	[[UIColor greenColor] set];
	
	// Draw text
	NSString* ss;
	for (NSUInteger c = 0, n = w, l = [s length], row = ch; c < l; c = n, n += w, row += ch)
	{
		ss = [s substringWithRange:NSMakeRange(c, (n<l?n:l)-c)];
		CGContextShowTextAtPoint(context,
					 2,
					 row,
					 [ss cStringUsingEncoding:NSMacOSRomanStringEncoding],
					 [ss lengthOfBytesUsingEncoding:NSMacOSRomanStringEncoding]);
	}
}

While there’s not really anything here that we haven’t seen before, I would point out the “2” in the call to CGContextShowTextAtPoint(): This is an experimentally determined “magic value” used to (roughly) center 33 columns of 16-point text on a 320-point display.

Next up are the less-interesting “icurses” functions:

- (void)clear
{
	[self setNeedsDisplay];

	memset(screen, ' ', w*h);
	p = CGPointZero;
}


- (void)move:(CGPoint)np
{
	p = np;
}

Followed by printw:, which updates the screen buffer for later rendering:

- (void)printw:(char*)fmt, ...
{
	[self setNeedsDisplay];
	
	static char s[1024];
	
	va_list ap;
	va_start(ap, fmt);
	vsnprintf(s, sizeof(s), fmt, ap);
	va_end(ap);
	
	int i, l, r, c;
	for (i=0, l=strlen(s), r=p.y, c=p.x; i < l; i++)
	{
		if (s[i] == '\n')
		{
			r += 1; c = 0;
		}
		else if (s[i] == '\b')
		{
			if (c) c--;
		}
		else if (r*w+c < w*h)
		{
			screen[r*w+c] = s[i]; c += 1;
			if (c >= w)
			{
				r += 1; c = 0;
			}
		}
	}
	p.x = c;
	p.y = r;
}

Finally, dealloc handles any necessary cleanup:

- (void)dealloc
{
	[font release];
	if (screen) free(screen);
	[super dealloc];
}

Usage

You could draw to one of these views with code like the following:

- (void)render
{
	clear();
	move(3, 2); printw("------------------------");
	move(4, 1); printw("|");
	move(4, 15); printw("%10d",1);
	move(4, 26); printw("|");
	move(5, 1); printw("|");
	move(5, 15); printw("%10d",100);
	move(5, 26); printw("|");
	move(6, 1); printw("|");
	move(6, 15); printw("%10d",10000);
	move(6, 26); printw("|");
	move(7, 1); printw("|");
	move(7, 15); printw("%10d",1000000);
	move(7, 26); printw("|");
	move(8, 2); printw("------------------------");
}

… assuming that the render method belonged to a class with a CursesView member called display, and that you’d defined these convenience macros:

#define clear()			[display clear]
#define move(r,c)		[display move:CGPointMake((c), (r))]
#define printw(...)		[display printw:__VA_ARGS__]

Note that you don’t need to trigger drawing explicitly after invoking, e.g., printw: — there are [self setNeedsDisplay] calls embedded in the relevant functions.

Share and Enjoy:
  • Twitter
  • Facebook
  • Digg
  • Reddit
  • HackerNews
  • del.icio.us
  • Google Bookmarks
  • Slashdot
This entry was posted in iPhone. Bookmark the permalink.

Comments are closed.