216 lines
8.1 KiB
Markdown
216 lines
8.1 KiB
Markdown
|
|
# 19. Menus and dialogs
|
||
|
|
|
||
|
|
Everything in this chapter needs the SDL build and a UI device. Without one each verb
|
||
|
|
refuses by name:
|
||
|
|
|
||
|
|
```basic requires=noakgl
|
||
|
|
10 MENU 1, "START", "QUIT"
|
||
|
|
```
|
||
|
|
|
||
|
|
```output
|
||
|
|
? 10 : RUNTIME ERROR MENU needs a UI device and this runtime has none
|
||
|
|
|
||
|
|
```
|
||
|
|
|
||
|
|
Every picture in this chapter is generated by running the listing above it; see
|
||
|
|
`MAINTENANCE.md` if you are editing one.
|
||
|
|
|
||
|
|
## Three widgets, and no fourth
|
||
|
|
|
||
|
|
A menu the player picks from, a panel that says something, and a line of text pinned to
|
||
|
|
a corner. That is the whole set, and it is the set because it is the set
|
||
|
|
[libakgl](../deps/libakgl/docs/22-ui.md) supplies — these verbs are a thin skin over
|
||
|
|
`akgl_ui_menu`, `akgl_ui_dialog` and `akgl_ui_label`, and they are deliberately not a
|
||
|
|
window toolkit. *What is not here*, at the end, says what to do when you want more.
|
||
|
|
|
||
|
|
## MENU
|
||
|
|
|
||
|
|
```basic requires=akgl ui=1 screenshot=menu
|
||
|
|
10 UISTYLE 1, 8, 2
|
||
|
|
20 MENU 1, "START", "OPTIONS", "QUIT"
|
||
|
|
```
|
||
|
|
|
||
|
|

|
||
|
|
|
||
|
|
`MENU n, item$ [, item$ ...]` puts menu `n` on screen with those entries. `n` runs 1 to
|
||
|
|
4, so a pause menu can sit over a title menu, and there are at most 16 entries — that is
|
||
|
|
libakgl's own ceiling and not a number invented here.
|
||
|
|
|
||
|
|
The player moves the highlight with the cursor keys or a D-pad and confirms with Return
|
||
|
|
or a gamepad's south button. The mouse works too: moving onto a row highlights it and
|
||
|
|
clicking one confirms it. A pointer sitting still claims nothing, so a mouse parked over
|
||
|
|
the list does not fight the keyboard for the highlight.
|
||
|
|
|
||
|
|
Defining a menu **resets its highlight to the first entry and forgets any unread
|
||
|
|
choice.** The entries have just changed meaning; an index into the old list is not worth
|
||
|
|
carrying across, and a stale choice of an entry that no longer exists is worse.
|
||
|
|
|
||
|
|
`MENU n` with nothing after it retires that menu. `MENU` on its own retires all four —
|
||
|
|
the same convention `SOLID` uses, where no rectangle retires one and no arguments retire
|
||
|
|
them all.
|
||
|
|
|
||
|
|
## GETMENU
|
||
|
|
|
||
|
|
```basic norun
|
||
|
|
10 MENU 1, "START", "OPTIONS", "QUIT"
|
||
|
|
20 GETMENU 1, C%
|
||
|
|
30 ON C% GOTO 500, 600, 900
|
||
|
|
```
|
||
|
|
|
||
|
|
`GETMENU n, var` parks the program until somebody chooses, then puts the entry's number
|
||
|
|
in `var` — 1 for the first — and carries on. The variable has to be a numeric one with a
|
||
|
|
sigil, `C%` or `C!`; a bare `C` is a *label* in this dialect and cannot be assigned to at
|
||
|
|
all, which is the same rule `GETKEY` keeps.
|
||
|
|
|
||
|
|
**Parking is not blocking.** The interpreter never stops returning to its host: while a
|
||
|
|
`GETMENU` is unanswered every step comes straight back without advancing the program, so
|
||
|
|
the frame rate holds, sprites keep moving, `PLAY` keeps draining its queue, and a game
|
||
|
|
that embeds the interpreter stays in control of its own loop. What stops is the program,
|
||
|
|
which is the part you asked for.
|
||
|
|
|
||
|
|
Two things end the wait without a choice, and both assign 0 rather than leaving the
|
||
|
|
program parked forever: retiring the menu, and the host taking the UI device away. A
|
||
|
|
script waiting on something that no longer exists is worse than a script told nothing
|
||
|
|
happened.
|
||
|
|
|
||
|
|
## RMENU
|
||
|
|
|
||
|
|
```basic norun
|
||
|
|
100 REM the same menu, without parking on it
|
||
|
|
110 IF RMENU(1,1) THEN C% = RMENU(1,0) : GOSUB 800
|
||
|
|
120 GOSUB 900 : REM move everything else
|
||
|
|
130 GOTO 110
|
||
|
|
```
|
||
|
|
|
||
|
|
`RMENU(n, 0)` is the entry currently highlighted. `RMENU(n, 1)` is -1 once the menu has
|
||
|
|
been confirmed and 0 otherwise, **and reading it clears it** — exactly the contract
|
||
|
|
`BUMP()` carries, and for the same reason: without it a program that polls in a loop sees
|
||
|
|
the same choice forever.
|
||
|
|
|
||
|
|
Reading the highlight does not clear anything. Only field 1 consumes.
|
||
|
|
|
||
|
|
This is what a game with a loop of its own wants. `GETMENU` is what a title screen wants.
|
||
|
|
|
||
|
|
## DIALOG
|
||
|
|
|
||
|
|
```basic requires=akgl ui=1 size=480x200 screenshot=dialog
|
||
|
|
10 UISTYLE 1, 8, 2
|
||
|
|
20 DIALOG "IT IS PITCH BLACK."
|
||
|
|
```
|
||
|
|
|
||
|
|

