about summary refs log tree commit diff stats
path: root/draw.c
blob: a3c526ede838a242392fa05987761669938eb3f8 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
/*
 * (C)opyright MMIV-MMVI Anselm R. Garbe <garbeam at gmail dot com>
 * See LICENSE file for license details.
 */

#include <stdio.h>
#include <string.h>

#include "draw.h"
#include "util.h"

static void
drawborder(Display *dpy, Brush *b)
{
	XPoint points[5];
	XSetLineAttributes(dpy, b->gc, 1, LineSolid, CapButt, JoinMiter);
	XSetForeground(dpy, b->gc, b->border);
	points[0].x = b->rect.x;
	points[0].y = b->rect.y;
	points[1].x = b->rect.width - 1;
	points[1].y = 0;
	points[2].x = 0;
	points[2].y = b->rect.height - 1;
	points[3].x = -(b->rect.width - 1);
	points[3].y = 0;
	points[4].x = 0;
	points[4].y = -(b->rect.height - 1);
	XDrawLines(dpy, b->drawable, b->gc, points, 5, CoordModePrevious);
}

void
draw(Display *dpy, Brush *b, Bool border, const char *text)
{
	unsigned int x, y, w, h, len;
	static char buf[256];
	XGCValues gcv;

	XSetForeground(dpy, b->gc, b->bg);
	XFillRectangles(dpy, b->drawable, b->gc, &b->rect, 1);

	if(border)
		drawborder(dpy, b);

	if(!text)
		return;

	len = strlen(text);
	if(len >= sizeof(buf))
		len = sizeof(buf) - 1;
	memcpy(buf, text, len);
	buf[len] = 0;

	h = b->font.ascent + b->font.descent;
	y = b->rect.y + (b->rect.height / 2) - (h / 2) + b->font.ascent;
	x = b->rect.x + (h / 2);

	/* shorten text if necessary */
	while(len && (w = textwidth_l(&b->font, buf, len)) > b->rect.width - h)
		buf[--len] = 0;

	if(w > b->rect.width)
		return; /* too long */

	gcv.foreground = b->fg;
	gcv.background = b->bg;
	if(b->font.set) {
		XChangeGC(dpy, b->gc, GCForeground | GCBackground, &gcv);
		XmbDrawImageString(dpy, b->drawable, b->font.set, b->gc,
				x, y, buf, len);
	}
	else {
		gcv.font = b->font.xfont->fid;
		XChangeGC(dpy, b->gc, GCForeground | GCBackground | GCFont, &gcv);
		XDrawImageString(dpy, b->drawable, b->gc, x, y, buf, len);
	}
}

static unsigned long
xloadcolors(Display *dpy, Colormap cmap, const char *colstr)
{
	XColor color;
	XAllocNamedColor(dpy, cmap, colstr, &color, &color);
	return color.pixel;
}

void
loadcolors(Display *dpy, int screen, Brush *b,
		const char *bg, const char *fg, const char *border)
{
	Colormap cmap = DefaultColormap(dpy, screen);
	b->bg = xloadcolors(dpy, cmap, bg);
	b->fg = xloadcolors(dpy, cmap, fg);
	b->border = xloadcolors(dpy, cmap, border);
}

unsigned int
textwidth_l(Fnt *font, char *text, unsigned int len)
{
	if(font->set) {
		XRectangle r;
		XmbTextExtents(font->set, text, len, 0, &r);
		return r.width;
	}
	return XTextWidth(font->xfont, text, len);
}

unsigned int
textwidth(Fnt *font, char *text)
{
	return textwidth_l(font, text, strlen(text));
}

