/* xmenu * a simple fuzzy-selection menu made with Xlib */ #include #include #include #include #include #include #include "ui.h" #include "dynarr.h" static Display *dsp; static Window win; static XrmDatabase db; static XIM im; static XIC ic; static int scr; static XPoint ic_pos; static XVaNestedList ic_pos_list; static unsigned long fg, bg; static unsigned long selfg, selbg; static unsigned long inpfg, inpbg; static XSizeHints szhint; static GC gc; static Atom wm_delete_window; static XFontSet ft_opt, ft_inp; static inline unsigned long color(const char *name) { XColor exact, screen; Colormap cmap = DefaultColormap(dsp, scr); Status ok = XLookupColor(dsp, cmap, name, &exact, &screen); /* this is probably non-portable but the pixel fields in * both exact and screen are absolute bunk, seemingly only * have one channel */ unsigned long px = ((screen.red << 8) & 0xff0000) | (screen.green & 0xff00) | (screen.blue >> 8); if (!ok) fprintf(stderr, "bad color %s!\n", name); return px; } static inline XFontSet ft_load(const char *name) { char **missing_charset_name; int missing_charset_count; char *def_str; XFontSet fs = XCreateFontSet(dsp, name, &missing_charset_name, &missing_charset_count, &def_str); if (missing_charset_count > 0) { fputs("[Missing fonts: ", stderr); for (int i = 0; i < missing_charset_count; i++) { if (i > 0) fputs(", ", stderr); fputs(missing_charset_name[i], stderr); } fputs("]\n", stderr); } XFreeStringList(missing_charset_name); return fs; } static void ft_free(XFontSet fs) { XFreeFontSet(dsp, fs); } static inline void ft_ascent_descent(XFontSet fs, int *asc, int *desc) { XFontStruct **font; char **name; int n = XFontsOfFontSet(fs, &font, &name); int ascent = 0, descent = 0; for (int i = 0; i < n; i++) { if (font[i]->ascent > ascent) ascent = font[i]->ascent; if (font[i]->descent > descent) descent = font[i]->descent; } *asc = ascent; *desc = descent; } static inline int ft_width(XFontSet f, const char *s, int n) { XRectangle ink, log; Xutf8TextExtents(f, s, n, &ink, &log); return log.width; } static inline void set_ic_pos(const char *s, int i) { int w = ft_width(ft_inp, s, i); ic_pos.x = 16 + w; ic_pos.y = 24; XSetICValues(ic, XNPreeditAttributes, ic_pos_list, NULL); } void ui_init(int argc, char **argv, UiOpts opt) { if (!setlocale(LC_ALL, "")) err(1, "failed to set locale"); dsp = XOpenDisplay(NULL); scr = DefaultScreen(dsp); db = XrmGetDatabase(dsp); fg = BlackPixel(dsp, scr); bg = color("lavender"); selfg = bg; selbg = color("steelblue"); inpfg = color("firebrick"); inpbg = bg; int scr_width = XWidthOfScreen(ScreenOfDisplay(dsp, scr)); int scr_height = XHeightOfScreen(ScreenOfDisplay(dsp, scr)); /* TODO: error checking */ ft_opt = ft_load("-*-new century schoolbook-medium-r-*-*-24-*-*-*-*-*-*-*"); ft_inp = ft_load("-*-new century schoolbook-medium-i-*-*-24-*-*-*-*-*-*-*"); if (!ft_opt) ft_opt = ft_load("fixed"); if (!ft_inp) ft_inp = ft_load("fixed"); /* calculate window size */ int w = 0; for (int i = 0; i < opt.n; i++) { int ow = ft_width(ft_opt, opt.v[i].s, opt.v[i].n); if (ow > w) w = ow; } int h = 24 * (opt.n + 1); int margin = 64; if (w > scr_width - 2*margin) w = scr_width - 2*margin; if (h > scr_height - 2*margin) h = scr_height - 2*margin; szhint = (XSizeHints) { 0 }; szhint.x = (scr_width - w - 32) / 2; szhint.y = (scr_height - h - 32) / 2; szhint.width = w + 32; szhint.height = h + 32; szhint.flags = PSize; win = XCreateWindow( dsp, DefaultRootWindow(dsp), szhint.x, szhint.y, szhint.width, szhint.height, 0, XDefaultDepth(dsp, scr), InputOutput, XDefaultVisual(dsp, scr), CWBackPixel | CWBorderPixel | CWBitGravity | CWEventMask | CWColormap, &(XSetWindowAttributes) { .background_pixel = bg, .border_pixel = fg, .bit_gravity = NorthWestGravity, .win_gravity = CenterGravity, .event_mask = FocusChangeMask | KeyPressMask | KeyReleaseMask | ExposureMask | VisibilityChangeMask | StructureNotifyMask | ButtonMotionMask | ButtonPressMask | ButtonReleaseMask, }); /* does setting the urgency hint auto-focus in ctwm? */ XSetWMProperties(dsp, win, NULL, NULL, argv, argc, &szhint, &(XWMHints) { .flags = InputHint | XUrgencyHint, .input = 1 }, &(XClassHint) { "xmenu", "xmenu" }); gc = XCreateGC(dsp, win, 0, 0); XSetBackground(dsp, gc, bg); XSetForeground(dsp, gc, fg); wm_delete_window = XInternAtom(dsp, "WM_DELETE_WINDOW", False); XSetWMProtocols(dsp, win, &wm_delete_window, 1); XSetTransientForHint(dsp, win, DefaultRootWindow(dsp)); im = XOpenIM(dsp, NULL, NULL, NULL); ic = XCreateIC(im, XNInputStyle, XIMPreeditNothing | XIMStatusNothing, XNClientWindow, win, NULL); ic_pos_list = XVaCreateNestedList(0, XNSpotLocation, &ic_pos, NULL); set_ic_pos("", 0); XMapRaised(dsp, win); } void ui_fini(void) { XFree(ic_pos_list); XDestroyIC(ic); XCloseIM(im); ft_free(ft_opt); ft_free(ft_inp); XFreeGC(dsp, gc); XDestroyWindow(dsp, win); XCloseDisplay(dsp); } UiKey xksym_to_uik(KeySym sym) { switch (sym) { case XK_Escape: return UIK_ESCAPE; case XK_BackSpace: return UIK_BACKSPACE; case XK_Up: return UIK_UP; case XK_Down: return UIK_DOWN; case XK_Left: return UIK_LEFT; case XK_Right: return UIK_RIGHT; case XK_Return: return UIK_RETURN; case XK_Home: return UIK_HOME; case XK_End: return UIK_END; case XK_Page_Down: return UIK_PGDN; case XK_Page_Up: return UIK_PGUP; default: return UIK_UNKNOWN; } } UiModMask xkstate_to_uim(unsigned state) { UiModMask m = 0; if (state & Mod1Mask) m |= UIM_ALT; if (state & ShiftMask) m |= UIM_SHIFT; if (state & ControlMask) m |= UIM_CTRL; return m; } /* TODO: allow selecting options with mouse */ /* TODO: change to use XOpenIM input method stuff */ int ui_wait_event(UiEvent *e) { XEvent ev; KeySym sym; Status status; loop: XNextEvent(dsp, &ev); if (XFilterEvent(&ev, None)) goto loop; switch (ev.type) { case KeyPress: e->type = UI_KEY_DOWN; e->key.strn = Xutf8LookupString(ic, &ev.xkey, e->key.str, sizeof(e->key.str), &sym, &status); e->key.key = xksym_to_uik(sym); e->key.mod = xkstate_to_uim(ev.xkey.state); return 1; case KeyRelease: e->type = UI_KEY_UP; e->key.strn = Xutf8LookupString(ic, &ev.xkey, e->key.str, sizeof(e->key.str), &sym, &status); e->key.key = xksym_to_uik(sym); return 1; case Expose: e->type = UI_REDRAW; return 1; case ClientMessage: if (ev.xclient.data.l[0] == wm_delete_window) { e->type = UI_QUIT; return 1; } break; case FocusIn: XSetICFocus(ic); break; case FocusOut: XUnsetICFocus(ic); break; } goto loop; return 0; } void ui_draw(Str input, int inpi, int seli, UiOpts o) { XWindowAttributes attr; XClearWindow(dsp, win); XGetWindowAttributes(dsp, win, &attr); set_ic_pos(input.s, inpi); /* draw input */ int w = ft_width(ft_inp, input.s, inpi); int y = 32; int ft_opt_asc, ft_opt_dsc, ft_inp_asc, ft_inp_dsc; ft_ascent_descent(ft_opt, &ft_opt_asc, &ft_opt_dsc); ft_ascent_descent(ft_inp, &ft_inp_asc, &ft_inp_dsc); XSetForeground(dsp, gc, inpbg); XFillRectangle(dsp, win, gc, 12, y - ft_opt_asc, w + 8, ft_opt_asc + ft_opt_dsc); XSetForeground(dsp, gc, inpfg); XDrawLine(dsp, win, gc, 16 + w, y - ft_inp_asc, 16 + w, y + ft_inp_dsc); Xutf8DrawString(dsp, win, ft_inp, gc, 16, y, input.s, input.n); XSetForeground(dsp, gc, fg); y += 24; /* draw options */ int lines = (attr.height - 32) / 24; int tline = lines / 2; int bline = lines - tline; int scroll = seli; if (scroll > o.n - bline) scroll = o.n - bline; if (scroll < tline) scroll = tline; int top = scroll - tline; int bot = scroll + bline; if (top < 0) top = 0; if (bot > o.n) bot = o.n; for (int i = top; i < bot; i++) { if (i == seli) { int w = ft_width(ft_opt, o.v[i].s, o.v[i].n); XSetForeground(dsp, gc, selbg); XFillRectangle(dsp, win, gc, 12, y - ft_opt_asc, w + 8, ft_opt_asc + ft_opt_dsc); XSetForeground(dsp, gc, selfg); Xutf8DrawString(dsp, win, ft_opt, gc, 16, y, o.v[i].s, o.v[i].n); XSetForeground(dsp, gc, fg); } else { Xutf8DrawString(dsp, win, ft_opt, gc, 16, y, o.v[i].s, o.v[i].n); } y += 24; } }