From: Nick Bowler Date: Sun, 13 Mar 2022 04:21:41 +0000 (-0500) Subject: Add timer display. X-Git-Url: http://git.draconx.ca/gitweb/rrace.git/commitdiff_plain/4ba1a1949117408cf81132b4f168a4e7f0a79ac3 Add timer display. --- diff --git a/Makefile.am b/Makefile.am index 60ec476..15f3a5d 100644 --- a/Makefile.am +++ b/Makefile.am @@ -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) diff --git a/m4/.gitignore b/m4/.gitignore index a671356..5ee6340 100644 --- a/m4/.gitignore +++ b/m4/.gitignore @@ -3,6 +3,7 @@ /clock_time.m4 /extensions.m4 /extern-inline.m4 +/flexmember.m4 /gethrxtime.m4 /getopt.m4 /gettime.m4 diff --git a/m4/gnulib-cache.m4 b/m4/gnulib-cache.m4 index 165069a..6b6352e 100644 --- a/m4/gnulib-cache.m4 +++ b/m4/gnulib-cache.m4 @@ -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 diff --git a/src/game.c b/src/game.c index 999d526..2ad3a7e 100644 --- a/src/game.c +++ b/src/game.c @@ -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++) { diff --git a/src/game.h b/src/game.h index 176cb36..958cc60 100644 --- a/src/game.h +++ b/src/game.h @@ -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). diff --git a/src/motif.c b/src/motif.c index 94e959b..7396c0b 100644 --- a/src/motif.c +++ b/src/motif.c @@ -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); } diff --git a/src/motif.h b/src/motif.h index a515293..cdbbe48 100644 --- a/src/motif.h +++ b/src/motif.h @@ -27,11 +27,13 @@ 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); diff --git a/src/motif_ui.c b/src/motif_ui.c index 4559ff6..a49dfec 100644 --- a/src/motif_ui.c +++ b/src/motif_ui.c @@ -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) ": ResizeGameArea()\n" ": 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, diff --git a/src/motifgui.dat b/src/motifgui.dat index 18583eb..2e6a675 100644 --- a/src/motifgui.dat +++ b/src/motifgui.dat @@ -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 index 0000000..8266107 --- /dev/null +++ b/src/xcounter.c @@ -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 . + */ + +#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); +} diff --git a/src/xcounter.h b/src/xcounter.h new file mode 100644 index 0000000..6109b05 --- /dev/null +++ b/src/xcounter.h @@ -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 . + */ + +/* + * 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