improved algorithm that draw menus
[xmenu] / xmenu.c
CommitLineData
a7732690 1#include <err.h>
2#include <stdio.h>
3#include <stdlib.h>
4#include <string.h>
5#include <unistd.h>
6#include <X11/Xlib.h>
7#include <X11/Xutil.h>
8
9/* macros */
10#define LEN(x) (sizeof (x) / sizeof (x[0]))
f1583285 11#define MAX(x,y) ((x)>(y)?(x):(y))
a7732690 12
13/* color enum */
14enum {ColorFG, ColorBG, ColorLast};
15
16/* draw context structure */
17struct DC {
18 unsigned long unpressed[ColorLast];
19 unsigned long pressed[ColorLast];
20 unsigned long decoration[ColorLast];
21
22 Drawable d;
23 GC gc;
24 XFontStruct *font;
25 int fonth;
26};
27
28/* menu geometry structure */
29struct Geometry {
30 int itemb; /* item border */
31 int itemw; /* item width */
32 int itemh; /* item height */
a80fee22 33 int border; /* window border width */
34 int separator; /* menu separator width */
a7732690 35};
36
37/* screen geometry structure */
38struct ScreenGeometry {
39 int cursx, cursy; /* cursor position */
40 int screenw, screenh; /* screen width and height */
41};
42
43/* menu item structure */
44struct Item {
45 char *label;
46 char *output;
7fbd1c5e 47 int y;
48 int h;
f1583285 49 size_t labellen;
a7732690 50 struct Item *next;
51 struct Menu *submenu;
52};
53
54/* menu structure */
55struct Menu {
56 struct Menu *parent;
d888f2ca 57 struct Item *caller;
a7732690 58 struct Item *list;
59 struct Item *selected;
60 int x, y, w, h;
61 unsigned level;
f15fc339 62 Drawable pixmap;
a7732690 63 Window win;
64};
65
66/* function declarations */
67static unsigned long getcolor(const char *s);
68static void setupdc(void);
69static void setupgeom(void);
70static void setupgrab(void);
a80fee22 71static struct Item *allocitem(const char *label, const char *output);
a7732690 72static struct Menu *allocmenu(struct Menu *parent, struct Item *list, unsigned level);
f15fc339 73static void getmenuitem(Window win, int y, struct Menu **menu_ret, struct Item **item_ret);
a7732690 74static void drawmenu(void);
75static void calcscreengeom(void);
76static void calcmenu(struct Menu *menu);
77static void setcurrmenu(struct Menu *currmenu_new);
78static void parsestdin(void);
79static void run(void);
f15fc339 80static void freewindow(struct Menu *menu);
a7732690 81static void cleanupexit(void);
82static void usage(void);
83
84/* X variables */
85static Colormap colormap;
86static Display *dpy;
87static Window rootwin;
88static int screen;
89static struct DC dc;
90
91/* menu variables */
92static struct Menu *rootmenu = NULL;
93static struct Menu *currmenu = NULL;
94
95/* geometry variables */
96static struct Geometry geom;
a80fee22 97static struct ScreenGeometry screengeom;
a7732690 98
99/* flag variables */
100static Bool override_redirect = True;
101
102#include "config.h"
103
104int
105main(int argc, char *argv[])
106{
107 int ch;
108
109 while ((ch = getopt(argc, argv, "w")) != -1) {
110 switch (ch) {
111 case 'w':
112 override_redirect = False;
113 break;
114 default:
115 usage();
116 break;
117 }
118 }
119 argc -= optind;
120 argv += optind;
121
122 /* open connection to server and set X variables */
123 if ((dpy = XOpenDisplay(NULL)) == NULL)
124 errx(1, "cannot open display");
125 screen = DefaultScreen(dpy);
126 rootwin = RootWindow(dpy, screen);
127 colormap = DefaultColormap(dpy, screen);
128
129 /* setup */
130 setupdc();
131 setupgeom();
132 setupgrab();
133
134 /* generate menus and recalculate them */
135 parsestdin();
136 if (rootmenu == NULL)
137 errx(1, "no menu generated");
138 calcscreengeom();
139 calcmenu(rootmenu);
140
141 /* run event loop */
142 run();
143
144 return 1; /* UNREACHABLE */
145}
146
147/* get color from color string */
148static unsigned long
149getcolor(const char *s)
150{
151 XColor color;
152
153 if(!XAllocNamedColor(dpy, colormap, s, &color, &color))
154 errx(1, "cannot allocate color: %s", s);
155 return color.pixel;
156}
157
158/* init draw context */
159static void
160setupdc(void)
161{
162 /* get color pixels */
163 dc.unpressed[ColorBG] = getcolor(UNPRESSEDBG);
164 dc.unpressed[ColorFG] = getcolor(UNPRESSEDFG);
165 dc.pressed[ColorBG] = getcolor(PRESSEDBG);
166 dc.pressed[ColorFG] = getcolor(PRESSEDFG);
167 dc.decoration[ColorBG] = getcolor(DECORATIONBG);
168 dc.decoration[ColorFG] = getcolor(DECORATIONFG);
169
170 /* try to get font */
171 if ((dc.font = XLoadQueryFont(dpy, FONT)) == NULL)
172 errx(1, "cannot load font");
173 dc.fonth = dc.font->ascent + dc.font->descent;
174
175 /* create GC and set its font */
176 dc.gc = XCreateGC(dpy, rootwin, 0, NULL);
177 XSetFont(dpy, dc.gc, dc.font->fid);
178}
179
180/* init menu geometry values */
181static void
182setupgeom(void)
183{
184 geom.itemb = ITEMB;
185 geom.itemh = dc.fonth + ITEMB * 2;
186 geom.itemw = ITEMW;
187 geom.border = BORDER;
a80fee22 188 geom.separator = SEPARATOR;
a7732690 189}
190
191/* grab pointer */
192static void
193setupgrab(void)
194{
195 XGrabPointer(dpy, rootwin, True, ButtonPressMask | ButtonReleaseMask,
196 GrabModeAsync, GrabModeAsync, None, None, CurrentTime);
197}
198
199/* allocate an item */
200static struct Item *
a80fee22 201allocitem(const char *label, const char *output)
a7732690 202{
203 struct Item *item;
204
205 if ((item = malloc(sizeof *item)) == NULL)
206 err(1, "malloc");
7fbd1c5e 207 if (*label == '\0') {
208 item->label = NULL;
209 item->output = NULL;
210 } else {
211 if ((item->label = strdup(label)) == NULL)
212 err(1, "strdup");
213 if ((item->output = strdup(output)) == NULL)
214 err(1, "strdup");
215 }
a80fee22 216 item->y = 0;
7fbd1c5e 217 item->h = item->label ? geom.itemh : geom.separator;
f1583285 218 if (item->label == NULL)
219 item->labellen = 0;
220 else
221 item->labellen = strlen(item->label);
a7732690 222 item->next = NULL;
223 item->submenu = NULL;
224
225 return item;
226}
227
228/* allocate a menu */
229static struct Menu *
230allocmenu(struct Menu *parent, struct Item *list, unsigned level)
231{
232 XSetWindowAttributes swa;
233 struct Menu *menu;
234
235 if ((menu = malloc(sizeof *menu)) == NULL)
236 err(1, "malloc");
237 menu->parent = parent;
238 menu->list = list;
d888f2ca 239 menu->caller = NULL;
a7732690 240 menu->selected = NULL;
a7732690 241 menu->w = geom.itemw;
a80fee22 242 menu->h = 0; /* calculated by calcmenu() */
243 menu->x = 0; /* calculated by calcmenu() */
244 menu->y = 0; /* calculated by calcmenu() */
a7732690 245 menu->level = level;
a7732690 246
247 swa.override_redirect = override_redirect;
248 swa.background_pixel = dc.decoration[ColorBG];
249 swa.border_pixel = dc.decoration[ColorFG];
250 swa.event_mask = ExposureMask | KeyPressMask | ButtonPressMask | ButtonReleaseMask
f15fc339 251 | PointerMotionMask | LeaveWindowMask;
a7732690 252 menu->win = XCreateWindow(dpy, rootwin, 0, 0, geom.itemw, geom.itemh, geom.border,
253 CopyFromParent, CopyFromParent, CopyFromParent,
254 CWOverrideRedirect | CWBackPixel | CWBorderPixel | CWEventMask,
255 &swa);
256
257 return menu;
258}
259
260/* create menus and items from the stdin */
261static void
262parsestdin(void)
263{
264 char *s, buf[BUFSIZ];
265 char *label, *output;
266 unsigned level = 0;
267 unsigned i;
a80fee22 268 struct Item *curritem = NULL; /* item currently being read */
269 struct Menu *prevmenu = NULL; /* menu the previous item was added to */
270 struct Item *item; /* dummy item for for loops */
271 struct Menu *menu; /* dummy menu for for loops */
a7732690 272 size_t count = 0; /* number of items in the current menu */
273
274 while (fgets(buf, BUFSIZ, stdin) != NULL) {
275 level = 0;
276 s = buf;
277
278 while (*s == '\t') {
279 level++;
280 s++;
281 }
282
283 label = output = s;
284
285 while (*s != '\0' && *s != '\t' && *s != '\n')
286 s++;
287
288 while (*s == '\t')
289 *s++ = '\0';
290
291 if (*s != '\0' && *s != '\n')
292 output = s;
293
294 while (*s != '\0' && *s != '\n')
295 s++;
296
297 if (*s == '\n')
298 *s = '\0';
299
a80fee22 300 curritem = allocitem(label, output);
a7732690 301
302 if (prevmenu == NULL) { /* there is no menu yet */
a80fee22 303 menu = allocmenu(NULL, curritem, level);
a7732690 304 rootmenu = menu;
305 prevmenu = menu;
306 count = 1;
307 } else if (level < prevmenu->level) { /* item is continuation of a parent menu*/
308 for (menu = prevmenu, i = level;
309 menu != NULL && i < prevmenu->level;
310 menu = menu->parent, i++)
311 ;
312
313 if (menu == NULL)
314 errx(1, "reached NULL menu");
315
a80fee22 316 for (item = menu->list; item->next != NULL; item = item->next)
a7732690 317 ;
318
a80fee22 319 item->next = curritem;
a7732690 320 prevmenu = menu;
321 } else if (level == prevmenu->level) { /* item is a continuation of current menu */
a80fee22 322 for (item = prevmenu->list; item->next != NULL; item = item->next)
a7732690 323 ;
a80fee22 324 item->next = curritem;
a7732690 325 } else if (level > prevmenu->level) { /* item begins a new menu */
a80fee22 326 menu = allocmenu(prevmenu, curritem, level);
a7732690 327
a80fee22 328 for (item = prevmenu->list; item->next != NULL; item = item->next)
a7732690 329 ;
330
a80fee22 331 item->submenu = menu;
d888f2ca 332 menu->caller = item;
a7732690 333
334 prevmenu = menu;
335 }
a80fee22 336 count++;
a7732690 337 }
338}
339
340/* calculate screen geometry */
341static void
342calcscreengeom(void)
343{
344 Window w1, w2; /* unused variables */
345 int a, b; /* unused variables */
346 unsigned mask; /* unused variable */
347
a80fee22 348 XQueryPointer(dpy, rootwin, &w1, &w2, &screengeom.cursx, &screengeom.cursy, &a, &b, &mask);
349 screengeom.screenw = DisplayWidth(dpy, screen);
350 screengeom.screenh = DisplayHeight(dpy, screen);
a7732690 351}
352
353/* recursivelly calculate height and position of the menus */
354static void
355calcmenu(struct Menu *menu)
356{
357 XWindowChanges changes;
09c13122 358 XSizeHints sizeh;
a80fee22 359 struct Item *item;
f1583285 360 int labelwidth;
a7732690 361
f1583285 362 /* calculate items positions and menu width and height */
363 menu->w = geom.itemw;
a80fee22 364 for (item = menu->list; item != NULL; item = item->next) {
365 item->y = menu->h;
7fbd1c5e 366 if (item->label == NULL) /* height for separator item */
a80fee22 367 menu->h += geom.separator;
368 else
369 menu->h += geom.itemh;
f1583285 370
371 labelwidth = XTextWidth(dc.font, item->label, item->labellen) + dc.fonth * 2;
372 menu->w = MAX(menu->w, labelwidth);
a80fee22 373 }
a7732690 374
375 /* calculate menu's x and y positions */
376 if (menu->parent == NULL) { /* if root menu, calculate in respect to cursor */
a80fee22 377 if (screengeom.screenw - screengeom.cursx >= menu->w)
378 menu->x = screengeom.cursx;
379 else if (screengeom.cursx > menu->w)
380 menu->x = screengeom.cursx - menu->w;
381
382 if (screengeom.screenh - screengeom.cursy >= menu->h)
383 menu->y = screengeom.cursy;
384 else if (screengeom.screenh > menu->h)
385 menu->y = screengeom.screenh - menu->h;
a7732690 386 } else { /* else, calculate in respect to parent menu */
387
388 /* search for the item in parent menu that generates this menu */
a80fee22 389 for (item = menu->parent->list; item->submenu != menu; item = item->next)
a7732690 390 ;
391
a80fee22 392 if (screengeom.screenw - (menu->parent->x + menu->parent->w) >= menu->w)
a7732690 393 menu->x = menu->parent->x + menu->parent->w;
394 else if (menu->parent->x > menu->w)
395 menu->x = menu->parent->x - menu->w;
396
a80fee22 397 if (screengeom.screenh - (item->y + menu->parent->y) > menu->h)
398 menu->y = item->y + menu->parent->y;
399 else if (screengeom.screenh - menu->parent->y > menu->h)
a7732690 400 menu->y = menu->parent->y;
a80fee22 401 else if (screengeom.screenh > menu->h)
402 menu->y = screengeom.screenh - menu->h;
a7732690 403 }
404
405 /* update menu geometry */
406 changes.height = menu->h;
f1583285 407 changes.width = menu->w;
a7732690 408 changes.x = menu->x;
409 changes.y = menu->y;
f1583285 410 XConfigureWindow(dpy, menu->win, CWWidth | CWHeight | CWX | CWY, &changes);
a7732690 411
09c13122 412 /* set window manager size hints */
413 sizeh.flags = PMaxSize | PMinSize;
414 sizeh.min_width = sizeh.max_width = menu->w;
415 sizeh.min_height = sizeh.max_height = menu->h;
416 XSetWMNormalHints(dpy, menu->win, &sizeh);
417
f15fc339 418 /* create pixmap */
419 menu->pixmap = XCreatePixmap(dpy, menu->win, menu->w, menu->h,
420 DefaultDepth(dpy, screen));
421
a80fee22 422 /* calculate positions of submenus */
a7732690 423 for (item = menu->list; item != NULL; item = item->next) {
424 if (item->submenu != NULL)
425 calcmenu(item->submenu);
426 }
427}
428
429/* get menu and item of given window and position */
430static void
a80fee22 431getmenuitem(Window win, int y,
a7732690 432 struct Menu **menu_ret, struct Item **item_ret)
433{
434 struct Menu *menu = NULL;
435 struct Item *item = NULL;
436
437 for (menu = currmenu; menu != NULL; menu = menu->parent) {
438 if (menu->win == win) {
439 for (item = menu->list; item != NULL; item = item->next) {
7fbd1c5e 440 if (y >= item->y && y <= item->y + item->h) {
a7732690 441 goto done;
442 }
443 }
444 }
445 }
446
447
448done:
449 *menu_ret = menu;
450 *item_ret = item;
451}
452
453/* set currentmenu to menu, umap previous menus and map current menu and its parents */
454static void
455setcurrmenu(struct Menu *currmenu_new)
456{
457 struct Menu *menu;
d888f2ca 458 struct Item *item;
a7732690 459
460 if (currmenu_new == currmenu)
461 return;
462
463 for (menu = currmenu; menu != NULL; menu = menu->parent) {
464 XUnmapWindow(dpy, menu->win);
465 }
466
467 currmenu = currmenu_new;
468
d888f2ca 469 item = NULL;
470 for (menu = currmenu; menu != NULL; menu = menu->parent) {
a7732690 471 XMapWindow(dpy, menu->win);
d888f2ca 472 if (item != NULL)
473 menu->selected = item;
474 item = menu->caller;
475 }
a7732690 476}
477
478/* draw items of the current menu and of its ancestors */
479static void
480drawmenu(void)
481{
482 struct Menu *menu;
483 struct Item *item;
484
485 for (menu = currmenu; menu != NULL; menu = menu->parent) {
a7732690 486 for (item = menu->list; item != NULL; item = item->next) {
487 unsigned long *color;
a7732690 488 int labelx, labely;
a7732690 489
490 /* determine item color */
7fbd1c5e 491 if (item->label == NULL)
492 color = dc.decoration;
493 else if (item == menu->selected)
a7732690 494 color = dc.pressed;
495 else
496 color = dc.unpressed;
497
a7732690 498 /* draw item box */
499 XSetForeground(dpy, dc.gc, color[ColorBG]);
d888f2ca 500 XDrawRectangle(dpy, menu->pixmap, dc.gc, 0, item->y,
501 menu->w, item->h);
f15fc339 502 XFillRectangle(dpy, menu->pixmap, dc.gc, 0, item->y,
f1583285 503 menu->w, item->h);
7fbd1c5e 504
505 /* continue if item is a separator */
506 if (item->label == NULL)
507 continue;
a7732690 508
509 /* draw item label */
a7732690 510 labelx = 0 + dc.fonth;
7fbd1c5e 511 labely = item->y + dc.fonth + geom.itemb;
a7732690 512 XSetForeground(dpy, dc.gc, color[ColorFG]);
f1583285 513 XDrawString(dpy, menu->pixmap, dc.gc, labelx, labely,
514 item->label, item->labellen);
a7732690 515
516 /* draw triangle, if item contains a submenu */
517 if (item->submenu != NULL) {
f1583285 518 int trianglex = menu->w - dc.fonth + geom.itemb - 1;
2b6968f9 519 int triangley = item->y + (3 * item->h)/8 -1;
a7732690 520
521 XPoint triangle[] = {
522 {trianglex, triangley},
2b6968f9 523 {trianglex + item->h/8 + 1, item->y + item->h/2},
524 {trianglex, triangley + item->h/4 + 2},
a7732690 525 {trianglex, triangley}
526 };
527
f15fc339 528 XFillPolygon(dpy, menu->pixmap, dc.gc, triangle, LEN(triangle),
a7732690 529 Convex, CoordModeOrigin);
530 }
f15fc339 531
532 XCopyArea(dpy, menu->pixmap, menu->win, dc.gc, 0, item->y,
533 menu->w, item->h, 0, item->y);
a7732690 534 }
535 }
536}
537
538/* run event loop */
539static void
540run(void)
541{
542 struct Menu *menu;
543 struct Item *item;
544 struct Item *previtem = NULL;
545 XEvent ev;
546
547 setcurrmenu(rootmenu);
548
549 while (!XNextEvent(dpy, &ev)) {
550 switch(ev.type) {
551 case Expose:
552 drawmenu();
553 break;
554 case MotionNotify:
a80fee22 555 getmenuitem(ev.xbutton.window, ev.xbutton.y, &menu, &item);
a7732690 556 if (menu != NULL && item != NULL) {
557 if (previtem != item) {
558 if (item->submenu != NULL)
559 setcurrmenu(item->submenu);
560 else
561 setcurrmenu(menu);
562 previtem = item;
d888f2ca 563 drawmenu();
564 } else if (menu->selected != item) {
a7732690 565 menu->selected = item;
d888f2ca 566 drawmenu();
567 }
a7732690 568 }
a7732690 569 break;
570 case ButtonRelease:
a80fee22 571 getmenuitem(ev.xbutton.window, ev.xbutton.y, &menu, &item);
a7732690 572 if (menu != NULL && item != NULL) {
7fbd1c5e 573 if (item->label == NULL)
574 break; /* ignore separators */
a7732690 575 if (item->submenu != NULL) {
576 setcurrmenu(item->submenu);
577 } else {
578 printf("%s\n", item->output);
579 cleanupexit();
580 }
581 drawmenu();
582 } else {
583 cleanupexit();
584 }
585 break;
f15fc339 586 case LeaveNotify:
587 currmenu->selected = NULL;
588 drawmenu();
589 break;
a7732690 590 }
591 }
592}
593
f15fc339 594/* recursivelly free a pixmap */
595static void
596freewindow(struct Menu *menu)
597{
598 struct Item *item;
599
600 for (item = menu->list; item != NULL; item = item->next)
601 if (item->submenu != NULL)
602 freewindow(item->submenu);
603
604 XFreePixmap(dpy, menu->pixmap);
605 XDestroyWindow(dpy, menu->win);
606}
607
a7732690 608/* cleanup and exit */
609static void
610cleanupexit(void)
611{
f15fc339 612 freewindow(rootmenu);
613 XFreeFont(dpy, dc.font);
614 XFreeGC(dpy, dc.gc);
a7732690 615 XCloseDisplay(dpy);
616 exit(0);
617}
618
619/* show usage */
620static void
621usage(void)
622{
623 (void)fprintf(stderr, "usage: xmenu [-w] menuname\n");
624 exit(1);
625}