]> git.draconx.ca Git - rrace.git/commitdiff
Add timer display.
authorNick Bowler <nbowler@draconx.ca>
Sun, 13 Mar 2022 04:21:41 +0000 (23:21 -0500)
committerNick Bowler <nbowler@draconx.ca>
Thu, 17 Mar 2022 00:08:51 +0000 (20:08 -0400)
Makefile.am
m4/.gitignore
m4/gnulib-cache.m4
src/game.c
src/game.h
src/motif.c
src/motif.h
src/motif_ui.c
src/motifgui.dat
src/xcounter.c [new file with mode: 0644]
src/xcounter.h [new file with mode: 0644]

index 60ec4761f4792825c058ccf83bd5f0a6f9edb2ec..15f3a5d3901ca14ba2c84acc7cfeff93872d307b 100644 (file)
@@ -23,7 +23,8 @@ dist_man_MANS = doc/rrace-motif.1
 endif
 
 rrace_motif_SOURCES = src/game.c src/x11.c src/game.h src/motif.h \
-                      src/colour.h src/ewmhicon.c src/ewmhicon.h
+                      src/colour.h src/ewmhicon.c src/ewmhicon.h \
+                      src/xcounter.c src/xcounter.h
 rrace_motif_LDADD = $(libmotifmain_a_OBJECTS) $(libmotifui_a_OBJECTS) \
                     $(libglohelp_a_OBJECTS) libgnu.a $(MOTIF_LIBS) \
                     $(LIB_CLOCK_GETTIME) $(LIB_GETHRXTIME)
index a6713567cbde6b414613b4cb21304ffe955c5920..5ee6340f89d6b5f192fef2c749735834521236f5 100644 (file)
@@ -3,6 +3,7 @@
 /clock_time.m4
 /extensions.m4
 /extern-inline.m4
+/flexmember.m4
 /gethrxtime.m4
 /getopt.m4
 /gettime.m4
index 165069a5f7b960db3a18aa30e69ebcf00181f75f..6b6352e434cf95a840807aadac99f12606f40ab8 100644 (file)
@@ -39,6 +39,7 @@
 #  --no-libtool \
 #  --macro-prefix=gl \
 #  --no-vc-files \
+#  flexmember \
 #  gethrxtime \
 #  getopt-gnu \
 #  inline
