The GUI

VIOS boots into a windowed desktop. The windows, menus, widgets and animations are not drawn by the eZ80: they live in a display server on the ESP32 that also generates the VGA picture. Your Forth program states what exists ("a window here, a button in it, this text in that list") and later asks what the user did. Everything that must react within a frame, such as moving the focus, opening a menu, scrolling a list or editing a line of text, happens on the ESP without the CPU.

This chapter explains the model and every UI- word. The next chapter, The GUI Tools, describes the Browser, Workspace, Inspector and Manager built on top of it, and has the complete keyboard reference.

The VIOS desktop in the Smalltalk style, with the Console window
The VIOS desktop in the Smalltalk style, with the Console window

How it works

The CPU and the ESP talk over the VDP serial link:

  • Ops go one way, in batches. Each UI- word encodes one small op (a few bytes) and sends it inside a VDU 23,0,&E4,len,... packet. The ESP checks the length of the whole packet before applying any of it, so a damaged packet is dropped without side effects.
  • Composition is the ESP's job. At every vertical blank the ESP composes the damaged parts of the screen, top to bottom, from the desktop pattern, the windows (back to front, with shadows) and the sprites, and never shows a half-drawn window. About 3 ms of each 16.7 ms frame go to composing; damage that does not fit carries over to the next frame.
  • Events are pulled. Keys go to the ESP's window manager first. What it uses (a menu key, Tab between widgets, typing into an input field) never reaches the CPU; what it produces are semantic events: "button 5 pressed", "row 3 selected", "command 100 chosen", "close requested". They wait in a 16-entry queue on the ESP until the CPU asks for them with UI-NEXT or UI-EVENT. When the GUI tools are running they switch on the nudge: the ESP sends a one-byte packet when an event arrives, so the CPU only polls when there is something to fetch.
  • Unused keys still reach Forth. A key that no window-manager binding and no widget wants goes to the CPU's keyboard input as before, unless the focused window drops it (see UI-KEYS).

At power-on the ESP starts the display server before the eZ80 has loaded, so the desktop and the Console window (the REPL) appear at once. Ctrl+Alt+F1 switches between the desktop and a full-screen text console at any time; the server keeps all windows while hidden and repaints them when you come back.

Most UI- words only send bytes, so they also run on the host build and in background tasks (they check their arguments and report errors the same way). Five words wait for a reply from the ESP and therefore work only in the foreground on the board: UI-SYNC, UI-EVENT, UI-NEXT, UI-WGET and UI-TEXT@.

Ids and limits

All objects are numbered. Some numbers belong to the system:

Object Range for the CPU Taken by the system Use in your programs
Windows 0..26 20..24 GUI tools, 26 workbench 0..19
Surfaces 0..10 5..9 GUI tools, 10 workbench 0..4
Widgets 0..63, shared by all windows 40..63 GUI tools 0..39
Menus 0..15 0 world menu or menu bar, 12, 14, 15 tool pane menus 1..11, 13
Commands ids 0..65023, 64 defined at a time $7F00..$7FFF GUI tools (11 commands) other ids
Sprites 0..15 15 is the pointer 0..14

Other limits: window titles 31 characters, command labels 23, menu titles 15, 16 items per menu and 3 levels of submenus, widget text 47, list rows 31 characters and 1024 rows shared by all lists, 4 TextViews of 4096 bytes each (shared with the tools), 16 running tweens, 4096 bytes of sprite pixels, 8 events buffered on the CPU and 16 on the ESP.

Surfaces and cells

Text lives in surfaces: grids of character cells, up to 80 x 60. A cell is a character and an attribute byte, fg + 16 * bg, where both are palette indices. Indices 0..3 are black, red, yellow and white in every style; 4..15 belong to the style, so a surface meant to look the same everywhere uses 0..3. The attribute 48 (3 16 * 0 +) is black on white.

A window shows a surface (or part of one). Several windows may show the same surface, and you can draw into a surface whether or not a window shows it.

0 40 10 48 UI-SURFACE                    \ surface 0: 40 x 10 cells, black on white
1 0 160 120 40 10 UI-VISIBLE UI-DECOR OR UI-SHADOW OR UI-WINDOW
S" Notes" 1 UI-TITLE
S" Hello from the CPU" 0 1 1 48 UI-TYPE  \ surface 0, column 1, row 1
0 0 3 40 1 2 48 UI-FILL                  \ row 3: glyph 2 is a one-pixel horizontal line
0 19 1 3 TRUE UI-CARET                   \ a blinking bar caret after the text
1 UI-FOCUS  UI-COMMIT

UI-SURFACE ( id cols rows attr -- ) creates or clears a surface. UI-TYPE ( addr len id col row attr -- ) writes text, UI-FILL ( id col row w h char attr -- ) fills a rectangle, UI-SCROLL ( id dy attr -- ) scrolls the whole surface (a positive dy moves the text up) and UI-CARET ( id col row shape blink -- ) places a caret (shape 0 off, 1 block, 2 underline, 3 bar) that the ESP blinks by itself. Characters 1 and 2 are the server's one-pixel vertical and horizontal line glyphs.

Windows

UI-WINDOW ( id surface x y cols rows flags -- ) opens a window. x y is the pixel position of the top-left corner of the client area (the frame and the title are drawn around it); cols rows is its size in cells. The flags add up:

Flag Value Meaning
UI-VISIBLE 1 shown
UI-DECOR 2 frame and title in the current style; only visible, decorated windows can take the focus
UI-SHADOW 4 drop shadow
UI-ACTIVE 8 drawn as the active window (the server manages it as the focus moves)
UI-FREE-X 16 never snap this window's x
UI-MODAL 32 a dialog: dims everything below it, keeps the focus, Esc becomes a close request

