Added support for fallback fonts.

Also changed license.
This commit is contained in:
phillbush 2020-07-29 00:51:08 -03:00
parent 675a2008a6
commit cdeaefaaa2
5 changed files with 277 additions and 131 deletions

22
README
View File

@ -71,4 +71,24 @@ Read the manual for more information on running XMenu.
§ License § License
This software is in public domain and is provided AS IS, with NO WARRANTY. MIT/X Consortium License
© 2020 phillbush
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.

View File

@ -1,6 +1,6 @@
static struct Config config = { static struct Config config = {
/* font */ /* font, separate different fonts with comma */
.font = "monospace:size=9", /* for regular items */ .font = "monospace:size=9,DejaVuSansMono:size=9",
/* colors */ /* colors */
.background_color = "#FFFFFF", .background_color = "#FFFFFF",

View File

@ -97,6 +97,8 @@ understands the following X resources.
.TP .TP
.B xmenu.font .B xmenu.font
The font in which the labels should be drawn. The font in which the labels should be drawn.
Multiple fonts can be added as fallback fonts;
they must be separated by a comma.
.TP .TP
.B xmenu.background .B xmenu.background
The background color of non-selected items in the menu. The background color of non-selected items in the menu.

365
xmenu.c
View File

@ -1,3 +1,4 @@
#include <ctype.h>
#include <err.h> #include <err.h>
#include <errno.h> #include <errno.h>
#include <stdio.h> #include <stdio.h>
@ -23,6 +24,7 @@
static void parseposition(const char *optarg); static void parseposition(const char *optarg);
/* initializers, and their helper routines */ /* initializers, and their helper routines */
static void parsefonts(const char *s);
static void ealloccolor(const char *s, XftColor *color); static void ealloccolor(const char *s, XftColor *color);
static void initresources(void); static void initresources(void);
static void initdc(void); static void initdc(void);
@ -38,6 +40,13 @@ static struct Menu *parsestdin(void);
/* image loader */ /* image loader */
static Imlib_Image loadicon(const char *file); static Imlib_Image loadicon(const char *file);
/* utf8 utils */
static FcChar32 getnextutf8char(const char *s, const char **end_ret);
/* pixmap drawers */
static int drawtext(XftDraw *draw, XftColor *color, int x, int y, unsigned h, const char *text);
static void drawitems(struct Menu *menu);
/* structure setters, and their helper routines */ /* structure setters, and their helper routines */
static void setupitems(struct Menu *menu); static void setupitems(struct Menu *menu);
static void setupmenupos(struct Menu *menu); static void setupmenupos(struct Menu *menu);
@ -49,14 +58,16 @@ static void grabkeyboard(void);
/* window drawers and mappers */ /* window drawers and mappers */
static void mapmenu(struct Menu *currmenu); static void mapmenu(struct Menu *currmenu);
static void drawseparator(struct Menu *menu, struct Item *item); static void copypixmaps(struct Menu *currmenu);
static void drawitem(struct Menu *menu, struct Item *item, XftColor *color);
static void drawmenu(struct Menu *currmenu);
/* main event loop, and its helper routines */ /* getters */
static struct Menu *getmenu(struct Menu *currmenu, Window win); static struct Menu *getmenu(struct Menu *currmenu, Window win);
static struct Item *getitem(struct Menu *menu, int y); static struct Item *getitem(struct Menu *menu, int y);
/* cycle through items */
static struct Item *itemcycle(struct Menu *currmenu, int direction); static struct Item *itemcycle(struct Menu *currmenu, int direction);
/* main event loop */
static void run(struct Menu *currmenu); static void run(struct Menu *currmenu);
/* cleaners */ /* cleaners */
@ -202,6 +213,40 @@ error:
errx(1, "improper position: %s", optarg); errx(1, "improper position: %s", optarg);
} }
/* parse color string */
static void
parsefonts(const char *s)
{
const char *p;
char buf[1024];
size_t nfont = 0;
dc.nfonts = 1;
for (p = s; *p; p++)
if (*p == ',')
dc.nfonts++;
if ((dc.fonts = calloc(dc.nfonts, sizeof *dc.fonts)) == NULL)
err(1, "calloc");
p = s;
while (*p != '\0') {
size_t i;
i = 0;
while (isspace(*p))
p++;
while (*p != '\0' && *p != ',') {
buf[i++] = *p++;
}
if (*p == ',')
p++;
buf[i] = '\0';
if ((dc.fonts[nfont++] = XftFontOpenName(dpy, screen, buf)) == NULL)
errx(1, "cannot load font");
}
}
/* get color from color string */ /* get color from color string */
static void static void
ealloccolor(const char *s, XftColor *color) ealloccolor(const char *s, XftColor *color)
@ -271,9 +316,8 @@ initdc(void)
ealloccolor(config.separator_color, &dc.separator); ealloccolor(config.separator_color, &dc.separator);
ealloccolor(config.border_color, &dc.border); ealloccolor(config.border_color, &dc.border);
/* try to get font */ /* parse fonts */
if ((dc.font = XftFontOpenName(dpy, screen, config.font)) == NULL) parsefonts(config.font);
errx(1, "cannot load font");
/* create common GC */ /* create common GC */
dc.gc = XCreateGC(dpy, rootwin, 0, NULL); dc.gc = XCreateGC(dpy, rootwin, 0, NULL);
@ -507,6 +551,182 @@ loadicon(const char *file)
return icon; return icon;
} }
/* get next utf8 char from s return its codepoint and set next_ret to pointer to next character */
static FcChar32
getnextutf8char(const char *s, const char **next_ret)
{
/* */
static const unsigned char utfbyte[] = {0x80, 0x00, 0xC0, 0xE0, 0xF0};
/* */
static const unsigned char utfmask[] = {0xC0, 0x80, 0xE0, 0xF0, 0xF8};
/* 0xFFFD is the replacement character, used to represent unknown characters */
static const FcChar32 utfmin[] = {0, 0x00, 0x80, 0x800, 0x10000};
static const FcChar32 utfmax[] = {0, 0x7F, 0x7FF, 0xFFFF, 0x10FFFF};
static const FcChar32 unknown = 0xFFFD;
FcChar32 ucode; /* FcChar32 type holds 32 bits */
size_t usize = 0; /* n' of bytes of the utf8 character */
size_t i;
*next_ret = s+1;
/* get code of first byte of utf8 character */
for (i = 0; i < sizeof utfmask; i++) {
if (((unsigned char)*s & utfmask[i]) == utfbyte[i]) {
usize = i;
ucode = (unsigned char)*s & ~utfmask[i];
break;
}
}
/* if first byte is a continuation byte or is not allowed, return unknown */
if (i == sizeof utfmask || usize == 0)
return unknown;
/* check the other usize-1 bytes */
s++;
for (i = 1; i < usize; i++) {
*next_ret = s+1;
/* if byte is EOS or is not a continuation byte, return unknown */
if (*s == '\0' || ((unsigned char)*s & utfmask[0]) != utfbyte[0])
return unknown;
/* 6 is the number of relevant bits in the continuation byte */
ucode = (ucode << 6) | ((unsigned char)*s & ~utfmask[0]);
s++;
}
/* check if ucode is invalid or in utf-16 surrogate halves */
if (!BETWEEN(ucode, utfmin[usize], utfmax[usize])
|| BETWEEN (ucode, 0xD800, 0xDFFF))
return unknown;
return ucode;
}
/* draw text into XftDraw */
static int
drawtext(XftDraw *draw, XftColor *color, int x, int y, unsigned h, const char *text)
{
const char *s, *nexts;
FcChar32 ucode;
XftFont *currfont;
int textlen = 0;
s = text;
while (*s) {
XGlyphInfo ext;
int charexists;
size_t len;
size_t i;
charexists = 0;
ucode = getnextutf8char(s, &nexts);
for (i = 0; i < dc.nfonts; i++) {
charexists = XftCharExists(dpy, dc.fonts[i], ucode);
if (charexists)
break;
}
if (charexists)
currfont = dc.fonts[i];
len = nexts - s;
XftTextExtentsUtf8(dpy, currfont, (XftChar8 *)s,
len, &ext);
textlen += ext.xOff;
if (draw) {
int texty;
texty = y + (h + currfont->ascent) / 2;
XftDrawStringUtf8(draw, color, currfont, x, texty,
(XftChar8 *)s, len);
x += ext.xOff;
}
s = nexts;
}
return textlen;
}
/* draw pixmap for the selected and unselected version of each item on menu */
static void
drawitems(struct Menu *menu)
{
struct Item *item;
for (item = menu->list; item != NULL; item = item->next) {
XftDraw *dsel, *dunsel;
int x, y;
item->unsel = XCreatePixmap(dpy, menu->win, menu->w, item->h,
DefaultDepth(dpy, screen));
XSetForeground(dpy, dc.gc, dc.normal[ColorBG].pixel);
XFillRectangle(dpy, item->unsel, dc.gc, 0, 0, menu->w, item->h);
if (item->label == NULL) { /* item is separator */
y = item->h/2;
XSetForeground(dpy, dc.gc, dc.separator.pixel);
XDrawLine(dpy, item->unsel, dc.gc, config.horzpadding, y,
menu->w - config.horzpadding, y);
item->sel = item->unsel;
} else {
item->sel = XCreatePixmap(dpy, menu->win, menu->w, item->h,
DefaultDepth(dpy, screen));
XSetForeground(dpy, dc.gc, dc.selected[ColorBG].pixel);
XFillRectangle(dpy, item->sel, dc.gc, 0, 0, menu->w, item->h);
/* draw text */
x = config.horzpadding;
x += (iflag) ? 0 : config.horzpadding + config.iconsize;
dsel = XftDrawCreate(dpy, item->sel, visual, colormap);
dunsel = XftDrawCreate(dpy, item->unsel, visual, colormap);
XSetForeground(dpy, dc.gc, dc.selected[ColorFG].pixel);
drawtext(dsel, &dc.selected[ColorFG], x, 0, item->h, item->label);
XSetForeground(dpy, dc.gc, dc.normal[ColorFG].pixel);
drawtext(dunsel, &dc.normal[ColorFG], x, 0, item->h, item->label);
XftDrawDestroy(dsel);
XftDrawDestroy(dunsel);
/* draw triangle */
if (item->submenu != NULL) {
x = menu->w - config.triangle_width - config.horzpadding;
y = (item->h - config.triangle_height + 1) / 2;
XPoint triangle[] = {
{x, y},
{x + config.triangle_width, y + config.triangle_height/2},
{x, y + config.triangle_height},
{x, y}
};
XSetForeground(dpy, dc.gc, dc.selected[ColorFG].pixel);
XFillPolygon(dpy, item->sel, dc.gc, triangle, LEN(triangle),
Convex, CoordModeOrigin);
XSetForeground(dpy, dc.gc, dc.normal[ColorFG].pixel);
XFillPolygon(dpy, item->unsel, dc.gc, triangle, LEN(triangle),
Convex, CoordModeOrigin);
}
/* draw icon */
if (item->file != NULL && !iflag) {
item->icon = loadicon(item->file);
imlib_context_set_drawable(item->sel);
imlib_context_set_image(item->icon);
imlib_render_image_on_drawable(config.horzpadding, config.iconpadding);
imlib_context_set_drawable(item->unsel);
imlib_context_set_image(item->icon);
imlib_render_image_on_drawable(config.horzpadding, config.iconpadding);
}
}
}
}
/* setup the height, width and icon of the items of a menu */ /* setup the height, width and icon of the items of a menu */
static void static void
setupitems(struct Menu *menu) setupitems(struct Menu *menu)
@ -526,7 +746,7 @@ setupitems(struct Menu *menu)
menu->h += item->h; menu->h += item->h;
/* get length of item->label rendered in the font */ /* get length of item->label rendered in the font */
XftTextExtentsUtf8(dpy, dc.font, (XftChar8 *)item->label, XftTextExtentsUtf8(dpy, dc.fonts[0], (XftChar8 *)item->label,
item->labellen, &ext); item->labellen, &ext);
/* /*
@ -544,31 +764,6 @@ setupitems(struct Menu *menu)
itemwidth = ext.xOff + config.triangle_width + config.horzpadding * 3; itemwidth = ext.xOff + config.triangle_width + config.horzpadding * 3;
itemwidth += (iflag) ? 0 : config.iconsize + config.horzpadding; itemwidth += (iflag) ? 0 : config.iconsize + config.horzpadding;
menu->w = MAX(menu->w, itemwidth); menu->w = MAX(menu->w, itemwidth);
/* create icon */
if (item->file != NULL && !iflag) {
item->icon = loadicon(item->file);
item->sel = XCreatePixmap(dpy, menu->win,
config.iconsize, config.iconsize,
DefaultDepth(dpy, screen));
XSetForeground(dpy, dc.gc, dc.selected[ColorBG].pixel);
XFillRectangle(dpy, item->sel, dc.gc, 0, 0,
config.iconsize, config.iconsize);
imlib_context_set_drawable(item->sel);
imlib_context_set_image(item->icon);
imlib_render_image_on_drawable(0, 0);
item->unsel = XCreatePixmap(dpy, menu->win,
config.iconsize, config.iconsize,
DefaultDepth(dpy, screen));
XSetForeground(dpy, dc.gc, dc.normal[ColorBG].pixel);
XFillRectangle(dpy, item->unsel, dc.gc, 0, 0,
config.iconsize, config.iconsize);
imlib_context_set_drawable(item->unsel);
imlib_context_set_image(item->icon);
imlib_render_image_on_drawable(0, 0);
}
} }
} }
@ -617,6 +812,7 @@ setupmenu(struct Menu *menu, XClassHint *classh)
/* setup size and position of menus */ /* setup size and position of menus */
setupitems(menu); setupitems(menu);
drawitems(menu);
setupmenupos(menu); setupmenupos(menu);
/* update menu geometry */ /* update menu geometry */
@ -641,11 +837,6 @@ setupmenu(struct Menu *menu, XClassHint *classh)
sizeh.min_height = sizeh.max_height = menu->h; sizeh.min_height = sizeh.max_height = menu->h;
XSetWMProperties(dpy, menu->win, &wintitle, NULL, NULL, 0, &sizeh, NULL, classh); XSetWMProperties(dpy, menu->win, &wintitle, NULL, NULL, 0, &sizeh, NULL, classh);
/* create pixmap and XftDraw */
menu->pixmap = XCreatePixmap(dpy, menu->win, menu->w, menu->h,
DefaultDepth(dpy, screen));
menu->draw = XftDrawCreate(dpy, menu->pixmap, visual, colormap);
/* set WM protocols and ewmh window properties */ /* set WM protocols and ewmh window properties */
XSetWMProtocols(dpy, menu->win, &wmdelete, 1); XSetWMProtocols(dpy, menu->win, &wmdelete, 1);
XChangeProperty(dpy, menu->win, netatom[NetWMName], utf8string, 8, XChangeProperty(dpy, menu->win, netatom[NetWMName], utf8string, 8,
@ -753,91 +944,22 @@ mapmenu(struct Menu *currmenu)
prevmenu = currmenu; prevmenu = currmenu;
} }
/* draw separator item */ /* copy pixmaps of items of the current menu and of its ancestors into menu window */
static void static void
drawseparator(struct Menu *menu, struct Item *item) copypixmaps(struct Menu *currmenu)
{
int y;
y = item->y + item->h/2;
XSetForeground(dpy, dc.gc, dc.separator.pixel);
XDrawLine(dpy, menu->pixmap, dc.gc, config.horzpadding, y,
menu->w - config.horzpadding, y);
}
/* draw regular item */
static void
drawitem(struct Menu *menu, struct Item *item, XftColor *color)
{
int x, y;
x = config.horzpadding;
x += (iflag) ? 0 : config.horzpadding + config.iconsize;
y = item->y + (item->h + dc.font->ascent) / 2;
XSetForeground(dpy, dc.gc, color[ColorFG].pixel);
XftDrawStringUtf8(menu->draw, &color[ColorFG], dc.font,
x, y, (XftChar8 *)item->label, item->labellen);
/* draw triangle, if item contains a submenu */
if (item->submenu != NULL) {
x = menu->w - config.triangle_width - config.horzpadding;
y = item->y + (item->h - config.triangle_height + 1) / 2;
XPoint triangle[] = {
{x, y},
{x + config.triangle_width, y + config.triangle_height/2},
{x, y + config.triangle_height},
{x, y}
};
XFillPolygon(dpy, menu->pixmap, dc.gc, triangle, LEN(triangle),
Convex, CoordModeOrigin);
}
/* draw icon */
if (item->icon != NULL) {
x = config.horzpadding;
y = item->y + config.iconpadding;
if (color == dc.selected)
XCopyArea(dpy, item->sel, menu->pixmap, dc.gc, 0, 0,
menu->w, menu->h, x, y);
else
XCopyArea(dpy, item->unsel, menu->pixmap, dc.gc, 0, 0,
menu->w, menu->h, x, y);
}
}
/* draw items of the current menu and of its ancestors */
static void
drawmenu(struct Menu *currmenu)
{ {
struct Menu *menu; struct Menu *menu;
struct Item *item; struct Item *item;
for (menu = currmenu; menu != NULL; menu = menu->parent) { for (menu = currmenu; menu != NULL; menu = menu->parent) {
for (item = menu->list; item != NULL; item = item->next) { for (item = menu->list; item != NULL; item = item->next) {
XftColor *color; if (item == menu->selected)
XCopyArea(dpy, item->sel, menu->win, dc.gc, 0, 0,
/* determine item color */ menu->w, item->h, 0, item->y);
if (item == menu->selected && item->label != NULL)
color = dc.selected;
else else
color = dc.normal; XCopyArea(dpy, item->unsel, menu->win, dc.gc, 0, 0,
menu->w, item->h, 0, item->y);
/* draw item box */
XSetForeground(dpy, dc.gc, color[ColorBG].pixel);
XFillRectangle(dpy, menu->pixmap, dc.gc, 0, item->y,
menu->w, item->h);
if (item->label == NULL) /* item is a separator */
drawseparator(menu, item);
else /* item is a regular item */
drawitem(menu, item, color);
} }
XCopyArea(dpy, menu->pixmap, menu->win, dc.gc, 0, 0,
menu->w, menu->h, 0, 0);
} }
} }
@ -927,7 +1049,7 @@ run(struct Menu *currmenu)
switch(ev.type) { switch(ev.type) {
case Expose: case Expose:
if (ev.xexpose.count == 0) if (ev.xexpose.count == 0)
drawmenu(currmenu); copypixmaps(currmenu);
break; break;
case MotionNotify: case MotionNotify:
menu = getmenu(currmenu, ev.xbutton.window); menu = getmenu(currmenu, ev.xbutton.window);
@ -943,7 +1065,7 @@ run(struct Menu *currmenu)
currmenu = menu; currmenu = menu;
} }
mapmenu(currmenu); mapmenu(currmenu);
drawmenu(currmenu); copypixmaps(currmenu);
break; break;
case ButtonRelease: case ButtonRelease:
menu = getmenu(currmenu, ev.xbutton.window); menu = getmenu(currmenu, ev.xbutton.window);
@ -961,7 +1083,7 @@ selectitem:
} }
mapmenu(currmenu); mapmenu(currmenu);
currmenu->selected = currmenu->list; currmenu->selected = currmenu->list;
drawmenu(currmenu); copypixmaps(currmenu);
break; break;
case ButtonPress: case ButtonPress:
menu = getmenu(currmenu, ev.xbutton.window); menu = getmenu(currmenu, ev.xbutton.window);
@ -997,12 +1119,12 @@ selectitem:
} else } else
break; break;
currmenu->selected = item; currmenu->selected = item;
drawmenu(currmenu); copypixmaps(currmenu);
break; break;
case LeaveNotify: case LeaveNotify:
previtem = NULL; previtem = NULL;
currmenu->selected = NULL; currmenu->selected = NULL;
drawmenu(currmenu); copypixmaps(currmenu);
break; break;
case ConfigureNotify: case ConfigureNotify:
menu = getmenu(currmenu, ev.xconfigure.window); menu = getmenu(currmenu, ev.xconfigure.window);
@ -1037,6 +1159,9 @@ cleanmenu(struct Menu *menu)
if (item->submenu != NULL) if (item->submenu != NULL)
cleanmenu(item->submenu); cleanmenu(item->submenu);
tmp = item; tmp = item;
XFreePixmap(dpy, item->unsel);
if (tmp->label != NULL)
XFreePixmap(dpy, item->sel);
if (tmp->label != tmp->output) if (tmp->label != tmp->output)
free(tmp->label); free(tmp->label);
free(tmp->output); free(tmp->output);
@ -1051,8 +1176,6 @@ cleanmenu(struct Menu *menu)
free(tmp); free(tmp);
} }
XFreePixmap(dpy, menu->pixmap);
XftDrawDestroy(menu->draw);
XDestroyWindow(dpy, menu->win); XDestroyWindow(dpy, menu->win);
free(menu); free(menu);
} }