|
||
|
|
|
||
|
|
A panel across the bottom of the screen with wrapped text on it. `DIALOG text$` puts it
|
||
|
|
up; `DIALOG` on its own takes it down.
|
||
|
|
|
||
|
|
**There is no dismiss key and no return value.** libakgl's dialog has neither — a panel
|
||
|
|
is up because the frame declares it — so showing and hiding is the program's business
|
||
|
|
and `DIALOG` with no argument *is* the dismissal. The usual shape is a `GETKEY` between
|
||
|
|
the two:
|
||
|
|
|
||
|
|
```basic norun
|
||
|
|
10 DIALOG "YOU FOUND A KEY."
|
||
|
|
20 GETKEY K$
|
||
|
|
30 DIALOG
|
||
|
|
```
|
||
|
|
|
||
|
|
The text wraps but does not scroll or page. A message longer than the panel is clipped by
|
||
|
|
it, visibly, which is deliberate on libakgl's side: a clipped line is a bug you can see.
|
||
|
|
|
||
|
|
## HUD
|
||
|
|
|
||
|
|
```basic requires=akgl ui=1 size=480x200 screenshot=hud
|
||
|
|
10 UISTYLE 1, 8, 2
|
||
|
|
20 HUD 1, 0, "LIVES 3"
|
||
|
|
30 HUD 2, 1, "SCORE 00120"
|
||
|
|
40 HUD 3, 4, "PAUSED"
|
||
|
|
```
|
||
|
|
|
||
|
|

|
||
|
|
|
||
|
|
`HUD n, anchor, text$` pins a line of text somewhere on the screen. `n` is a slot, 1 to
|
||
|
|
8, and the anchor is one of five places:
|
||
|
|
|
||
|
|
| Anchor | Where |
|
||
|
|
|---|---|
|
||
|
|
| 0 | Top left |
|
||
|
|
| 1 | Top right |
|
||
|
|
| 2 | Bottom left |
|
||
|
|
| 3 | Bottom right |
|
||
|
|
| 4 | Centre |
|
||
|
|
|
||
|
|
Five, and the numbering is not the reading order, because these are libakgl's five
|
||
|
|
`akgl_UiAnchor` values and its numbering. **There is no top-centre or bottom-centre.**
|
||
|
|
A title bar is two corners or the middle.
|
||
|
|
|
||
|
|
A slot keeps its text until something replaces or retires it, so a score display is one
|
||
|
|
`HUD` per change rather than one per frame. `HUD n` retires slot `n` and `HUD` retires
|
||
|
|
all eight.
|
||
|
|
|
||
|
|
## UISTYLE
|
||
|
|
|
||
|
|
```basic requires=akgl ui=1 screenshot=uistyle
|
||
|
|
10 UISTYLE 3, 7, 2, 14, 8
|
||
|
|
20 MENU 1, "ONE", "TWO"
|
||
|
|
30 HUD 1, 0, "STYLED"
|
||
|
|
```
|
||
|
|
|
||
|
|

