#include "motif.h"
#include "motifstr.h"
#include "motifgui.h"
+#include "xcounter.h"
#define SPLIT_NUMERATOR 75
#define SPLIT_DENOMINATOR 100
XtVaSetValues(goal, XmNwidth, goalsz, XmNheight, goalsz, (char *)NULL);
}
+static char *timer_text(int_fast32_t ms, char *buf)
+{
+ unsigned min, sec;
+
+ sec = ms / 1000, ms %= 1000;
+ min = sec / 60, sec %= 60;
+ sprintf(buf, "Time: %u:%.2u.%.3u", min, sec, (unsigned)ms);
+
+ return buf;
+}
+
+void ui_timer_update(struct app_state *state, int_fast32_t ms)
+{
+ char buf[100];
+
+ if (ms < 0) {
+ xcounter_simple_update(state->timer, "\n");
+ return;
+ }
+
+ xcounter_simple_update(state->timer, timer_text(ms, buf));
+}
+
static void configure_mainwin(struct app_state *state, Widget form)
{
Widget gamearea = XtNameToWidget(form, &tree_strtab[gameArea]);
Widget goalarea = XtNameToWidget(form, &tree_strtab[goalArea]);
XtActionsRec resize_rec;
+ char xc_template[100];
assert(gamearea && goalarea);
XtVaSetValues(form, XmNfractionBase, SPLIT_DENOMINATOR, (char *)NULL);
XmNtopWidget, gamearea,
(char *)NULL);
+ state->timer = XtNameToWidget(form, &tree_strtab[timeDisplay]);
+ XtVaSetValues(state->timer, XmNleftAttachment, XmATTACH_WIDGET,
+ XmNleftWidget, gamearea,
+ XmNtopAttachment, XmATTACH_WIDGET,
+ XmNtopWidget, goalarea,
+ XmNrightAttachment, XmATTACH_FORM,
+ (char *)NULL);
+
+ xcounter_simple_setup(state->timer, timer_text(20000, xc_template));
+ ui_timer_update(state, -1);
+
resize_rec.string = "ResizeGameArea";
resize_rec.proc = ResizeGameArea;
XtAppAddActions(XtWidgetToApplicationContext(form), &resize_rec, 1);
"<Configure>: ResizeGameArea()\n"
"<Map>: ResizeGameArea()\n"
));
+
+ /*
+ * Performing the initial update of the layout seems to avoid
+ * some weird problems on Motif 2.1
+ */
+ ResizeGameArea(form, 0, 0, 0);
}
static Widget create_widget(const struct ui_widget *item, Widget parent,
--- /dev/null
+/*
+ * 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 <https://www.gnu.org/licenses/>.
+ */
+
+#include <config.h>
+#include <string.h>
+#include <inttypes.h>
+#include <assert.h>
+#include <Xm/XmAll.h>
+
+#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);
+}
--- /dev/null
+/*
+ * 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 <https://www.gnu.org/licenses/>.
+ */
+
+/*
+ * A set of functions to implement a simple counter display with better support
+ * for rapid updates than regular XmLabel (and other Motif text display widgets).
+ *
+ * This is intended to avoid two specific practical problems with these widgets:
+ *
+ * (1) Any update to the text involves dynamic allocation of XmStrings
+ * (2) Updating the text causes the entire label to be redrawn.
+ *
+ * Point #2 can lead to annoying flicker on static portions of the label when
+ * updates are occurring continuously.
+ *
+ * These functions are intended for displaying strings that consist of non-
+ * changing static text with digits interspersed. Specifically, strings such
+ * as "The time is 12:30:57."
+ *
+ * Passing this string as the template argument to xcounter_init will pre-
+ * allocate an XmString values for each of the four maximal nondigit
+ * substrings. These nondigit parts cannot be changed.
+ *
+ * On a subsequent call to xcounter_update, a different string may be passed.
+ * The digits in this string are used to construct a sequence of XmString
+ * values that includes the precomputed static portions interspersed with
+ * digits. This new string is expected to be in the same format as the
+ * template although there are two possible exceptions:
+ *
+ * - The nondigit substrings do not need to match the template; they are
+ * used only to separate different groups of digits and otherwise have
+ * no effect on the output.
+ *
+ * - If the new string may be a prefix of the template, only that portion
+ * will be output. However it is still not possible to alter the static
+ * portions: they are either present in their entirety or omitted.
+ *
+ * The xcounter_redraw function can then be used to redraw only the portions of
+ * the string that have changed since the last redraw.
+ */
+
+#ifndef XCOUNTER_H_
+#define XCOUNTER_H_
+
+/*
+ * Create xcounter based on template, which should be representative of
+ * the number of digits expected to preallocate a typical number of digits.
+ *
+ * Note that the provided template string will be modified by this function,
+ * but restored to its original value before returning.
+ */
+struct xcounter *xcounter_init(Widget w, char *template);
+
+/*
+ * Update xcounter segments according to str.
+ *
+ * The nondigit sequences are assumed to match those from the original
+ * template.
+ */
+void xcounter_update(Widget w, struct xcounter *xc, const char *str);
+
+/*
+ * Redraw the counter in response to an expose event.
+ */
+void xcounter_expose(Widget w, struct xcounter *xc, XExposeEvent *e);
+
+/*
+ * Resize and expose callbacks that can be registered on a drawing area widget.
+ */
+void xcounter_resize_cb(Widget w, void *data, void *cb_data);
+void xcounter_expose_cb(Widget w, void *data, void *cb_data);
+
+static inline void xcounter_simple_setup(Widget w, char *template)
+{
+ struct xcounter *xc = xcounter_init(w, template);
+
+ XtVaSetValues(w, XmNuserData, (void *)xc, (char *)NULL);
+ XtAddCallback(w, XmNresizeCallback, xcounter_resize_cb, xc);
+ XtAddCallback(w, XmNexposeCallback, xcounter_expose_cb, xc);
+}
+
+static inline void xcounter_simple_update(Widget w, const char *str)
+{
+ void *xc;
+
+ XtVaGetValues(w, XmNuserData, &xc, (char *)NULL);
+ xcounter_update(w, xc, str);
+}
+
+#endif