/** * @file runtime_ui.c * @brief The group K verbs: MENU, GETMENU, RMENU, DIALOG, HUD and UISTYLE. * * Nothing here includes an SDL or a libakgl header. Every call goes through the * akbasic_UiBackend record the host attached, which is what lets the whole group * be tested in a build with no SDL on the machine. * * None of these is in the Go reference and none is in Commodore BASIC 7.0 -- * a C128 had no widget toolkit to wrap. They exist because libakgl 0.9.0 grew * one, and their shapes come from the verbs already here rather than from * anywhere else: MENU retires the way SOLID does, GETMENU holds the step loop * the way GETKEY does, and RMENU reads-and-clears the way BUMP does. * * The backend is a set of *setters*, not a set of draw calls. libakgl's UI is * immediate mode -- widgets are re-declared inside a frame bracket every frame * and clay borrows the text until the bracket closes -- and a BASIC program is * not. See include/akbasic/ui.h for the whole of that reasoning. */ #include #include #include #include #include #include #include #include "verbs.h" /* Most verbs answer "did something happen"; this is that answer. */ #define SUCCEED_TRUE(__obj, __dest) \ do { \ *(__dest) = &(__obj)->staticTrueValue; \ } while ( 0 ) /** * @brief Refuse politely when the host lent us no UI device. * * Every verb in this file opens with it, and it has to name the verb: "no UI * device" on its own tells a program author nothing about which line to look at. */ static akerr_ErrorContext *require_ui(akbasic_Runtime *obj, const char *verb) { PREPARE_ERROR(errctx); FAIL_ZERO_RETURN(errctx, (obj != NULL && verb != NULL), AKERR_NULLPOINTER, "NULL argument in require_ui"); FAIL_ZERO_RETURN(errctx, (obj->ui != NULL), AKBASIC_ERR_DEVICE, "%s needs a UI device and this runtime has none", verb); SUCCEED_RETURN(errctx); } /** * @brief Turn a BASIC menu number into a zero-based slot, or refuse. * * Menus are numbered from one, the way sprites and colour sources are. Nothing * in this dialect is numbered from zero and a UI group is a poor place to start. */ static akerr_ErrorContext *menu_index(int64_t n, const char *verb, int *dest) { PREPARE_ERROR(errctx); FAIL_ZERO_RETURN(errctx, (n >= 1 && n <= AKBASIC_UI_MAX_MENUS), AKBASIC_ERR_BOUNDS, "%s: menu %" PRId64 " is outside 1..%d", verb, n, AKBASIC_UI_MAX_MENUS); *dest = (int)(n - 1); SUCCEED_RETURN(errctx); } /** @brief The same for a HUD slot. */ static akerr_ErrorContext *label_index(int64_t n, const char *verb, int *dest) { PREPARE_ERROR(errctx); FAIL_ZERO_RETURN(errctx, (n >= 1 && n <= AKBASIC_UI_MAX_LABELS), AKBASIC_ERR_BOUNDS, "%s: slot %" PRId64 " is outside 1..%d", verb, n, AKBASIC_UI_MAX_LABELS); *dest = (int)(n - 1); SUCCEED_RETURN(errctx); } /** * @brief The nth argument of a command or function, or NULL past the end. * * A plain walk rather than a wrapper around akbasic_args_numbers(), because * every verb in this file mixes numbers and strings in one list and that helper * refuses a string by design -- correctly, for the groups it was written for. */ static akbasic_ASTLeaf *nth_argument(akbasic_ASTLeaf *expr, int n) { akbasic_ASTLeaf *arg = NULL; int i = 0; if ( expr == NULL ) { return NULL; } arg = akbasic_leaf_first_argument(expr); for ( i = 0; i < n && arg != NULL; i++ ) { arg = arg->next; } return arg; } /** @brief How many arguments the verb was given. */ static int argument_count(akbasic_ASTLeaf *expr) { akbasic_ASTLeaf *arg = NULL; int count = 0; if ( expr == NULL ) { return 0; } for ( arg = akbasic_leaf_first_argument(expr); arg != NULL; arg = arg->next ) { count += 1; } return count; } /** @brief Evaluate argument @p n and insist it is a number. */ static akerr_ErrorContext *nth_number(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, const char *verb, int n, int64_t *dest) { PREPARE_ERROR(errctx); akbasic_ASTLeaf *arg = nth_argument(expr, n); akbasic_Value *value = NULL; FAIL_ZERO_RETURN(errctx, (arg != NULL), AKBASIC_ERR_SYNTAX, "%s is missing argument %d", verb, n + 1); PASS(errctx, akbasic_runtime_evaluate(obj, arg, &value)); FAIL_NONZERO_RETURN(errctx, (value->valuetype == AKBASIC_TYPE_STRING), AKBASIC_ERR_TYPE, "%s expected a number in argument %d", verb, n + 1); *dest = (value->valuetype == AKBASIC_TYPE_FLOAT) ? (int64_t)value->floatval : value->intval; SUCCEED_RETURN(errctx); } /** * @brief Evaluate argument @p n, insist it is a string, and copy it out. * * Copied rather than pointed at. The value it came from lives in the per-line * pool, and a caller collecting sixteen of these has no promise that sixteen * separate slots were used -- so a table of pointers into the pool could quietly * be a table of sixteen pointers to the same string. */ static akerr_ErrorContext *nth_string(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, const char *verb, int n, char *dest, size_t len) { PREPARE_ERROR(errctx); akbasic_ASTLeaf *arg = nth_argument(expr, n); akbasic_Value *value = NULL; FAIL_ZERO_RETURN(errctx, (arg != NULL), AKBASIC_ERR_SYNTAX, "%s is missing argument %d", verb, n + 1); PASS(errctx, akbasic_runtime_evaluate(obj, arg, &value)); FAIL_ZERO_RETURN(errctx, (value->valuetype == AKBASIC_TYPE_STRING), AKBASIC_ERR_TYPE, "%s expected a string in argument %d", verb, n + 1); PASS(errctx, aksl_strcpy(dest, len, value->stringval)); SUCCEED_RETURN(errctx); } /** @brief Hand back an integer from a function. */ static akerr_ErrorContext *integer_result(akbasic_Runtime *obj, int64_t value, akbasic_Value **dest) { PREPARE_ERROR(errctx); akbasic_Value *out = NULL; PASS(errctx, akbasic_environment_new_value(obj->environment, &out)); PASS(errctx, akbasic_value_zero(out)); out->valuetype = AKBASIC_TYPE_INTEGER; out->intval = value; *dest = out; SUCCEED_RETURN(errctx); } /* ---------------------------------------------------------------- MENU --- */ akerr_ErrorContext *akbasic_cmd_menu(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest) { PREPARE_ERROR(errctx); /* * The entries are copied here rather than pointed at, so that what reaches * the backend is sixteen distinct strings whatever the value pool did. Four * kilobytes of stack, once, in a verb that is not on any hot path. */ char items[AKBASIC_UI_MAX_MENU_ITEMS][AKBASIC_MAX_STRING_LENGTH]; const char *pointers[AKBASIC_UI_MAX_MENU_ITEMS]; int count = 0; int entries = 0; int slot = 0; int i = 0; int64_t n = 0; (void)lval; (void)rval; PASS(errctx, require_ui(obj, "MENU")); count = argument_count(expr); /* No arguments retires them all, the way a bare SOLID does. */ if ( count == 0 ) { for ( i = 0; i < AKBASIC_UI_MAX_MENUS; i++ ) { PASS(errctx, obj->ui->menu(obj->ui, i, NULL, 0)); obj->ui_state.menudefined[i] = false; } SUCCEED_TRUE(obj, dest); SUCCEED_RETURN(errctx); } PASS(errctx, nth_number(obj, expr, "MENU", 0, &n)); PASS(errctx, menu_index(n, "MENU", &slot)); /* A number and nothing else retires that one. */ if ( count == 1 ) { PASS(errctx, obj->ui->menu(obj->ui, slot, NULL, 0)); obj->ui_state.menudefined[slot] = false; SUCCEED_TRUE(obj, dest); SUCCEED_RETURN(errctx); } entries = count - 1; FAIL_ZERO_RETURN(errctx, (entries <= AKBASIC_UI_MAX_MENU_ITEMS), AKBASIC_ERR_SYNTAX, "MENU takes at most %d entries and was given %d", AKBASIC_UI_MAX_MENU_ITEMS, entries); for ( i = 0; i < entries; i++ ) { PASS(errctx, nth_string(obj, expr, "MENU", i + 1, items[i], sizeof(items[i]))); pointers[i] = items[i]; } PASS(errctx, obj->ui->menu(obj->ui, slot, pointers, entries)); obj->ui_state.menudefined[slot] = true; SUCCEED_TRUE(obj, dest); SUCCEED_RETURN(errctx); } /* ------------------------------------------------------------- GETMENU --- */ /** * @brief Identify the numeric variable a GETMENU assigns into. * * Numeric only. The answer is an entry number, and a program that wanted the * entry's *text* already has it -- it wrote the MENU. */ static akerr_ErrorContext *target_variable(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Variable **dest, char *name, size_t len) { PREPARE_ERROR(errctx); akbasic_ASTLeaf *arg = nth_argument(expr, 1); FAIL_ZERO_RETURN(errctx, (arg != NULL), AKBASIC_ERR_SYNTAX, "GETMENU expected a menu number and a variable"); /* * Either numeric spelling -- `C%` or `C!` -- and not a bare `C`, which in * this dialect is a *label* rather than a variable and cannot be assigned to * at all. GETKEY refuses one for the same reason. A string is refused * because the answer is an entry *number*: a program that wanted the entry's * text already has it, since it wrote the MENU. */ FAIL_ZERO_RETURN(errctx, (arg->leaftype == AKBASIC_LEAF_IDENTIFIER_INT || arg->leaftype == AKBASIC_LEAF_IDENTIFIER_FLOAT), AKBASIC_ERR_TYPE, "GETMENU expected a numeric variable such as C%%"); PASS(errctx, akbasic_environment_get(obj->environment, arg->identifier, dest)); FAIL_ZERO_RETURN(errctx, (*dest != NULL), AKBASIC_ERR_UNDEFINED, "GETMENU could not reach the variable %s", arg->identifier); PASS(errctx, aksl_strcpy(name, len, arg->identifier)); SUCCEED_RETURN(errctx); } /** @brief Write an entry number -- or 0 for "released without a choice" -- into a variable. */ static akerr_ErrorContext *store_entry(akbasic_Variable *variable, int64_t entry) { PREPARE_ERROR(errctx); int64_t zerosubscript[1] = { 0 }; PASS(errctx, akbasic_variable_set_integer(variable, entry, zerosubscript, 1)); SUCCEED_RETURN(errctx); } akerr_ErrorContext *akbasic_cmd_getmenu(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest) { PREPARE_ERROR(errctx); akbasic_Variable *variable = NULL; char name[64]; int slot = 0; int selected = 0; bool activated = false; int64_t n = 0; (void)lval; (void)rval; PASS(errctx, require_ui(obj, "GETMENU")); PASS(errctx, nth_number(obj, expr, "GETMENU", 0, &n)); PASS(errctx, menu_index(n, "GETMENU", &slot)); PASS(errctx, target_variable(obj, expr, &variable, name, sizeof(name))); FAIL_ZERO_RETURN(errctx, obj->ui_state.menudefined[slot], AKBASIC_ERR_STATE, "GETMENU: menu %" PRId64 " has no entries; define it with MENU first", n); PASS(errctx, obj->ui->menu_state(obj->ui, slot, &selected, &activated, true)); if ( activated ) { PASS(errctx, store_entry(variable, (int64_t)selected + 1)); SUCCEED_TRUE(obj, dest); SUCCEED_RETURN(errctx); } /* * Nobody has chosen yet. A C128 would sit here; the library may not, so the * step loop is told to stop advancing the program counter instead. The * program stays on this line, the host keeps its frame rate, and the sprite, * audio and collision services -- which run before this one -- keep running * underneath. See akbasic_ui_service(). */ obj->ui_state.waiting = true; obj->ui_state.waitmenu = slot; PASS(errctx, aksl_strcpy(obj->ui_state.variable, sizeof(obj->ui_state.variable), name)); SUCCEED_TRUE(obj, dest); SUCCEED_RETURN(errctx); } /* --------------------------------------------------------------- RMENU --- */ akerr_ErrorContext *akbasic_fn_rmenu(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest) { PREPARE_ERROR(errctx); int slot = 0; int selected = 0; bool activated = false; int64_t n = 0; int64_t field = 0; (void)lval; (void)rval; FAIL_ZERO_RETURN(errctx, (obj != NULL && dest != NULL), AKERR_NULLPOINTER, "NULL argument in RMENU"); PASS(errctx, require_ui(obj, "RMENU")); PASS(errctx, nth_number(obj, expr, "RMENU", 0, &n)); PASS(errctx, nth_number(obj, expr, "RMENU", 1, &field)); PASS(errctx, menu_index(n, "RMENU", &slot)); FAIL_ZERO_RETURN(errctx, (field == 0 || field == 1), AKBASIC_ERR_BOUNDS, "RMENU: field %" PRId64 " is outside 0..1", field); /* * Field 1 clears the latch and field 0 does not, which is the whole * difference between them: reading which entry is highlighted must not * consume the fact that one was chosen, and reading that one was chosen must * consume it or a polling loop sees the same choice forever. Same contract * BUMP() carries, and for the same reason. */ PASS(errctx, obj->ui->menu_state(obj->ui, slot, &selected, &activated, (field == 1))); if ( field == 1 ) { /* * An integer -1 or 0, not a BOOLEAN. A BOOLEAN value prints as "true" or * "false" -- the Go reference's %t, kept deliberately and recorded in * src/value.c -- and `PRINT RMENU(1,1)` saying "false" where every other * predicate in this dialect says 0 would be a surprise with no upside. * Both work in `IF RMENU(1,1) THEN`. */ PASS(errctx, integer_result(obj, activated ? AKBASIC_TRUE : 0, dest)); SUCCEED_RETURN(errctx); } PASS(errctx, integer_result(obj, (int64_t)selected + 1, dest)); SUCCEED_RETURN(errctx); } /* -------------------------------------------------------------- DIALOG --- */ akerr_ErrorContext *akbasic_cmd_dialog(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest) { PREPARE_ERROR(errctx); char text[AKBASIC_MAX_STRING_LENGTH]; int count = 0; (void)lval; (void)rval; PASS(errctx, require_ui(obj, "DIALOG")); count = argument_count(expr); /* * No argument takes the panel down. libakgl has no dismiss contract at all * -- a declarative frame is the visibility flag -- so this verb is both the * only way up and the only way down. */ if ( count == 0 ) { PASS(errctx, obj->ui->dialog(obj->ui, NULL)); SUCCEED_TRUE(obj, dest); SUCCEED_RETURN(errctx); } FAIL_ZERO_RETURN(errctx, (count == 1), AKBASIC_ERR_SYNTAX, "DIALOG takes one string, or nothing at all"); PASS(errctx, nth_string(obj, expr, "DIALOG", 0, text, sizeof(text))); PASS(errctx, obj->ui->dialog(obj->ui, text)); SUCCEED_TRUE(obj, dest); SUCCEED_RETURN(errctx); } /* ----------------------------------------------------------------- HUD --- */ akerr_ErrorContext *akbasic_cmd_hud(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest) { PREPARE_ERROR(errctx); char text[AKBASIC_MAX_STRING_LENGTH]; int count = 0; int slot = 0; int i = 0; int64_t n = 0; int64_t anchor = 0; (void)lval; (void)rval; PASS(errctx, require_ui(obj, "HUD")); count = argument_count(expr); if ( count == 0 ) { for ( i = 0; i < AKBASIC_UI_MAX_LABELS; i++ ) { PASS(errctx, obj->ui->label(obj->ui, i, AKBASIC_UI_ANCHOR_TOP_LEFT, NULL)); } SUCCEED_TRUE(obj, dest); SUCCEED_RETURN(errctx); } PASS(errctx, nth_number(obj, expr, "HUD", 0, &n)); PASS(errctx, label_index(n, "HUD", &slot)); if ( count == 1 ) { PASS(errctx, obj->ui->label(obj->ui, slot, AKBASIC_UI_ANCHOR_TOP_LEFT, NULL)); SUCCEED_TRUE(obj, dest); SUCCEED_RETURN(errctx); } FAIL_ZERO_RETURN(errctx, (count == 3), AKBASIC_ERR_SYNTAX, "HUD expected a slot, an anchor and a string"); PASS(errctx, nth_number(obj, expr, "HUD", 1, &anchor)); FAIL_ZERO_RETURN(errctx, (anchor >= 0 && anchor < AKBASIC_UI_ANCHOR_LIMIT), AKBASIC_ERR_BOUNDS, "HUD: anchor %" PRId64 " is outside 0..%d", anchor, (int)AKBASIC_UI_ANCHOR_LIMIT - 1); PASS(errctx, nth_string(obj, expr, "HUD", 2, text, sizeof(text))); PASS(errctx, obj->ui->label(obj->ui, slot, (int)anchor, text)); SUCCEED_TRUE(obj, dest); SUCCEED_RETURN(errctx); } /* ------------------------------------------------------------- UISTYLE --- */ akerr_ErrorContext *akbasic_cmd_uistyle(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest) { PREPARE_ERROR(errctx); double args[5]; akbasic_Color fill; akbasic_Color edge; akbasic_Color ink; int count = 0; double padding = 0.0; double radius = 0.0; (void)lval; (void)rval; PASS(errctx, require_ui(obj, "UISTYLE")); PASS(errctx, akbasic_args_numbers(obj, expr, "UISTYLE", args, 5, &count)); /* * No arguments hands the widgets back to libakgl's own default, which is a * dark textbox with parchment ink. Three NULLs say "the library's", not * "black": a style is not a thing this interpreter can spell in full, and * inventing values here would make UISTYLE with no arguments mean something * different from never having called it. */ if ( count == 0 ) { PASS(errctx, obj->ui->style(obj->ui, NULL, NULL, NULL, 0.0, 0.0)); SUCCEED_TRUE(obj, dest); SUCCEED_RETURN(errctx); } FAIL_ZERO_RETURN(errctx, (count >= 3), AKBASIC_ERR_SYNTAX, "UISTYLE expected a fill, an edge and an ink colour"); PASS(errctx, akbasic_graphics_palette((int)args[0], &fill)); PASS(errctx, akbasic_graphics_palette((int)args[1], &edge)); PASS(errctx, akbasic_graphics_palette((int)args[2], &ink)); padding = (count >= 4) ? args[3] : 8.0; radius = (count >= 5) ? args[4] : 0.0; FAIL_NONZERO_RETURN(errctx, (padding < 0.0 || radius < 0.0), AKBASIC_ERR_VALUE, "UISTYLE: padding and radius cannot be negative"); PASS(errctx, obj->ui->style(obj->ui, &fill, &edge, &ink, padding, radius)); SUCCEED_TRUE(obj, dest); SUCCEED_RETURN(errctx); } /* ------------------------------------------------------- the step loop --- */ akerr_ErrorContext *akbasic_ui_state_init(akbasic_UiState *obj) { PREPARE_ERROR(errctx); FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER, "NULL argument in ui_state_init"); PASS(errctx, aksl_memset(obj, 0, sizeof(*obj))); SUCCEED_RETURN(errctx); } akerr_ErrorContext *akbasic_ui_service(akbasic_Runtime *obj, bool *blocked) { PREPARE_ERROR(errctx); akbasic_Variable *variable = NULL; int selected = 0; bool activated = false; bool released = false; FAIL_ZERO_RETURN(errctx, (obj != NULL && blocked != NULL), AKERR_NULLPOINTER, "NULL argument in ui_service"); *blocked = false; if ( !obj->ui_state.waiting ) { SUCCEED_RETURN(errctx); } /* * Two ways out that are not a choice, and neither may wedge the script. A * host is allowed to change its mind about what it lends out, and a program * is allowed to retire the menu it is waiting on -- from an interrupt * handler, which is the only place it could -- so both release the hold and * assign 0. Same rule akbasic_input_service() keeps for a withdrawn keyboard. */ if ( obj->ui == NULL ) { released = true; } else if ( !obj->ui_state.menudefined[obj->ui_state.waitmenu] ) { released = true; } else { PASS(errctx, obj->ui->menu_state(obj->ui, obj->ui_state.waitmenu, &selected, &activated, true)); if ( !activated ) { *blocked = true; SUCCEED_RETURN(errctx); } } obj->ui_state.waiting = false; PASS(errctx, akbasic_environment_get(obj->environment, obj->ui_state.variable, &variable)); FAIL_ZERO_RETURN(errctx, (variable != NULL), AKBASIC_ERR_UNDEFINED, "GETMENU could not reach the variable %s", obj->ui_state.variable); PASS(errctx, store_entry(variable, released ? 0 : (int64_t)selected + 1)); SUCCEED_RETURN(errctx); }