/* * Helpers for implementing a rapid-update counter display in Motif. * Copyright © 2022 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 #include "xcounter.h" #define DIGIT_LIST "0123456789" #define MARGIN 2 static XmString digits[10]; #define XC_FLAG_STATIC 1u #define XC_FLAG_CHANGED 2u #define MIN(a, b) ((a) < (b) ? (a) : (b)) #define MAX(a, b) ((a) > (b) ? (a) : (b)) struct xcounter { XmRenderTable rt; GC gc; uint_least8_t num_static, num_segments, total_segments; /* Prior dimensions for resize handling. */ Dimension old_w, old_h; struct xcounter_segment { XmString str; uint_least16_t width; uint_least8_t flags; } *segments; XmString static_strings[FLEXIBLE_ARRAY_MEMBER]; }; static void init_digits(void) { int i; if (digits[0]) return; for (i = 0; i < 10; i++) { char s[2] = {'0' + i}; digits[i] = XmStringCreateLocalized(s); } } static void alloc_segments(struct xcounter *xc, int num_digits) { int count = num_digits + xc->num_static; assert(num_digits >= 0 && count <= (uint_least8_t)-1); xc->segments = (void *)XtRealloc((void *)xc->segments, count * sizeof xc->segments[0]); while (xc->total_segments < count) { struct xcounter_segment new_seg = {0}; new_seg.flags = XC_FLAG_CHANGED; xc->segments[xc->total_segments++] = new_seg; } assert(xc->total_segments == count); } /* Add as many entries to the segments array as there are digits in s. */ static void realloc_segments(struct xcounter *xc, const char *s) { int new_digits = 0; int l; for (; s[0]; s += strcspn(s, DIGIT_LIST)) { l = strspn(s, DIGIT_LIST); new_digits += l; s += l; } alloc_segments(xc, xc->total_segments - xc->num_static + new_digits); } struct xcounter *xcounter_init(Widget w, char *template) { int l, num_static = 0, num_digits = 0; struct xcounter *xc; XmString *static_tab; XmRenderTable rt; Widget label; char *s; init_digits(); /* Count static segments and digits from template */ for (s = template; s[0];) { if ((l = strcspn(s, DIGIT_LIST))) { num_static++; s += l; } if ((l = strspn(s, DIGIT_LIST))) { num_digits += l; s += l; } } assert(num_static >= 0 && num_static < (uint_least8_t)-1); xc = (void *)XtMalloc(sizeof *xc + num_static * sizeof xc->static_strings[0]); xc->num_static = num_static; xc->gc = XtAllocateGC(w, 0, 0, 0, 0, 0); xc->num_segments = xc->total_segments = 0; xc->old_w = xc->old_h = 0; xc->segments = NULL; /* Use a dummy label widget's render table */ label = XmCreateLabelGadget(w, "text", NULL, 0); XtVaGetValues(label, XmNrenderTable, &rt, (char *)NULL); xc->rt = XmRenderTableCopy(rt, NULL, 0); XtDestroyWidget(label); /* Initialize static string table */ static_tab = xc->static_strings; for (s = template; s[0]; s += strspn(s, DIGIT_LIST)) { if ((l = strcspn(s, DIGIT_LIST))) { char tmp; tmp = s[l]; s[l] = '\0'; *static_tab++ = XmStringCreateLocalized(s); s[l] = tmp; s += l; } } alloc_segments(xc, num_digits); return xc; } static void update_static(struct xcounter *xc, int seg_index, int str_index, unsigned *new_xpos) { struct xcounter_segment *seg = &xc->segments[seg_index]; assert(str_index < xc->num_static); if (seg->str != xc->static_strings[str_index]) { seg->flags |= XC_FLAG_CHANGED; seg->flags |= XC_FLAG_STATIC; seg->str = xc->static_strings[str_index]; seg->width = XmStringWidth(xc->rt, seg->str); } *new_xpos += seg->width; } static void update_digit(struct xcounter *xc, int seg_index, unsigned digit, unsigned *new_xpos) { struct xcounter_segment *seg = &xc->segments[seg_index]; assert(digit >= '0' && digit <= '9'); digit -= '0'; if (seg->str != digits[digit]) { seg->flags |= XC_FLAG_CHANGED; seg->flags &= ~XC_FLAG_STATIC; seg->str = digits[digit]; seg->width = XmStringWidth(xc->rt, seg->str); } *new_xpos += seg->width; } static void check_position(struct xcounter *xc, int i, unsigned *prev_xpos, unsigned new_xpos) { struct xcounter_segment *seg = &xc->segments[i]; if (i >= xc->num_segments) { seg->flags |= XC_FLAG_CHANGED; return; } if (*prev_xpos != new_xpos) seg->flags |= XC_FLAG_CHANGED; *prev_xpos += seg->width; } static void resize(Widget w, struct xcounter *xc, Dimension width) { Dimension line_height; line_height = XmStringHeight(xc->rt, digits[0]); XtVaSetValues(w, XmNwidth, width+4, XmNheight, line_height+4, (char *)NULL); } static int in_rect(XRectangle *r, int x, int y, int width, int height) { int i_x1, i_y1, i_x2, i_y2; if (!r) return 0; i_x1 = MAX(r->x, x); i_y1 = MAX(r->y, y); i_x2 = MIN(r->x + r->width, x + width); i_y2 = MIN(r->y + r->height, y + height); return i_x2 > i_x1 && i_y2 > i_y1; } /* * Redraw the changed portion of the text. There are two types of changes: * * - The text is explicitly updated via xcounter_update, or * - A region needs to be redrawn due to expose events. */ static void redraw(Widget w, struct xcounter *xc, XRectangle *expose) { Display *display = XtDisplay(w); Window window = XtWindow(w); Dimension line_height, width, xpos = MARGIN, ypos = MARGIN; unsigned i; XtVaGetValues(w, XmNwidth, &width, (char *)NULL); line_height = XmStringHeight(xc->rt, digits[0]); for (i = 0; i < xc->num_segments && xpos < width; i++) { struct xcounter_segment *seg = &xc->segments[i]; int exposed, changed; exposed = in_rect(expose, xpos, ypos, seg->width, line_height); changed = seg->flags & XC_FLAG_CHANGED; if (changed) XClearArea(display, window, xpos, ypos, seg->width, line_height, 0); if (changed || exposed) { XmStringDraw(display, window, xc->rt, seg->str, xc->gc, xpos, ypos, seg->width, XmALIGNMENT_BEGINNING, XmSTRING_DIRECTION_L_TO_R, expose); seg->flags &= ~XC_FLAG_CHANGED; } xpos += seg->width; } XClearArea(display, window, MIN(xpos, width-MARGIN), MARGIN, -1, line_height, 0); } void xcounter_update(Widget w, struct xcounter *xc, const char *str) { int i, l, num_static = 0; unsigned prev_xpos = 0, new_xpos = 0; const char *s; for (i = 0, s = str; s[0];) { if ((l = strcspn(s, DIGIT_LIST))) { if (i >= xc->total_segments) { assert(s > str && s[-1] >= '0' && s[-1] <= '9'); realloc_segments(xc, s-1); } check_position(xc, i, &prev_xpos, new_xpos); update_static(xc, i++, num_static++, &new_xpos); s += l; } for (l = strspn(s, DIGIT_LIST); l; l--) { if (i >= xc->total_segments) realloc_segments(xc, s); check_position(xc, i, &prev_xpos, new_xpos); update_digit(xc, i++, *s, &new_xpos); s++; } } xc->num_segments = i; resize(w, xc, new_xpos); if (XtIsRealized(w)) redraw(w, xc, NULL); } void xcounter_expose(Widget w, struct xcounter *xc, XExposeEvent *e) { XRectangle rect = { e->x, e->y, e->width, e->height }; XClearArea(XtDisplay(w), XtWindow(w), e->x, e->y, e->width, e->height, 0); redraw(w, xc, &rect); } void xcounter_resize(Widget w, struct xcounter *xc, Dimension width, Dimension height) { if (XtIsRealized(w)) { if (width > xc->old_w) { XClearArea(XtDisplay(w), XtWindow(w), xc->old_w - MARGIN, 0, -1, height, 1); } if (height > xc->old_h) { XClearArea(XtDisplay(w), XtWindow(w), 0, xc->old_h, xc->old_w, -1, 1); } } xc->old_w = MAX(width, MARGIN)-MARGIN; xc->old_h = MAX(height, MARGIN)-MARGIN; } void xcounter_resize_cb(Widget w, void *data, void *cb_data) { Dimension width, height; XtVaGetValues(w, XmNwidth, &width, XmNheight, &height, (char *)NULL); xcounter_resize(w, data, width, height); } void xcounter_expose_cb(Widget w, void *data, void *cb_data) { XmDrawingAreaCallbackStruct *cbs = cb_data; if (cbs->reason == XmCR_EXPOSE) xcounter_expose(w, data, &cbs->event->xexpose); }