The usual value for an application window is 7 (visible, decorated, shadow).

Snapping. At 4 bits per pixel a cell is exactly four bytes wide, so text draws fastest when the client area starts on a multiple of 8 pixels. By default the server therefore rounds every window's x to 8; y is free. 1 UI-SNAP allows any x for all windows, 8 UI-SNAP restores the default, and the UI-FREE-X flag exempts a single window.

Once a window exists:

1 200 80 40 10 UI-PLACE                  \ move (pixels) and resize (cells) in one step
1 UI-RAISE                               \ to the front, without the keyboard
1 0 1 UI-VIEW                            \ show the surface from row 1
1 0 0 UI-VIEW
1 UI-FOCUS                               \ to the front, with the keyboard
UI-COMMIT
  • UI-TITLE ( addr len id -- ) sets the title. Smalltalk draws it in a tab above the top-left corner, Turbo Vision centres it in the top frame.
  • UI-FOCUS ( id -- ) raises the window, marks it active and sends a FOCUS event; UI-RAISE only raises it.
  • UI-FLAGS ( id flags -- ) replaces the flags (clearing UI-VISIBLE hides a window without closing it), UI-CLIENT ( id surface -- ) shows another surface, and UI-VIEW ( id col row -- ) scrolls a window over a surface larger than itself.
  • UI-CLOSE ( id -- ) closes the window. Its widgets are forgotten, its surface stays.

The user moves windows with the keyboard too, all handled on the ESP: F6 and Shift+F6 cycle the focus, F5 zooms the focused window to nearly the whole screen and back, Ctrl+F5 enters move/resize mode (arrows move by 8 pixels, Shift+arrows resize by a cell, Enter keeps, Esc reverts), and Ctrl+W or Alt+F3 ask to close it. The server never closes a window by itself: a close request arrives as a WIN event, and your program decides (see Events). Moves, zooms and restores are reported the same way.

Styles: Smalltalk and Turbo Vision

The same windows, widgets and commands can be presented in two styles, switched at any time with UI-STYLE: 0 UI-STYLE for Smalltalk (the default) and 1 UI-STYLE for Turbo Vision. Every window is re-decorated and repainted; nothing else changes.

Smalltalk (0) Turbo Vision (1)
Desktop grey dither blue dither
Windows white body, black 1-pixel border, title in a tab above the top-left corner blue body, double frame when active, title centred in the top frame
Shadow 4 pixels two cells right, one cell down
Menu 0 the world menu, a pop-up opened with F10 the menu bar along the top row, opened with F10 or Alt+letter
Status line none bottom row, lists commands flagged UI-STATUS with their keys
Selection inverted in the focused widget, grey bar elsewhere green bar
1 UI-STYLE                               \ Turbo Vision
0 UI-STYLE                               \ back to Smalltalk
The same desktop in the Turbo Vision style
The same desktop in the Turbo Vision style
Switching between the two styles at run time
Switching between the two styles at run time

Because the GUI tools put their four commands directly into menu 0 (not into submenus), the Turbo Vision menu bar shows Browser Workspace Inspector Manager as top-level entries: F10 or Alt+B then opens the Browser at once instead of a pulldown.

The GUI tools in the Turbo Vision style: Browser, Workspace and Inspector
The GUI tools in the Turbo Vision style: Browser, Workspace and Inspector

Batching: UI-BEGIN and UI-COMMIT

Without a batch, every UI- word sends its op at once. To avoid showing a half-built window, the ESP holds composition after any change until it sees a COMMIT op, or for at most 33 ms. Wrap anything bigger than one op in UI-BEGIN and UI-COMMIT:

UI-BEGIN
  S" Notes (edited)" 1 UI-TITLE
  S" Second line" 0 1 2 48 UI-TYPE
UI-COMMIT

UI-BEGIN collects the ops in a 250-byte buffer on the CPU (a full buffer is sent and collecting continues), and UI-COMMIT sends the rest with a COMMIT, so the whole change appears in one frame. Batching also saves time: the link carries one packet instead of many. A lone UI-COMMIT after a few unbatched words is fine too; it just ends the hold early.

UI-SYNC ( -- epoch generation errors ) asks the server for its state: how many resets it has seen, how many batches it has applied and how many ops it has refused since the last reset. A growing error count means some op referred to something that does not exist.

UI-SYNC . . .                            \ errors generation epoch

Commands and menus

A command is an action with a number, a label and optionally a key. You define it once with UI-COMMAND ( addr len id flags vk mods ascii -- ); the ESP then offers it in menus, in the command palette and through its accelerator, in both styles, and sends a COMMAND event when it is chosen.

  • The label may mark a mnemonic letter with & (S" &Quit"); && is a literal ampersand.
  • flags adds UI-ENABLED (2, needed to be chosen at all), UI-CHECKED (4, a * in menus) and UI-STATUS (8, shown on the Turbo Vision status line).
  • The accelerator is vk (a virtual key, for example 159..170 for F1..F12; 0 for none) or else the character ascii, plus mods: UI-CTRL 1, UI-SHIFT 2, UI-ALT 4. For Ctrl+letter pass the letter (113 for Ctrl+Q). Menus show the key text, such as Ctrl+Q.

UI-MENU ( item1 .. itemN n addr len menu -- ) defines menu 0..15. Note the order: the items, then their count, then the title. An item is a command id, UI-SEPARATOR, or m UI-SUBMENU to open menu m as a submenu. UI-POPUP ( id menu -- ) binds a menu to a window as its pop-up menu, opened with Shift+F10 while the window has the focus (255 restores the default, the world menu). UI-STATE ( id flags -- ) changes a command's flags later, for example to grey it out or tick it.

