/*
* 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);
}