void
loadfont(Display *dpy, Fnt *font, const char *fontstr)
{
	char **missing, *def;
	int n;

	missing = NULL;
	def = "?";
	setlocale(LC_ALL, "");
	if(font->set)
		XFreeFontSet(dpy, font->set);
	font->set = XCreateFontSet(dpy, fontstr, &missing, &n, &def);
	if(missing) {
		while(n--)
			fprintf(stderr, "missing fontset: %s\n", missing[n]);
		XFreeStringList(missing);
		if(font->set) {
			XFreeFontSet(dpy, font->set);
			font->set = NULL;
		}
	}
	if(font->set) {
		XFontSetExtents *font_extents;
		XFontStruct **xfonts;
		char **font_names;
		unsigned int i;

		font->ascent = font->descent = 0;
		font_extents = XExtentsOfFontSet(font->set);
		n = XFontsOfFontSet(font->set, &xfonts, &font_names);
		for(i = 0, font->ascent = 0, font->descent = 0; i < n; i++) {
			if(font->ascent < (*xfonts)->ascent)
				font->ascent = (*xfonts)->ascent;
			if(font->descent < (*xfonts)->descent)
				font->descent = (*xfonts)->descent;
			xfonts++;
		}
	}
	else {
		if(font->xfont)
			XFreeFont(dpy, font->xfont);
		font->xfont = NULL;
		font->xfont = XLoadQueryFont(dpy, fontstr);
		if (!font->xfont)
			font->xfont = XLoadQueryFont(dpy, "fixed");
		if (!font->xfont)
			error("error, cannot load 'fixed' font\n");
		font->ascent = font->xfont->ascent;
		font->descent = font->xfont->descent;
	}
	font->height = font->ascent + font->descent;
}