|
||
|
|
|
||
|
|
`UISTYLE fill, edge, ink [, padding [, radius]]` sets the one look every widget draws
|
||
|
|
with. The three colours are ordinary palette indices, 1 to 16, the same ones `COLOR`
|
||
|
|
takes. `padding` is the gap in pixels between a panel's edge and its text, and between a
|
||
|
|
`HUD` label and the screen edge it hugs; `radius` rounds the corners, and 0 is square.
|
||
|
|
Padding defaults to 8 and radius to 0.
|
||
|
|
|
||
|
|
`UISTYLE` with no arguments goes back to libakgl's own default, a dark textbox with
|
||
|
|
parchment ink. It goes back to the *library's* default rather than to a copy of it, so
|
||
|
|
"never styled" and "styled back" stay the same thing.
|
||
|
|
|
||
|
|
There is one style, shared. Per-widget looks are not implemented — see below.
|
||
|
|
|
||
|
|
## Who owns the keyboard
|
||
|
|
|
||
|
|
**A menu that is up owns the cursor keys and Return.** It has to: those are how a menu is
|
||
|
|
driven, and something has to have them. While at least one menu has entries, Up, Down and
|
||
|
|
Return go to it and never reach `GET`, `GETKEY` or the line editor.
|
||
|
|
|
||
|
|
Everything else still gets through — letters, Escape, function keys, the gamepad buttons
|
||
|
|
a menu does not use. And retiring the menu gives the keys straight back:
|
||
|
|
|
||
|
|
```basic norun
|
||
|
|
10 MENU 1, "YES", "NO"
|
||
|
|
20 GETMENU 1, C%
|
||
|
|
30 MENU 1 : REM now INPUT can have Return again
|
||
|
|
40 INPUT "NAME"; N$
|
||
|
|
```
|
||
|
|
|
||
|
|
Forget line 30 and the `INPUT` on line 40 will never see the Return that ends it. That is
|
||
|
|
the one way these verbs can surprise a program, so it is worth the line.
|
||
|
|
|
||
|
|
## What is not here
|
||
|
|
|
||
|
|
The rest of clay. libakgl builds its widgets on a full immediate-mode layout engine —
|
||
|
|
containers, flexible sizing, scrolling regions, images inside panels, custom elements,
|
||
|
|
hit-testing any element — and none of that is reachable from BASIC. A verb per clay
|
||
|
|
concept would be a second language inside this one.
|
||
|
|
|
||
|
|
Specifically absent, and each on purpose:
|
||
|
|
|
||
|
|
- **Per-widget styles.** One `UISTYLE`, shared by everything.
|
||
|
|
- **Scrolling.** Neither the dialog nor the menu scrolls; 16 entries is the menu's limit
|
||
|
|
and a longer one wants sub-screens.
|
||
|
|
- **Top-centre and bottom-centre HUD anchors**, because `akgl_UiAnchor` has neither.
|
||
|
|
- **Mouse position, hover state, or clicks on anything but a menu row.** A program cannot
|
||
|
|
ask where the pointer is.
|
||
|
|
- **Any way to dismiss a dialog from inside it.**
|
||
|
|
|
||
|
|
A game that needs those is writing C against `akgl/ui.h`, which is the route libakgl
|
||
|
|
documents for exactly this case, and it can do that in the same window this interpreter
|
||
|
|
is drawing into — [Chapter 10](10-embedding.md) is how the two meet.
|
||
|
|
|
||
|
|
One cost worth knowing about: libakgl re-rasterizes every line of widget text every
|
||
|
|
frame. A `HUD` that changes once a level is free; one rebuilt every frame is the
|
||
|
|
expensive case, and a program that has one should say so with `TI#` before blaming the
|
||
|
|
interpreter.
|