/*
* 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);
}