/* * X11 GUI for slide puzzle game * Copyright © 2022-2023 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 #include "motif.h" #include "motifgui.h" #include "xcounter.h" #include "version.h" #define tree_strtab strtab /* XXX generate this list? */ enum { widgetMainWindow, widgetForm, widgetFrame, widgetDrawingArea, widgetCascadeButton, widgetPushButton, widgetMax, /* Pseudo-widgets */ widgetMenuBar = widgetMax, widgetMessageDialog, widgetScrolledText, widgetUnmanaged = 128 }; static WidgetClass * const widgets[widgetMax] = { &xmMainWindowWidgetClass, &xmFormWidgetClass, &xmFrameWidgetClass, &xmDrawingAreaWidgetClass, &xmCascadeButtonWidgetClass, &xmPushButtonWidgetClass }; static const struct ui_widget { uint_least16_t name; uint_least8_t subtree; uint_least8_t widget_type; } mainwin[] = { MAINWIN_INITIALIZER }; static const struct ui_menuitem { struct ui_widget w; uint_least16_t label; } mainmenu[] = { MAINMENU_INITIALIZER }; 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_update(state->timer, "\n"); return; } xcounter_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]); Widget timer = XtNameToWidget(form, &tree_strtab[timeDisplay]); XtActionsRec resize_rec; char xc_template[100]; assert(gamearea && goalarea && timer); state->game = XtNameToWidget(gamearea, &tree_strtab[gameCanvas]); XtVaSetValues(gamearea, XmNleftAttachment, XmATTACH_FORM, XmNtopAttachment, XmATTACH_FORM, (char *)NULL); state->goal = XtNameToWidget(goalarea, &tree_strtab[goalCanvas]); XtVaSetValues(goalarea, XmNleftAttachment, XmATTACH_WIDGET, XmNleftWidget, gamearea, XmNtopAttachment, XmATTACH_OPPOSITE_WIDGET, XmNtopWidget, gamearea, (char *)NULL); XtVaSetValues(timer, XmNleftAttachment, XmATTACH_WIDGET, XmNleftWidget, gamearea, XmNtopAttachment, XmATTACH_WIDGET, XmNtopWidget, goalarea, XmNrightAttachment, XmATTACH_FORM, (char *)NULL); state->timer = xcounter_simple_init(timer, timer_text(0, xc_template)); ui_timer_update(state, -1); XtOverrideTranslations(form, XtParseTranslationTable( ": ResizeGameArea()\n" ": ResizeGameArea()\n" )); /* * Performing the initial update of the layout seems to avoid * some weird initial sizing problems on Motif 2.1 */ XtCallActionProc(form, "ResizeGameArea", 0, 0, 0); } static Widget create_widget(const struct ui_widget *item, Widget parent, ArgList args, Cardinal num_args) { String name = (void *)&tree_strtab[item->name]; unsigned type = item->widget_type & widgetUnmanaged-1; switch (type) { case widgetMenuBar: return XmCreateMenuBar(parent, name, args, num_args); case widgetMessageDialog: return XmCreateMessageDialog(parent, name, args, num_args); case widgetScrolledText: return XmCreateScrolledText(parent, name, args, num_args); } assert(type < widgetMax); return XtCreateWidget(name, *widgets[type], parent, args, num_args); } static void construct_widgets(const struct ui_widget *root, Widget parent, unsigned i) { const struct ui_widget *item; for (item = &root[i]; item->name; item++) { Widget w = create_widget(item, parent, NULL, 0); if (item->subtree) construct_widgets(root, w, item->subtree); if (!(item->widget_type & widgetUnmanaged)) XtManageChild(w); } } static void menu_cb(Widget w, void *data, void *cb_data) { XmRowColumnCallbackStruct *cbs = cb_data; XtCallActionProc(cbs->widget, XtName(cbs->widget), cbs->event, NULL, 0); } static Widget create_pulldown(Widget parent) { Widget w; w = XmCreatePulldownMenu(parent, XtName(parent), NULL, 0); XtVaSetValues(parent, XmNsubMenuId, w, (char *)NULL); XtAddCallback(w, XmNentryCallback, menu_cb, NULL); return w; } static void construct_menu(const struct ui_menuitem *root, Widget parent, unsigned i) { const struct ui_menuitem *item; for (item = &root[i]; item->w.name; item++) { const char *label = &strtab[item->label]; unsigned n = 0; Arg args[2]; XmString s; Widget w; if (XtClass(parent) == *widgets[widgetCascadeButton]) parent = create_pulldown(parent); if (label[0] && label[1] == '|') { XtSetArg(args[n], XmNmnemonic, label[0]); n++; label += 2; } s = XmStringCreateLocalized((void *)label); XtSetArg(args[n], XmNlabelString, s); n++; w = create_widget(&item->w, parent, args, n); XmStringFree(s); if (item->w.subtree) construct_menu(root, w, item->w.subtree); XtManageChild(w); } } /* Figure out which tiles intersect a rectangle. */ static uint_fast32_t x11_expose_mask(XExposeEvent *e, int tile_sz) { return board_rect( e->x/tile_sz, e->y/tile_sz, (e->x+e->width-1)/tile_sz, (e->y+e->height-1)/tile_sz ); } static void resize(Widget w, void *data, void *cb_data) { struct app_state *state = data; x11_queue_render(data, -1, 1 << (w == state->game)); } static void expose(Widget w, void *data, void *cb_data) { XmDrawingAreaCallbackStruct *cbs = cb_data; XExposeEvent *e = &cbs->event->xexpose; struct app_state *state = data; uint_fast32_t mask; Dimension tile_sz; if (w == state->game) { uint_least32_t *gp = state->board.game; /* * Only draw exposed nonempty tiles; exposed areas are filled * with the background automatically and thus exposed empty * spaces don't need to be drawn again. */ tile_sz = state->game_tile_sz; mask = gp[0] | gp[1] | gp[2]; } else { /* Goal area never has empty tiles. */ tile_sz = state->goal_tile_sz; mask = -1; } if (tile_sz) mask &= x11_expose_mask(e, tile_sz); x11_queue_render(state, mask, 1<<(w == state->game)); } void ui_initialize(struct app_state *state, Widget shell) { Widget menubar, help; construct_widgets(mainwin, shell, 0); menubar = XtNameToWidget(shell, &strtab[glob_menuBar]); construct_menu(mainmenu, menubar, 0); help = XtNameToWidget(menubar, tree_strtab+helpMenu); XtVaSetValues(menubar, XmNmenuHelpWidget, help, (char *)NULL); configure_mainwin(state, XtNameToWidget(shell, &strtab[glob_game])); XtAddCallback(state->game, XmNresizeCallback, resize, state); XtAddCallback(state->game, XmNexposeCallback, expose, state); XtAddCallback(state->goal, XmNresizeCallback, resize, state); XtAddCallback(state->goal, XmNexposeCallback, expose, state); } static void dialog_close(Widget w, void *data, void *cb_data) { XtDestroyWidget(w); } /* * Expands to an XtVaSetValues argument list to set the given resource with * a typed string value, which is internally converted to the appropriate * resource type. * * Note that str is expanded twice and thus should not have side effects. */ #define STRING_ARG(resource, str) \ XtVaTypedArg, (resource), XtRString, (str), strlen((str))+1 void ui_show_about(struct app_state *state, Widget shell) { static const struct ui_widget dialog[] = { ABOUTDIALOG_INITIALIZER }; Widget w, l; char *msg; construct_widgets(dialog, shell, 0); w = XtNameToWidget(shell, &strtab[glob_aboutDialog]); XtUnmanageChild(XmMessageBoxGetChild(w, XmDIALOG_CANCEL_BUTTON)); XtAddCallback(w, XmNunmapCallback, dialog_close, NULL); XtVaSetValues(w, STRING_ARG(XmNokLabelString, "Close"), (char *)NULL); msg = version_format_head("rrace-motif"); l = XmMessageBoxGetChild(w, XmDIALOG_MESSAGE_LABEL); XtVaSetValues(l, XmNlabelType, XmSTRING, #if HAVE_MOTIF_PIXMAP_AND_STRING XmNlabelType, XmPIXMAP_AND_STRING, #endif XmNlabelPixmap, state->icon_pixmap, STRING_ARG(XmNlabelString, msg), (char *)NULL); free(msg); l = XtNameToWidget(w, &strtab[glob_licenseBlurb]); XmTextSetString(l, "This program is free software: you can redistribute it and/or modify\n" "it under the terms of the GNU General Public License as published by\n" "the Free Software Foundation, either version 3 of the License, or\n" "(at your option) any later version.\n\n" "This program is distributed in the hope that it will be useful,\n" "but WITHOUT ANY WARRANTY; without even the implied warranty of\n" "MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n" "GNU General Public License for more details.\n\n" "You should have received a copy of the GNU General Public License\n" "along with this program. If not, see ."); XtVaSetValues(l, XmNeditMode, XmMULTI_LINE_EDIT, XmNeditable, FALSE, XmNresizeWidth, TRUE, XmNrows, 5, (char *)NULL); XtManageChild(w); }