S" &New note"   200 UI-ENABLED 0 UI-CTRL 110 UI-COMMAND     \ Ctrl+N
S" &Word wrap"  201 UI-ENABLED UI-CHECKED OR 0 0 0 UI-COMMAND
S" &Large font" 202 0 0 0 0 UI-COMMAND                       \ defined but disabled
S" &Quit"       203 UI-ENABLED UI-STATUS OR 0 UI-CTRL 113 UI-COMMAND
201 202 2 S" View" 2 UI-MENU                                 \ menu 2: View
200 2 UI-SUBMENU UI-SEPARATOR 203 4 S" Notes" 1 UI-MENU      \ menu 1: Notes
1 1 UI-POPUP                                                 \ Shift+F10 in window 1
201 UI-ENABLED UI-STATE                                      \ untick Word wrap
UI-COMMIT
A pane menu with a submenu, a separator, a disabled item and accelerators
A pane menu with a submenu, a separator, a disabled item and accelerators

The user reaches commands in several ways, all on the ESP:

  • Menus. F10 opens menu 0 (the world menu in Smalltalk, the first entry of the menu bar in Turbo Vision); Alt+letter opens the menu-bar entry with that mnemonic (Turbo Vision); Shift+F10 opens the focused window's pop-up menu. Inside a menu, Up/Down move, Enter or the mnemonic letter chooses, Right opens a submenu (or the next menu-bar entry), Left closes it (or goes to the previous entry), Esc closes one level.
  • The command palette. Ctrl+K or Ctrl+Shift+P opens a list of every enabled command; typing filters it (letters in order, case ignored), Backspace removes a letter, Enter runs the selection.
  • Accelerators work whenever no menu is open, before the focused widget sees the key. They are global: Ctrl+Q above fires whichever window has the focus, and the COMMAND event tells you which one.

Menu 0 is shared: the GUI tools define it as their World menu with Browser, Workspace, Inspector and Manager. Redefining menu 0 replaces it.

The command palette filtering commands as you type
The command palette filtering commands as you type

Widgets

Widgets are controls drawn by the ESP into a window's surface. They take keys and clicks locally and report what happened as events. Create one with UI-WIDGET ( id window kind parent col row w h flags -- ):

Kind Value What it is Text Value
UI-BOX 1 invisible container that lays out its children - -
UI-LABEL 2 one line of text caption -
UI-BUTTON 3 push button [ Go ] caption -
UI-CHECK 4 check box [x] caption 0 or 1
UI-RADIO 5 radio button (*); same parent = one group caption 0 or 1
UI-INPUT 6 one-line input field, 47 characters contents caret position
UI-LIST 7 scrolling list or table table header selected row
UI-PROGRESS 8 progress bar with percentage - 0..100
UI-TEXTVIEW 9 multi-line text editor (Text views) - dirty flag
UI-TABS 10 a row of tabs labels separated by \| selected tab

