/* * Helpers for implementing a rapid-update counter display in Motif. * Copyright © 2022-2023 Nick Bowler * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include #include #include #include #include /* Define to 1 to add highlights and delays for visually debugging redraws */ #if X11_RENDER_DEBUG # include #endif #include "xcounter.h" #define MAX_STRINGS 20 #define DIGIT_LIST "0123456789" #define MARGIN 2 #define MIN(a, b) ((a) < (b) ? (a) : (b)) #define MAX(a, b) ((a) > (b) ? (a) : (b)) /* render flags in the upper bits are ORed into render_mask */ #define XC_TICK_INSTALLED (1ul << 31) #define XC_PROC_INSTALLED (1ul << 30) #define XC_RESIZING (1ul << 29) struct xcounter { Widget widget; GC gc; Pixmap pixmap; uint_least32_t render_mask; /* Cached width of the widget */ uint_least16_t render_width; unsigned char seq[23], num_strings; /* * xpos[n] is the x position of the nth string image within the * pixmap, plus the width of that image. */ uint_least16_t xpos[FLEXIBLE_ARRAY_MEMBER]; }; #define XC_SIZE(n) offsetof(struct xcounter, xpos[n]) static struct xcounter * xc_alloc(Widget w, XmRenderTable rt, XmString *strings, int nstr) { Display *display = XtDisplay(w); Screen *screen = XtScreen(w); unsigned long fg, bg; Drawable root = RootWindowOfScreen(screen); unsigned depth = DefaultDepthOfScreen(screen); unsigned wsum = 0, height = 0; struct xcounter *xc; int i; assert(nstr >= 0 && nstr < MAX_STRINGS); xc = (void *)XtMalloc(XC_SIZE(nstr)); xc->gc = XtAllocateGC(w, 0, 0, 0, GCForeground, GCBackground); memset(xc->seq, -1, sizeof xc->seq); xc->num_strings = nstr; xc->render_mask = 0; xc->widget = w; for (i = 0; i < nstr; i++) { unsigned h; if ((h = XmStringHeight(rt, strings[i])) > height) height = h + 2*MARGIN; wsum += XmStringWidth(rt, strings[i]); xc->xpos[i] = wsum; } XtVaSetValues(w, XmNheight, height + 2*MARGIN, (char *)NULL); XtVaGetValues(w, XmNforeground, &fg, XmNbackground, &bg, (char *)NULL); xc->pixmap = XCreatePixmap(display, root, wsum, height, depth); XSetForeground(display, xc->gc, bg); XFillRectangle(display, xc->pixmap, xc->gc, 0, 0, wsum, height); XSetForeground(display, xc->gc, fg); for (i = 0; i < nstr; i++) { unsigned xpos = i ? xc->xpos[i-1] : 0; unsigned w = xc->xpos[i] - xpos; XmStringDraw(display, xc->pixmap, rt, strings[i], xc->gc, xpos, 0, w, XmALIGNMENT_BEGINNING, XmSTRING_DIRECTION_L_TO_R, NULL); } return xc; } static struct xcounter *xc_configure(Widget w, XmRenderTable rt, char *tpl) { XmString strings[MAX_STRINGS]; struct xcounter *xc; int i, l; char *s; for (i = 0; i < 10; i++) { char s[2] = { '0' + i }; strings[i] = XmStringCreateLocalized(s); } for (s = tpl; s[0];) { if ((l = strspn(s, DIGIT_LIST))) { s += l; } if ((l = strcspn(s, DIGIT_LIST))) { char tmp = s[l]; s[l] = 0; strings[i++] = XmStringCreateLocalized(s); *(s += l) = tmp; } } xc = xc_alloc(w, rt, strings, i); for (i--; i >= 0; i--) { XmStringFree(strings[i]); } return xc; } struct xcounter *xcounter_init(Widget w, char *template) { struct xcounter *xc; XmRenderTable rt; Widget dummy; /* Use a dummy label widget's render table */ dummy = XmCreateLabelGadget(w, "text", NULL, 0); XtVaGetValues(dummy, XmNrenderTable, &rt, (char *)NULL); xc = xc_configure(w, rt, template); XtDestroyWidget(dummy); return xc; } /* * Internal helper to imlpement the xc_foreach_seq macro. */ static int xc_seq_iter(struct xcounter *xc, int i, unsigned *src_x, unsigned *w, uint_fast32_t *mask) { int seq_i; if ((seq_i = xc->seq[i]) == (unsigned char)-1) return 0; *src_x = seq_i ? xc->xpos[seq_i-1] : 0; *w = xc->xpos[seq_i] - *src_x; *mask <<= 1; return 1; } /* * xc_foreach_seq(xc, int, unsigned, unsigned, unsigned, uint_fast32_t) * * Helper macro to iterate through the display sequence. The first argument is * a pointer to the state structure. The remaining 5 arguments are lvalues of * the given type designating objects that are updated on each iteration. * * i - index into the sequence * out_x - x position for drawing of this element * src_x - x position in the pixmap of this element * w - width of this element * mask - shifted left by 1 each iteration */ #define xc_foreach_seq(xc, i, out_x, src_x, w, mask) for \ ( (out_x) = MARGIN, (i) = 0 \ ; (i) < sizeof (xc)->seq \ && xc_seq_iter(xc, i, &(src_x), &(w), &(mask)) \ ; (i)++, (out_x) += (w) ) /* * Redraw the changed portion of the text. The mask bitmap indicates which * elements of the sequence to redraw, with bit 0 corresponding to the last * possible element, bit 1 the second last, and so on. */ static void xc_redraw(struct xcounter *xc, uint_fast32_t mask) { Display *display = XtDisplay(xc->widget); Drawable d = XtWindow(xc->widget); unsigned out_x = MARGIN, src_x, w; Dimension max_w; int i; max_w = MAX(MARGIN, xc->render_width) - MARGIN; xc_foreach_seq(xc, i, out_x, src_x, w, mask) { if (out_x + w > max_w) { w = max_w - out_x; } if (mask & (1ul << sizeof xc->seq)) { #if X11_RENDER_DEBUG XRectangle r = { out_x, MARGIN, w, -1 }; XSetForeground(display, xc->gc, 0xff0000); XFillRectangles(display, d, xc->gc, &r, 1); XFlush(display); usleep(18000); #endif /* * NB: we want to copy the entire vertical contents * of the pixmap; using any large height value will * work but not too large otherwise there are visual * problems on Intel DDX (<=821722853 seems OK). */ XCopyArea(display, xc->pixmap, d, xc->gc, src_x, 0, w, 4096, out_x, MARGIN); } } assert(out_x <= max_w); XClearArea(display, d, out_x, 0, -1, -1, 0); } static Boolean xc_do_render(void *data) { struct xcounter *xc = data; xc_redraw(xc, xc->render_mask); xc->render_mask = 0; return True; } static void xc_start_render(void *data, XtIntervalId *id) { struct xcounter *xc = data; XtAppContext app; if (xc->render_mask & XC_PROC_INSTALLED) return; xc->render_mask |= XC_PROC_INSTALLED; app = XtWidgetToApplicationContext(xc->widget); XtAppAddWorkProc(app, xc_do_render, xc); } static void xc_queue_render(struct xcounter *xc, uint_fast32_t mask) { uint_fast32_t changed = xc->render_mask |= mask; XtAppContext app; if (changed & XC_TICK_INSTALLED || !changed) return; xc->render_mask |= XC_TICK_INSTALLED; app = XtWidgetToApplicationContext(xc->widget); XtAppAddTimeOut(app, 3, xc_start_render, xc); } static uint_fast32_t xc_add_seq(struct xcounter *xc, int i, unsigned char val) { uint_fast32_t ret; ret = xc->seq[i] != val; xc->seq[i] = val; return ret; } void xcounter_update(struct xcounter *xc, const char *str) { int static_seq = 10, i = 0, l; uint_fast32_t mask = 0; const char *s; for (s = str; s[0];) { if ((l = strcspn(s, DIGIT_LIST))) { mask <<= 1; mask |= xc_add_seq(xc, i++, static_seq++); s += l; } while (s[0] >= '0' && s[0] <= '9') { mask <<= 1; mask |= xc_add_seq(xc, i++, s[0] - '0'); s++; } } assert(static_seq <= MAX_STRINGS); assert(i <= sizeof xc->seq); mask <<= sizeof xc->seq - i; memset(xc->seq+i, -1, sizeof xc->seq - i); if (XtIsRealized(xc->widget)) xc_queue_render(xc, mask); } void xcounter_expose(struct xcounter *xc, XExposeEvent *e) { unsigned left = e->x, right = left + e->width; unsigned out_x = MARGIN, src_x, w; uint_fast32_t mask = 0; int i; xc_foreach_seq(xc, i, out_x, src_x, w, mask) { mask |= out_x + w >= left && out_x < right; } mask <<= sizeof xc->seq - i; xc_queue_render(xc, mask); } void xcounter_resize_cb(Widget w, void *data, void *cb_data) { struct xcounter *xc = data; Dimension width; XtVaGetValues(xc->widget, XmNwidth, &width, (char *)NULL); xc->render_width = width; xc_queue_render(xc, XC_RESIZING); } void xcounter_expose_cb(Widget w, void *data, void *cb_data) { XmDrawingAreaCallbackStruct *cbs = cb_data; if (cbs->reason == XmCR_EXPOSE) xcounter_expose(data, &cbs->event->xexpose); }