]> git.draconx.ca Git - rrace.git/blob - src/xcounter.c
motif: Don't query widget width every timer update.
[rrace.git] / src / xcounter.c
1 /*
2  * Helpers for implementing a rapid-update counter display in Motif.
3  * Copyright © 2022-2023 Nick Bowler
4  *
5  * This program is free software: you can redistribute it and/or modify
6  * it under the terms of the GNU General Public License as published by
7  * the Free Software Foundation, either version 3 of the License, or
8  * (at your option) any later version.
9  *
10  * This program is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13  * GNU General Public License for more details.
14  *
15  * You should have received a copy of the GNU General Public License
16  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
17  */
18
19 #include <config.h>
20 #include <string.h>
21 #include <inttypes.h>
22 #include <assert.h>
23 #include <Xm/XmAll.h>
24
25 /* Define to 1 to add highlights and delays for visually debugging redraws */
26 #if X11_RENDER_DEBUG
27 #  include <unistd.h>
28 #endif
29
30 #include "xcounter.h"
31
32 #define MAX_STRINGS 20
33 #define DIGIT_LIST "0123456789"
34 #define MARGIN 2
35
36 #define MIN(a, b) ((a) < (b) ? (a) : (b))
37 #define MAX(a, b) ((a) > (b) ? (a) : (b))
38
39 /* render flags in the upper bits are ORed into render_mask */
40 #define XC_TICK_INSTALLED (1ul << 31)
41 #define XC_PROC_INSTALLED (1ul << 30)
42 #define XC_RESIZING       (1ul << 29)
43
44 struct xcounter {
45         Widget widget;
46         GC gc;
47
48         Pixmap pixmap;
49
50         uint_least32_t render_mask;
51
52         /* Cached width of the widget */
53         uint_least16_t render_width;
54
55         unsigned char seq[23], num_strings;
56
57         /*
58          * xpos[n] is the x position of the nth string image within the
59          * pixmap, plus the width of that image.
60          */
61         uint_least16_t xpos[FLEXIBLE_ARRAY_MEMBER];
62 };
63 #define XC_SIZE(n) offsetof(struct xcounter, xpos[n])
64
65 static struct xcounter *
66 xc_alloc(Widget w, XmRenderTable rt, XmString *strings, int nstr)
67 {
68         Display *display = XtDisplay(w);
69         Screen *screen = XtScreen(w);
70         unsigned long fg, bg;
71
72         Drawable root = RootWindowOfScreen(screen);
73         unsigned depth = DefaultDepthOfScreen(screen);
74         unsigned wsum = 0, height = 0;
75         struct xcounter *xc;
76         int i;
77
78         assert(nstr >= 0 && nstr < MAX_STRINGS);
79         xc = (void *)XtMalloc(XC_SIZE(nstr));
80         xc->gc = XtAllocateGC(w, 0, 0, 0, GCForeground, GCBackground);
81         memset(xc->seq, -1, sizeof xc->seq);
82         xc->num_strings = nstr;
83         xc->render_mask = 0;
84         xc->widget = w;
85
86         for (i = 0; i < nstr; i++) {
87                 unsigned h;
88
89                 if ((h = XmStringHeight(rt, strings[i])) > height)
90                         height = h + 2*MARGIN;
91
92                 wsum += XmStringWidth(rt, strings[i]);
93                 xc->xpos[i] = wsum;
94         }
95
96         XtVaSetValues(w, XmNheight, height + 2*MARGIN, (char *)NULL);
97         XtVaGetValues(w, XmNforeground, &fg, XmNbackground, &bg, (char *)NULL);
98
99         xc->pixmap = XCreatePixmap(display, root, wsum, height, depth);
100         XSetForeground(display, xc->gc, bg);
101         XFillRectangle(display, xc->pixmap, xc->gc, 0, 0, wsum, height);
102
103         XSetForeground(display, xc->gc, fg);
104         for (i = 0; i < nstr; i++) {
105                 unsigned xpos = i ? xc->xpos[i-1] : 0;
106                 unsigned w = xc->xpos[i] - xpos;
107
108                 XmStringDraw(display, xc->pixmap, rt, strings[i], xc->gc,
109                              xpos, 0, w, XmALIGNMENT_BEGINNING,
110                              XmSTRING_DIRECTION_L_TO_R, NULL);
111         }
112
113         return xc;
114 }
115
116 static struct xcounter *xc_configure(Widget w, XmRenderTable rt, char *tpl)
117 {
118         XmString strings[MAX_STRINGS];
119         struct xcounter *xc;
120         int i, l;
121         char *s;
122
123         for (i = 0; i < 10; i++) {
124                 char s[2] = { '0' + i };
125
126                 strings[i] = XmStringCreateLocalized(s);
127         }
128
129         for (s = tpl; s[0];) {
130                 if ((l = strspn(s, DIGIT_LIST))) {
131                         s += l;
132                 }
133
134                 if ((l = strcspn(s, DIGIT_LIST))) {
135                         char tmp = s[l];
136
137                         s[l] = 0;
138                         strings[i++] = XmStringCreateLocalized(s);
139                         *(s += l) = tmp;
140                 }
141         }
142
143         xc = xc_alloc(w, rt, strings, i);
144
145         for (i--; i >= 0; i--) {
146                 XmStringFree(strings[i]);
147         }
148
149         return xc;
150 }
151
152 struct xcounter *xcounter_init(Widget w, char *template)
153 {
154         struct xcounter *xc;
155         XmRenderTable rt;
156         Widget dummy;
157
158         /* Use a dummy label widget's render table */
159         dummy = XmCreateLabelGadget(w, "text", NULL, 0);
160         XtVaGetValues(dummy, XmNrenderTable, &rt, (char *)NULL);
161
162         xc = xc_configure(w, rt, template);
163
164         XtDestroyWidget(dummy);
165
166         return xc;
167 }
168
169 /*
170  * Internal helper to imlpement the xc_foreach_seq macro.
171  */
172 static int xc_seq_iter(struct xcounter *xc, int i, unsigned *src_x,
173                                             unsigned *w, uint_fast32_t *mask)
174 {
175         int seq_i;
176
177         if ((seq_i = xc->seq[i]) == (unsigned char)-1)
178                 return 0;
179
180         *src_x = seq_i ? xc->xpos[seq_i-1] : 0;
181         *w = xc->xpos[seq_i] - *src_x;
182         *mask <<= 1;
183         return 1;
184 }
185
186 /*
187  * xc_foreach_seq(xc, int, unsigned, unsigned, unsigned, uint_fast32_t)
188  *
189  * Helper macro to iterate through the display sequence.  The first argument is
190  * a pointer to the state structure.  The remaining 5 arguments are lvalues of
191  * the given type designating objects that are updated on each iteration.
192  *
193  *   i     - index into the sequence
194  *   out_x - x position for drawing of this element
195  *   src_x - x position in the pixmap of this element
196  *   w     - width of this element
197  *   mask  - shifted left by 1 each iteration
198  */
199 #define xc_foreach_seq(xc, i, out_x, src_x, w, mask) for \
200         ( (out_x) = MARGIN, (i) = 0 \
201         ; (i) < sizeof (xc)->seq \
202           && xc_seq_iter(xc, i, &(src_x), &(w), &(mask)) \
203         ; (i)++, (out_x) += (w) )
204
205 /*
206  * Redraw the changed portion of the text.  The mask bitmap indicates which
207  * elements of the sequence to redraw, with bit 0 corresponding to the last
208  * possible element, bit 1 the second last, and so on.
209  */
210 static void xc_redraw(struct xcounter *xc, uint_fast32_t mask)
211 {
212         Display *display = XtDisplay(xc->widget);
213         Drawable d = XtWindow(xc->widget);
214
215         unsigned out_x = MARGIN, src_x, w;
216         Dimension max_w;
217         int i;
218
219         max_w = MAX(MARGIN, xc->render_width) - MARGIN;
220         xc_foreach_seq(xc, i, out_x, src_x, w, mask) {
221                 if (out_x + w > max_w) {
222                         w = max_w - out_x;
223                 }
224
225                 if (mask & (1ul << sizeof xc->seq)) {
226 #if X11_RENDER_DEBUG
227                         XRectangle r = { out_x, MARGIN, w, -1 };
228
229                         XSetForeground(display, xc->gc, 0xff0000);
230                         XFillRectangles(display, d, xc->gc, &r, 1);
231                         XFlush(display);
232                         usleep(18000);
233 #endif
234                         /*
235                          * NB: we want to copy the entire vertical contents
236                          * of the pixmap; using any large height value will
237                          * work but not too large otherwise there are visual
238                          * problems on Intel DDX (<=821722853 seems OK).
239                          */
240                         XCopyArea(display, xc->pixmap, d, xc->gc,
241                                   src_x, 0, w, 4096,
242                                   out_x, MARGIN);
243                 }
244         }
245
246         assert(out_x <= max_w);
247         XClearArea(display, d, out_x, 0, -1, -1, 0);
248 }
249
250 static Boolean xc_do_render(void *data)
251 {
252         struct xcounter *xc = data;
253
254         xc_redraw(xc, xc->render_mask);
255         xc->render_mask = 0;
256         return True;
257 }
258
259 static void xc_start_render(void *data, XtIntervalId *id)
260 {
261         struct xcounter *xc = data;
262         XtAppContext app;
263
264         if (xc->render_mask & XC_PROC_INSTALLED)
265                 return;
266
267         xc->render_mask |= XC_PROC_INSTALLED;
268         app = XtWidgetToApplicationContext(xc->widget);
269         XtAppAddWorkProc(app, xc_do_render, xc);
270 }
271
272 static void xc_queue_render(struct xcounter *xc, uint_fast32_t mask)
273 {
274         uint_fast32_t changed = xc->render_mask |= mask;
275         XtAppContext app;
276
277         if (changed & XC_TICK_INSTALLED || !changed)
278                 return;
279
280         xc->render_mask |= XC_TICK_INSTALLED;
281         app = XtWidgetToApplicationContext(xc->widget);
282         XtAppAddTimeOut(app, 3, xc_start_render, xc);
283 }
284
285 static uint_fast32_t xc_add_seq(struct xcounter *xc, int i, unsigned char val)
286 {
287         uint_fast32_t ret;
288
289         ret = xc->seq[i] != val;
290         xc->seq[i] = val;
291
292         return ret;
293 }
294
295 void xcounter_update(struct xcounter *xc, const char *str)
296 {
297         int static_seq = 10, i = 0, l;
298         uint_fast32_t mask = 0;
299         const char *s;
300
301         for (s = str; s[0];) {
302                 if ((l = strcspn(s, DIGIT_LIST))) {
303
304                         mask <<= 1;
305                         mask |= xc_add_seq(xc, i++, static_seq++);
306                         s += l;
307                 }
308
309                 while (s[0] >= '0' && s[0] <= '9') {
310                         mask <<= 1;
311                         mask |= xc_add_seq(xc, i++, s[0] - '0');
312                         s++;
313                 }
314         }
315
316         assert(static_seq <= MAX_STRINGS);
317         assert(i <= sizeof xc->seq);
318
319         mask <<= sizeof xc->seq - i;
320         memset(xc->seq+i, -1, sizeof xc->seq - i);
321
322         if (XtIsRealized(xc->widget))
323                 xc_queue_render(xc, mask);
324 }
325
326 void xcounter_expose(struct xcounter *xc, XExposeEvent *e)
327 {
328         unsigned left = e->x, right = left + e->width;
329         unsigned out_x = MARGIN, src_x, w;
330         uint_fast32_t mask = 0;
331         int i;
332
333         xc_foreach_seq(xc, i, out_x, src_x, w, mask) {
334                 mask |= out_x + w >= left && out_x < right;
335         }
336         mask <<= sizeof xc->seq - i;
337
338         xc_queue_render(xc, mask);
339 }
340
341 void xcounter_resize_cb(Widget w, void *data, void *cb_data)
342 {
343         struct xcounter *xc = data;
344         Dimension width;
345
346         XtVaGetValues(xc->widget, XmNwidth, &width, (char *)NULL);
347         xc->render_width = width;
348
349         xc_queue_render(xc, XC_RESIZING);
350 }
351
352 void xcounter_expose_cb(Widget w, void *data, void *cb_data)
353 {
354         XmDrawingAreaCallbackStruct *cbs = cb_data;
355
356         if (cbs->reason == XmCR_EXPOSE)
357                 xcounter_expose(data, &cbs->event->xexpose);
358 }