Flags: UI-HIDDEN 1, UI-DISABLED 2, UI-WF-FILL 4 (a root box that covers the whole client area and follows the window's size), UI-READONLY 8 (TextView), UI-TREE 16 and UI-PAGER 32 (lists).

Widget ids are global: id 5 is one widget, whatever window it is in, and creating a widget with an id in use replaces the old one. The widgets of a window are forgotten when it closes.

Layout. You can place a widget at col row w h in client cells, but it is easier to let boxes do it. UI-LAYOUT ( id layout gap -- ) makes a box arrange its children in creation order, as a column (UI-COLUMN, 1) or a row (UI-ROW, 2), with gap empty cells between them. UI-WSIZE ( id fixed weight -- ) gives a child a fixed number of cells along the box's direction, or (with fixed 0) a share of what is left, by weight. Boxes nest, and the layout follows the window when it is resized or zoomed. In the gap between two panes (lists, text views, boxes) the server draws a one-pixel divider line; gaps next to buttons, labels and inputs stay blank.

Setting and reading. UI-WTEXT ( addr len id -- ) and UI-WVALUE ( id value -- ) set the text and value columns of the table above. UI-WGET ( id addr -- value len ) reads them back (at most 25 characters of text). UI-WFOCUS ( id -- ) moves the keyboard to a widget, UI-WDELETE ( id -- ) deletes it with its children.

Lists and tables. UI-ITEMS ( id count -- ) sets the number of rows, UI-ITEM ( addr len id index -- ) sets one row. UI-COLUMNS ( w1 .. wn n id -- ) turns a list into a table of up to 8 columns: rows and the header (the widget text) then separate their cells with a tab character (9). A UI-TREE list sends EXPAND events for Right and Left; your program rewrites the rows (indent them, mark them with + or -). A UI-PAGER list sends PAGE events when the user moves past its first or last row, so a long or endless sequence can be shown a page at a time (the Inspector's memory view works this way).

The keys each widget understands are listed in the keyboard reference. In short: Tab and Shift+Tab move between the focusable widgets of the window in creation order, Space presses buttons and toggles check boxes and radios, Enter presses buttons and activates inputs and lists, lists move with the arrows, PgUp/PgDn, Home/End and type-ahead, and Ctrl+Tab changes the tab of the window's tabs widget.

This program builds a window with one widget of every kind. The small helpers W (create a widget in this window) and TAB2 (join two strings with a tab) keep it short:

4 CONSTANT GAL   3 CONSTANT GSF
CREATE RB 48 ALLOT   VARIABLE RL
: R+ ( addr len -- )  DUP >R  RB RL @ + SWAP MOVE  R> RL +! ;
: TAB2 ( a1 u1 a2 u2 -- addr len )  2SWAP 0 RL !  R+  9 RB RL @ + C!  1 RL +!  R+  RB RL @ ;
CREATE NL 10 C,
: W ( id kind parent -- )  >R GAL SWAP R> 0 0 0 0 0 UI-WIDGET ;
: GALLERY ( -- )
  UI-BEGIN
  GSF 62 11 48 UI-SURFACE
  GAL GSF 24 24 62 11 UI-VISIBLE UI-DECOR OR UI-SHADOW OR UI-WINDOW
  S" Widgets" GAL UI-TITLE
  0 GAL UI-BOX UI-NONE 0 0 0 0 UI-WF-FILL UI-WIDGET   0 UI-COLUMN 1 UI-LAYOUT
  1 UI-TABS 0 W   1 1 0 UI-WSIZE   S" Controls|Lists|Text" 1 UI-WTEXT
  2 UI-BOX 0 W    2 0 1 UI-WSIZE   2 UI-ROW 1 UI-LAYOUT
  3 UI-BOX 2 W    3 24 0 UI-WSIZE  3 UI-COLUMN 0 UI-LAYOUT
  4 UI-LABEL 3 W     4 1 0 UI-WSIZE   S" A label" 4 UI-WTEXT
  5 UI-BUTTON 3 W    5 1 0 UI-WSIZE   S" &Button" 5 UI-WTEXT
  6 UI-CHECK 3 W     6 1 0 UI-WSIZE   S" &Check box" 6 UI-WTEXT   6 1 UI-WVALUE
  7 UI-RADIO 3 W     7 1 0 UI-WSIZE   S" Radio &one" 7 UI-WTEXT   7 1 UI-WVALUE
  8 UI-RADIO 3 W     8 1 0 UI-WSIZE   S" Radio &two" 8 UI-WTEXT
  9 UI-INPUT 3 W     9 1 0 UI-WSIZE   S" an input field" 9 UI-WTEXT
  11 UI-PROGRESS 3 W 11 1 0 UI-WSIZE  11 60 UI-WVALUE
  10 UI-LIST 2 W     10 0 1 UI-WSIZE
  8 10 2 10 UI-COLUMNS   S" Word" S" Kind" TAB2 10 UI-WTEXT
  10 3 UI-ITEMS
  S" DUP" S" primitive" TAB2 10 0 UI-ITEM
  S" SQUARE" S" colon" TAB2 10 1 UI-ITEM
  S" BASE" S" variable" TAB2 10 2 UI-ITEM
  12 UI-TEXTVIEW 2 W  12 0 1 UI-WSIZE
  S" : SQUARE ( n -- n*n )" 12 UI-TEXT  NL 1 12 UI-TEXT+  S"   DUP * ;" 12 UI-TEXT+
  13 UI-LABEL 0 W    13 1 0 UI-WSIZE   S" Tab: next widget   Ctrl+Tab: next tab" 13 UI-WTEXT
  GAL UI-FOCUS
  UI-COMMIT ;
GALLERY
One widget of every kind, Smalltalk style
One widget of every kind, Smalltalk style
The same window in the Turbo Vision style
The same window in the Turbo Vision style

Appending rows: UI-ROW+

UI-ROW+ ( addr len id -- ) appends one row to list id (protocol op 92), like UI-TEXT+ appends text. UI-ROW without the + is the layout constant 2 for UI-LAYOUT.

10 0 UI-ITEMS                            \ list 10 starts empty
S" first" 10 UI-ROW+  S" second" 10 UI-ROW+

Text views

A TextView (UI-TEXTVIEW) is a small text editor run by the ESP: up to 4096 bytes in lines ending in a line feed, with a caret, a selection, scrolling and a 1 KB clipboard, all without the CPU. The GUI tools use it for the Browser's code pane, the Workspace and the read-only report panes. Four can exist at a time, including the tools' own.

  • UI-TEXT ( addr len id -- ) replaces the text, UI-TEXT+ appends to it.
  • UI-SELECT ( id caret anchor -- ) sets the caret and the selection (byte offsets; equal values mean no selection).
  • UI-INSERT ( addr len id -- ) replaces the selection with up to 200 bytes and selects what it inserted.
  • UI-TEXT@ ( id addr max -- len ) copies the text back to the CPU.
  • UI-WVALUE on a TextView sets or clears its dirty flag. The first edit after the flag was cleared sends one CHANGE event.
12 8 2 UI-SELECT                         \ select "SQUARE" in the gallery's text view
S" SQ" 12 UI-INSERT                      \ now ": SQ ( n -- n*n )", "SQ" selected
12 0 UI-WVALUE                           \ not dirty
UI-COMMIT
CREATE BUF 256 ALLOT
12 BUF 256 UI-TEXT@ BUF SWAP TYPE        \ needs the ESP: prints the text

With the UI-READONLY flag the caret moves and text can be selected and copied, but not changed. A TextView also turns four keys into TEXT events for the program: Ctrl+S (accept), Ctrl+D (do it), Ctrl+P (print it) and Ctrl+I (inspect it). For the last three, with nothing selected, the server first selects the caret's line. The event carries the selection, so the program can read exactly that range and evaluate it; this is how the Workspace works. Ctrl+[ and Ctrl+] add or remove two spaces at the start of every selected line. Tab leaves the view (there is no tab character).

Events

Every user action that matters to the program becomes an event in the ESP's queue. UI-NEXT ( -- kind id arg true | false ) takes the next one, decoded; UI-EVENT ( addr -- len ) stores the raw bytes (seq kind id and up to 26 argument bytes) and returns the length, 0 if none. Both first look in the CPU's 8-event ring and poll the ESP when it is empty.

arg is the first two argument bytes as an unsigned number (just the first if there is only one). Most events need nothing more:

Kind Value id arg More (UI-EVENT bytes)
UI-EV-ACTIVATE 2 widget window
UI-EV-SELECT 3 widget (list, tabs) row or tab window
UI-EV-CHANGE 4 widget new value (input: text length) window
UI-EV-TEXT 5 TextView low byte: 1 accept, 2 do it, 3 print it, 4 inspect it selection start, end, window
UI-EV-FOCUS 6 window now focused (255 none) previous window
UI-EV-POINTER 7 window under the pointer (255 desktop) x y, column, row, part, buttons
UI-EV-EXPAND 8 tree list row window, 1 open / 0 close
UI-EV-PAGE 9 pager list low byte: 1/255 down/up, 2/254 page down/up window
UI-EV-OVERFLOW 14 0 events lost
UI-EV-ERROR 15 0 refused op + 256 * offset
UI-EV-WIN 16 window low byte: 1 moved, 2 close request, 3 zoomed, 4 restored x, y, cols, rows
UI-EV-COMMAND 17 focused window command id source: 1 menu, 2 key, 3 palette
UI-EV-ANIMDONE 18 window or sprite target + 256 * property
UI-EV-CANVAS 19 canvas x in canvas pixels y, the item under the point (255 none)

For "low byte" rows take arg 255 AND, because the second byte of arg is already the next argument.

Kind 20 carries the text lines of /dev/win/N/event files to the handle layer (see Windows as files: /dev/win); while such a file is open, that node's events never reach UI-NEXT.

Some practical points:

  • Polling costs a round trip over the serial link. Handle every waiting event, then wait a little (20 MS) before asking again, rather than spinning.
  • The ESP queue holds 16 events. If your program does not read them, the oldest are dropped and an OVERFLOW event says how many.
  • The event words wait for the ESP, so they run only in the foreground task, never in a background task.
  • The GUI tools take their own events (commands $7F00..$7FFF, their windows 20 to 24 and widgets 40 to 63) out of the stream before UI-NEXT sees it, so your loop never receives them. They handle them when the REPL is idle again.

A convenient pattern is to copy the event into three variables and test them with a small helper:

VARIABLE KIND   VARIABLE WHO   VARIABLE ARG
: EVENT? ( -- flag )  UI-NEXT DUP IF >R ARG ! WHO ! KIND ! R> THEN ;
: IS? ( kind id -- flag )  WHO @ =  SWAP KIND @ =  AND ;
: CLOSING? ( window -- flag )  UI-EV-WIN SWAP IS?  ARG @ 255 AND 2 =  AND ;

UI-EV-ACTIVATE 5 IS? is then true when button 5 was pressed, and 1 CLOSING? when the user asked to close window 1.

Tutorial: a window with widgets and an event loop

This section builds a small "Greeter" window step by step: a label, an input field, a check box, an answer line and two buttons. Type each step at the console (or put them in a file and LOAD it). It reuses EVENT?, IS? and CLOSING? from the previous section.

Step 1: a surface and a window. Window 2 shows surface 1, 40 x 9 cells, at pixel 160, 120. The window needs a surface even if only widgets will be in it, because widgets draw into the surface.

2 CONSTANT GWIN   1 CONSTANT GSURF
GSURF 40 9 48 UI-SURFACE
GWIN GSURF 160 120 40 9 UI-VISIBLE UI-DECOR OR UI-SHADOW OR UI-WINDOW
S" Greeter" GWIN UI-TITLE
GWIN UI-FOCUS  UI-COMMIT

Step 2: the widgets. A root box (id 20) with UI-WF-FILL covers the client area and stacks its children in a column with one empty row between them. Each child takes one row (1 0 UI-WSIZE). The two buttons sit side by side in a second box (id 25) laid out as a row. Building everything inside UI-BEGIN ... UI-COMMIT makes the window appear complete in one frame.

UI-BEGIN
20 GWIN UI-BOX UI-NONE 0 0 0 0 UI-WF-FILL UI-WIDGET   20 UI-COLUMN 1 UI-LAYOUT
21 GWIN UI-LABEL 20 0 0 0 0 0 UI-WIDGET   21 1 0 UI-WSIZE   S" Your name:" 21 UI-WTEXT
22 GWIN UI-INPUT 20 0 0 0 0 0 UI-WIDGET   22 1 0 UI-WSIZE
23 GWIN UI-CHECK 20 0 0 0 0 0 UI-WIDGET   23 1 0 UI-WSIZE   S" &Shout" 23 UI-WTEXT
24 GWIN UI-LABEL 20 0 0 0 0 0 UI-WIDGET   24 1 0 UI-WSIZE
25 GWIN UI-BOX 20 0 0 0 0 0 UI-WIDGET     25 1 0 UI-WSIZE   25 UI-ROW 2 UI-LAYOUT
26 GWIN UI-BUTTON 25 0 0 0 0 0 UI-WIDGET  S" &Greet" 26 UI-WTEXT
27 GWIN UI-BUTTON 25 0 0 0 0 0 UI-WIDGET  S" &Close" 27 UI-WTEXT
22 UI-WFOCUS
UI-COMMIT

You can already use the window: type into the field, Tab to the check box and press Space, Tab to the buttons. All of that happens on the ESP; the CPU is not involved yet.

The Greeter window after typing a name
The Greeter window after typing a name

Step 3: reading the field and answering. GREET reads the input field with UI-WGET (which returns the value, here the caret position, and the text length), builds "Hello, name!" in a buffer, upper-cases it when the check box is ticked, and shows it in label 24.

CREATE NAME 32 ALLOT
CREATE ANSWER 48 ALLOT   VARIABLE ALEN
: A+ ( addr len -- )  DUP >R  ANSWER ALEN @ + SWAP MOVE  R> ALEN +! ;
: UPCASE ( addr len -- )
  BOUNDS ?DO I C@ DUP 97 123 WITHIN IF 32 - I C! ELSE DROP THEN LOOP ;
: GREET ( -- )
  0 ALEN !  S" Hello, " A+
  22 NAME UI-WGET NIP  NAME SWAP A+  S" !" A+
  23 NAME UI-WGET DROP  IF ANSWER ALEN @ UPCASE THEN
  ANSWER ALEN @ 24 UI-WTEXT ;

Step 4: the event loop. GSTEP handles every event that is waiting: the Greet button and Enter in the input field (both ACTIVATE events) greet, the Close button and a close request (Ctrl+W) end the loop. GREETER repeats it every 20 ms until done, then closes the window.

VARIABLE GDONE
: GHANDLE ( -- )
  UI-EV-ACTIVATE 26 IS?  UI-EV-ACTIVATE 22 IS?  OR IF GREET THEN
  UI-EV-ACTIVATE 27 IS?  GWIN CLOSING?  OR IF TRUE GDONE ! THEN ;
: GSTEP ( -- )  BEGIN EVENT? WHILE GHANDLE REPEAT ;
: GREETER ( -- )
  FALSE GDONE !
  BEGIN GSTEP 20 MS  GDONE @ UNTIL
  GWIN UI-CLOSE ;

Run it on the board:

GREETER

The REPL is busy while GREETER runs. To keep the console free, give the loop its own key to stop it, or run the loop only from the console when you need it: the event words cannot run in a background task. When the loop ends, GWIN UI-CLOSE removes the window and its widgets; run steps 1 and 2 again to reopen it.

Animation and tweens

The ESP can move windows and sprites smoothly by itself. UI-ANIMATE ( target id prop from to ms easing -- ) starts a tween:

The tween advances once per frame, so it costs the CPU one op however long it runs. A new tween on the same object and property replaces the running one; up to 16 run at once. UI-STOP ( target id prop -- ) freezes one where it is. When a tween ends, an ANIMDONE event reports it.

UI-TWEEN-WINDOW 1 UI-Y 480 80 700 UI-SPRING UI-ANIMATE   \ the Notes window drops in
A window dropping in with spring easing
A window dropping in with spring easing

Window x positions are still snapped to 8 pixels during a tween unless the window has UI-FREE-X or the snap is 1; y moves pixel by pixel.

Canvases and icons

A canvas puts pixel graphics into a window: lines, rectangles, filled boxes, ovals, discs, two-colour stipples, text at any pixel and icons. You describe the picture once as a display list that the ESP keeps, then change single items. The CPU never sends pixels and keeps no copy of the picture: only a two-byte pen (the current canvas and colour).

A canvas: a bar chart drawn with UI-BAR, UI-LINE and UI-PRINT, and the eight built-in icons
A canvas: a bar chart drawn with UI-BAR, UI-LINE and UI-PRINT, and the eight built-in icons

The model.

  • A canvas is a w × h pixel rectangle placed on a surface at pixel position x y (x a multiple of 8). Every window that shows the surface shows the canvas, and it scrolls with the window. When the surface's cells scroll (console style), the canvas stays where it is.
  • It has a background colour, or it is transparent (the surface's text shows through wherever nothing is drawn).
  • Its items have ids 0–254 and draw in id order, higher ids on top. Setting an item that exists replaces it, so you can leave gaps in the numbering and fill them later.
  • The ESP keeps a 4-bit backing raster per canvas and redraws only the part an edit touched, at the start of the next frame and within the frame's time budget. A canvas therefore costs about what the same area of text costs to compose, however many items it has.
  • Colours are palette indices 0–15. Indices 4–15 change with the style (Smalltalk or Turbo Vision); 0–3 look the same in both.
Limit Value
Canvases 8 (ids 0–7)
Canvas size up to 640 × 480; all rasters together at most 256 KB
Items 255 per canvas (ids 0–254)
Text item up to 20 characters
Bitmaps 32 ids: 0–23 your own (up to 64 × 64, 4 bits per pixel), 24–31 built-in 16 × 16 icons

Drawing. Create the canvas with UI-CANVAS, choose it and a colour with UI-PEN, then set items:

Word Stack Item
UI-DOT ( item x y -- ) one pixel
UI-LINE ( item x0 y0 x1 y1 -- ) a line, both ends included
UI-RECT ( item x y w h -- ) a 1-pixel outline
UI-BAR ( item x y w h -- ) a filled rectangle
UI-OVAL ( item x y w h -- ) an ellipse outline in the box
UI-DISC ( item x y w h -- ) a filled ellipse
UI-STIPPLE ( item x y w h colour2 -- ) a checker of the pen colour and colour2: the 16-colour way to shade
UI-PRINT ( addr n item x y -- ) text in the 8×8 font at any pixel, transparent background
UI-ICON ( item x y bitmap -- ) a bitmap; its transparent colour is skipped

The shapes all go through one native word, UI-SHAPE, so they cost no dictionary slots beyond their colon definitions. Coordinates are canvas pixels and may be negative; everything is clipped to the canvas.

Editing. UI-NUDGE ( item dx dy -- ) moves an item, UI-INK ( item colour -- ) recolours it, UI-ERASE ( item -- ) deletes it, and UI-WIPE deletes them all. UI-CANVAS-AT moves the whole canvas on its surface, and UI-CANVAS-FLAGS shows it, hides it, makes it transparent or (with flags −1) deletes it. A tween can slide a canvas without the CPU: target UI-TWEEN-CANVAS in UI-ANIMATE.

Editing a canvas: each step replaces three bars, then recolours one item and nudges another; the ESP redraws only what changed
Editing a canvas: each step replaces three bars, then recolours one item and nudges another; the ESP redraws only what changed

A bar chart in which one bar later changes:

2 40 18 48 UI-SURFACE                                \ surface 2: 40x18 cells
5 2 160 100 40 18 UI-VISIBLE UI-DECOR OR UI-SHADOW OR UI-WINDOW
S" Sales" 5 UI-TITLE
3 2 0 0 320 144 3 UI-CV-VISIBLE UI-CANVAS             \ canvas 3 fills it, white
3 0 UI-PEN  0 10 130 300 130 UI-LINE                  \ item 0: the axis
3 4 UI-PEN  1 20 60 30 70 UI-BAR                      \ item 1: a bar in colour 4
            2 70 90 30 40 UI-BAR                      \ item 2: another
3 0 UI-PEN  S" Q1  Q2" 3 20 134 UI-PRINT              \ item 3: the captions
UI-COMMIT
1 0 -20 UI-NUDGE  UI-COMMIT                           \ later: bar 1 grows 20 px

Icons. Eight 16 × 16 icons are built into the firmware as bitmaps 24–31: UI-ICON-DOCUMENT, UI-ICON-FOLDER, UI-ICON-WORD, UI-ICON-MODULE, UI-ICON-INFO, UI-ICON-WARNING, UI-ICON-ERROR and UI-ICON-DISK. Your own bitmaps (ids 0–23) are defined with UI-BITMAP ( bitmap w h transparent -- ) and filled with UI-BITS ( addr n bitmap offset -- ): 4 bits per pixel, two pixels a byte, rows packed. The bitmap pool is shared by all canvases.

4 2 0 0 160 24 3 UI-CV-VISIBLE UI-CANVAS              \ a strip of icons on surface 2
4 0 UI-PEN
: ICONS ( -- ) 8 0 DO  I  I 20 * 4 +  4  I 24 +  UI-ICON  LOOP ;
ICONS UI-COMMIT

Events. A click inside a visible canvas also queues a UI-EV-CANVAS event: the canvas id, the point in canvas pixels and the topmost item there (255 for none), besides the ordinary pointer event. With the keyboard pointer (Ctrl+F12) this works without a mouse.

Resetting. UI-RESET drops every canvas and your bitmaps; the built-in icons stay.

Sprites and the pointer

Sprites are small 4-bit bitmaps drawn above all windows, higher ids on top. A sprite is up to 64 x 64 pixels; all sprites share a 4096-byte pixel pool on the ESP.

  • UI-SPRITE ( id w h hotx hoty transparent -- ) defines one, hidden: its size, its hotspot (the pixel that sits at the sprite's position) and the colour index that is transparent.
  • UI-PIXELS ( addr len id offset -- ) sends the pixels: rows of (w + 1) / 2 bytes, the high nibble is the left pixel.
  • UI-SPRITE-AT ( id x y -- ) moves it, UI-SPRITE-SHOW ( id flag -- ) shows or hides it.
HEX
CREATE BALL                              \ 8 x 8, colour F transparent, 0 outline, 2 yellow
  FF C, 00 C, 00 C, FF C,   F0 C, 22 C, 22 C, 0F C,
  02 C, 22 C, 22 C, 20 C,   02 C, 22 C, 22 C, 20 C,
  02 C, 22 C, 22 C, 20 C,   02 C, 22 C, 22 C, 20 C,
  F0 C, 22 C, 22 C, 0F C,   FF C, 00 C, 00 C, FF C,
DECIMAL
1 8 8 4 4 15 UI-SPRITE
BALL 32 1 0 UI-PIXELS
1 320 60 UI-SPRITE-AT  1 TRUE UI-SPRITE-SHOW
UI-TWEEN-SPRITE 1 UI-Y 60 400 900 UI-SPRING UI-ANIMATE  \ and it bounces

Sprite 15 (UI-CURSOR) is the pointer. UI-ARROW ( id -- ) gives a sprite the built-in 12 x 16 arrow, and UI-POINTER ( x y buttons -- ) reports a pointer position and button state to the server the way a mouse driver would: the arrow moves there, a press focuses the window under it (and clicks the widget there), and each button change sends a POINTER event.

UI-CURSOR UI-ARROW
320 240 0 UI-POINTER                     \ the arrow appears at the centre

Without a mouse, Ctrl+F12 turns the keyboard into the pointer: the arrows move it by 8 pixels (Shift: 1 pixel), Space or Enter clicks, Esc or Ctrl+F12 again leaves.

Raw mode and reset

UI-RAW ( flag -- ) hands the screen to the classic VDU drawing words: TRUE UI-RAW stops the compositor from drawing, FALSE UI-RAW takes the screen back and repaints everything. Use it around code that draws with PLOT and friends directly (see Display and Graphics).

UI-RESET resets the whole server: every window, surface, sprite, widget, command and menu goes, the style returns to Smalltalk and the snap to 8. The server's own Console window comes back at once, and the GUI tools reinstall their commands and world menu, so the desktop stays usable. Close your own windows with UI-CLOSE when you only want to tidy up, and keep UI-RESET for test setups.

Windows as files: /dev/win

Every window and widget the display server holds is also a set of text files under /dev/win, in the spirit of Plan 9's window system. You can build, inspect and drive a user interface with nothing but OPEN, READ, WRITE and CLOSE (see Handles, Pipes and Devices), from Forth, from a task, or from the Mac. The ESP renders and parses the text, so the files always show the server's real state; the CPU only keeps the read position of each open handle.

File Read Write
/dev/win one line per node: id kind parent name rect (windows first, then widgets in creation order) —
/dev/win/new the id of the node made last kind parent name, one node per line
/dev/win/N/ctl the node's properties, one per line commands, one per line
/dev/win/N/text the content: a label's text, a list's rows, a text view's text, a window's title replaces the content (later writes on the same handle append)
/dev/win/N/event the node's events as text lines (a stream: a read waits for the next) —
/dev/ui/tree Forth source that rebuilds every window and widget —

N is a widget id (0–63) or w and a window id (w0–w26). Names show spaces as _ and - when empty. Kinds are window, box, label, button, check, radio, input, list, progress, text (a text view) and tabs.

Making nodes. Write window - Title to /dev/win/new for a window (it gets the lowest free id and its own 32 × 12 surface), or kind parent name for a widget whose parent is a window (w0) or a box. The new node takes the lowest free id; reading /dev/win/new gives the id made last, and /dev/win lists them all.

ctl commands: rect a b c d (a window: x y cols rows in pixels and cells; a widget: col row w h), text T, name T, show 0|1, flags F, focus, raise (a window), value V, layout L G and size F W (boxes and their children), style S (a widget's look: 0 normal, 1 strong, 2 fault, drawn in red), theme T (the whole display: 0 Smalltalk, 1 Turbo Vision), close (a window, freeing the surface it was given) and delete. From Forth, UI-WSTYLE ( id style -- ) sets a widget's style.

A small window built from the console with two helper words:

CREATE BUF 256 ALLOT
: SPIT ( addr u path pu -- ) 2 OPEN >R R@ WRITE DROP R> CLOSE DROP ;
: SLURP ( path pu -- ) 1 OPEN >R BUF 256 R@ READ BUF SWAP TYPE R> CLOSE DROP ;
S" window - Greeter" S" /dev/win/new" SPIT
S" /dev/win" SLURP
w0 window - Greeter 64 64 32 12
S" label w0 msg" S" /dev/win/new" SPIT
S" button w0 hello" S" /dev/win/new" SPIT
S" rect 2 2 20 1" S" /dev/win/0/ctl" SPIT
S" Hello from a file!" S" /dev/win/0/text" SPIT
S" rect 2 5 12 1" S" /dev/win/1/ctl" SPIT
S" text &Again" S" /dev/win/1/ctl" SPIT
S" /dev/win/1/ctl" SLURP
kind button
parent w0
name hello
rect 2 5 12 1
show 1
flags 0
value 0
count 0
layout 0 0
size 0 1
focus 1
style 0
text &Again
A window built only by writing to files under /dev/win: a label, a list, a button, a label in the fault style and a progress bar
A window built only by writing to files under /dev/win: a label, a list, a button, a label in the fault style and a progress bar

Events as a stream. Opening /dev/win/N/event routes that node's events into a pipe as lines such as activate, change 1, select 7, page -1, moved 80 48 40 16, close or command 12 1, instead of UI-NEXT. A task can wait on it with READ-WAIT without polling the display server; closing the file hands the node's events back to UI-NEXT.

CREATE EV 64 ALLOT
: CLICKS ( -- )   \ a task: count the button's activations on the console
  S" /dev/win/1/event" 1 OPEN >R
  BEGIN EV 64 R@ READ-WAIT DUP 0 > WHILE EV SWAP TYPE REPEAT
  DROP R> CLOSE DROP ;
' CLICKS SPAWN-CON .

Snapshots. /dev/ui/tree, and the word UI-SNAPSHOT that prints it, give Forth source (UI-SURFACE, UI-WINDOW, UI-WIDGET, UI-ITEM... lines) that rebuilds the current windows and widgets. Save it to a file and LOAD it to restore a layout, text views' text included (written with UI-TEXT, UI-TEXT+ and UI-CH for newlines and quotes). UI-WNAME ( addr len id -- ) gives a widget the name /dev/win shows.

Off-screen composition

Most changes damage a small part of the screen and are composed directly into the visible framebuffer within the frame budget. A change that damages more than half the screen (the workbench opening or closing, the desktop coming back after Ctrl+Alt+F1) would otherwise sweep down the screen over a dozen frames. For those, the ESP copies the screen into a 150 KB shadow framebuffer in PSRAM, composes there, and copies the finished rows back at once, so you see the old picture and then the new one. After half a second it presents whatever is done, so a continuous flood of output cannot freeze the screen.

Performance notes

  • Batch. One UI-BEGIN ... UI-COMMIT around a window's construction is both faster on the link and nicer to look at. The ESP also defers widget drawing to the end of each batch, so a widget is drawn once, not once per op.
  • Let the ESP do the work. Keep text in surfaces, lists and TextViews and let the ESP scroll, edit and select; move things with tweens instead of sending a new position every frame.
  • Do not spin on events. Poll, handle everything that waited, then MS.
  • Keep x on 8 pixels for windows that show a lot of text. Free x costs two partial stores per glyph row instead of one.
  • Reuse ids. Recreating a widget with the same id replaces it cleanly; UI-ITEMS with a smaller count shrinks a list without touching other lists.