536 lines
20 KiB
C
536 lines
20 KiB
C
|
|
/**
|
||
|
|
* @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 <inttypes.h>
|
||
|
|
#include <string.h>
|
||
|
|
|
||
|
|
#include <akerror.h>
|
||
|
|
|
||
|
|
#include <akbasic/args.h>
|
||
|
|
#include <akbasic/error.h>
|
||
|
|
#include <akbasic/runtime.h>
|
||
|
|
#include <akbasic/ui.h>
|
||
|
|
|
||
|
|
#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);
|
||
|
|
strncpy(dest, value->stringval, len - 1);
|
||
|
|
dest[len - 1] = '\0';
|
||
|
|
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);
|
||
|
|
strncpy(name, arg->identifier, len - 1);
|
||
|
|
name[len - 1] = '\0';
|
||
|
|
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;
|
||
|
|
strncpy(obj->ui_state.variable, name, sizeof(obj->ui_state.variable) - 1);
|
||
|
|
obj->ui_state.variable[sizeof(obj->ui_state.variable) - 1] = '\0';
|
||
|
|
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");
|
||
|
|
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);
|
||
|
|
}
|