@@ -46,6 +47,7 @@
 # Specification in the form of a few gnulib-tool.m4 macro invocations:
 gl_LOCAL_DIR([])
 gl_MODULES([
+  flexmember
   gethrxtime
   getopt-gnu
   inline
index 999d52674abd10007334811ec3a8d8562c40d801..2ad3a7ed083b885cfddbc3c372b5865bbc72f67b 100644 (file)
@@ -218,14 +218,14 @@ void game_begin(struct board *board)
        board->time_start = gethrxtime();
 }
 
-static int_fast32_t elapsed(struct board *board)
+int_fast32_t game_elapsed(struct board *board)
 {
        return (gethrxtime() - board->time_start) / 1000000;
 }
 
 int_fast32_t game_finish(struct board *board)
 {
-       int_fast32_t t = elapsed(board);
+       int_fast32_t t = game_elapsed(board);
        int i;
 
        for (i = 0; i < 4; i++) {
index 176cb367a51e18671a5a8315961a0cda92658bb2..958cc60f94168489d966fc5ff21466d93c7ff2df 100644 (file)
@@ -188,6 +188,11 @@ void game_reset(struct board *board);
  */
 void game_begin(struct board *board);
 
+/*
+ * Return the total elapsed time (in ms) since the last call to game_begin.
+ */
+int_fast32_t game_elapsed(struct board *board);
+
 /*
  * Disable new moves and clear all tile bits other than the 9 goal tiles.
  * Returns the total elapsed time (in ms).
index 94e959b113f5d835b2a151550379449f015f2946..7396c0b7b7fdca7e156b45274b020bde1137443d 100644 (file)
@@ -28,6 +28,8 @@
 #include "motifopt.h"
 #include "game.h"
 
+#define TIMER_UPDATE_MS 33
+
 #define PROGNAME "rrace"
 static const char *progname = PROGNAME;
 static const struct option lopts[] = { LOPTS_INITIALIZER, {0} };
@@ -125,6 +127,23 @@ static Widget early_setup(XtAppContext *app, int argc, char **argv)
        return shell;
 }
 
+static void timer_tick(void *data, XtIntervalId *id)
+{
+       struct app_state *state = data;
+       XtAppContext app;
+
+       if (state->board.x > 4) {
+               /* Game is over */
+               state->timer_tick = 0;
+               return;
+       }
+
+       app = XtWidgetToApplicationContext(state->timer);
+       ui_timer_update(state, game_elapsed(&state->board));
+       state->timer_tick = XtAppAddTimeOut(app, TIMER_UPDATE_MS,
+                                           timer_tick, state);
+}
+
 static void do_input_move(struct app_state *state, int x, int y)
 {
        uint_fast32_t mask;
@@ -134,15 +153,13 @@ static void do_input_move(struct app_state *state, int x, int y)
                        int_fast32_t ms = game_finish(&state->board);
                        unsigned min, sec;
 
-                       /* Negative time just means clock jumps and
-                        * display headaches. */
-                       if (ms < 0)
-                               ms = 0;
+                       ui_timer_update(state, ms);
 
                        sec = ms / 1000, ms %= 1000;
                        min = sec / 60, sec %= 60;
-                       printf("You won!  Time was %u:%.2u:%.3u\n",
+                       printf("You won!  Time was %u:%.2u.%.3u\n",
                               min, sec, (unsigned)ms);
+
                        mask |= ~GOAL_MASK;
                }
 
@@ -211,6 +228,12 @@ static void proc_new_game(Widget w, XEvent *e, String *argv, Cardinal *argc)
        x11_redraw_game(&state, -1);
        x11_redraw_icon(&state, shell);
 
+       if (!state.timer_tick) {
+               XtAppContext app = XtWidgetToApplicationContext(w);
+               state.timer_tick = XtAppAddTimeOut(app, TIMER_UPDATE_MS,
+                                                  timer_tick, &state);
+       }
+
        game_begin(&state.board);
 }
 
index a5152939886fada6bd629e49d0515baf9a1fddec..cdbbe487434cd85afe4ed227e549432405edcf28 100644 (file)
 struct app_state {
        struct board board;
 
-       Widget game, goal;
+       Widget game, goal, timer;
 
        /* Current window width/height for resize handling. */
        Dimension game_sz[2], goal_sz[2];
 
+       XtIntervalId timer_tick;
+
        /* If true, the goal will be displayed over the main play area. */
        int view_goal_on_game;
 
@@ -44,6 +46,8 @@ struct app_state {
 };
 
 void ui_initialize(struct app_state *state, Widget shell);
+void ui_timer_update(struct app_state *state, int_fast32_t elapsed);
+
 void x11_initialize(struct app_state *state, Widget shell);
 void x11_redraw_icon(struct app_state *state, Widget shell);
 void x11_redraw_goal(struct app_state *state, uint_fast32_t mask);
index 4559ff664e6e6461f50dff86a5f90b22113b5274..a49dfec0717bd58ca985bcebcb48ae8b4128b00b 100644 (file)
@@ -24,6 +24,7 @@
 #include "motif.h"
 #include "motifstr.h"
 #include "motifgui.h"
+#include "xcounter.h"
 
 #define SPLIT_NUMERATOR    75
 #define SPLIT_DENOMINATOR 100
@@ -95,11 +96,35 @@ ResizeGameArea(Widget form, XEvent *e, String *args, Cardinal *num_args)
        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);
@@ -116,6 +141,17 @@ static void configure_mainwin(struct app_state *state, Widget form)
                                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);
@@ -123,6 +159,12 @@ static void configure_mainwin(struct app_state *state, Widget form)
                "<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,
index 18583eb9c8eae52127ddd813b3a9be4106268bb1..2e6a67545c006bc3f369a81b30df0f15dc884e36 100644 (file)
@@ -6,6 +6,7 @@ MAINWIN
     gameCanvas, 0, widgetDrawingArea
    goalArea, goalArea_OFFSET, widgetFrame
     goalCanvas, 0, widgetDrawingArea
+   timeDisplay, 0, widgetDrawingArea
 
 MAINMENU
  gameMenu, gameMenu_OFFSET, widgetCascadeButton, gameMenuLabel
diff --git a/src/xcounter.c b/src/xcounter.c
new file mode 100644 (file)
index 0000000..8266107
--- /dev/null
@@ -0,0 +1,347 @@
+/*
+ * 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);
+}
diff --git a/src/xcounter.h b/src/xcounter.h
new file mode 100644 (file)
index 0000000..6109b05
--- /dev/null
@@ -0,0 +1,105 @@
+/*
+ * 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