improved algorithm that draw menus
[xmenu] / xmenu.c
... / ...
CommitLineData
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]))
11#define MAX(x,y) ((x)>(y)?(x):(y))
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 */
33 int border; /* window border width */
34 int separator; /* menu separator width */
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;
47 int y;
48 int h;
49 size_t labellen;
50 struct Item *next;
51 struct Menu *submenu;
52};
53
54/* menu structure */
55struct Menu {
56 struct Menu *parent;
57 struct Item *caller;
58 struct Item *list;
59 struct Item *selected;
60 int x, y, w, h;
61 unsigned level;
62 Drawable pixmap;
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);
71static struct Item *allocitem(const char *label, const char *output);
72static struct Menu *allocmenu(struct Menu *parent, struct Item *list, unsigned level);
73static void getmenuitem(Window win, int y, struct Menu **menu_ret, struct Item **item_ret);
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);
80static void freewindow(struct Menu *menu);
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;
97static struct ScreenGeometry screengeom;
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;
188 geom.separator = SEPARATOR;
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 *
201allocitem(const char *label, const char *output)
202{
203 struct Item *item;
204
205 if ((item = malloc(sizeof *item)) == NULL)
206 err(1, "malloc");
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 }
216 item->y = 0;
217 item->h = item->label ? geom.itemh : geom.separator;
218 if (item->label == NULL)
219 item->labellen = 0;
220 else
221 item->labellen = strlen(item->label);
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;
239 menu->caller = NULL;
240 menu->selected = NULL;
241 menu->w = geom.itemw;
242 menu->h = 0; /* calculated by calcmenu() */
243 menu->x = 0; /* calculated by calcmenu() */
244 menu->y = 0; /* calculated by calcmenu() */
245 menu->level = level;
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
251 | PointerMotionMask | LeaveWindowMask;
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;
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 */
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
300 curritem = allocitem(label, output);
301
302 if (prevmenu == NULL) { /* there is no menu yet */
303 menu = allocmenu(NULL, curritem, level);
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
316 for (item = menu->list; item->next != NULL; item = item->next)
317 ;
318
319 item->next = curritem;
320 prevmenu = menu;
321 } else if (level == prevmenu->level) { /* item is a continuation of current menu */
322 for (item = prevmenu->list; item->next != NULL; item = item->next)
323 ;
324 item->next = curritem;
325 } else if (level > prevmenu->level) { /* item begins a new menu */
326 menu = allocmenu(prevmenu, curritem, level);
327
328 for (item = prevmenu->list; item->next != NULL; item = item->next)
329 ;
330
331 item->submenu = menu;
332 menu->caller = item;
333
334 prevmenu = menu;
335 }
336 count++;
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
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);
351}
352
353/* recursivelly calculate height and position of the menus */
354static void
355calcmenu(struct Menu *menu)
356{
357 XWindowChanges changes;
358 XSizeHints sizeh;
359 struct Item *item;
360 int labelwidth;
361
362 /* calculate items positions and menu width and height */
363 menu->w = geom.itemw;
364 for (item = menu->list; item != NULL; item = item->next) {
365 item->y = menu->h;
366 if (item->label == NULL) /* height for separator item */
367 menu->h += geom.separator;
368 else
369 menu->h += geom.itemh;
370
371 labelwidth = XTextWidth(dc.font, item->label, item->labellen) + dc.fonth * 2;
372 menu->w = MAX(menu->w, labelwidth);
373 }
374
375 /* calculate menu's x and y positions */
376 if (menu->parent == NULL) { /* if root menu, calculate in respect to cursor */
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;
386 } else { /* else, calculate in respect to parent menu */
387
388 /* search for the item in parent menu that generates this menu */
389 for (item = menu->parent->list; item->submenu != menu; item = item->next)
390 ;
391
392 if (screengeom.screenw - (menu->parent->x + menu->parent->w) >= menu->w)
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
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)
400 menu->y = menu->parent->y;
401 else if (screengeom.screenh > menu->h)
402 menu->y = screengeom.screenh - menu->h;
403 }
404
405 /* update menu geometry */
406 changes.height = menu->h;
407 changes.width = menu->w;
408 changes.x = menu->x;
409 changes.y = menu->y;
410 XConfigureWindow(dpy, menu->win, CWWidth | CWHeight | CWX | CWY, &changes);
411
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
418 /* create pixmap */
419 menu->pixmap = XCreatePixmap(dpy, menu->win, menu->w, menu->h,
420 DefaultDepth(dpy, screen));
421
422 /* calculate positions of submenus */
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
431getmenuitem(Window win, int y,
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) {
440 if (y >= item->y && y <= item->y + item->h) {
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;
458 struct Item *item;
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
469 item = NULL;
470 for (menu = currmenu; menu != NULL; menu = menu->parent) {
471 XMapWindow(dpy, menu->win);
472 if (item != NULL)
473 menu->selected = item;
474 item = menu->caller;
475 }
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) {
486 for (item = menu->list; item != NULL; item = item->next) {
487 unsigned long *color;
488 int labelx, labely;
489
490 /* determine item color */
491 if (item->label == NULL)
492 color = dc.decoration;
493 else if (item == menu->selected)
494 color = dc.pressed;
495 else
496 color = dc.unpressed;
497
498 /* draw item box */
499 XSetForeground(dpy, dc.gc, color[ColorBG]);
500 XDrawRectangle(dpy, menu->pixmap, dc.gc, 0, item->y,
501 menu->w, item->h);
502 XFillRectangle(dpy, menu->pixmap, dc.gc, 0, item->y,
503 menu->w, item->h);
504
505 /* continue if item is a separator */
506 if (item->label == NULL)
507 continue;
508
509 /* draw item label */
510 labelx = 0 + dc.fonth;
511 labely = item->y + dc.fonth + geom.itemb;
512 XSetForeground(dpy, dc.gc, color[ColorFG]);
513 XDrawString(dpy, menu->pixmap, dc.gc, labelx, labely,
514 item->label, item->labellen);
515
516 /* draw triangle, if item contains a submenu */
517 if (item->submenu != NULL) {
518 int trianglex = menu->w - dc.fonth + geom.itemb - 1;
519 int triangley = item->y + (3 * item->h)/8 -1;
520
521 XPoint triangle[] = {
522 {trianglex, triangley},
523 {trianglex + item->h/8 + 1, item->y + item->h/2},
524 {trianglex, triangley + item->h/4 + 2},
525 {trianglex, triangley}
526 };
527
528 XFillPolygon(dpy, menu->pixmap, dc.gc, triangle, LEN(triangle),
529 Convex, CoordModeOrigin);
530 }
531
532 XCopyArea(dpy, menu->pixmap, menu->win, dc.gc, 0, item->y,
533 menu->w, item->h, 0, item->y);
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:
555 getmenuitem(ev.xbutton.window, ev.xbutton.y, &menu, &item);
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;
563 drawmenu();
564 } else if (menu->selected != item) {
565 menu->selected = item;
566 drawmenu();
567 }
568 }
569 break;
570 case ButtonRelease:
571 getmenuitem(ev.xbutton.window, ev.xbutton.y, &menu, &item);
572 if (menu != NULL && item != NULL) {
573 if (item->label == NULL)
574 break; /* ignore separators */
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;
586 case LeaveNotify:
587 currmenu->selected = NULL;
588 drawmenu();
589 break;
590 }
591 }
592}
593
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
608/* cleanup and exit */
609static void
610cleanupexit(void)
611{
612 freewindow(rootmenu);
613 XFreeFont(dpy, dc.font);
614 XFreeGC(dpy, dc.gc);
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}