View File

@ -8,6 +8,7 @@
#define LEN(x) (sizeof (x) / sizeof (x[0])) #define LEN(x) (sizeof (x) / sizeof (x[0]))
#define MAX(x,y) ((x)>(y)?(x):(y)) #define MAX(x,y) ((x)>(y)?(x):(y))
#define MIN(x,y) ((x)<(y)?(x):(y)) #define MIN(x,y) ((x)<(y)?(x):(y))
#define BETWEEN(x, a, b) ((a) <= (x) && (x) <= (b))
/* color enum */ /* color enum */
enum {ColorFG, ColorBG, ColorLast}; enum {ColorFG, ColorBG, ColorLast};
@ -49,7 +50,9 @@ struct DC {
XftColor separator; XftColor separator;
GC gc; GC gc;
XftFont *font;
XftFont **fonts;
size_t nfonts;
}; };
/* menu item structure */ /* menu item structure */
@ -63,7 +66,7 @@ struct Item {
struct Item *prev; /* previous item */ struct Item *prev; /* previous item */
struct Item *next; /* next item */ struct Item *next; /* next item */
struct Menu *submenu; /* submenu spawned by clicking on item */ struct Menu *submenu; /* submenu spawned by clicking on item */
Drawable sel, unsel; /* pixmap for selected and unselected icons */ Drawable sel, unsel; /* pixmap for selected and unselected item */
Imlib_Image icon; Imlib_Image icon;
}; };
@ -75,7 +78,5 @@ struct Menu {
struct Item *selected; /* item currently selected in the menu */ struct Item *selected; /* item currently selected in the menu */
int x, y, w, h; /* menu geometry */ int x, y, w, h; /* menu geometry */
unsigned level; /* menu level relative to root */ unsigned level; /* menu level relative to root */
Drawable pixmap; /* pixmap to draw the menu on */
XftDraw *draw;
Window win; /* menu window to map on the screen */ Window win; /* menu window to map on the screen */
}; };