/* xmenu * a simple fuzzy-selection menu made with Xlib */ #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 XFontStruct *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 void set_ic_pos(const char *s, int i) { int w = XTextWidth(ft_inp, s, i); ic_pos.x = 16 + w; ic_pos.y = 24; XSetICValues(ic, /* XNFocusWindow, win, */ XNPreeditAttributes, ic_pos_list, NULL); XSetICFocus(ic); } void ui_init(int argc, char **argv, UiOpts opt) { 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)); szhint = (XSizeHints) { 0 }; szhint.width = 350; szhint.height = 250; 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, .event_mask = KeyPressMask | KeyReleaseMask | ExposureMask }); /* TODO: replace with modern XSetWMProperties */ /* char title[] = "xmenu"; XSetStandardProperties(dsp, win, title, title, None, argv, argc, &szhint); */ XSetWMProperties(dsp, win, NULL, NULL, argv, argc, &szhint, &(XWMHints) { .flags = InputHint, .input = 1 }, &(XClassHint) { "xmenu", "xmenu" }); gc = XCreateGC(dsp, win, 0, 0); XSetBackground(dsp, gc, bg); XSetForeground(dsp, gc, fg); //XSelectInput(dsp, win, KeyPressMask | ExposureMask); wm_delete_window = XInternAtom(dsp, "WM_DELETE_WINDOW", False); XSetWMProtocols(dsp, win, &wm_delete_window, 1); /* TODO: error checking */ ft_opt = XLoadQueryFont(dsp, "-*-new century schoolbook-medium-r-*-*-24-*-*-*-*-*-*-*"); ft_inp = XLoadQueryFont(dsp, "-*-new century schoolbook-medium-i-*-*-24-*-*-*-*-*-*-*"); if (!ft_opt) ft_opt = XLoadQueryFont(dsp, "fixed"); if (!ft_inp) ft_inp = XLoadQueryFont(dsp, "fixed"); int w = 0; for (int i = 0; i < opt.n; i++) { int ow = XTextWidth(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; XResizeWindow(dsp, win, w + 32, h + 32); XSetTransientForHint(dsp, win, DefaultRootWindow(dsp)); XMoveWindow(dsp, win, (scr_width - w - 32) / 2, (scr_height - h - 32) / 2 ); 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); XFreeFont(dsp, ft_opt); XFreeFont(dsp, 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; } fprintf(stderr, "[Unknown event %d]\n", ev.type); 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 = XTextWidth(ft_inp, input.s, inpi); int y = 32; XSetForeground(dsp, gc, inpbg); XFillRectangle(dsp, win, gc, 12, y - ft_opt->ascent, w + 8, ft_opt->descent + ft_opt->ascent); XSetForeground(dsp, gc, inpfg); XSetFont(dsp, gc, ft_inp->fid); XDrawLine(dsp, win, gc, 16 + w, y - ft_inp->ascent, 16 + w, y + ft_inp->descent); XDrawString(dsp, win, gc, 16, y, input.s, input.n); XSetForeground(dsp, gc, fg); y += 24; /* draw options */ XSetFont(dsp, gc, ft_opt->fid); 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 = XTextWidth(ft_opt, o.v[i].s, o.v[i].n); XSetForeground(dsp, gc, selbg); XFillRectangle(dsp, win, gc, 12, y - ft_opt->ascent, w + 8, ft_opt->descent + ft_opt->ascent); XSetForeground(dsp, gc, selfg); XDrawString(dsp, win, gc, 16, y, o.v[i].s, o.v[i].n); XSetForeground(dsp, gc, fg); } else { XDrawString(dsp, win, gc, 16, y, o.v[i].s, o.v[i].n); } y += 24; } }