unsigned int
labelheight(Fnt *font)
{
	return font->height + 4;
}
s # initialize cursor to top of screen y <- get-address *result, before-cursor:offset *y <- copy *init # initial render to screen, just for some old tests _, _, screen, result <- render screen, result <editor-initialization> reply result ] recipe insert-text [ local-scope editor:address:editor-data <- next-ingredient text:address:array:character <- next-ingredient # early exit if text is empty reply-unless text, editor/same-as-ingredient:0 len:number <- length *text reply-unless len, editor/same-as-ingredient:0 idx:number <- copy 0 # now we can start appending the rest, character by character curr:address:duplex-list <- get *editor, data:offset { done?:boolean <- greater-or-equal idx, len break-if done? c:character <- index *text, idx insert-duplex c, curr # next iter curr <- next-duplex curr idx <- add idx, 1 loop } reply editor/same-as-ingredient:0 ] scenario editor-initializes-without-data [ assume-screen 5/width, 3/height run [ 1:address:editor-data <- new-editor 0/data, screen:address, 2/left, 5/right 2:editor-data <- copy *1:address:editor-data ] memory-should-contain [ # 2 (data) <- just the § sentinel # 3 (top of screen) <- the § sentinel 4 <- 0 # bottom-of-screen; null since text fits on screen # 5 (before cursor) <- the § sentinel 6 <- 2 # left 7 <- 4 # right (inclusive) 8 <- 1 # cursor row 9 <- 2 # cursor column ] screen-should-contain [ . . . . . . ] ] # last-row:number, last-column:number, screen, editor <- render screen:address, editor:address:editor-data # # Assumes cursor should be at coordinates (cursor-row, cursor-column) and # updates before-cursor to match. Might also move coordinates if they're # outside text. recipe render [ local-scope screen:address <- next-ingredient editor:address:editor-data <- next-ingredient reply-unless editor, 1/top, 0/left, screen/same-as-ingredient:0, editor/same-as-ingredient:1 left:number <- get *editor, left:offset screen-height:number <- screen-height screen right:number <- get *editor, right:offset # traversing editor curr:address:duplex-list <- get *editor, top-of-screen:offset prev:address:duplex-list <- copy curr # just in case curr becomes null and we can't compute prev-duplex curr <- next-duplex curr # traversing screen +render-loop-initialization color:number <- copy 7/white row:number <- copy 1/top column:number <- copy left cursor-row:address:number <- get-address *editor, cursor-row:offset cursor-column:address:number <- get-address *editor, cursor-column:offset before-cursor:address:address:duplex-list <- get-address *editor, before-cursor:offset screen <- move-cursor screen, row, column { +next-character break-unless curr off-screen?:boolean <- greater-or-equal row, screen-height break-if off-screen? # update editor-data.before-cursor # Doing so at the start of each iteration ensures it stays one step behind # the current character. { at-cursor-row?:boolean <- equal row, *cursor-row break-unless at-cursor-row? at-cursor?:boolean <- equal column, *cursor-column break-unless at-cursor? *before-cursor <- copy prev } c:character <- get *curr, value:offset <character-c-received> { # newline? move to left rather than 0 newline?:boolean <- equal c, 10/newline break-unless newline? # adjust cursor if necessary { at-cursor-row?:boolean <- equal row, *cursor-row break-unless at-cursor-row? left-of-cursor?:boolean <- lesser-than column, *cursor-column break-unless left-of-cursor? *cursor-column <- copy column *before-cursor <- prev-duplex curr } # clear rest of line in this window clear-line-delimited screen, column, right # skip to next line row <- add row, 1 column <- copy left screen <- move-cursor screen, row, column curr <- next-duplex curr prev <- next-duplex prev loop +next-character:label } { # at right? wrap. even if there's only one more letter left; we need # room for clicking on the cursor after it. at-right?:boolean <- equal column, right break-unless at-right? # print wrap icon print-character screen, 8617/loop-back-to-left, 245/grey column <- copy left row <- add row, 1 screen <- move-cursor screen, row, column # don't increment curr loop +next-character:label } print-character screen, c, color curr <- next-duplex curr prev <- next-duplex prev column <- add column, 1 loop } # save first character off-screen bottom-of-screen:address:address:duplex-list <- get-address *editor, bottom-of-screen:offset *bottom-of-screen <- copy curr # is cursor to the right of the last line? move to end { at-cursor-row?:boolean <- equal row, *cursor-row cursor-outside-line?:boolean <- lesser-or-equal column, *cursor-column before-cursor-on-same-line?:boolean <- and at-cursor-row?, cursor-outside-line? above-cursor-row?:boolean <- lesser-than row, *cursor-row before-cursor?:boolean <- or before-cursor-on-same-line?, above-cursor-row? break-unless before-cursor? *cursor-row <- copy row *cursor-column <- copy column *before-cursor <- copy prev } reply row, column, screen/same-as-ingredient:0, editor/same-as-ingredient:1 ] recipe clear-line-delimited [ local-scope screen:address <- next-ingredient column:number <- next-ingredient right:number <- next-ingredient { done?:boolean <- greater-than column, right break-if done? print-character screen, 32/space column <- add column, 1 loop } ] recipe clear-screen-from [ local-scope screen:address <- next-ingredient row:number <- next-ingredient column:number <- next-ingredient left:number <- next-ingredient right:number <- next-ingredient # if it's the real screen, use the optimized primitive { break-if screen clear-display-from row, column, left, right reply screen/same-as-ingredient:0 } # if not, go the slower route screen <- move-cursor screen, row, column clear-line-delimited screen, column, right clear-rest-of-screen screen, row, left, right reply screen/same-as-ingredient:0 ] recipe clear-rest-of-screen [ local-scope screen:address <- next-ingredient row:number <- next-ingredient left:number <- next-ingredient right:number <- next-ingredient row <- add row, 1 screen <- move-cursor screen, row, left screen-height:number <- screen-height screen { at-bottom-of-screen?:boolean <- greater-or-equal row, screen-height break-if at-bottom-of-screen? screen <- move-cursor screen, row, left clear-line-delimited screen, left, right row <- add row, 1 loop } ] scenario editor-initially-prints-multiple-lines [ assume-screen 5/width, 5/height run [ s:address:array:character <- new [abc def] new-editor s:address:array:character, screen:address, 0/left, 5/right ] screen-should-contain [ . . .abc . .def . . . ] ] scenario editor-initially-handles-offsets [ assume-screen 5/width, 5/height run [ s:address:array:character <- new [abc] new-editor s:address:array:character, screen:address, 1/left, 5/right ] screen-should-contain [ . . . abc . . . ] ] scenario editor-initially-prints-multiple-lines-at-offset [ assume-screen 5/width, 5/height run [ s:address:array:character <- new [abc def] new-editor s:address:array:character, screen:address, 1/left, 5/right ] screen-should-contain [ . . . abc . . def . . . ] ] scenario editor-initially-wraps-long-lines [ assume-screen 5/width, 5/height run [ s:address:array:character <- new [abc def] new-editor s:address:array:character, screen:address, 0/left, 5/right ] screen-should-contain [ . . .abc . .def . . . ] screen-should-contain-in-color 245/grey [ . . . . . . . . ] ] scenario editor-initially-wraps-barely-long-lines [ assume-screen 5/width, 5/height run [ s:address:array:character <- new [abcde] new-editor s:address:array:character, screen:address, 0/left, 5/right ] # still wrap, even though the line would fit. We need room to click on the # end of the line screen-should-contain [ . . .abcd. .e . . . ] screen-should-contain-in-color 245/grey [ . . . . . . . . ] ] scenario editor-initializes-empty-text [ assume-screen 5/width, 5/height run [ 1:address:array:character <- new [] 2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0/left, 5/right 3:number <- get *2:address:editor-data, cursor-row:offset 4:number <- get *2:address:editor-data, cursor-column:offset ] screen-should-contain [ . . . . . . ] memory-should-contain [ 3 <- 1 # cursor row 4 <- 0 # cursor column ] ] # just a little color for mu code scenario render-colors-comments [ assume-screen 5/width, 5/height run [ s:address:array:character <- new [abc # de f] new-editor s:address:array:character, screen:address, 0/left, 5/right ] screen-should-contain [ . . .abc . .# de . .f . . . ] screen-should-contain-in-color 12/lightblue, [ . . . . .# de . . . . . ] screen-should-contain-in-color 7/white, [ . . .abc . . . .f . . . ] ] after <character-c-received> [ color <- get-color color, c ] # color <- get-color color:number, c:character # so far the previous color is all the information we need; that may change recipe get-color [ local-scope color:number <- next-ingredient c:character <- next-ingredient color-is-white?:boolean <- equal color, 7/white # if color is white and next character is '#', switch color to blue { break-unless color-is-white? starting-comment?:boolean <- equal c, 35/# break-unless starting-comment? trace 90, [app], [switch color back to blue] color <- copy 12/lightblue jump +exit:label } # if color is blue and next character is newline, switch color to white { color-is-blue?:boolean <- equal color, 12/lightblue break-unless color-is-blue? ending-comment?:boolean <- equal c, 10/newline break-unless ending-comment? trace 90, [app], [switch color back to white] color <- copy 7/white jump +exit:label } # if color is white (no comments) and next character is '<', switch color to red { break-unless color-is-white? starting-assignment?:boolean <- equal c, 60/< break-unless starting-assignment? color <- copy 1/red jump +exit:label } # if color is red and next character is space, switch color to white { color-is-red?:boolean <- equal color, 1/red break-unless color-is-red? ending-assignment?:boolean <- equal c, 32/space break-unless ending-assignment? color <- copy 7/white jump +exit:label } # otherwise no change +exit reply color ] scenario render-colors-assignment [ assume-screen 8/width, 5/height run [ s:address:array:character <- new [abc d <- e f] new-editor s:address:array:character, screen:address, 0/left, 8/right ] screen-should-contain [ . . .abc . .d <- e . .f . . . ] screen-should-contain-in-color 1/red, [ . . . . . <- . . . . . ] ]