]> git.draconx.ca Git - rrace.git/blob - src/xcounter.c
8266107e11fb8f6c13703c8d0d06bf764af479c4
[rrace.git] / src / xcounter.c
1 /*
2  * Helpers for implementing a rapid-update counter display in Motif.
3  * Copyright © 2022 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 #include "xcounter.h"
26
27 #define DIGIT_LIST "0123456789"
28 #define MARGIN 2
29
30 static XmString digits[10];
31
32 #define XC_FLAG_STATIC  1u
33 #define XC_FLAG_CHANGED 2u
34
35 #define MIN(a, b) ((a) < (b) ? (a) : (b))
36 #define MAX(a, b) ((a) > (b) ? (a) : (b))
37
38 struct xcounter {
39         XmRenderTable rt;
40         GC gc;
41
42         uint_least8_t num_static, num_segments, total_segments;
43
44         /* Prior dimensions for resize handling. */
45         Dimension old_w, old_h;
46
47         struct xcounter_segment {
48                 XmString str;
49                 uint_least16_t width;
50                 uint_least8_t flags;
51         } *segments;
52
53         XmString static_strings[FLEXIBLE_ARRAY_MEMBER];
54 };
55
56 static void init_digits(void)
57 {
58         int i;
59
60         if (digits[0])
61                 return;
62
63         for (i = 0; i < 10; i++) {
64                 char s[2] = {'0' + i};
65                 digits[i] = XmStringCreateLocalized(s);
66         }
67 }
68
69 static void alloc_segments(struct xcounter *xc, int num_digits)
70 {
71         int count = num_digits + xc->num_static;
72
73         assert(num_digits >= 0 && count <= (uint_least8_t)-1);
74         xc->segments = (void *)XtRealloc((void *)xc->segments,
75                                          count * sizeof xc->segments[0]);
76
77         while (xc->total_segments < count) {
78                 struct xcounter_segment new_seg = {0};
79
80                 new_seg.flags = XC_FLAG_CHANGED;
81                 xc->segments[xc->total_segments++] = new_seg;
82         }
83         assert(xc->total_segments == count);
84 }
85
86 /* Add as many entries to the segments array as there are digits in s. */
87 static void realloc_segments(struct xcounter *xc, const char *s)
88 {
89         int new_digits = 0;
90         int l;
91
92         for (; s[0]; s += strcspn(s, DIGIT_LIST)) {
93                 l = strspn(s, DIGIT_LIST);
94                 new_digits += l;
95                 s += l;
96         }
97
98         alloc_segments(xc, xc->total_segments - xc->num_static + new_digits);
99 }
100
101 struct xcounter *xcounter_init(Widget w, char *template)
102 {
103         int l, num_static = 0, num_digits = 0;
104         struct xcounter *xc;
105         XmString *static_tab;
106         XmRenderTable rt;
107         Widget label;
108         char *s;
109
110         init_digits();
111
112         /* Count static segments and digits from template */
113         for (s = template; s[0];) {
114                 if ((l = strcspn(s, DIGIT_LIST))) {
115                         num_static++;
116                         s += l;
117                 }
118
119                 if ((l = strspn(s, DIGIT_LIST))) {
120                         num_digits += l;
121                         s += l;
122                 }
123         }
124
125         assert(num_static >= 0 && num_static < (uint_least8_t)-1);
126         xc = (void *)XtMalloc(sizeof *xc
127                             + num_static * sizeof xc->static_strings[0]);
128         xc->num_static = num_static;
129         xc->gc = XtAllocateGC(w, 0, 0, 0, 0, 0);
130         xc->num_segments = xc->total_segments = 0;
131         xc->old_w = xc->old_h = 0;
132         xc->segments = NULL;
133
134         /* Use a dummy label widget's render table */
135         label = XmCreateLabelGadget(w, "text", NULL, 0);
136         XtVaGetValues(label, XmNrenderTable, &rt, (char *)NULL);
137         xc->rt = XmRenderTableCopy(rt, NULL, 0);
138         XtDestroyWidget(label);
139
140         /* Initialize static string table */
141         static_tab = xc->static_strings;
142         for (s = template; s[0]; s += strspn(s, DIGIT_LIST)) {
143                 if ((l = strcspn(s, DIGIT_LIST))) {
144                         char tmp;
145
146                         tmp = s[l];
147                         s[l] = '\0';
148                         *static_tab++ = XmStringCreateLocalized(s);
149                         s[l] = tmp;
150                         s += l;
151                 }
152         }
153
154         alloc_segments(xc, num_digits);
155         return xc;
156 }
157
158 static void update_static(struct xcounter *xc, int seg_index,
159                           int str_index, unsigned *new_xpos)
160 {
161         struct xcounter_segment *seg = &xc->segments[seg_index];
162
163         assert(str_index < xc->num_static);
164
165         if (seg->str != xc->static_strings[str_index]) {
166                 seg->flags |= XC_FLAG_CHANGED;
167                 seg->flags |= XC_FLAG_STATIC;
168
169                 seg->str = xc->static_strings[str_index];
170                 seg->width = XmStringWidth(xc->rt, seg->str);
171         }
172         *new_xpos += seg->width;
173 }
174
175 static void update_digit(struct xcounter *xc, int seg_index,
176                          unsigned digit, unsigned *new_xpos)
177 {
178         struct xcounter_segment *seg = &xc->segments[seg_index];
179
180         assert(digit >= '0' && digit <= '9');
181         digit -= '0';
182
183         if (seg->str != digits[digit]) {
184                 seg->flags |= XC_FLAG_CHANGED;
185                 seg->flags &= ~XC_FLAG_STATIC;
186
187                 seg->str = digits[digit];
188                 seg->width = XmStringWidth(xc->rt, seg->str);
189         }
190         *new_xpos += seg->width;
191 }
192
193 static void check_position(struct xcounter *xc, int i, unsigned *prev_xpos,
194                                                        unsigned new_xpos)
195 {
196         struct xcounter_segment *seg = &xc->segments[i];
197
198         if (i >= xc->num_segments) {
199                 seg->flags |= XC_FLAG_CHANGED;
200                 return;
201         }
202
203         if (*prev_xpos != new_xpos)
204                 seg->flags |= XC_FLAG_CHANGED;
205         *prev_xpos += seg->width;
206 }
207
208 static void resize(Widget w, struct xcounter *xc, Dimension width)
209 {
210         Dimension line_height;
211
212         line_height = XmStringHeight(xc->rt, digits[0]);
213         XtVaSetValues(w, XmNwidth, width+4,
214                          XmNheight, line_height+4,
215                          (char *)NULL);
216 }
217
218 static int in_rect(XRectangle *r, int x, int y, int width, int height)
219 {
220         int i_x1, i_y1, i_x2, i_y2;
221
222         if (!r)
223                 return 0;
224
225         i_x1 = MAX(r->x, x);
226         i_y1 = MAX(r->y, y);
227         i_x2 = MIN(r->x + r->width, x + width);
228         i_y2 = MIN(r->y + r->height, y + height);
229
230         return i_x2 > i_x1 && i_y2 > i_y1;
231 }
232
233 /*
234  * Redraw the changed portion of the text.  There are two types of changes:
235  *
236  *   - The text is explicitly updated via xcounter_update, or
237  *   - A region needs to be redrawn due to expose events.
238  */
239 static void redraw(Widget w, struct xcounter *xc, XRectangle *expose)
240 {
241         Display *display = XtDisplay(w);
242         Window window = XtWindow(w);
243
244         Dimension line_height, width, xpos = MARGIN, ypos = MARGIN;
245         unsigned i;
246
247         XtVaGetValues(w, XmNwidth, &width, (char *)NULL);
248         line_height = XmStringHeight(xc->rt, digits[0]);
249
250         for (i = 0; i < xc->num_segments && xpos < width; i++) {
251                 struct xcounter_segment *seg = &xc->segments[i];
252                 int exposed, changed;
253
254                 exposed = in_rect(expose, xpos, ypos, seg->width, line_height);
255                 changed = seg->flags & XC_FLAG_CHANGED;
256
257                 if (changed)
258                         XClearArea(display, window, xpos, ypos, seg->width, line_height, 0);
259                 if (changed || exposed) {
260                         XmStringDraw(display, window, xc->rt, seg->str, xc->gc, xpos,
261                                 ypos, seg->width, XmALIGNMENT_BEGINNING,
262                                 XmSTRING_DIRECTION_L_TO_R, expose);
263                         seg->flags &= ~XC_FLAG_CHANGED;
264                 }
265
266                 xpos += seg->width;
267         }
268
269         XClearArea(display, window, MIN(xpos, width-MARGIN), MARGIN,
270                                     -1, line_height, 0);
271 }
272
273 void xcounter_update(Widget w, struct xcounter *xc, const char *str)
274 {
275         int i, l, num_static = 0;
276         unsigned prev_xpos = 0, new_xpos = 0;
277         const char *s;
278
279         for (i = 0, s = str; s[0];) {
280                 if ((l = strcspn(s, DIGIT_LIST))) {
281                         if (i >= xc->total_segments) {
282                                 assert(s > str && s[-1] >= '0' && s[-1] <= '9');
283                                 realloc_segments(xc, s-1);
284                         }
285                         check_position(xc, i, &prev_xpos, new_xpos);
286                         update_static(xc, i++, num_static++, &new_xpos);
287                         s += l;
288                 }
289
290                 for (l = strspn(s, DIGIT_LIST); l; l--) {
291                         if (i >= xc->total_segments)
292                                 realloc_segments(xc, s);
293                         check_position(xc, i, &prev_xpos, new_xpos);
294                         update_digit(xc, i++, *s, &new_xpos);
295                         s++;
296                 }
297         }
298         xc->num_segments = i;
299
300         resize(w, xc, new_xpos);
301         if (XtIsRealized(w))
302                 redraw(w, xc, NULL);
303 }
304
305 void xcounter_expose(Widget w, struct xcounter *xc, XExposeEvent *e)
306 {
307         XRectangle rect = { e->x, e->y, e->width, e->height };
308
309         XClearArea(XtDisplay(w), XtWindow(w),
310                    e->x, e->y, e->width, e->height, 0);
311         redraw(w, xc, &rect);
312 }
313
314 void xcounter_resize(Widget w, struct xcounter *xc,
315                      Dimension width, Dimension height)
316 {
317         if (XtIsRealized(w)) {
318                 if (width > xc->old_w) {
319                         XClearArea(XtDisplay(w), XtWindow(w),
320                                    xc->old_w - MARGIN, 0, -1, height, 1);
321                 }
322
323                 if (height > xc->old_h) {
324                         XClearArea(XtDisplay(w), XtWindow(w),
325                                    0, xc->old_h, xc->old_w, -1, 1);
326                 }
327         }
328
329         xc->old_w = MAX(width, MARGIN)-MARGIN;
330         xc->old_h = MAX(height, MARGIN)-MARGIN;
331 }
332
333 void xcounter_resize_cb(Widget w, void *data, void *cb_data)
334 {
335         Dimension width, height;
336
337         XtVaGetValues(w, XmNwidth, &width, XmNheight, &height, (char *)NULL);
338         xcounter_resize(w, data, width, height);
339 }
340
341 void xcounter_expose_cb(Widget w, void *data, void *cb_data)
342 {
343         XmDrawingAreaCallbackStruct *cbs = cb_data;
344
345         if (cbs->reason == XmCR_EXPOSE)
346                 xcounter_expose(w, data, &cbs->event->xexpose);
347 }